| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 10 | "github.com/FelineStateMachine/tinyrelay/internal/storage" |
| 11 | ) |
| 12 | |
| 13 | func (t *Tenant) browseChatActivity(ctx context.Context, actor string, params []json.RawMessage) (any, error) { |
| 14 | var q struct { |
| 15 | Room string `json:"room"` |
| 16 | Root string `json:"root"` |
| 17 | } |
| 18 | if len(params) != 1 || json.Unmarshal(params[0], &q) != nil || q.Room == "" { |
| 19 | return nil, errors.New("invalid: chat activity room" ) |
| 20 | } |
| 21 | if _, _, err := t.roomFor(ctx, actor, q.Room); err != nil { |
| 22 | return nil, err |
| 23 | } |
| 24 | if q.Root != "" { |
| 25 | root, err := t.roomThreadRoot(ctx, actor, q.Room, q.Root) |
| 26 | if err != nil { |
| 27 | return nil, err |
| 28 | } |
| 29 | q.Root = root.ID |
| 30 | } |
| 31 | rows := []event.Event{} |
| 32 | for _, filter := range []event.Filter{ |
| 33 | {Kinds: approvalKinds, Tags: map[string][]string{"h" : {q.Room}}}, |
| 34 | {Kinds: jobRequestKinds, Tags: map[string][]string{"h" : {q.Room}}}, |
| 35 | } { |
| 36 | page, err := t.store.Query(ctx, filter, storage.QueryOptions{Access: storage.Access{PubKeys: []string{actor}}, Limit: 1000}) |
| 37 | if err != nil { |
| 38 | return nil, err |
| 39 | } |
| 40 | rows = append(rows, page.Events...) |
| 41 | } |
| 42 | session := browseSession(t, actor) |
| 43 | jobs, approvals := []jobItem{}, []approvalItem{} |
| 44 | now := time.Now().Unix() |
| 45 | roots := map[string]string{} |
| 46 | for _, row := range rows { |
| 47 | _, isApproval := approvalRequest(row) |
| 48 | if !event.IsJobRequest(row.Kind) && !isApproval { |
| 49 | continue |
| 50 | } |
| 51 | if !t.gate.CanSee(ctx, row, session, nil) { |
| 52 | continue |
| 53 | } |
| 54 | if q.Root != "" && !t.activityInThread(ctx, actor, q.Root, row, roots) { |
| 55 | continue |
| 56 | } |
| 57 | if event.IsJobRequest(row.Kind) && t.Policy().Features.Jobs && (event.Expiration(row) == 0 || event.Expiration(row) > now) { |
| 58 | jobs = append(jobs, jobItemFrom(row)) |
| 59 | } else if kind, ok := approvalRequest(row); ok && (row.PubKey == actor || containsString(approvalAsked(row), actor)) { |
| 60 | approvals = append(approvals, approvalItemFrom(row, kind, t.publicURL)) |
| 61 | } |
| 62 | } |
| 63 | // Keep the inline panel bounded even when a busy room has accumulated a |
| 64 | // large amount of historical task and approval traffic. The store returns |
| 65 | // each filter newest first, so retaining the first 50 preserves the useful |
| 66 | // recent context while the dedicated views remain available for history. |
| 67 | if len(jobs) > 50 { |
| 68 | jobs = jobs[:50] |
| 69 | } |
| 70 | if len(approvals) > 50 { |
| 71 | approvals = approvals[:50] |
| 72 | } |
| 73 | answers, err := t.jobAnswers(ctx, jobs, session, now) |
| 74 | if err != nil { |
| 75 | return nil, err |
| 76 | } |
| 77 | for i := range jobs { |
| 78 | jobSettle(&jobs[i], answers[jobs[i].ID]) |
| 79 | } |
| 80 | decisions, err := t.approvalAnswers(ctx, approvals, now) |
| 81 | if err != nil { |
| 82 | return nil, err |
| 83 | } |
| 84 | for i := range approvals { |
| 85 | approvalSettle(&approvals[i], decisions[approvals[i].ID], now) |
| 86 | } |
| 87 | return map[string]any{"jobs" : jobs, "approvals" : approvals}, nil |
| 88 | } |
| 89 | |
| 90 | func (b backend) ReadChatActivity(ctx context.Context, actor, room, root string) (any, error) { |
| 91 | raw, err := json.Marshal(map[string]string{"room" : room, "root" : root}) |
| 92 | if err != nil { |
| 93 | return nil, err |
| 94 | } |
| 95 | return b.tenant.Execute(ctx, actor, "browsechatactivity" , []json.RawMessage{raw}) |
| 96 | } |
| 97 | |
| 98 | // Resolve parent-only references once per parent while keeping unrelated tasks |
| 99 | // out of the thread. roomThreadRoot also enforces room and reader access. |
| 100 | func (t *Tenant) activityInThread(ctx context.Context, actor, root string, row event.Event, roots map[string]string) bool { |
| 101 | if row.ID == root { |
| 102 | return true |
| 103 | } |
| 104 | reference := event.RoomReplyRoot(row) |
| 105 | if reference == "" && event.IsJobRequest(row.Kind) { |
| 106 | reference = event.Tag(row, "e" ) |
| 107 | } |
| 108 | if reference == root { |
| 109 | return true |
| 110 | } |
| 111 | if reference == "" { |
| 112 | return false |
| 113 | } |
| 114 | canonical, known := roots[reference] |
| 115 | if !known { |
| 116 | parent, err := t.roomThreadRoot(ctx, actor, event.Tag(row, "h" ), reference) |
| 117 | if err == nil { |
| 118 | canonical = parent.ID |
| 119 | } |
| 120 | roots[reference] = canonical |
| 121 | } |
| 122 | return canonical == root |
| 123 | } |