clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/standalone-relay
tinyrelay / internal / daemon / wiki_approval_items.go
wiki_approval_items.go 7.7K
1package daemon23import (4 "context"5 "net/url"6 "strings"7 "time"89 "github.com/FelineStateMachine/tinyrelay/internal/event"10 "github.com/FelineStateMachine/tinyrelay/internal/storage"11)1213// wikiApprovalItems adapts wiki proposals and merge requests to the common14// approvals shape. A proposal is addressed to the owner and moderators; a15// 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, err21 }22 items, more, err := t.wikiProposalApprovalItems(ctx, actor, decides, roles, before, limit)23 if err != nil {24 return nil, false, err25 }26 merges, mergeMore, err := t.wikiMergeApprovalItems(ctx, actor, before, limit)27 if err != nil {28 return nil, false, err29 }30 items = append(items, merges...)31 return items, more || mergeMore, nil32}3334func (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, nil37 }38 asked := t.wikiApprovalAudience(ctx)39 items := make([]approvalItem, 0, limit)40 more := false41 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, err45 }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, err51 }52 if !proposer {53 continue54 }55 version := wikiVersionFrom(row, false)56 item := approvalItemFrom(row, approvalWikiProposal, strings.TrimRight(t.publicURL, "/"))57 item.Asked = asked58 item.Subject = version.Title59 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)-163 break64 }65 }66 if len(items) == limit || !page.More || len(page.Events) == 0 {67 break68 }69 }70 answers, err := t.approvalAnswers(ctx, items, nowUnix())71 if err != nil {72 return nil, false, err73 }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, err78 }79 }80 return items, more, nil81}8283func (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, err87 }88 items := make([]approvalItem, 0, len(page.Events))89 for _, row := range page.Events {90 merge := wikiMergeFrom(row)91 if merge.Destination != actor {92 continue93 }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, err103 }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, err109 }110 if status.Status == wikiMergeOpen {111 items[i].State = "open"112 items[i].Answer = nil113 } else {114 items[i].State = "answered"115 answer, err := t.wikiReactionAnswer(ctx, status.Reaction)116 if err != nil {117 return nil, false, err118 }119 items[i].Answer = answer120 }121 }122 return items, page.More, nil123}124125func (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 err130 }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 err137 }138 item.Answer = answer139 default:140 item.State = "open"141 item.Answer = nil142 }143 return nil144}145146func (t *Tenant) wikiReactionAnswer(ctx context.Context, id string) (*approvalAnswer, error) {147 if id == "" {148 return nil, nil149 }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, err153 }154 reaction := rows.Events[0]155 decision := approvalDecision(reaction.Content)156 if decision == "" {157 return nil, nil158 }159 return &approvalAnswer{ID: reaction.ID, Kind: reaction.Kind, Author: reaction.PubKey, Decision: decision, Content: reaction.Content, CreatedAt: reaction.CreatedAt}, nil160}161162// wikiApprovalByID returns a wiki request only when the caller is one of the163// people allowed to decide it, or the author of the proposed version. This164// keeps the detail endpoint consistent with the approvals listing while165// 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, err170 }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, err177 }178 decides, err := roles.decides(ctx, actor)179 if err != nil {180 return approvalItem{}, false, err181 }182 if !proposer || (!decides && row.PubKey != actor) {183 return approvalItem{}, false, nil184 }185 version := wikiVersionFrom(row, false)186 item := approvalItemFrom(row, approvalWikiProposal, strings.TrimRight(t.publicURL, "/"))187 item.Asked = t.wikiApprovalAudience(ctx)188 item.Subject = version.Title189 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, nil191 }192 merge := wikiMergeFrom(row)193 if merge.Destination == "" || merge.Destination != actor {194 return approvalItem{}, false, nil195 }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, nil201}202203func (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}210211func nowUnix() int64 {212 return time.Now().Unix()213}