tinyrelayfile
tinyrelay/internal/daemon/approvals_browse.go/

internal/daemon/approvals_browse.go raw
1package daemon
2
3// Approval browsing: the requests addressed to the caller with their state
4// and counts, and one request with every answer it received. Both read
5// through the tenant gate with the caller's session, like the other browse
6// methods.
7
8import (
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "strings"
14 "time"
15
16 "github.com/FelineStateMachine/tinyrelay/internal/event"
17 "github.com/FelineStateMachine/tinyrelay/internal/storage"
18)
19
20type approvalBrowseRequest struct {
21 Limit int `json:"limit"`
22 Cursor string `json:"cursor"`
23 State string `json:"state"`
24 ID string `json:"id"`
25 Event string `json:"event"`
26}
27
28// approvalDevice is one of the caller's registered devices, as a fact for
29// the panel: when it was registered and which categories it wakes for.
30type approvalDevice struct {
31 CreatedAt int64 `json:"created_at"`
32 Categories []string `json:"categories"`
33}
34
35func approvalBrowseMethod(method string) bool {
36 return method == "browseapprovals" || method == "browseapproval"
37}
38
39func (t *Tenant) executeApprovals(ctx context.Context, actor, method string, params []json.RawMessage) (any, error) {
40 if actor == "" {
41 return nil, errors.New("auth-required: sign in to see requests addressed to you")
42 }
43 q := approvalBrowseRequest{}
44 if len(params) > 0 {
45 if err := json.Unmarshal(params[0], &q); err != nil {
46 return nil, fmt.Errorf("invalid: approval parameters: %w", err)
47 }
48 }
49 if q.Limit <= 0 || q.Limit > 100 {
50 q.Limit = 50
51 }
52 switch method {
53 case "browseapprovals":
54 return t.browseApprovals(ctx, actor, q)
55 case "browseapproval":
56 if q.ID == "" {
57 q.ID = q.Event
58 }
59 return t.browseApproval(ctx, actor, q.ID)
60 }
61 return nil, errors.New("unsupported: browse operation")
62}
63
64// browseApprovals lists the requests addressed to the caller. The counts
65// cover every request the walk saw, so the panel can say how many wait even
66// when the page shows only one state.
67func (t *Tenant) browseApprovals(ctx context.Context, actor string, q approvalBrowseRequest) (any, error) {
68 if q.State == "" {
69 q.State = "all"
70 }
71 if !containsString([]string{"open", "answered", "expired", "all"}, q.State) {
72 return nil, errors.New("invalid: approval state")
73 }
74 cursor, err := parseCollaborationCursor(q.Cursor)
75 if err != nil {
76 return nil, errors.New("invalid: approval cursor")
77 }
78 session := browseSession(t, actor)
79 items := make([]approvalItem, 0, q.Limit)
80 counts := map[string]int{"open": 0, "answered": 0, "expired": 0}
81 var oldest int64
82 next := ""
83 for scanned := 0; scanned < approvalScan; scanned += 100 {
84 page, more, err := t.approvalsFor(ctx, actor, cursor, 100)
85 if err != nil {
86 return nil, err
87 }
88 for _, item := range page {
89 cursor = &storage.EventCursor{CreatedAt: item.CreatedAt, ID: item.ID}
90 if !t.gate.CanSee(ctx, item.Event, session, nil) {
91 continue
92 }
93 counts[item.State]++
94 if item.State == "open" && (oldest == 0 || item.CreatedAt < oldest) {
95 oldest = item.CreatedAt
96 }
97 if q.State != "all" && q.State != item.State {
98 continue
99 }
100 if len(items) < q.Limit {
101 items = append(items, item)
102 } else if next == "" {
103 next = approvalCursor(items[len(items)-1])
104 }
105 }
106 if !more || len(page) == 0 {
107 break
108 }
109 if next == "" && len(items) == q.Limit {
110 next = approvalCursor(items[len(items)-1])
111 }
112 }
113 devices, err := t.approvalDevices(ctx, actor)
114 if err != nil {
115 return nil, err
116 }
117 return map[string]any{"items": items, "next_cursor": next, "counts": counts, "oldest_open": oldest, "devices": devices}, nil
118}
119
120// browseApproval returns one request with every reaction and reply from the
121// asked keys, newest first. The asked people and the asker may read it.
122func (t *Tenant) browseApproval(ctx context.Context, actor, id string) (any, error) {
123 if len(id) != 64 {
124 return nil, errors.New("invalid: approval id")
125 }
126 if item, ok, err := t.wikiApprovalByID(ctx, actor, id); err != nil {
127 return nil, err
128 } else if ok {
129 answers, err := t.approvalAnswers(ctx, []approvalItem{item}, time.Now().Unix())
130 if err != nil {
131 return nil, err
132 }
133 approvalSettle(&item, answers[id], time.Now().Unix())
134 if item.Type == approvalWikiProposal {
135 if err := t.settleWikiProposal(ctx, &item, t.wikiRoles()); err != nil {
136 return nil, err
137 }
138 } else if item.Type == approvalWikiMerge {
139 status, err := t.wikiMergeStatus(ctx, item.ID, actor)
140 if err != nil {
141 return nil, err
142 }
143 if status.Status == wikiMergeOpen {
144 item.State = "open"
145 item.Answer = nil
146 } else {
147 item.State = "answered"
148 item.Answer, err = t.wikiReactionAnswer(ctx, status.Reaction)
149 if err != nil {
150 return nil, err
151 }
152 }
153 }
154 history := answers[id]
155 if history == nil {
156 history = []event.Event{}
157 }
158 return map[string]any{"item": item, "answers": history}, nil
159 }
160 session := browseSession(t, actor)
161 rows, err := t.Query(ctx, []event.Filter{{IDs: []string{id}, Kinds: approvalKinds, Limit: intPtr(1)}}, session)
162 if err != nil {
163 return nil, err
164 }
165 if len(rows) != 1 {
166 return nil, errors.New("not found: request")
167 }
168 kind, ok := approvalRequest(rows[0])
169 if !ok {
170 return nil, errors.New("not found: request")
171 }
172 item := approvalItemFrom(rows[0], kind, strings.TrimRight(t.publicURL, "/"))
173 if item.Asker != actor && !containsString(item.Asked, actor) {
174 return nil, errors.New("restricted: this request is not addressed to you")
175 }
176 now := time.Now().Unix()
177 answers, err := t.approvalAnswers(ctx, []approvalItem{item}, now)
178 if err != nil {
179 return nil, err
180 }
181 approvalSettle(&item, answers[item.ID], now)
182 t.reviewGrantApproval(ctx, &item, now)
183 history := answers[item.ID]
184 if history == nil {
185 history = []event.Event{}
186 }
187 return map[string]any{"item": item, "answers": history}, nil
188}
189
190// approvalDevices lists the caller's registered devices with the categories
191// each one chose. Endpoints and keys stay out of the result.
192func (t *Tenant) approvalDevices(ctx context.Context, pubkey string) ([]approvalDevice, error) {
193 rows, err := t.store.DB().QueryContext(ctx, "SELECT created_at, categories FROM web_push WHERE pubkey=? ORDER BY created_at", pubkey)
194 if err != nil {
195 return nil, err
196 }
197 defer rows.Close()
198 devices := []approvalDevice{}
199 for rows.Next() {
200 var device approvalDevice
201 var raw string
202 if err := rows.Scan(&device.CreatedAt, &raw); err != nil {
203 return nil, err
204 }
205 device.Categories = pushCategories
206 if allowed := decodeCategories(raw); allowed != nil {
207 device.Categories = []string{}
208 for _, category := range pushCategories {
209 if allowed[category] {
210 device.Categories = append(device.Categories, category)
211 }
212 }
213 }
214 devices = append(devices, device)
215 }
216 return devices, rows.Err()
217}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs