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"
23 "html/template"
24 "log"
25 "net/http"
26 "os"
27 "regexp"
28 "strings"
29 "sync"
30
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 zilences := getzilences(userid)
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 = unpucker(h.Precis)
71 h.Noise = unpucker(h.Noise)
72 h.Open = "open"
73 if badword := unsee(zilences, h.Precis, h.Noise); badword != "" {
74 if h.Precis == "" {
75 h.Precis = badword
76 }
77 h.Open = ""
78 } else if h.Precis == "unspecified horror" {
79 h.Precis = ""
80 }
81 h.HTPrecis, _ = filt.String(h.Precis)
82 h.HTML, _ = filt.String(h.Noise)
83 emuxifier := func(e string) string {
84 for _, d := range h.Donks {
85 if d.Name == e {
86 zap[d] = true
87 if d.Local {
88 return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
89 }
90 }
91 }
92 return e
93 }
94 h.HTPrecis = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTPrecis), emuxifier))
95 h.HTML = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTML), emuxifier))
96 j := 0
97 for i := 0; i < len(h.Donks); i++ {
98 if !zap[h.Donks[i]] {
99 h.Donks[j] = h.Donks[i]
100 j++
101 }
102 }
103 h.Donks = h.Donks[:j]
104 }
105}
106
107func translate(honk *Honk) {
108 if honk.Format == "html" {
109 return
110 }
111 noise := honk.Noise
112 if strings.HasPrefix(noise, "DZ:") {
113 idx := strings.Index(noise, "\n")
114 if idx == -1 {
115 honk.Precis = noise
116 noise = ""
117 } else {
118 honk.Precis = noise[:idx]
119 noise = noise[idx+1:]
120 }
121 }
122 honk.Precis = strings.TrimSpace(honk.Precis)
123
124 noise = strings.TrimSpace(noise)
125 noise = quickrename(noise, honk.UserID)
126 noise = obfusbreak(noise)
127
128 honk.Noise = noise
129 honk.Onts = oneofakind(ontologies(honk.Noise))
130}
131
132func unsee(zilences []*regexp.Regexp, precis string, noise string) string {
133 for _, z := range zilences {
134 if z.MatchString(precis) || z.MatchString(noise) {
135 if precis == "" {
136 w := z.String()
137 return w[6 : len(w)-3]
138 }
139 return precis
140 }
141 }
142 return ""
143}
144
145func osmosis(honks []*Honk, userid int64) []*Honk {
146 zords := getzords(userid)
147 j := 0
148outer:
149 for _, h := range honks {
150 for _, z := range zords {
151 if z.MatchString(h.Precis) || z.MatchString(h.Noise) {
152 continue outer
153 }
154 }
155 honks[j] = h
156 j++
157 }
158 honks = honks[0:j]
159 return honks
160}
161
162func shortxid(xid string) string {
163 idx := strings.LastIndexByte(xid, '/')
164 if idx == -1 {
165 return xid
166 }
167 return xid[idx+1:]
168}
169
170func xfiltrate() string {
171 letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
172 var b [18]byte
173 rand.Read(b[:])
174 for i, c := range b {
175 b[i] = letters[c&63]
176 }
177 s := string(b[:])
178 return s
179}
180
181var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
182
183func ontologies(s string) []string {
184 m := re_hashes.FindAllString(s, -1)
185 j := 0
186 for _, h := range m {
187 if h[0] == '&' {
188 continue
189 }
190 if h[0] != '#' {
191 h = h[1:]
192 }
193 m[j] = h
194 j++
195 }
196 return m[:j]
197}
198
199type Mention struct {
200 who string
201 where string
202}
203
204var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
205var re_urltions = regexp.MustCompile(`@https://\S+`)
206
207func grapevine(s string) []string {
208 var mentions []string
209 m := re_mentions.FindAllString(s, -1)
210 for i := range m {
211 where := gofish(m[i])
212 if where != "" {
213 mentions = append(mentions, where)
214 }
215 }
216 m = re_urltions.FindAllString(s, -1)
217 for i := range m {
218 mentions = append(mentions, m[i][1:])
219 }
220 return mentions
221}
222
223func bunchofgrapes(s string) []Mention {
224 m := re_mentions.FindAllString(s, -1)
225 var mentions []Mention
226 for i := range m {
227 where := gofish(m[i])
228 if where != "" {
229 mentions = append(mentions, Mention{who: m[i], where: where})
230 }
231 }
232 m = re_urltions.FindAllString(s, -1)
233 for i := range m {
234 mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
235 }
236 return mentions
237}
238
239type Emu struct {
240 ID string
241 Name string
242}
243
244var re_link = regexp.MustCompile(`@?https?://[^\s"]+[\w/)]`)
245var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
246
247func herdofemus(noise string) []Emu {
248 m := re_emus.FindAllString(noise, -1)
249 m = oneofakind(m)
250 var emus []Emu
251 for _, e := range m {
252 fname := e[1 : len(e)-1]
253 _, err := os.Stat("emus/" + fname + ".png")
254 if err != nil {
255 continue
256 }
257 url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
258 emus = append(emus, Emu{ID: url, Name: e})
259 }
260 return emus
261}
262
263var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
264
265func memetize(honk *Honk) {
266 repl := func(x string) string {
267 name := x[5:]
268 if name[0] == ' ' {
269 name = name[1:]
270 }
271 fd, err := os.Open("memes/" + name)
272 if err != nil {
273 log.Printf("no meme for %s", name)
274 return x
275 }
276 var peek [512]byte
277 n, _ := fd.Read(peek[:])
278 ct := http.DetectContentType(peek[:n])
279 fd.Close()
280
281 url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
282 res, err := stmtSaveFile.Exec("", name, name, url, ct, 0, "")
283 if err != nil {
284 log.Printf("error saving meme: %s", err)
285 return x
286 }
287 var d Donk
288 d.FileID, _ = res.LastInsertId()
289 d.XID = ""
290 d.Name = name
291 d.Media = ct
292 d.URL = url
293 d.Local = false
294 honk.Donks = append(honk.Donks, &d)
295 return ""
296 }
297 honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
298}
299
300var re_bolder = regexp.MustCompile(`(^|\W)\*\*([\w\s,.!?'-]+)\*\*($|\W)`)
301var re_italicer = regexp.MustCompile(`(^|\W)\*([\w\s,.!?'-]+)\*($|\W)`)
302var re_bigcoder = regexp.MustCompile("```\n?((?s:.*?))\n?```\n?")
303var re_coder = regexp.MustCompile("`([^`]*)`")
304var re_quoter = regexp.MustCompile(`(?m:^> (.*)\n?)`)
305
306func markitzero(s string) string {
307 var bigcodes []string
308 bigsaver := func(code string) string {
309 bigcodes = append(bigcodes, code)
310 return "``````"
311 }
312 s = re_bigcoder.ReplaceAllStringFunc(s, bigsaver)
313 var lilcodes []string
314 lilsaver := func(code string) string {
315 lilcodes = append(lilcodes, code)
316 return "`x`"
317 }
318 s = re_coder.ReplaceAllStringFunc(s, lilsaver)
319 s = re_bolder.ReplaceAllString(s, "$1<b>$2</b>$3")
320 s = re_italicer.ReplaceAllString(s, "$1<i>$2</i>$3")
321 s = re_quoter.ReplaceAllString(s, "<blockquote>$1</blockquote><p>")
322 lilun := func(s string) string {
323 code := lilcodes[0]
324 lilcodes = lilcodes[1:]
325 return code
326 }
327 s = re_coder.ReplaceAllStringFunc(s, lilun)
328 bigun := func(s string) string {
329 code := bigcodes[0]
330 bigcodes = bigcodes[1:]
331 return code
332 }
333 s = re_bigcoder.ReplaceAllStringFunc(s, bigun)
334 s = re_bigcoder.ReplaceAllString(s, "<pre><code>$1</code></pre><p>")
335 s = re_coder.ReplaceAllString(s, "<code>$1</code>")
336 return s
337}
338
339func obfusbreak(s string) string {
340 s = strings.TrimSpace(s)
341 s = strings.Replace(s, "\r", "", -1)
342 s = html.EscapeString(s)
343 // dammit go
344 s = strings.Replace(s, "'", "'", -1)
345 linkfn := func(url string) string {
346 if url[0] == '@' {
347 return url
348 }
349 addparen := false
350 adddot := false
351 if strings.HasSuffix(url, ")") && strings.IndexByte(url, '(') == -1 {
352 url = url[:len(url)-1]
353 addparen = true
354 }
355 if strings.HasSuffix(url, ".") {
356 url = url[:len(url)-1]
357 adddot = true
358 }
359 url = fmt.Sprintf(`<a class="mention u-url" href="%s">%s</a>`, url, url)
360 if adddot {
361 url += "."
362 }
363 if addparen {
364 url += ")"
365 }
366 return url
367 }
368 s = re_link.ReplaceAllStringFunc(s, linkfn)
369
370 s = markitzero(s)
371
372 s = strings.Replace(s, "\n", "<br>", -1)
373 return s
374}
375
376var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+ ")
377
378func quickrename(s string, userid int64) string {
379 return re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
380 prefix := ""
381 if m[0] == ' ' {
382 prefix = " "
383 m = m[1:]
384 }
385 prefix += "@"
386 m = m[1:]
387 m = m[:len(m)-1]
388
389 row := stmtOneHonker.QueryRow(m, userid)
390 var xid string
391 err := row.Scan(&xid)
392 if err == nil {
393 _, name := handles(xid)
394 if name != "" {
395 m = name
396 }
397 }
398 return prefix + m + " "
399 })
400}
401
402func mentionize(s string) string {
403 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
404 where := gofish(m)
405 if where == "" {
406 return m
407 }
408 who := m[0 : 1+strings.IndexByte(m[1:], '@')]
409 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
410 html.EscapeString(where), html.EscapeString(who))
411 })
412 s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
413 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
414 html.EscapeString(m[1:]), html.EscapeString(m))
415 })
416 return s
417}
418
419func ontologize(s string) string {
420 s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
421 if o[0] == '&' {
422 return o
423 }
424 p := ""
425 h := o
426 if h[0] != '#' {
427 p = h[:1]
428 h = h[1:]
429 }
430 return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
431 strings.ToLower(h[1:]), h)
432 })
433 return s
434}
435
436var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
437var re_urlhost = regexp.MustCompile("https://([^/]+)")
438
439func originate(u string) string {
440 m := re_urlhost.FindStringSubmatch(u)
441 if len(m) > 1 {
442 return m[1]
443 }
444 return ""
445}
446
447var allhandles = make(map[string]string)
448var handlelock sync.Mutex
449
450// handle, handle@host
451func handles(xid string) (string, string) {
452 if xid == "" {
453 return "", ""
454 }
455 handlelock.Lock()
456 handle := allhandles[xid]
457 handlelock.Unlock()
458 if handle == "" {
459 handle = findhandle(xid)
460 handlelock.Lock()
461 allhandles[xid] = handle
462 handlelock.Unlock()
463 }
464 if handle == xid {
465 return xid, xid
466 }
467 return handle, handle + "@" + originate(xid)
468}
469
470func findhandle(xid string) string {
471 row := stmtGetXonker.QueryRow(xid, "handle")
472 var handle string
473 err := row.Scan(&handle)
474 if err != nil {
475 p, _ := investigate(xid)
476 if p == nil {
477 m := re_unurl.FindStringSubmatch(xid)
478 if len(m) > 2 {
479 handle = m[2]
480 } else {
481 handle = xid
482 }
483 } else {
484 handle = p.Handle
485 }
486 _, err = stmtSaveXonker.Exec(xid, handle, "handle")
487 if err != nil {
488 log.Printf("error saving handle: %s", err)
489 }
490 }
491 return handle
492}
493
494var handleprelock sync.Mutex
495
496func prehandle(xid string) {
497 handleprelock.Lock()
498 defer handleprelock.Unlock()
499 handles(xid)
500}
501
502func prepend(s string, x []string) []string {
503 return append([]string{s}, x...)
504}
505
506// pleroma leaks followers addressed posts to followers
507func butnottooloud(aud []string) {
508 for i, a := range aud {
509 if strings.HasSuffix(a, "/followers") {
510 aud[i] = ""
511 }
512 }
513}
514
515func keepitquiet(aud []string) bool {
516 for _, a := range aud {
517 if a == thewholeworld {
518 return false
519 }
520 }
521 return true
522}
523
524func firstclass(honk *Honk) bool {
525 return honk.Audience[0] == thewholeworld
526}
527
528func oneofakind(a []string) []string {
529 var x []string
530 for n, s := range a {
531 if s != "" {
532 x = append(x, s)
533 for i := n + 1; i < len(a); i++ {
534 if a[i] == s {
535 a[i] = ""
536 }
537 }
538 }
539 }
540 return x
541}
542
543var ziggies = make(map[string]*rsa.PrivateKey)
544var zaggies = make(map[string]*rsa.PublicKey)
545var ziggylock sync.Mutex
546
547func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
548 ziggylock.Lock()
549 key = ziggies[username]
550 ziggylock.Unlock()
551 if key == nil {
552 db := opendatabase()
553 row := db.QueryRow("select seckey from users where username = ?", username)
554 var data string
555 row.Scan(&data)
556 var err error
557 key, _, err = httpsig.DecodeKey(data)
558 if err != nil {
559 log.Printf("error decoding %s seckey: %s", username, err)
560 return
561 }
562 ziggylock.Lock()
563 ziggies[username] = key
564 ziggylock.Unlock()
565 }
566 keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
567 return
568}
569
570func zaggy(keyname string) (key *rsa.PublicKey) {
571 ziggylock.Lock()
572 key = zaggies[keyname]
573 ziggylock.Unlock()
574 if key != nil {
575 return
576 }
577 row := stmtGetXonker.QueryRow(keyname, "pubkey")
578 var data string
579 err := row.Scan(&data)
580 if err != nil {
581 log.Printf("hitting the webs for missing pubkey: %s", keyname)
582 j, err := GetJunk(keyname)
583 if err != nil {
584 log.Printf("error getting %s pubkey: %s", keyname, err)
585 return
586 }
587 keyobj, ok := j.GetMap("publicKey")
588 if ok {
589 j = keyobj
590 }
591 data, ok = j.GetString("publicKeyPem")
592 if !ok {
593 log.Printf("error finding %s pubkey", keyname)
594 return
595 }
596 _, ok = j.GetString("owner")
597 if !ok {
598 log.Printf("error finding %s pubkey owner", keyname)
599 return
600 }
601 _, key, err = httpsig.DecodeKey(data)
602 if err != nil {
603 log.Printf("error decoding %s pubkey: %s", keyname, err)
604 return
605 }
606 _, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
607 if err != nil {
608 log.Printf("error saving key: %s", err)
609 }
610 } else {
611 _, key, err = httpsig.DecodeKey(data)
612 if err != nil {
613 log.Printf("error decoding %s pubkey: %s", keyname, err)
614 return
615 }
616 }
617 ziggylock.Lock()
618 zaggies[keyname] = key
619 ziggylock.Unlock()
620 return
621}
622
623func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
624 _, err := stmtDeleteXonker.Exec(keyname, "pubkey")
625 if err != nil {
626 log.Printf("error deleting key: %s", err)
627 }
628 ziggylock.Lock()
629 delete(zaggies, keyname)
630 ziggylock.Unlock()
631 return httpsig.VerifyRequest(r, payload, zaggy)
632}
633
634var thumbbiters map[int64]map[string]bool
635var zordses map[int64][]*regexp.Regexp
636var zilences map[int64][]*regexp.Regexp
637var thumblock sync.Mutex
638
639func bitethethumbs() {
640 rows, err := stmtThumbBiters.Query()
641 if err != nil {
642 log.Printf("error getting thumbbiters: %s", err)
643 return
644 }
645 defer rows.Close()
646
647 thumblock.Lock()
648 defer thumblock.Unlock()
649 thumbbiters = make(map[int64]map[string]bool)
650 zordses = make(map[int64][]*regexp.Regexp)
651 zilences = make(map[int64][]*regexp.Regexp)
652 for rows.Next() {
653 var userid int64
654 var name, wherefore string
655 err = rows.Scan(&userid, &name, &wherefore)
656 if err != nil {
657 log.Printf("error scanning zonker: %s", err)
658 continue
659 }
660 if wherefore == "zord" || wherefore == "zilence" {
661 zord := "\\b(?i:" + name + ")\\b"
662 re, err := regexp.Compile(zord)
663 if err != nil {
664 log.Printf("error compiling zord: %s", err)
665 } else {
666 if wherefore == "zord" {
667 zordses[userid] = append(zordses[userid], re)
668 } else {
669 zilences[userid] = append(zilences[userid], re)
670 }
671 }
672 continue
673 }
674 m := thumbbiters[userid]
675 if m == nil {
676 m = make(map[string]bool)
677 thumbbiters[userid] = m
678 }
679 m[name] = true
680 }
681}
682
683func getzords(userid int64) []*regexp.Regexp {
684 thumblock.Lock()
685 defer thumblock.Unlock()
686 return zordses[userid]
687}
688
689func getzilences(userid int64) []*regexp.Regexp {
690 thumblock.Lock()
691 defer thumblock.Unlock()
692 return zilences[userid]
693}
694
695func thoudostbitethythumb(userid int64, who []string, objid string) bool {
696 thumblock.Lock()
697 biters := thumbbiters[userid]
698 thumblock.Unlock()
699 objwhere := originate(objid)
700 if objwhere != "" && biters[objwhere] {
701 log.Printf("thumbbiter: %s", objid)
702 return true
703 }
704 for _, w := range who {
705 if biters[w] {
706 log.Printf("thumbbiter: %s", w)
707 return true
708 }
709 where := originate(w)
710 if where != "" {
711 if biters[where] {
712 log.Printf("thumbbiter: %s", w)
713 return true
714 }
715 }
716 }
717 return false
718}
719
720func keymatch(keyname string, actor string) string {
721 hash := strings.IndexByte(keyname, '#')
722 if hash == -1 {
723 hash = len(keyname)
724 }
725 owner := keyname[0:hash]
726 if owner == actor {
727 return originate(actor)
728 }
729 return ""
730}