1package replication23import (4 "context"5 "encoding/json"6 "errors"7 "net/url"8 "strings"910 "github.com/FelineStateMachine/tinyrelay/internal/event"11 "github.com/FelineStateMachine/tinyrelay/internal/storage"12)1314type CallbackPolicy struct {15 HostOrigins []string16 OwnerOrigins []string17}1819type PushRegistration struct {20 ID string21 Owner string22 Relay string23 Callback string24 Filters []event.Filter25 Ignore []event.Filter26 IncludeEvent bool27}2829func ParsePushRegistration(e event.Event, policy CallbackPolicy, self string) (PushRegistration, error) {30 if e.Kind != event.KIND_PUSH_REGISTRATION {31 return PushRegistration{}, errors.New("replication: wrong push registration kind")32 }33 registration := PushRegistration{ID: e.ID, Owner: e.PubKey}34 registration.Relay = exactTag(e, "relay", self)35 if registration.Relay == "" {36 return PushRegistration{}, errors.New("replication: registration does not name this relay")37 }38 callback := exactOriginTag(e, "callback")39 if callback == "" || !contains(policy.HostOrigins, callback) || !contains(policy.OwnerOrigins, callback) {40 return PushRegistration{}, errors.New("replication: callback origin is not approved")41 }42 registration.Callback = callback43 filters, err := parseFilterTags(e, "filter")44 if err != nil || len(filters) == 0 {45 return PushRegistration{}, errors.New("replication: registration needs a filter")46 }47 registration.Filters = filters48 registration.Ignore, err = parseFilterTags(e, "ignore")49 if err != nil {50 return PushRegistration{}, err51 }52 registration.IncludeEvent = hasTag(e, "include_event")53 return registration, nil54}5556func PushMatches(reg PushRegistration, e event.Event) bool {57 if !matchesAny(reg.Filters, e) {58 return false59 }60 return !matchesAny(reg.Ignore, e)61}6263func CallbackPayload(reg PushRegistration, e event.Event) ([]byte, error) {64 value := map[string]any{"id": e.ID, "relay": reg.Relay}65 if reg.IncludeEvent {66 value["event"] = e67 }68 return json.Marshal(value)69}7071// PrepareCallbackIntents persists references only. Callback I/O is performed72// later by a worker after registration and event visibility are rechecked.73func PrepareCallbackIntents(ctx context.Context, e event.Event, registrations []PushRegistration) []storage.Intent {74 if ctx.Err() != nil || privateKind(e.Kind) || hasProtectedTag(e) {75 return nil76 }77 out := make([]storage.Intent, 0, len(registrations))78 for _, registration := range registrations {79 if !PushMatches(registration, e) {80 continue81 }82 payload, err := json.Marshal(struct {83 Registration PushRegistration `json:"registration"`84 }{registration})85 if err != nil {86 continue87 }88 out = append(out, storage.Intent{Kind: "callback", EventID: e.ID, Target: registration.ID, Payload: string(payload)})89 }90 return out91}9293func parseFilterTags(e event.Event, name string) ([]event.Filter, error) {94 values := event.TagValues(e, name)95 filters := make([]event.Filter, 0, len(values))96 for _, value := range values {97 filter, err := event.ParseFilter([]byte(value))98 if err != nil {99 return nil, errors.New("replication: invalid push filter")100 }101 filters = append(filters, filter)102 }103 return filters, nil104}105106func exactTag(e event.Event, name, wanted string) string {107 for _, value := range event.TagValues(e, name) {108 if strings.TrimRight(value, "/") == strings.TrimRight(wanted, "/") && safeRelayURL(value) {109 return strings.TrimRight(value, "/")110 }111 }112 return ""113}114115func exactOriginTag(e event.Event, name string) string {116 for _, value := range event.TagValues(e, name) {117 parsed, err := url.Parse(value)118 if err == nil && parsed.Scheme == "https" && parsed.User == nil && parsed.Port() == "" && parsed.Path == "" && parsed.RawQuery == "" && parsed.Fragment == "" {119 return parsed.Scheme + "://" + parsed.Host120 }121 }122 return ""123}124125func matchesAny(filters []event.Filter, e event.Event) bool {126 for _, filter := range filters {127 if event.Matches(filter, e) {128 return true129 }130 }131 return false132}133134func hasTag(e event.Event, name string) bool {135 for _, tag := range e.Tags {136 if len(tag) > 0 && tag[0] == name {137 return true138 }139 }140 return false141}