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