| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "database/sql" |
| 6 | "errors" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/community" |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 11 | "github.com/FelineStateMachine/tinyrelay/internal/policy" |
| 12 | "github.com/FelineStateMachine/tinyrelay/internal/relay" |
| 13 | "github.com/FelineStateMachine/tinyrelay/internal/replication" |
| 14 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 15 | ) |
| 16 | |
| 17 | type metadataProjection struct { |
| 18 | next policy.Policy |
| 19 | changed bool |
| 20 | } |
| 21 | |
| 22 | func (t *Tenant) publishReport(ctx context.Context, e event.Event) (string, error) { |
| 23 | target := "" |
| 24 | targetType := "" |
| 25 | reportType := "" |
| 26 | for _, tag := range e.Tags { |
| 27 | if len(tag) > 1 && tag[0] == "e" && target == "" { |
| 28 | target = tag[1] |
| 29 | targetType = "event" |
| 30 | if len(tag) > 2 { |
| 31 | reportType = tag[2] |
| 32 | } |
| 33 | } |
| 34 | if len(tag) > 1 && tag[0] == "p" && target == "" { |
| 35 | target = tag[1] |
| 36 | targetType = "pubkey" |
| 37 | if len(tag) > 2 { |
| 38 | reportType = tag[2] |
| 39 | } |
| 40 | } |
| 41 | if len(tag) > 1 && tag[0] == "x" && target == "" { |
| 42 | target = tag[1] |
| 43 | targetType = "blob" |
| 44 | if len(tag) > 2 { |
| 45 | reportType = tag[2] |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | if target == "" { |
| 50 | return "" , errors.New("invalid: report needs an e or p tag" ) |
| 51 | } |
| 52 | if err := t.community.SubmitReportTarget(ctx, e.PubKey, target, targetType, reportType, e.Content, t.Policy().ReportThreshold); err != nil { |
| 53 | return "" , err |
| 54 | } |
| 55 | return "info: report received" , nil |
| 56 | } |
| 57 | |
| 58 | func (t *Tenant) prepareClientEvent(ctx context.Context, e event.Event, s relay.Session, now int64) (eventCommitPlan, *metadataProjection, error) { |
| 59 | metadata := &metadataProjection{} |
| 60 | plan, err := t.prepareStoredEvent(ctx, e, replication.OriginClient, now) |
| 61 | if err != nil { |
| 62 | return plan, metadata, err |
| 63 | } |
| 64 | opts := plan.options |
| 65 | if (e.Kind == event.KIND_EDIT_METADATA || e.Kind == event.KIND_PINS) && t.roomScope(e) == "" { |
| 66 | role, roleErr := t.community.Role(ctx, e.PubKey) |
| 67 | if roleErr != nil { |
| 68 | return plan, metadata, roleErr |
| 69 | } |
| 70 | if role != "owner" && role != "moderator" { |
| 71 | return plan, metadata, errors.New("restricted: not a group admin" ) |
| 72 | } |
| 73 | opts.AddBeforeCommit(func(txCtx context.Context, tx *sql.Tx) error { |
| 74 | return t.community.HandleProjectionEventTx(txCtx, tx, e, func(sideTx *sql.Tx) error { |
| 75 | if e.Kind == event.KIND_EDIT_METADATA { |
| 76 | metadata.next = t.Policy() |
| 77 | for _, tag := range e.Tags { |
| 78 | if len(tag) < 2 { |
| 79 | continue |
| 80 | } |
| 81 | switch tag[0] { |
| 82 | case "name" : |
| 83 | metadata.next.Name = tag[1][:min(200, len(tag[1]))] |
| 84 | case "about" : |
| 85 | metadata.next.Description = tag[1][:min(2000, len(tag[1]))] |
| 86 | case "picture" : |
| 87 | metadata.next.Icon = tag[1][:min(2000, len(tag[1]))] |
| 88 | } |
| 89 | } |
| 90 | metadata.changed = true |
| 91 | return storage.PutSetting(txCtx, sideTx, "policy" , metadata.next) |
| 92 | } |
| 93 | pins, err := parsePinTags(e.Tags) |
| 94 | if err != nil { |
| 95 | return err |
| 96 | } |
| 97 | return t.records.ReplacePinsTx(txCtx, sideTx, pins) |
| 98 | }) |
| 99 | }) |
| 100 | } |
| 101 | opts.Intents = append(opts.Intents, t.replication.Prepare(e, replication.OriginClient)...) |
| 102 | if e.Kind == event.KIND_MARMOT_GROUP { |
| 103 | principal, principalErr := t.gate.Principal(ctx, e, s) |
| 104 | if principalErr != nil { |
| 105 | return plan, metadata, principalErr |
| 106 | } |
| 107 | opts.AddBeforeCommit(func(txCtx context.Context, tx *sql.Tx) error { |
| 108 | _, err := tx.ExecContext(txCtx, `INSERT OR REPLACE INTO marmot_principals(event_id,pubkey) VALUES(?,?)` , e.ID, principal) |
| 109 | return err |
| 110 | }) |
| 111 | } |
| 112 | plan.options = opts |
| 113 | return plan, metadata, nil |
| 114 | } |
| 115 | |
| 116 | func (t *Tenant) persistClientEvent(ctx context.Context, e event.Event, opts storage.SaveOptions) (string, error) { |
| 117 | var err error |
| 118 | persist := func(tx *sql.Tx) error { |
| 119 | _, saveErr := storage.SaveTx(ctx, tx, e, opts) |
| 120 | return saveErr |
| 121 | } |
| 122 | // reason is the OK message; an access request tells the asker it waits, |
| 123 | // and a member asking again is told so. |
| 124 | reason := "" |
| 125 | room := t.roomScope(e) |
| 126 | switch { |
| 127 | case room != "" && community.RoomAdminKind(e.Kind): |
| 128 | _, err = t.community.HandleRoomEventTx(ctx, e, persist) |
| 129 | case room != "" && !event.IsEphemeral(e.Kind): |
| 130 | err = t.community.HandleRoomMessageTx(ctx, e, persist) |
| 131 | case e.Kind == event.KIND_JOIN, e.Kind == event.KIND_LEAVE, e.Kind == event.KIND_NIP43_JOIN, e.Kind == event.KIND_NIP43_LEAVE: |
| 132 | var membership community.MembershipResult |
| 133 | membership, err = t.community.HandleMembershipEventTx(ctx, e, persist) |
| 134 | if err == nil && (membership.AccessRequest || strings.HasPrefix(membership.Message, "duplicate:" )) { |
| 135 | reason = membership.Message |
| 136 | } |
| 137 | if err == nil && membership.NewRequest { |
| 138 | t.notifyJoinRequest(ctx, e) |
| 139 | } |
| 140 | case e.Kind == event.KIND_PUT_USER, e.Kind == event.KIND_REMOVE_USER, e.Kind == event.KIND_DELETE_EVENT, e.Kind == event.KIND_CREATE_INVITE: |
| 141 | _, err = t.community.HandleModerationEventTx(ctx, e, persist) |
| 142 | default: |
| 143 | _, err = t.store.Save(ctx, e, opts) |
| 144 | } |
| 145 | return reason, err |
| 146 | } |