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/cache"
32 "humungus.tedunangst.com/r/webs/htfilter"
33 "humungus.tedunangst.com/r/webs/httpsig"
34 "humungus.tedunangst.com/r/webs/templates"
35)
36
37var allowedclasses = make(map[string]bool)
38
39func init() {
40 allowedclasses["kw"] = true
41 allowedclasses["bi"] = true
42 allowedclasses["st"] = true
43 allowedclasses["nm"] = true
44 allowedclasses["tp"] = true
45 allowedclasses["op"] = true
46 allowedclasses["cm"] = true
47 allowedclasses["al"] = true
48 allowedclasses["dl"] = true
49}
50
51func reverbolate(userid int64, honks []*Honk) {
52 for _, h := range honks {
53 h.What += "ed"
54 if h.What == "tonked" {
55 h.What = "honked back"
56 h.Style = "subtle"
57 }
58 if !h.Public {
59 h.Style += " limited"
60 }
61 translate(h, false)
62 if h.Whofore == 2 || h.Whofore == 3 {
63 h.URL = h.XID
64 if h.What != "bonked" {
65 h.Noise = re_memes.ReplaceAllString(h.Noise, "")
66 h.Noise = mentionize(h.Noise)
67 h.Noise = ontologize(h.Noise)
68 }
69 h.Username, h.Handle = handles(h.Honker)
70 } else {
71 _, h.Handle = handles(h.Honker)
72 short := shortname(userid, h.Honker)
73 if short != "" {
74 h.Username = short
75 } else {
76 h.Username = h.Handle
77 if len(h.Username) > 20 {
78 h.Username = h.Username[:20] + ".."
79 }
80 }
81 if h.URL == "" {
82 h.URL = h.XID
83 }
84 }
85 if h.Oonker != "" {
86 _, h.Oondle = handles(h.Oonker)
87 }
88 h.Precis = demoji(h.Precis)
89 h.Noise = demoji(h.Noise)
90 h.Open = "open"
91
92 zap := make(map[string]bool)
93 {
94 var htf htfilter.Filter
95 htf.Imager = replaceimgsand(zap, false)
96 htf.SpanClasses = allowedclasses
97 p, _ := htf.String(h.Precis)
98 n, _ := htf.String(h.Noise)
99 h.Precis = string(p)
100 h.Noise = string(n)
101 }
102
103 if userid == -1 {
104 if h.Precis != "" {
105 h.Open = ""
106 }
107 } else {
108 unsee(userid, h)
109 if h.Open == "open" && h.Precis == "unspecified horror" {
110 h.Precis = ""
111 }
112 }
113 if len(h.Noise) > 6000 && h.Open == "open" {
114 if h.Precis == "" {
115 h.Precis = "really freaking long"
116 }
117 h.Open = ""
118 }
119
120 emuxifier := func(e string) string {
121 for _, d := range h.Donks {
122 if d.Name == e {
123 zap[d.XID] = true
124 if d.Local {
125 return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
126 }
127 }
128 }
129 return e
130 }
131 h.Precis = re_emus.ReplaceAllStringFunc(h.Precis, emuxifier)
132 h.Noise = re_emus.ReplaceAllStringFunc(h.Noise, emuxifier)
133
134 j := 0
135 for i := 0; i < len(h.Donks); i++ {
136 if !zap[h.Donks[i].XID] {
137 h.Donks[j] = h.Donks[i]
138 j++
139 }
140 }
141 h.Donks = h.Donks[:j]
142
143 h.HTPrecis = template.HTML(h.Precis)
144 h.HTML = template.HTML(h.Noise)
145 }
146}
147
148func replaceimgsand(zap map[string]bool, absolute bool) func(node *html.Node) string {
149 return func(node *html.Node) string {
150 src := htfilter.GetAttr(node, "src")
151 alt := htfilter.GetAttr(node, "alt")
152 //title := GetAttr(node, "title")
153 if htfilter.HasClass(node, "Emoji") && alt != "" {
154 return alt
155 }
156 d := finddonk(src)
157 if d != nil {
158 zap[d.XID] = true
159 base := ""
160 if absolute {
161 base = "https://" + serverName
162 }
163 return string(templates.Sprintf(`<img alt="%s" title="%s" src="%s/d/%s">`, alt, alt, base, d.XID))
164 }
165 return string(templates.Sprintf(`<img alt="%s" src="<a href="%s">%s<a>">`, alt, src, src))
166 }
167}
168
169func inlineimgsfor(honk *Honk) func(node *html.Node) string {
170 return func(node *html.Node) string {
171 src := htfilter.GetAttr(node, "src")
172 alt := htfilter.GetAttr(node, "alt")
173 d := savedonk(src, "image", alt, "image", true)
174 if d != nil {
175 honk.Donks = append(honk.Donks, d)
176 }
177 log.Printf("inline img with src: %s", src)
178 return ""
179 }
180}
181
182func imaginate(honk *Honk) {
183 var htf htfilter.Filter
184 htf.Imager = inlineimgsfor(honk)
185 htf.String(honk.Noise)
186}
187
188func translate(honk *Honk, redoimages bool) {
189 if honk.Format == "html" {
190 return
191 }
192 noise := honk.Noise
193 if strings.HasPrefix(noise, "DZ:") {
194 idx := strings.Index(noise, "\n")
195 if idx == -1 {
196 honk.Precis = noise
197 noise = ""
198 } else {
199 honk.Precis = noise[:idx]
200 noise = noise[idx+1:]
201 }
202 }
203 honk.Precis = strings.TrimSpace(honk.Precis)
204
205 noise = strings.TrimSpace(noise)
206 noise = quickrename(noise, honk.UserID)
207 noise = markitzero(noise)
208 honk.Noise = noise
209 honk.Onts = oneofakind(ontologies(honk.Noise))
210
211 if redoimages {
212 zap := make(map[string]bool)
213 {
214 var htf htfilter.Filter
215 htf.Imager = replaceimgsand(zap, true)
216 htf.SpanClasses = allowedclasses
217 p, _ := htf.String(honk.Precis)
218 n, _ := htf.String(honk.Noise)
219 honk.Precis = string(p)
220 honk.Noise = string(n)
221 }
222 j := 0
223 for i := 0; i < len(honk.Donks); i++ {
224 if !zap[honk.Donks[i].XID] {
225 honk.Donks[j] = honk.Donks[i]
226 j++
227 }
228 }
229 honk.Donks = honk.Donks[:j]
230 }
231}
232
233func shortxid(xid string) string {
234 idx := strings.LastIndexByte(xid, '/')
235 if idx == -1 {
236 return xid
237 }
238 return xid[idx+1:]
239}
240
241func xfiltrate() string {
242 letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
243 var b [18]byte
244 rand.Read(b[:])
245 for i, c := range b {
246 b[i] = letters[c&63]
247 }
248 s := string(b[:])
249 return s
250}
251
252var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
253
254func ontologies(s string) []string {
255 m := re_hashes.FindAllString(s, -1)
256 j := 0
257 for _, h := range m {
258 if h[0] == '&' {
259 continue
260 }
261 if h[0] != '#' {
262 h = h[1:]
263 }
264 m[j] = h
265 j++
266 }
267 return m[:j]
268}
269
270type Mention struct {
271 who string
272 where string
273}
274
275var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
276var re_urltions = regexp.MustCompile(`@https://\S+`)
277
278func grapevine(s string) []string {
279 var mentions []string
280 m := re_mentions.FindAllString(s, -1)
281 for i := range m {
282 where := gofish(m[i])
283 if where != "" {
284 mentions = append(mentions, where)
285 }
286 }
287 m = re_urltions.FindAllString(s, -1)
288 for i := range m {
289 mentions = append(mentions, m[i][1:])
290 }
291 return mentions
292}
293
294func bunchofgrapes(s string) []Mention {
295 m := re_mentions.FindAllString(s, -1)
296 var mentions []Mention
297 for i := range m {
298 where := gofish(m[i])
299 if where != "" {
300 mentions = append(mentions, Mention{who: m[i], where: where})
301 }
302 }
303 m = re_urltions.FindAllString(s, -1)
304 for i := range m {
305 mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
306 }
307 return mentions
308}
309
310type Emu struct {
311 ID string
312 Name string
313}
314
315var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
316
317func herdofemus(noise string) []Emu {
318 m := re_emus.FindAllString(noise, -1)
319 m = oneofakind(m)
320 var emus []Emu
321 for _, e := range m {
322 fname := e[1 : len(e)-1]
323 _, err := os.Stat("emus/" + fname + ".png")
324 if err != nil {
325 continue
326 }
327 url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
328 emus = append(emus, Emu{ID: url, Name: e})
329 }
330 return emus
331}
332
333var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
334
335func memetize(honk *Honk) {
336 repl := func(x string) string {
337 name := x[5:]
338 if name[0] == ' ' {
339 name = name[1:]
340 }
341 fd, err := os.Open("memes/" + name)
342 if err != nil {
343 log.Printf("no meme for %s", name)
344 return x
345 }
346 var peek [512]byte
347 n, _ := fd.Read(peek[:])
348 ct := http.DetectContentType(peek[:n])
349 fd.Close()
350
351 url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
352 fileid, err := savefile("", name, name, url, ct, false, nil)
353 if err != nil {
354 log.Printf("error saving meme: %s", err)
355 return x
356 }
357 d := &Donk{
358 FileID: fileid,
359 XID: "",
360 Name: name,
361 Media: ct,
362 URL: url,
363 Local: false,
364 }
365 honk.Donks = append(honk.Donks, d)
366 return ""
367 }
368 honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
369}
370
371var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+( |$)")
372
373func quickrename(s string, userid int64) string {
374 nonstop := true
375 for nonstop {
376 nonstop = false
377 s = re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
378 prefix := ""
379 if m[0] == ' ' {
380 prefix = " "
381 m = m[1:]
382 }
383 prefix += "@"
384 m = m[1:]
385 if m[len(m)-1] == ' ' {
386 m = m[:len(m)-1]
387 }
388
389 xid := fullname(m, userid)
390
391 if xid != "" {
392 _, name := handles(xid)
393 if name != "" {
394 nonstop = true
395 m = name
396 }
397 }
398 return prefix + m + " "
399 })
400 }
401 return s
402}
403
404var shortnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
405 honkers := gethonkers(userid)
406 m := make(map[string]string)
407 for _, h := range honkers {
408 m[h.XID] = h.Name
409 }
410 return m, true
411}, Invalidator: &honkerinvalidator})
412
413func shortname(userid int64, xid string) string {
414 var m map[string]string
415 ok := shortnames.Get(userid, &m)
416 if ok {
417 return m[xid]
418 }
419 return ""
420}
421
422var fullnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
423 honkers := gethonkers(userid)
424 m := make(map[string]string)
425 for _, h := range honkers {
426 m[h.Name] = h.XID
427 }
428 return m, true
429}, Invalidator: &honkerinvalidator})
430
431func fullname(name string, userid int64) string {
432 var m map[string]string
433 ok := fullnames.Get(userid, &m)
434 if ok {
435 return m[name]
436 }
437 return ""
438}
439
440func mentionize(s string) string {
441 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
442 where := gofish(m)
443 if where == "" {
444 return m
445 }
446 who := m[0 : 1+strings.IndexByte(m[1:], '@')]
447 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
448 html.EscapeString(where), html.EscapeString(who))
449 })
450 s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
451 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
452 html.EscapeString(m[1:]), html.EscapeString(m))
453 })
454 return s
455}
456
457func ontologize(s string) string {
458 s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
459 if o[0] == '&' {
460 return o
461 }
462 p := ""
463 h := o
464 if h[0] != '#' {
465 p = h[:1]
466 h = h[1:]
467 }
468 return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
469 strings.ToLower(h[1:]), h)
470 })
471 return s
472}
473
474var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
475var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
476
477func originate(u string) string {
478 m := re_urlhost.FindStringSubmatch(u)
479 if len(m) > 1 {
480 return m[1]
481 }
482 return ""
483}
484
485var allhandles = make(map[string]string)
486var handlelock sync.Mutex
487
488// handle, handle@host
489func handles(xid string) (string, string) {
490 if xid == "" {
491 return "", ""
492 }
493 handlelock.Lock()
494 handle := allhandles[xid]
495 handlelock.Unlock()
496 if handle == "" {
497 handle = findhandle(xid)
498 handlelock.Lock()
499 allhandles[xid] = handle
500 handlelock.Unlock()
501 }
502 if handle == xid {
503 return xid, xid
504 }
505 return handle, handle + "@" + originate(xid)
506}
507
508func findhandle(xid string) string {
509 row := stmtGetXonker.QueryRow(xid, "handle")
510 var handle string
511 err := row.Scan(&handle)
512 if err != nil {
513 p, _ := investigate(xid)
514 if p == nil {
515 m := re_unurl.FindStringSubmatch(xid)
516 if len(m) > 2 {
517 handle = m[2]
518 } else {
519 handle = xid
520 }
521 } else {
522 handle = p.Handle
523 }
524 _, err = stmtSaveXonker.Exec(xid, handle, "handle")
525 if err != nil {
526 log.Printf("error saving handle: %s", err)
527 }
528 }
529 return handle
530}
531
532var handleprelock sync.Mutex
533
534func prehandle(xid string) {
535 handleprelock.Lock()
536 defer handleprelock.Unlock()
537 handles(xid)
538}
539
540func prepend(s string, x []string) []string {
541 return append([]string{s}, x...)
542}
543
544// pleroma leaks followers addressed posts to followers
545func butnottooloud(aud []string) {
546 for i, a := range aud {
547 if strings.HasSuffix(a, "/followers") {
548 aud[i] = ""
549 }
550 }
551}
552
553func keepitquiet(aud []string) bool {
554 for _, a := range aud {
555 if a == thewholeworld {
556 return false
557 }
558 }
559 return true
560}
561
562func firstclass(honk *Honk) bool {
563 return honk.Audience[0] == thewholeworld
564}
565
566func oneofakind(a []string) []string {
567 seen := make(map[string]bool)
568 seen[""] = true
569 j := 0
570 for _, s := range a {
571 if !seen[s] {
572 seen[s] = true
573 a[j] = s
574 j++
575 }
576 }
577 return a[:j]
578}
579
580var ziggies = make(map[string]*rsa.PrivateKey)
581var zaggies = make(map[string]*rsa.PublicKey)
582var ziggylock sync.Mutex
583
584func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
585 ziggylock.Lock()
586 key = ziggies[username]
587 ziggylock.Unlock()
588 if key == nil {
589 db := opendatabase()
590 row := db.QueryRow("select seckey from users where username = ?", username)
591 var data string
592 row.Scan(&data)
593 var err error
594 key, _, err = httpsig.DecodeKey(data)
595 if err != nil {
596 log.Printf("error decoding %s seckey: %s", username, err)
597 return
598 }
599 ziggylock.Lock()
600 ziggies[username] = key
601 ziggylock.Unlock()
602 }
603 keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
604 return
605}
606
607func zaggy(keyname string) (key *rsa.PublicKey) {
608 ziggylock.Lock()
609 key = zaggies[keyname]
610 ziggylock.Unlock()
611 if key != nil {
612 return
613 }
614 row := stmtGetXonker.QueryRow(keyname, "pubkey")
615 var data string
616 err := row.Scan(&data)
617 if err != nil {
618 log.Printf("hitting the webs for missing pubkey: %s", keyname)
619 j, err := GetJunk(keyname)
620 if err != nil {
621 log.Printf("error getting %s pubkey: %s", keyname, err)
622 return
623 }
624 keyobj, ok := j.GetMap("publicKey")
625 if ok {
626 j = keyobj
627 }
628 data, ok = j.GetString("publicKeyPem")
629 if !ok {
630 log.Printf("error finding %s pubkey", keyname)
631 return
632 }
633 _, ok = j.GetString("owner")
634 if !ok {
635 log.Printf("error finding %s pubkey owner", keyname)
636 return
637 }
638 _, key, err = httpsig.DecodeKey(data)
639 if err != nil {
640 log.Printf("error decoding %s pubkey: %s", keyname, err)
641 return
642 }
643 _, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
644 if err != nil {
645 log.Printf("error saving key: %s", err)
646 }
647 } else {
648 _, key, err = httpsig.DecodeKey(data)
649 if err != nil {
650 log.Printf("error decoding %s pubkey: %s", keyname, err)
651 return
652 }
653 }
654 ziggylock.Lock()
655 zaggies[keyname] = key
656 ziggylock.Unlock()
657 return
658}
659
660func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
661 _, err := stmtDeleteXonker.Exec(keyname, "pubkey")
662 if err != nil {
663 log.Printf("error deleting key: %s", err)
664 }
665 ziggylock.Lock()
666 delete(zaggies, keyname)
667 ziggylock.Unlock()
668 return httpsig.VerifyRequest(r, payload, zaggy)
669}
670
671func keymatch(keyname string, actor string) string {
672 hash := strings.IndexByte(keyname, '#')
673 if hash == -1 {
674 hash = len(keyname)
675 }
676 owner := keyname[0:hash]
677 if owner == actor {
678 return originate(actor)
679 }
680 return ""
681}