tinyrelayfile
tinyrelay/tinyclient/approvals.go/

tinyclient/approvals.go raw
1package tinyclient
2
3import (
4 "context"
5 "encoding/json"
6 "strconv"
7 "strings"
8)
9
10// approvalView is one request as the Approvals page renders it. The backend
11// answers browseapprovals with typed items; this flattens what the template
12// needs so the markup stays plain.
13type approvalView struct {
14 ID, Kind, Type, State, Asker string
15 CreatedAt, Expires int64
16 // Where the request was made: the room, the repository or the relay.
17 Where string
18 // Title is the subject, or the content when there is no subject; Body is
19 // the content when a subject is present.
20 Title, Body string
21 // About summarizes the a or e reference in one mono line, and Open is
22 // the relay path that shows it.
23 About, Open, Coordinate string
24 Decision string
25 AnsweredAt int64
26 AnswerContent string
27 Grant *grantRequestView
28}
29
30type grantRequestView struct {
31 Raw string
32 Error string
33 Rows []grantScopeRow
34}
35
36type grantScopeRow struct{ Label, Before, After string }
37
38// approvalViews returns the items in the given states, in the order the
39// backend listed them.
40func approvalViews(value any, base string, states ...string) []approvalView {
41 var views []approvalView
42 for _, raw := range collaborationSlice(value, "items") {
43 item := valueMap(raw)
44 state := plainString(item["state"])
45 wanted := len(states) == 0
46 for _, s := range states {
47 wanted = wanted || s == state
48 }
49 if !wanted {
50 continue
51 }
52 views = append(views, approvalViewFrom(item, base))
53 }
54 return views
55}
56
57func approvalViewFrom(item map[string]any, base string) approvalView {
58 view := approvalView{ID: plainString(item["id"]), Kind: plainString(item["kind"]), Type: plainString(item["type"]), State: plainString(item["state"]), Asker: plainString(item["asker"]), CreatedAt: unixSeconds(item["created_at"]), Expires: unixSeconds(item["expires"])}
59 subject, content := strings.TrimSpace(plainString(item["subject"])), strings.TrimSpace(plainString(item["content"]))
60 view.Title, view.Body = subject, content
61 if subject == "" {
62 view.Title, view.Body = content, ""
63 }
64 view.Where = "the relay"
65 view.Open = "/e/" + view.ID
66 if about := valueMap(item["about"]); len(about) > 0 {
67 coordinate, referenced, kind := plainString(about["coordinate"]), plainString(about["event"]), plainString(about["kind"])
68 var parts []string
69 if coordinate != "" {
70 parts = append(parts, "about "+coordinate)
71 if fields := strings.SplitN(coordinate, ":", 3); len(fields) == 3 && fields[0] == "30617" {
72 view.Coordinate = coordinate
73 view.Where = "repository " + fields[2]
74 }
75 }
76 if referenced != "" {
77 label := "event " + shortID(referenced)
78 if kind != "" {
79 label = "kind " + kind + " " + shortID(referenced)
80 }
81 parts = append(parts, label)
82 }
83 view.About = strings.Join(parts, " | ")
84 if url := plainString(about["url"]); url != "" {
85 view.Open = relayPath(url, base)
86 }
87 }
88 if room := plainString(item["room"]); room != "" {
89 view.Where = "#" + room
90 }
91 if answer := valueMap(item["answer"]); len(answer) > 0 {
92 view.Decision, view.AnsweredAt, view.AnswerContent = plainString(answer["decision"]), unixSeconds(answer["created_at"]), strings.TrimSpace(plainString(answer["content"]))
93 }
94 if view.Type == "grant" {
95 raw := valueMap(item["grant"])
96 rows := []grantScopeRow{}
97 if len(raw) > 0 {
98 rows = grantScopeRows(raw)
99 } else {
100 raw = map[string]any{"request_id": view.ID, "agent": view.Asker, "operator": joinJSONValues(item["asked"])}
101 }
102 encoded, _ := json.Marshal(raw)
103 view.Grant = &grantRequestView{Raw: string(encoded), Error: plainString(item["grant_error"]), Rows: rows}
104 }
105 return view
106}
107
108func grantScopeRows(review map[string]any) []grantScopeRow {
109 before := valueMap(review["before"])
110 after := valueMap(review["after"])
111 bs, as := valueMap(before["scope"]), valueMap(after["scope"])
112 rows := []grantScopeRow{{"agent", shortID(plainString(before["pubkey"])), shortID(plainString(after["pubkey"]))}, {"name", plainString(before["name"]), plainString(after["name"])}, {"rooms", joinJSONValues(bs["rooms"]), joinJSONValues(as["rooms"])}, {"repos", joinGrantRepos(bs["repos"]), joinGrantRepos(as["repos"])}, {"wiki", plainString(bs["wiki"]), plainString(as["wiki"])}, {"jobs", plainString(bs["jobs"]), plainString(as["jobs"])}, {"sites", joinGrantSites(bs["sites"]), joinGrantSites(as["sites"])}, {"kinds", joinJSONValues(bs["kinds"]), joinJSONValues(as["kinds"])}, {"rate", plainString(bs["rate"]), plainString(as["rate"])}}
113 for i := range rows {
114 if rows[i].Before == "" {
115 rows[i].Before = "none"
116 }
117 if rows[i].After == "" {
118 rows[i].After = "none"
119 }
120 }
121 return rows
122}
123
124func joinJSONValues(value any) string {
125 // JSON numbers do not unmarshal into []string. Render both numeric kind
126 // lists and textual lists through their JSON representation so the review
127 // table never silently loses a proposed permission.
128 var values []any
129 if encoded, err := json.Marshal(value); err == nil {
130 _ = json.Unmarshal(encoded, &values)
131 }
132 parts := make([]string, 0, len(values))
133 for _, item := range values {
134 switch typed := item.(type) {
135 case string:
136 parts = append(parts, typed)
137 case float64:
138 parts = append(parts, strconv.FormatInt(int64(typed), 10))
139 default:
140 if encoded, err := json.Marshal(item); err == nil {
141 parts = append(parts, string(encoded))
142 }
143 }
144 }
145 return strings.Join(parts, ", ")
146}
147
148func joinGrantRepos(value any) string {
149 var repos []map[string]any
150 if encoded, err := json.Marshal(value); err == nil {
151 _ = json.Unmarshal(encoded, &repos)
152 }
153 parts := make([]string, 0, len(repos))
154 for _, repo := range repos {
155 parts = append(parts, plainString(repo["owner"])+":"+plainString(repo["identifier"])+":"+plainString(repo["level"]))
156 }
157 return strings.Join(parts, ", ")
158}
159
160func joinGrantSites(value any) string {
161 var sites []map[string]any
162 if encoded, err := json.Marshal(value); err == nil {
163 _ = json.Unmarshal(encoded, &sites)
164 }
165 parts := make([]string, 0, len(sites))
166 for _, site := range sites {
167 part := plainString(site["label"])
168 if ttl := plainString(site["ttl"]); ttl != "" && ttl != "0" {
169 part += " ttl=" + ttl
170 }
171 if site["encrypted"] == true {
172 part += " encrypted"
173 }
174 parts = append(parts, part)
175 }
176 return strings.Join(parts, ", ")
177}
178
179// includeApproval makes sure the request a notification opened is on the
180// page: when the listing does not carry it, the single request is read and
181// placed first. A request that cannot be read leaves the listing as it is.
182func (a *App) includeApproval(ctx context.Context, actor string, result any, id string) any {
183 page := valueMap(result)
184 items := collaborationSlice(result, "items")
185 for _, item := range items {
186 if plainString(valueMap(item)["id"]) == id {
187 return result
188 }
189 }
190 raw, _ := json.Marshal(map[string]any{"id": id})
191 detail, err := a.backend.Query(ctx, "browseapproval", []json.RawMessage{raw}, actor)
192 if err != nil {
193 return result
194 }
195 item := valueMap(detail)["item"]
196 if item == nil {
197 return result
198 }
199 page["items"] = append([]any{item}, items...)
200 return page
201}
202
203// relayPath turns an absolute relay URL into the path the page links to, so
204// tenant prefixes are applied once when the page is written.
205func relayPath(url, base string) string {
206 base = strings.TrimRight(base, "/")
207 if base != "" && strings.HasPrefix(url, base+"/") {
208 return strings.TrimPrefix(url, base)
209 }
210 if i := strings.Index(url, "://"); i >= 0 {
211 if slash := strings.Index(url[i+3:], "/"); slash >= 0 {
212 return url[i+3+slash:]
213 }
214 return "/"
215 }
216 return url
217}
218
219// approvalCounts reads the counts object the backend returns beside the
220// items, defaulting every state to zero.
221func approvalCounts(value any) map[string]int {
222 counts := map[string]int{"open": 0, "answered": 0, "expired": 0}
223 encoded, err := json.Marshal(valueMap(value)["counts"])
224 if err == nil {
225 _ = json.Unmarshal(encoded, &counts)
226 }
227 return counts
228}
229
230// approvalDevices lists the caller's devices as "categories" strings.
231func approvalDevices(value any) []string {
232 var devices []string
233 for _, raw := range collaborationSlice(value, "devices") {
234 var categories []string
235 encoded, err := json.Marshal(valueMap(raw)["categories"])
236 if err == nil {
237 _ = json.Unmarshal(encoded, &categories)
238 }
239 if len(categories) == 0 {
240 devices = append(devices, "no categories")
241 continue
242 }
243 devices = append(devices, strings.Join(categories, ", "))
244 }
245 return devices
246}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs