fun.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "crypto/rand"
20 "crypto/rsa"
21 "fmt"
22 "html/template"
23 "log"
24 "net/http"
25 "os"
26 "regexp"
27 "strings"
28 "sync"
29
30 "golang.org/x/net/html"
31 "humungus.tedunangst.com/r/webs/htfilter"
32 "humungus.tedunangst.com/r/webs/httpsig"
33)
34
35func reverbolate(userid int64, honks []*Honk) {
36 filt := htfilter.New()
37 filt.Imager = replaceimg
38 for _, h := range honks {
39 h.What += "ed"
40 if h.What == "tonked" {
41 h.What = "honked back"
42 h.Style = "subtle"
43 }
44 if !h.Public {
45 h.Style += " limited"
46 }
47 translate(h)
48 if h.Whofore == 2 || h.Whofore == 3 {
49 h.URL = h.XID
50 if h.What != "bonked" {
51 h.Noise = re_memes.ReplaceAllString(h.Noise, "")
52 h.Noise = mentionize(h.Noise)
53 h.Noise = ontologize(h.Noise)
54 }
55 h.Username, h.Handle = handles(h.Honker)
56 } else {
57 _, h.Handle = handles(h.Honker)
58 h.Username = h.Handle
59 if len(h.Username) > 20 {
60 h.Username = h.Username[:20] + ".."
61 }
62 if h.URL == "" {
63 h.URL = h.XID
64 }
65 }
66 if h.Oonker != "" {
67 _, h.Oondle = handles(h.Oonker)
68 }
69 zap := make(map[*Donk]bool)
70 h.Precis = demoji(h.Precis)
71 h.Noise = demoji(h.Noise)
72 h.Open = "open"
73 if userid == -1 {
74 if h.Precis != "" {
75 h.Open = ""
76 }
77 } else {
78 unsee(userid, h)
79 if h.Open == "open" && h.Precis == "unspecified horror" {
80 h.Precis = ""
81 }
82 }
83 if len(h.Noise) > 6000 && h.Open == "open" {
84 if h.Precis == "" {
85 h.Precis = "really freaking long"
86 }
87 h.Open = ""
88 }
89
90 h.HTPrecis, _ = filt.String(h.Precis)
91 h.HTML, _ = filt.String(h.Noise)
92 emuxifier := func(e string) string {
93 for _, d := range h.Donks {
94 if d.Name == e {
95 zap[d] = true
96 if d.Local {
97 return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
98 }
99 }
100 }
101 return e
102 }
103 h.HTPrecis = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTPrecis), emuxifier))
104 h.HTML = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTML), emuxifier))
105 j := 0
106 for i := 0; i < len(h.Donks); i++ {
107 if !zap[h.Donks[i]] {
108 h.Donks[j] = h.Donks[i]
109 j++
110 }
111 }
112 h.Donks = h.Donks[:j]
113 }
114}
115
116func replaceimg(node *html.Node) string {
117 src := htfilter.GetAttr(node, "src")
118 alt := htfilter.GetAttr(node, "alt")
119 //title := GetAttr(node, "title")
120 if htfilter.HasClass(node, "Emoji") && alt != "" {
121 return alt
122 }
123 alt = html.EscapeString(alt)
124 src = html.EscapeString(src)
125 d := finddonk(src)
126 if d != nil {
127 src = fmt.Sprintf("https://%s/d/%s", serverName, d.XID)
128 return fmt.Sprintf(`<img alt="%s" title="%s" src="%s">`, alt, alt, src)
129 }
130 return fmt.Sprintf(`<img alt="%s" src="<a href="%s">%s<a>">`, alt, src, src)
131}
132
133func inlineimgs(node *html.Node) string {
134 src := htfilter.GetAttr(node, "src")
135 alt := htfilter.GetAttr(node, "alt")
136 //title := GetAttr(node, "title")
137 if htfilter.HasClass(node, "Emoji") && alt != "" {
138 return alt
139 }
140 alt = html.EscapeString(alt)
141 src = html.EscapeString(src)
142 if !strings.HasPrefix(src, "https://"+serverName+"/") {
143 d := savedonk(src, "image", alt, "image", true)
144 if d != nil {
145 src = fmt.Sprintf("https://%s/d/%s", serverName, d.XID)
146 }
147 }
148 log.Printf("inline img with src: %s", src)
149 return fmt.Sprintf(`<img alt="%s" title="%s" src="%s>`, alt, alt, src)
150}
151
152func translate(honk *Honk) {
153 if honk.Format == "html" {
154 return
155 }
156 noise := honk.Noise
157 if strings.HasPrefix(noise, "DZ:") {
158 idx := strings.Index(noise, "\n")
159 if idx == -1 {
160 honk.Precis = noise
161 noise = ""
162 } else {
163 honk.Precis = noise[:idx]
164 noise = noise[idx+1:]
165 }
166 }
167 honk.Precis = strings.TrimSpace(honk.Precis)
168
169 noise = strings.TrimSpace(noise)
170 noise = quickrename(noise, honk.UserID)
171 noise = markitzero(noise)
172
173 honk.Noise = noise
174 honk.Onts = oneofakind(ontologies(honk.Noise))
175}
176
177func shortxid(xid string) string {
178 idx := strings.LastIndexByte(xid, '/')
179 if idx == -1 {
180 return xid
181 }
182 return xid[idx+1:]
183}
184
185func xfiltrate() string {
186 letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
187 var b [18]byte
188 rand.Read(b[:])
189 for i, c := range b {
190 b[i] = letters[c&63]
191 }
192 s := string(b[:])
193 return s
194}
195
196var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
197
198func ontologies(s string) []string {
199 m := re_hashes.FindAllString(s, -1)
200 j := 0
201 for _, h := range m {
202 if h[0] == '&' {
203 continue
204 }
205 if h[0] != '#' {
206 h = h[1:]
207 }
208 m[j] = h
209 j++
210 }
211 return m[:j]
212}
213
214type Mention struct {
215 who string
216 where string
217}
218
219var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
220var re_urltions = regexp.MustCompile(`@https://\S+`)
221
222func grapevine(s string) []string {
223 var mentions []string
224 m := re_mentions.FindAllString(s, -1)
225 for i := range m {
226 where := gofish(m[i])
227 if where != "" {
228 mentions = append(mentions, where)
229 }
230 }
231 m = re_urltions.FindAllString(s, -1)
232 for i := range m {
233 mentions = append(mentions, m[i][1:])
234 }
235 return mentions
236}
237
238func bunchofgrapes(s string) []Mention {
239 m := re_mentions.FindAllString(s, -1)
240 var mentions []Mention
241 for i := range m {
242 where := gofish(m[i])
243 if where != "" {
244 mentions = append(mentions, Mention{who: m[i], where: where})
245 }
246 }
247 m = re_urltions.FindAllString(s, -1)
248 for i := range m {
249 mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
250 }
251 return mentions
252}
253
254type Emu struct {
255 ID string
256 Name string
257}
258
259var re_link = regexp.MustCompile(`@?https?://[^\s"]+[\w/)]`)
260var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
261
262func herdofemus(noise string) []Emu {
263 m := re_emus.FindAllString(noise, -1)
264 m = oneofakind(m)
265 var emus []Emu
266 for _, e := range m {
267 fname := e[1 : len(e)-1]
268 _, err := os.Stat("emus/" + fname + ".png")
269 if err != nil {
270 continue
271 }
272 url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
273 emus = append(emus, Emu{ID: url, Name: e})
274 }
275 return emus
276}
277
278var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
279
280func memetize(honk *Honk) {
281 repl := func(x string) string {
282 name := x[5:]
283 if name[0] == ' ' {
284 name = name[1:]
285 }
286 fd, err := os.Open("memes/" + name)
287 if err != nil {
288 log.Printf("no meme for %s", name)
289 return x
290 }
291 var peek [512]byte
292 n, _ := fd.Read(peek[:])
293 ct := http.DetectContentType(peek[:n])
294 fd.Close()
295
296 url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
297 fileid, err := savefile("", name, name, url, ct, false, nil)
298 if err != nil {
299 log.Printf("error saving meme: %s", err)
300 return x
301 }
302 var d Donk
303 d.FileID = fileid
304 d.XID = ""
305 d.Name = name
306 d.Media = ct
307 d.URL = url
308 d.Local = false
309 honk.Donks = append(honk.Donks, &d)
310 return ""
311 }
312 honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
313}
314
315var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+ ")
316
317func quickrename(s string, userid int64) string {
318 return re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
319 prefix := ""
320 if m[0] == ' ' {
321 prefix = " "
322 m = m[1:]
323 }
324 prefix += "@"
325 m = m[1:]
326 m = m[:len(m)-1]
327
328 row := stmtOneHonker.QueryRow(m, userid)
329 var xid string
330 err := row.Scan(&xid)
331 if err == nil {
332 _, name := handles(xid)
333 if name != "" {
334 m = name
335 }
336 }
337 return prefix + m + " "
338 })
339}
340
341func mentionize(s string) string {
342 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
343 where := gofish(m)
344 if where == "" {
345 return m
346 }
347 who := m[0 : 1+strings.IndexByte(m[1:], '@')]
348 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
349 html.EscapeString(where), html.EscapeString(who))
350 })
351 s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
352 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
353 html.EscapeString(m[1:]), html.EscapeString(m))
354 })
355 return s
356}
357
358func ontologize(s string) string {
359 s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
360 if o[0] == '&' {
361 return o
362 }
363 p := ""
364 h := o
365 if h[0] != '#' {
366 p = h[:1]
367 h = h[1:]
368 }
369 return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
370 strings.ToLower(h[1:]), h)
371 })
372 return s
373}
374
375var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
376var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
377
378func originate(u string) string {
379 m := re_urlhost.FindStringSubmatch(u)
380 if len(m) > 1 {
381 return m[1]
382 }
383 return ""
384}
385
386var allhandles = make(map[string]string)
387var handlelock sync.Mutex
388
389// handle, handle@host
390func handles(xid string) (string, string) {
391 if xid == "" {
392 return "", ""
393 }
394 handlelock.Lock()
395 handle := allhandles[xid]
396 handlelock.Unlock()
397 if handle == "" {
398 handle = findhandle(xid)
399 handlelock.Lock()
400 allhandles[xid] = handle
401 handlelock.Unlock()
402 }
403 if handle == xid {
404 return xid, xid
405 }
406 return handle, handle + "@" + originate(xid)
407}
408
409func findhandle(xid string) string {
410 row := stmtGetXonker.QueryRow(xid, "handle")
411 var handle string
412 err := row.Scan(&handle)
413 if err != nil {
414 p, _ := investigate(xid)
415 if p == nil {
416 m := re_unurl.FindStringSubmatch(xid)
417 if len(m) > 2 {
418 handle = m[2]
419 } else {
420 handle = xid
421 }
422 } else {
423 handle = p.Handle
424 }
425 _, err = stmtSaveXonker.Exec(xid, handle, "handle")
426 if err != nil {
427 log.Printf("error saving handle: %s", err)
428 }
429 }
430 return handle
431}
432
433var handleprelock sync.Mutex
434
435func prehandle(xid string) {
436 handleprelock.Lock()
437 defer handleprelock.Unlock()
438 handles(xid)
439}
440
441func prepend(s string, x []string) []string {
442 return append([]string{s}, x...)
443}
444
445// pleroma leaks followers addressed posts to followers
446func butnottooloud(aud []string) {
447 for i, a := range aud {
448 if strings.HasSuffix(a, "/followers") {
449 aud[i] = ""
450 }
451 }
452}
453
454func keepitquiet(aud []string) bool {
455 for _, a := range aud {
456 if a == thewholeworld {
457 return false
458 }
459 }
460 return true
461}
462
463func firstclass(honk *Honk) bool {
464 return honk.Audience[0] == thewholeworld
465}
466
467func oneofakind(a []string) []string {
468 var x []string
469 for n, s := range a {
470 if s != "" {
471 x = append(x, s)
472 for i := n + 1; i < len(a); i++ {
473 if a[i] == s {
474 a[i] = ""
475 }
476 }
477 }
478 }
479 return x
480}
481
482var ziggies = make(map[string]*rsa.PrivateKey)
483var zaggies = make(map[string]*rsa.PublicKey)
484var ziggylock sync.Mutex
485
486func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
487 ziggylock.Lock()
488 key = ziggies[username]
489 ziggylock.Unlock()
490 if key == nil {
491 db := opendatabase()
492 row := db.QueryRow("select seckey from users where username = ?", username)
493 var data string
494 row.Scan(&data)
495 var err error
496 key, _, err = httpsig.DecodeKey(data)
497 if err != nil {
498 log.Printf("error decoding %s seckey: %s", username, err)
499 return
500 }
501 ziggylock.Lock()
502 ziggies[username] = key
503 ziggylock.Unlock()
504 }
505 keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
506 return
507}
508
509func zaggy(keyname string) (key *rsa.PublicKey) {
510 ziggylock.Lock()
511 key = zaggies[keyname]
512 ziggylock.Unlock()
513 if key != nil {
514 return
515 }
516 row := stmtGetXonker.QueryRow(keyname, "pubkey")
517 var data string
518 err := row.Scan(&data)
519 if err != nil {
520 log.Printf("hitting the webs for missing pubkey: %s", keyname)
521 j, err := GetJunk(keyname)
522 if err != nil {
523 log.Printf("error getting %s pubkey: %s", keyname, err)
524 return
525 }
526 keyobj, ok := j.GetMap("publicKey")
527 if ok {
528 j = keyobj
529 }
530 data, ok = j.GetString("publicKeyPem")
531 if !ok {
532 log.Printf("error finding %s pubkey", keyname)
533 return
534 }
535 _, ok = j.GetString("owner")
536 if !ok {
537 log.Printf("error finding %s pubkey owner", keyname)
538 return
539 }
540 _, key, err = httpsig.DecodeKey(data)
541 if err != nil {
542 log.Printf("error decoding %s pubkey: %s", keyname, err)
543 return
544 }
545 _, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
546 if err != nil {
547 log.Printf("error saving key: %s", err)
548 }
549 } else {
550 _, key, err = httpsig.DecodeKey(data)
551 if err != nil {
552 log.Printf("error decoding %s pubkey: %s", keyname, err)
553 return
554 }
555 }
556 ziggylock.Lock()
557 zaggies[keyname] = key
558 ziggylock.Unlock()
559 return
560}
561
562func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
563 _, err := stmtDeleteXonker.Exec(keyname, "pubkey")
564 if err != nil {
565 log.Printf("error deleting key: %s", err)
566 }
567 ziggylock.Lock()
568 delete(zaggies, keyname)
569 ziggylock.Unlock()
570 return httpsig.VerifyRequest(r, payload, zaggy)
571}
572
573func keymatch(keyname string, actor string) string {
574 hash := strings.IndexByte(keyname, '#')
575 if hash == -1 {
576 hash = len(keyname)
577 }
578 owner := keyname[0:hash]
579 if owner == actor {
580 return originate(actor)
581 }
582 return ""
583}