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