clone

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

main
tinyrelay / tinyclient / wiki.go
wiki.go 5.9K
1package tinyclient23// Wiki pages: the view model derived from the browsewikipage result so the4// templates read plain fields, and the URL helpers for page names.56import (7 "html/template"8 "net/url"9 "strings"1011 "github.com/FelineStateMachine/tinyrelay/internal/wiki"12)1314// wikiView is one page as the templates see it. Version is the version15// being shown, Number its place counted from the oldest, Others the rest.16// Revision and Revisions number the shown version among its author's17// revisions; History lists every revision the viewer may see, newest18// first. Archived is set when an older revision was opened by id, and19// ReadersRevision names the approved revision readers see while the20// shown one is a proposal that is not approved.21type wikiView struct {22 D, Title string23 Version map[string]any24 Author string25 Number, Count int26 Revision, Revisions int27 Forks, Links int28 Versions []map[string]any29 Others []map[string]any30 History []map[string]any31 Archived bool32 ReadersRevision int33 Merges []map[string]any34 OpenMerges []map[string]any35 OpenCount int36 RedirectsTo []map[string]any37 RedirectsFrom []map[string]any38 // CanApprove is set when the viewer may accept or reject proposals.39 CanApprove bool40}4142func wikiInt(value any) int {43 number, _ := value.(float64)44 return int(number)45}4647// wikiProposalView is how a version's proposal state reads for one viewer.48// Shown is set for the people who may see the state: the owner and49// moderators, and the version's author. Decide is set when the viewer may50// accept or reject it, which is only while it is pending.51type wikiProposalView struct {52 Shown, Decide bool53 State string54 At any55 By string56}5758// wikiProposal reads a version's proposal state for the viewer. canApprove59// is the browse result's can_approve flag.60func wikiProposal(version map[string]any, actor string, canApprove any) wikiProposalView {61 if version == nil || version["proposal"] != true {62 return wikiProposalView{}63 }64 approver, _ := canApprove.(bool)65 if !approver && (actor == "" || actor != plainString(version["author"])) {66 return wikiProposalView{}67 }68 view := wikiProposalView{Shown: true, State: plainString(version["approval"]), At: version["approval_at"], By: plainString(version["approval_by"])}69 view.Decide = approver && view.State == "pending"70 return view71}7273func wikiMaps(value any) []map[string]any {74 items, _ := value.([]any)75 maps := make([]map[string]any, 0, len(items))76 for _, item := range items {77 if m := valueMap(item); m != nil {78 maps = append(maps, m)79 }80 }81 return maps82}8384// wikiPageView reads the browsewikipage result. Open merge requests aimed at85// the shown version's author come first in OpenMerges; OpenCount counts every86// open request on the page.87func wikiPageView(result any) wikiView {88 page := valueMap(result)89 view := wikiView{D: plainString(page["d"]), Title: plainString(page["title"])}90 view.Versions = wikiMaps(page["versions"])91 view.Merges = wikiMaps(page["merges"])92 view.RedirectsTo = wikiMaps(page["redirects_to"])93 view.RedirectsFrom = wikiMaps(page["redirects_from"])94 view.History = wikiMaps(page["history"])95 view.CanApprove, _ = page["can_approve"].(bool)96 view.Count = len(view.Versions)97 if current := valueMap(page["version"]); current != nil {98 view.Version = current99 view.Author = plainString(current["author"])100 view.Revision, view.Revisions = wikiInt(current["revision"]), wikiInt(current["revisions"])101 view.Archived = plainString(current["superseded_by"]) != "" && plainString(page["preferred_by"]) == "version"102 if links, ok := current["links"].([]any); ok {103 view.Links = len(links)104 }105 if current["proposal"] == true && plainString(current["approval"]) != "approved" {106 for _, h := range view.History {107 if plainString(h["author"]) == view.Author && plainString(h["approval"]) == "approved" {108 view.ReadersRevision = wikiInt(h["revision"])109 break110 }111 }112 }113 }114 if view.Title == "" {115 view.Title = view.D116 }117 id, coordinate := plainString(view.Version["id"]), plainString(view.Version["coordinate"])118 for i, v := range view.Versions {119 if plainString(v["id"]) == id {120 view.Number = view.Count - i121 continue122 }123 view.Others = append(view.Others, v)124 fork := valueMap(v["fork"])125 if fork != nil && (plainString(fork["e"]) == id || (coordinate != "" && plainString(fork["a"]) == coordinate)) {126 view.Forks++127 }128 }129 for _, m := range view.Merges {130 if plainString(m["status"]) != "open" {131 continue132 }133 view.OpenCount++134 if plainString(m["destination"]) == view.Author {135 view.OpenMerges = append(view.OpenMerges, m)136 }137 }138 return view139}140141// wikiURL builds the page path for a name, with optional query pairs.142func wikiURL(d string, pairs ...string) string {143 path := "/wiki/" + url.PathEscape(d)144 values := url.Values{}145 for i := 0; i+1 < len(pairs); i += 2 {146 if pairs[i] == "merge" {147 if pairs[i+1] != "" {148 path += "/proposals/" + pairs[i+1]149 }150 continue151 }152 if pairs[i+1] != "" {153 values.Set(pairs[i], pairs[i+1])154 }155 }156 if len(values) == 0 {157 return path158 }159 return path + "?" + values.Encode()160}161162func (a *App) wikiHTML(content any) template.HTML {163 return wiki.RenderHTMLWith(plainString(content), wiki.Options{Block: a.blocks()})164}165166// wikiPageName is the name in a /wiki/<d> path, normalized.167func wikiPageName(path string) string {168 rest := strings.TrimPrefix(path, "/wiki/")169 if i := strings.Index(rest, "/proposals/"); i >= 0 {170 rest = rest[:i]171 }172 name, err := url.PathUnescape(rest)173 if err != nil {174 name = rest175 }176 return wiki.Normalize(name)177}178179// wikiMergeID is the merge request behind /wiki/{page}/proposals/{id}, or "".180func wikiMergeID(path string) string {181 rest := strings.TrimPrefix(path, "/wiki/")182 i := strings.Index(rest, "/proposals/")183 if i < 0 {184 return ""185 }186 id := rest[i+len("/proposals/"):]187 if !eventIDPattern.MatchString(id) {188 return ""189 }190 return strings.ToLower(id)191}