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 filt := htfilter.New()
95 filt.Imager = replaceimgsand(zap, false)
96 filt.SpanClasses = allowedclasses
97 p, _ := filt.String(h.Precis)
98 n, _ := filt.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 if !strings.HasPrefix(src, "https://"+serverName+"/") {
174 d := savedonk(src, "image", alt, "image", true)
175 if d != nil {
176 honk.Donks = append(honk.Donks, d)
177 }
178 }
179 log.Printf("inline img with src: %s", src)
180 return ""
181 }
182}
183
184func imaginate(honk *Honk) {
185 imgfilt := htfilter.New()
186 imgfilt.Imager = inlineimgsfor(honk)
187 imgfilt.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 filt := htfilter.New()
217 filt.Imager = replaceimgsand(zap, true)
218 filt.SpanClasses = allowedclasses
219 p, _ := filt.String(honk.Precis)
220 n, _ := filt.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 shortxid(xid string) string {
236 idx := strings.LastIndexByte(xid, '/')
237 if idx == -1 {
238 return xid
239 }
240 return xid[idx+1:]
241}
242
243func xfiltrate() string {
244 letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
245 var b [18]byte
246 rand.Read(b[:])
247 for i, c := range b {
248 b[i] = letters[c&63]
249 }
250 s := string(b[:])
251 return s
252}
253
254var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
255
256func ontologies(s string) []string {
257 m := re_hashes.FindAllString(s, -1)
258 j := 0
259 for _, h := range m {
260 if h[0] == '&' {
261 continue
262 }
263 if h[0] != '#' {
264 h = h[1:]
265 }
266 m[j] = h
267 j++
268 }
269 return m[:j]
270}
271
272type Mention struct {
273 who string
274 where string
275}
276
277var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
278var re_urltions = regexp.MustCompile(`@https://\S+`)
279
280func grapevine(s string) []string {
281 var mentions []string
282 m := re_mentions.FindAllString(s, -1)
283 for i := range m {
284 where := gofish(m[i])
285 if where != "" {
286 mentions = append(mentions, where)
287 }
288 }
289 m = re_urltions.FindAllString(s, -1)
290 for i := range m {
291 mentions = append(mentions, m[i][1:])
292 }
293 return mentions
294}
295
296func bunchofgrapes(s string) []Mention {
297 m := re_mentions.FindAllString(s, -1)
298 var mentions []Mention
299 for i := range m {
300 where := gofish(m[i])
301 if where != "" {
302 mentions = append(mentions, Mention{who: m[i], where: where})
303 }
304 }
305 m = re_urltions.FindAllString(s, -1)
306 for i := range m {
307 mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
308 }
309 return mentions
310}
311
312type Emu struct {
313 ID string
314 Name string
315}
316
317var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
318
319func herdofemus(noise string) []Emu {
320 m := re_emus.FindAllString(noise, -1)
321 m = oneofakind(m)
322 var emus []Emu
323 for _, e := range m {
324 fname := e[1 : len(e)-1]
325 _, err := os.Stat("emus/" + fname + ".png")
326 if err != nil {
327 continue
328 }
329 url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
330 emus = append(emus, Emu{ID: url, Name: e})
331 }
332 return emus
333}
334
335var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
336
337func memetize(honk *Honk) {
338 repl := func(x string) string {
339 name := x[5:]
340 if name[0] == ' ' {
341 name = name[1:]
342 }
343 fd, err := os.Open("memes/" + name)
344 if err != nil {
345 log.Printf("no meme for %s", name)
346 return x
347 }
348 var peek [512]byte
349 n, _ := fd.Read(peek[:])
350 ct := http.DetectContentType(peek[:n])
351 fd.Close()
352
353 url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
354 fileid, err := savefile("", name, name, url, ct, false, nil)
355 if err != nil {
356 log.Printf("error saving meme: %s", err)
357 return x
358 }
359 d := &Donk{
360 FileID: fileid,
361 XID: "",
362 Name: name,
363 Media: ct,
364 URL: url,
365 Local: false,
366 }
367 honk.Donks = append(honk.Donks, d)
368 return ""
369 }
370 honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
371}
372
373var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+( |$)")
374
375func quickrename(s string, userid int64) string {
376 nonstop := true
377 for nonstop {
378 nonstop = false
379 s = 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 if m[len(m)-1] == ' ' {
388 m = m[:len(m)-1]
389 }
390
391 xid := fullname(m, userid)
392
393 if xid != "" {
394 _, name := handles(xid)
395 if name != "" {
396 nonstop = true
397 m = name
398 }
399 }
400 return prefix + m + " "
401 })
402 }
403 return s
404}
405
406var shortnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
407 honkers := gethonkers(userid)
408 m := make(map[string]string)
409 for _, h := range honkers {
410 m[h.XID] = h.Name
411 }
412 return m, true
413}, Invalidator: &honkerinvalidator})
414
415func shortname(userid int64, xid string) string {
416 var m map[string]string
417 ok := shortnames.Get(userid, &m)
418 if ok {
419 return m[xid]
420 }
421 return ""
422}
423
424var fullnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
425 honkers := gethonkers(userid)
426 m := make(map[string]string)
427 for _, h := range honkers {
428 m[h.Name] = h.XID
429 }
430 return m, true
431}, Invalidator: &honkerinvalidator})
432
433func fullname(name string, userid int64) string {
434 var m map[string]string
435 ok := fullnames.Get(userid, &m)
436 if ok {
437 return m[name]
438 }
439 return ""
440}
441
442func mentionize(s string) string {
443 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
444 where := gofish(m)
445 if where == "" {
446 return m
447 }
448 who := m[0 : 1+strings.IndexByte(m[1:], '@')]
449 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
450 html.EscapeString(where), html.EscapeString(who))
451 })
452 s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
453 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
454 html.EscapeString(m[1:]), html.EscapeString(m))
455 })
456 return s
457}
458
459func ontologize(s string) string {
460 s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
461 if o[0] == '&' {
462 return o
463 }
464 p := ""
465 h := o
466 if h[0] != '#' {
467 p = h[:1]
468 h = h[1:]
469 }
470 return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
471 strings.ToLower(h[1:]), h)
472 })
473 return s
474}
475
476var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
477var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
478
479func originate(u string) string {
480 m := re_urlhost.FindStringSubmatch(u)
481 if len(m) > 1 {
482 return m[1]
483 }
484 return ""
485}
486
487var allhandles = make(map[string]string)
488var handlelock sync.Mutex
489
490// handle, handle@host
491func handles(xid string) (string, string) {
492 if xid == "" {
493 return "", ""
494 }
495 handlelock.Lock()
496 handle := allhandles[xid]
497 handlelock.Unlock()
498 if handle == "" {
499 handle = findhandle(xid)
500 handlelock.Lock()
501 allhandles[xid] = handle
502 handlelock.Unlock()
503 }
504 if handle == xid {
505 return xid, xid
506 }
507 return handle, handle + "@" + originate(xid)
508}
509
510func findhandle(xid string) string {
511 row := stmtGetXonker.QueryRow(xid, "handle")
512 var handle string
513 err := row.Scan(&handle)
514 if err != nil {
515 p, _ := investigate(xid)
516 if p == nil {
517 m := re_unurl.FindStringSubmatch(xid)
518 if len(m) > 2 {
519 handle = m[2]
520 } else {
521 handle = xid
522 }
523 } else {
524 handle = p.Handle
525 }
526 _, err = stmtSaveXonker.Exec(xid, handle, "handle")
527 if err != nil {
528 log.Printf("error saving handle: %s", err)
529 }
530 }
531 return handle
532}
533
534var handleprelock sync.Mutex
535
536func prehandle(xid string) {
537 handleprelock.Lock()
538 defer handleprelock.Unlock()
539 handles(xid)
540}
541
542func prepend(s string, x []string) []string {
543 return append([]string{s}, x...)
544}
545
546// pleroma leaks followers addressed posts to followers
547func butnottooloud(aud []string) {
548 for i, a := range aud {
549 if strings.HasSuffix(a, "/followers") {
550 aud[i] = ""
551 }
552 }
553}
554
555func keepitquiet(aud []string) bool {
556 for _, a := range aud {
557 if a == thewholeworld {
558 return false
559 }
560 }
561 return true
562}
563
564func firstclass(honk *Honk) bool {
565 return honk.Audience[0] == thewholeworld
566}
567
568func oneofakind(a []string) []string {
569 seen := make(map[string]bool)
570 seen[""] = true
571 j := 0
572 for _, s := range a {
573 if !seen[s] {
574 seen[s] = true
575 a[j] = s
576 j++
577 }
578 }
579 return a[:j]
580}
581
582var ziggies = make(map[string]*rsa.PrivateKey)
583var zaggies = make(map[string]*rsa.PublicKey)
584var ziggylock sync.Mutex
585
586func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
587 ziggylock.Lock()
588 key = ziggies[username]
589 ziggylock.Unlock()
590 if key == nil {
591 db := opendatabase()
592 row := db.QueryRow("select seckey from users where username = ?", username)
593 var data string
594 row.Scan(&data)
595 var err error
596 key, _, err = httpsig.DecodeKey(data)
597 if err != nil {
598 log.Printf("error decoding %s seckey: %s", username, err)
599 return
600 }
601 ziggylock.Lock()
602 ziggies[username] = key
603 ziggylock.Unlock()
604 }
605 keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
606 return
607}
608
609func zaggy(keyname string) (key *rsa.PublicKey) {
610 ziggylock.Lock()
611 key = zaggies[keyname]
612 ziggylock.Unlock()
613 if key != nil {
614 return
615 }
616 row := stmtGetXonker.QueryRow(keyname, "pubkey")
617 var data string
618 err := row.Scan(&data)
619 if err != nil {
620 log.Printf("hitting the webs for missing pubkey: %s", keyname)
621 j, err := GetJunk(keyname)
622 if err != nil {
623 log.Printf("error getting %s pubkey: %s", keyname, err)
624 return
625 }
626 keyobj, ok := j.GetMap("publicKey")
627 if ok {
628 j = keyobj
629 }
630 data, ok = j.GetString("publicKeyPem")
631 if !ok {
632 log.Printf("error finding %s pubkey", keyname)
633 return
634 }
635 _, ok = j.GetString("owner")
636 if !ok {
637 log.Printf("error finding %s pubkey owner", keyname)
638 return
639 }
640 _, key, err = httpsig.DecodeKey(data)
641 if err != nil {
642 log.Printf("error decoding %s pubkey: %s", keyname, err)
643 return
644 }
645 _, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
646 if err != nil {
647 log.Printf("error saving key: %s", err)
648 }
649 } else {
650 _, key, err = httpsig.DecodeKey(data)
651 if err != nil {
652 log.Printf("error decoding %s pubkey: %s", keyname, err)
653 return
654 }
655 }
656 ziggylock.Lock()
657 zaggies[keyname] = key
658 ziggylock.Unlock()
659 return
660}
661
662func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
663 _, err := stmtDeleteXonker.Exec(keyname, "pubkey")
664 if err != nil {
665 log.Printf("error deleting key: %s", err)
666 }
667 ziggylock.Lock()
668 delete(zaggies, keyname)
669 ziggylock.Unlock()
670 return httpsig.VerifyRequest(r, payload, zaggy)
671}
672
673func keymatch(keyname string, actor string) string {
674 hash := strings.IndexByte(keyname, '#')
675 if hash == -1 {
676 hash = len(keyname)
677 }
678 owner := keyname[0:hash]
679 if owner == actor {
680 return originate(actor)
681 }
682 return ""
683}