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