| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 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. |
| 16 | func (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 | |
| 34 | func (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 | |
| 83 | func (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 | |
| 125 | func (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 | |
| 146 | func (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. |
| 166 | func (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 | |
| 203 | func (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 | |
| 211 | func nowUnix() int64 { |
| 212 | return time.Now().Unix() |
| 213 | } |