tinyrelayfile
tinyrelay/internal/daemon/wiki_approval_items.go/

internal/daemon/wiki_approval_items.go raw
1package daemon
2
3import (
4 "context"
5 "net/url"
6 "strings"
7 "time"
8
9 "github.com/FelineStateMachine/tinyrelay/internal/event"
10 "github.com/FelineStateMachine/tinyrelay/internal/storage"
11)
12
13// wikiApprovalItems adapts wiki proposals and merge requests to the common
14// approvals shape. A proposal is addressed to the owner and moderators; a
15// merge request is addressed only to its destination author.
16func (t *Tenant) wikiApprovalItems(ctx context.Context, actor string, before *storage.EventCursor, limit int) ([]approvalItem, bool, error) {
17 roles := t.wikiRoles()
18 decides, err := roles.decides(ctx, actor)
19 if err != nil {
20 return nil, false, err
21 }
22 items, more, err := t.wikiProposalApprovalItems(ctx, actor, decides, roles, before, limit)
23 if err != nil {
24 return nil, false, err
25 }
26 merges, mergeMore, err := t.wikiMergeApprovalItems(ctx, actor, before, limit)
27 if err != nil {
28 return nil, false, err
29 }
30 items = append(items, merges...)
31 return items, more || mergeMore, nil
32}
33
34func (t *Tenant) wikiProposalApprovalItems(ctx context.Context, actor string, decides bool, roles *wikiRoles, before *storage.EventCursor, limit int) ([]approvalItem, bool, error) {
35 if !decides {
36 return nil, false, nil
37 }
38 asked := t.wikiApprovalAudience(ctx)
39 items := make([]approvalItem, 0, limit)
40 more := false
41 for {
42 page, err := t.store.Query(ctx, event.Filter{Kinds: []int{kindWikiArticle}}, storage.QueryOptions{Now: nowUnix(), Access: storage.Access{All: true}, Limit: limit, Before: before})
43 if err != nil {
44 return nil, false, err
45 }
46 for i, row := range page.Events {
47 before = &storage.EventCursor{CreatedAt: row.CreatedAt, ID: row.ID}
48 proposer, err := roles.proposer(ctx, row.PubKey)
49 if err != nil {
50 return nil, false, err
51 }
52 if !proposer {
53 continue
54 }
55 version := wikiVersionFrom(row, false)
56 item := approvalItemFrom(row, approvalWikiProposal, strings.TrimRight(t.publicURL, "/"))
57 item.Asked = asked
58 item.Subject = version.Title
59 item.About = &approvalAbout{Coordinate: version.Coordinate, Event: row.ID, Kind: "30818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(version.D) + "?version=" + url.QueryEscape(row.ID)}
60 items = append(items, item)
61 if len(items) == limit {
62 more = page.More || i < len(page.Events)-1
63 break
64 }
65 }
66 if len(items) == limit || !page.More || len(page.Events) == 0 {
67 break
68 }
69 }
70 answers, err := t.approvalAnswers(ctx, items, nowUnix())
71 if err != nil {
72 return nil, false, err
73 }
74 for i := range items {
75 approvalSettle(&items[i], answers[items[i].ID], nowUnix())
76 if err := t.settleWikiProposal(ctx, &items[i], roles); err != nil {
77 return nil, false, err
78 }
79 }
80 return items, more, nil
81}
82
83func (t *Tenant) wikiMergeApprovalItems(ctx context.Context, actor string, before *storage.EventCursor, limit int) ([]approvalItem, bool, error) {
84 page, err := t.store.Query(ctx, event.Filter{Kinds: []int{kindWikiMerge}, Tags: map[string][]string{"p": {actor}}}, storage.QueryOptions{Now: nowUnix(), Access: storage.Access{All: true}, Limit: limit, Before: before})
85 if err != nil {
86 return nil, false, err
87 }
88 items := make([]approvalItem, 0, len(page.Events))
89 for _, row := range page.Events {
90 merge := wikiMergeFrom(row)
91 if merge.Destination != actor {
92 continue
93 }
94 item := approvalItemFrom(row, approvalWikiMerge, strings.TrimRight(t.publicURL, "/"))
95 item.Asked = []string{actor}
96 item.Subject = "Wiki merge request"
97 item.About = &approvalAbout{Coordinate: merge.Target, Event: row.ID, Kind: "818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(merge.TargetD) + "?merge=" + row.ID}
98 items = append(items, item)
99 }
100 answers, err := t.approvalAnswers(ctx, items, nowUnix())
101 if err != nil {
102 return nil, false, err
103 }
104 for i := range items {
105 approvalSettle(&items[i], answers[items[i].ID], nowUnix())
106 status, err := t.wikiMergeStatus(ctx, items[i].ID, actor)
107 if err != nil {
108 return nil, false, err
109 }
110 if status.Status == wikiMergeOpen {
111 items[i].State = "open"
112 items[i].Answer = nil
113 } else {
114 items[i].State = "answered"
115 answer, err := t.wikiReactionAnswer(ctx, status.Reaction)
116 if err != nil {
117 return nil, false, err
118 }
119 items[i].Answer = answer
120 }
121 }
122 return items, page.More, nil
123}
124
125func (t *Tenant) settleWikiProposal(ctx context.Context, item *approvalItem, roles *wikiRoles) error {
126 version := wikiVersionFrom(item.Event, false)
127 versions := []wikiVersion{version}
128 if err := t.wikiProposals(ctx, roles, versions); err != nil {
129 return err
130 }
131 switch versions[0].Approval {
132 case wikiProposalApproved, wikiProposalRejected:
133 item.State = "answered"
134 answer, err := t.wikiReactionAnswer(ctx, versions[0].ApprovalEvent)
135 if err != nil {
136 return err
137 }
138 item.Answer = answer
139 default:
140 item.State = "open"
141 item.Answer = nil
142 }
143 return nil
144}
145
146func (t *Tenant) wikiReactionAnswer(ctx context.Context, id string) (*approvalAnswer, error) {
147 if id == "" {
148 return nil, nil
149 }
150 rows, err := t.store.Query(ctx, event.Filter{IDs: []string{id}, Kinds: []int{kindReaction}, Limit: intPtr(1)}, storage.QueryOptions{Now: nowUnix(), Access: storage.Access{All: true}})
151 if err != nil || len(rows.Events) == 0 {
152 return nil, err
153 }
154 reaction := rows.Events[0]
155 decision := approvalDecision(reaction.Content)
156 if decision == "" {
157 return nil, nil
158 }
159 return &approvalAnswer{ID: reaction.ID, Kind: reaction.Kind, Author: reaction.PubKey, Decision: decision, Content: reaction.Content, CreatedAt: reaction.CreatedAt}, nil
160}
161
162// wikiApprovalByID returns a wiki request only when the caller is one of the
163// people allowed to decide it, or the author of the proposed version. This
164// keeps the detail endpoint consistent with the approvals listing while
165// allowing a proposer to inspect their own pending request.
166func (t *Tenant) wikiApprovalByID(ctx context.Context, actor, id string) (approvalItem, bool, error) {
167 rows, err := t.store.Query(ctx, event.Filter{IDs: []string{id}, Kinds: []int{kindWikiArticle, kindWikiMerge}, Limit: intPtr(1)}, storage.QueryOptions{Now: nowUnix(), Access: storage.Access{All: true}})
168 if err != nil || len(rows.Events) == 0 {
169 return approvalItem{}, false, err
170 }
171 row := rows.Events[0]
172 roles := t.wikiRoles()
173 if row.Kind == kindWikiArticle {
174 proposer, err := roles.proposer(ctx, row.PubKey)
175 if err != nil {
176 return approvalItem{}, false, err
177 }
178 decides, err := roles.decides(ctx, actor)
179 if err != nil {
180 return approvalItem{}, false, err
181 }
182 if !proposer || (!decides && row.PubKey != actor) {
183 return approvalItem{}, false, nil
184 }
185 version := wikiVersionFrom(row, false)
186 item := approvalItemFrom(row, approvalWikiProposal, strings.TrimRight(t.publicURL, "/"))
187 item.Asked = t.wikiApprovalAudience(ctx)
188 item.Subject = version.Title
189 item.About = &approvalAbout{Coordinate: version.Coordinate, Event: row.ID, Kind: "30818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(version.D) + "?version=" + url.QueryEscape(row.ID)}
190 return item, true, nil
191 }
192 merge := wikiMergeFrom(row)
193 if merge.Destination == "" || merge.Destination != actor {
194 return approvalItem{}, false, nil
195 }
196 item := approvalItemFrom(row, approvalWikiMerge, strings.TrimRight(t.publicURL, "/"))
197 item.Asked = []string{actor}
198 item.Subject = "Wiki merge request"
199 item.About = &approvalAbout{Coordinate: merge.Target, Event: row.ID, Kind: "818", URL: strings.TrimRight(t.publicURL, "/") + "/wiki/" + url.PathEscape(merge.TargetD) + "?merge=" + url.QueryEscape(row.ID)}
200 return item, true, nil
201}
202
203func (t *Tenant) wikiApprovalAudience(ctx context.Context) []string {
204 moderators, err := t.community.Moderators(ctx)
205 if err != nil {
206 return []string{t.Policy().Owner}
207 }
208 return uniqueStrings(append([]string{t.Policy().Owner}, moderators...))
209}
210
211func nowUnix() int64 {
212 return time.Now().Unix()
213}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs