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