tinyrelayfile
tinyrelay/tinyclient/social_pages.go/

tinyclient/social_pages.go raw
1package tinyclient
2
3import (
4 "encoding/json"
5 "io"
6 "net/http"
7 "net/url"
8 "strings"
9)
10
11func (a *App) handleSocialRoute(w http.ResponseWriter, r *http.Request) bool {
12 p := r.URL.Path
13 if p != "/articles" && p != "/social" && p != "/social.json" && p != "/social.xml" && !strings.HasPrefix(p, "/social/") {
14 return false
15 }
16 if !a.backend.Policy().Features.Pages {
17 http.NotFound(w, r)
18 return true
19 }
20 if p == "/social/preview" && r.Method == http.MethodPost {
21 a.socialPreviewPage(w, r)
22 return true
23 }
24 if r.Method != http.MethodGet && r.Method != http.MethodHead {
25 http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
26 return true
27 }
28 if p == "/articles" {
29 target := requestPrefix(r) + "/social"
30 if r.URL.RawQuery != "" {
31 target += "?" + r.URL.RawQuery
32 }
33 http.Redirect(w, r, target, http.StatusMovedPermanently)
34 return true
35 }
36 if p == "/social.json" || p == "/social.xml" {
37 a.socialSyndication(w, r)
38 return true
39 }
40 a.socialPage(w, r)
41 return true
42}
43
44func socialParameters(q url.Values) map[string]any {
45 params := map[string]any{"limit": 30}
46 for _, key := range []string{"kind", "author", "q", "cursor", "address"} {
47 if value := q.Get(key); value != "" {
48 params[key] = value
49 }
50 }
51 if q.Get("kind") == "" {
52 switch q.Get("kinds") {
53 case "1":
54 params["kind"] = "notes"
55 case "30023":
56 params["kind"] = "articles"
57 }
58 }
59 return params
60}
61
62func (a *App) socialPage(w http.ResponseWriter, r *http.Request) {
63 actor, _ := a.resolveActor(r)
64 params := socialParameters(r.URL.Query())
65 method, tab := "browsesocial", "social"
66 if strings.HasPrefix(r.URL.Path, "/social/") {
67 params["id"] = strings.TrimPrefix(r.URL.Path, "/social/")
68 method, tab = "browsesocialthread", "social-thread"
69 } else if r.URL.Query().Get("address") != "" {
70 method, tab = "browsesocialthread", "social-thread"
71 }
72 raw, err := json.Marshal(params)
73 if err != nil {
74 http.Error(w, "invalid social query", http.StatusBadRequest)
75 return
76 }
77 result, err := a.backend.Query(r.Context(), method, []json.RawMessage{raw}, actor)
78 data := PageData{Tab: tab, Title: "Social | " + a.backend.Slug(), Event: result, Query: r.URL.Query()}
79 if tab == "social" {
80 data.Feed = browseRows(result)
81 }
82 if err != nil {
83 data.Error = err.Error()
84 if strings.HasPrefix(err.Error(), "not found:") {
85 w.WriteHeader(http.StatusNotFound)
86 }
87 }
88 a.render(w, r, data)
89}
90
91func (a *App) socialPreviewPage(w http.ResponseWriter, r *http.Request) {
92 actor, err := a.resolveActor(r)
93 if err != nil || actor == "" {
94 http.Error(w, "Sign in to preview a draft.", http.StatusUnauthorized)
95 return
96 }
97 var input struct {
98 Content string `json:"content"`
99 Kind int `json:"kind"`
100 }
101 decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 256*1024))
102 decoder.DisallowUnknownFields()
103 if err := decoder.Decode(&input); err != nil {
104 http.Error(w, "Invalid preview.", http.StatusBadRequest)
105 return
106 }
107 if decoder.Decode(new(any)) != io.EOF || (input.Kind != 1 && input.Kind != 30023 && input.Kind != 1111) {
108 http.Error(w, "Invalid preview.", http.StatusBadRequest)
109 return
110 }
111 w.Header().Set("Cache-Control", "private, no-store")
112 writeJSON(w, http.StatusOK, map[string]any{"html": a.socialBody(map[string]any{"content": input.Content, "kind": input.Kind})})
113}
114
115func socialContext(value any, data PageData) map[string]any {
116 row := valueMap(value)
117 out := make(map[string]any, len(row)+4)
118 for key, value := range row {
119 out[key] = value
120 }
121 out["viewer"] = data.Actor
122 out["detail"] = data.Tab == "social-thread"
123 out["relay"] = wsURL(data.URL)
124 out["thread"] = valueMap(data.Event)["post"]
125 out["parent_url"] = socialReferenceURL(row, "parent_")
126 out["root_url"] = socialReferenceURL(row, "root_")
127 parent := plainString(row["parent_id"])
128 thread := valueMap(out["thread"])
129 out["nested"] = parent != "" && parent != plainString(thread["id"])
130 return out
131}
132
133func socialReferenceURL(row map[string]any, prefix string) string {
134 if address := plainString(row[prefix+"address"]); strings.HasPrefix(address, "30023:") {
135 return "/social?address=" + url.QueryEscape(address)
136 }
137 if id := plainString(row[prefix+"id"]); hexID.MatchString(id) {
138 return "/social/" + id
139 }
140 return ""
141}
142
143func socialThreadPage(path string, query url.Values, cursor string) string {
144 next := url.Values{}
145 if address := query.Get("address"); address != "" {
146 next.Set("address", address)
147 }
148 next.Set("cursor", cursor)
149 return path + "?" + next.Encode()
150}
151
152func socialReactionCount(value any, content string) int {
153 for _, row := range browseRows(valueMap(value)["reactions"]) {
154 if plainString(valueMap(row)["content"]) == content {
155 return int(unixSeconds(valueMap(row)["count"]))
156 }
157 }
158 return 0
159}
160
161func socialReacted(value any, content string) bool {
162 for _, row := range browseRows(valueMap(value)["reactions"]) {
163 if plainString(valueMap(row)["content"]) == content {
164 return valueMap(row)["reacted"] == true
165 }
166 }
167 return false
168}
169
170func socialQuery(query url.Values, key, value string) string {
171 next := url.Values{}
172 for _, name := range []string{"kind", "author", "q", "compose", "address"} {
173 if v := query.Get(name); v != "" {
174 next.Set(name, v)
175 }
176 }
177 if value == "" {
178 next.Del(key)
179 } else {
180 next.Set(key, value)
181 }
182 if encoded := next.Encode(); encoded != "" {
183 return "/social?" + encoded
184 }
185 return "/social"
186}
187
188func socialURL(value any) string {
189 row := valueMap(value)
190 if address := plainString(row["address"]); address != "" {
191 return "/social?address=" + url.QueryEscape(address)
192 }
193 return "/social/" + url.PathEscape(plainString(row["id"]))
194}
195
196func (a *App) socialSyndication(w http.ResponseWriter, r *http.Request) {
197 actor, _ := a.resolveActor(r)
198 raw, err := json.Marshal(socialParameters(r.URL.Query()))
199 if err != nil {
200 http.Error(w, "invalid social query", http.StatusBadRequest)
201 return
202 }
203 result, err := a.backend.Query(r.Context(), "browsesocial", []json.RawMessage{raw}, actor)
204 if err != nil {
205 http.Error(w, err.Error(), publicErrorStatus(err))
206 return
207 }
208 items := make([]map[string]any, 0)
209 for _, row := range browseRows(result) {
210 if item, ok := articleItem(row, a.backend.URL()); ok {
211 item["url"] = strings.TrimSuffix(a.backend.URL(), "/") + socialURL(row)
212 if title := plainString(valueMap(row)["title"]); title != "" {
213 item["title"] = title
214 }
215 items = append(items, item)
216 }
217 }
218 if r.URL.Path == "/social.xml" {
219 a.writeAtom(w, items)
220 return
221 }
222 writeJSON(w, http.StatusOK, map[string]any{"version": "https://jsonfeed.org/version/1.1", "title": a.backend.Slug() + " Social", "home_page_url": strings.TrimSuffix(a.backend.URL(), "/") + "/social", "feed_url": strings.TrimSuffix(a.backend.URL(), "/") + "/social.json", "items": items})
223}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs