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 "humungus.tedunangst.com/r/webs/mz"
38)
39
40var allowedclasses = make(map[string]bool)
41
42func init() {
43 allowedclasses["kw"] = true
44 allowedclasses["bi"] = true
45 allowedclasses["st"] = true
46 allowedclasses["nm"] = true
47 allowedclasses["tp"] = true
48 allowedclasses["op"] = true
49 allowedclasses["cm"] = true
50 allowedclasses["al"] = true
51 allowedclasses["dl"] = true
52}
53
54func reverbolate(userid int64, honks []*Honk) {
55 for _, h := range honks {
56 h.What += "ed"
57 if h.What == "tonked" {
58 h.What = "honked back"
59 h.Style += " subtle"
60 }
61 if !h.Public {
62 h.Style += " limited"
63 }
64 translate(h)
65 local := false
66 if h.Whofore == 2 || h.Whofore == 3 {
67 local = true
68 }
69 if local && h.What != "bonked" {
70 h.Noise = re_memes.ReplaceAllString(h.Noise, "")
71 h.Noise = mentionize(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 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 noise := ch.Noise
186 if ch.Format == "markdown" {
187 noise = markitzero(noise)
188 }
189 ch.HTML, _ = htf.String(noise)
190 n := string(ch.HTML)
191 if strings.HasPrefix(n, "<p>") {
192 ch.HTML = template.HTML(n[3:])
193 }
194 if short := shortname(ch.UserID, ch.Who); short != "" {
195 ch.Handle = short
196 } else {
197 ch.Handle, _ = handles(ch.Who)
198 }
199
200}
201
202func inlineimgsfor(honk *Honk) func(node *html.Node) string {
203 return func(node *html.Node) string {
204 src := htfilter.GetAttr(node, "src")
205 alt := htfilter.GetAttr(node, "alt")
206 d := savedonk(src, "image", alt, "image", true)
207 if d != nil {
208 honk.Donks = append(honk.Donks, d)
209 }
210 log.Printf("inline img with src: %s", src)
211 return ""
212 }
213}
214
215func imaginate(honk *Honk) {
216 var htf htfilter.Filter
217 htf.Imager = inlineimgsfor(honk)
218 htf.BaseURL, _ = url.Parse(honk.XID)
219 htf.String(honk.Noise)
220}
221
222func translate(honk *Honk) {
223 if honk.Format == "html" {
224 return
225 }
226 noise := honk.Noise
227 if strings.HasPrefix(noise, "DZ:") {
228 idx := strings.Index(noise, "\n")
229 if idx == -1 {
230 honk.Precis = noise
231 noise = ""
232 } else {
233 honk.Precis = noise[:idx]
234 noise = noise[idx+1:]
235 }
236 }
237 honk.Precis = markitzero(strings.TrimSpace(honk.Precis))
238
239 var marker mz.Marker
240 marker.HashLinker = ontoreplacer
241 noise = strings.TrimSpace(noise)
242 noise = marker.Mark(noise)
243 honk.Noise = noise
244 honk.Onts = oneofakind(marker.HashTags)
245}
246
247func redoimages(honk *Honk) {
248 zap := make(map[string]bool)
249 {
250 var htf htfilter.Filter
251 htf.Imager = replaceimgsand(zap, true)
252 htf.SpanClasses = allowedclasses
253 p, _ := htf.String(honk.Precis)
254 n, _ := htf.String(honk.Noise)
255 honk.Precis = string(p)
256 honk.Noise = string(n)
257 }
258 j := 0
259 for i := 0; i < len(honk.Donks); i++ {
260 if !zap[honk.Donks[i].XID] {
261 honk.Donks[j] = honk.Donks[i]
262 j++
263 }
264 }
265 honk.Donks = honk.Donks[:j]
266
267 honk.Noise = re_memes.ReplaceAllString(honk.Noise, "")
268 honk.Noise = mentionize(honk.Noise)
269 honk.Noise = strings.Replace(honk.Noise, "<a href=", "<a class=\"mention u-url\" href=", -1)
270}
271
272func xcelerate(b []byte) string {
273 letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
274 for i, c := range b {
275 b[i] = letters[c&63]
276 }
277 s := string(b)
278 return s
279}
280
281func shortxid(xid string) string {
282 h := sha512.New512_256()
283 io.WriteString(h, xid)
284 return xcelerate(h.Sum(nil)[:20])
285}
286
287func xfiltrate() string {
288 var b [18]byte
289 rand.Read(b[:])
290 return xcelerate(b[:])
291}
292
293var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
294var re_urltions = regexp.MustCompile(`@https://\S+`)
295
296func grapevine(mentions []Mention) []string {
297 var s []string
298 for _, m := range mentions {
299 s = append(s, m.Where)
300 }
301 return s
302}
303
304func bunchofgrapes(s string) []Mention {
305 m := re_mentions.FindAllString(s, -1)
306 var mentions []Mention
307 for i := range m {
308 where := gofish(m[i])
309 if where != "" {
310 mentions = append(mentions, Mention{Who: m[i], Where: where})
311 }
312 }
313 m = re_urltions.FindAllString(s, -1)
314 for i := range m {
315 mentions = append(mentions, Mention{Who: m[i][1:], Where: m[i][1:]})
316 }
317 return mentions
318}
319
320type Emu struct {
321 ID string
322 Name string
323}
324
325var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
326
327var emucache = cache.New(cache.Options{Filler: func(ename string) (Emu, bool) {
328 fname := ename[1 : len(ename)-1]
329 _, err := os.Stat(dataDir + "/emus/" + fname + ".png")
330 if err != nil {
331 return Emu{Name: ename, ID: ""}, true
332 }
333 url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
334 return Emu{ID: url, Name: ename}, true
335}, Duration: 10 * time.Second})
336
337func herdofemus(noise string) []Emu {
338 m := re_emus.FindAllString(noise, -1)
339 m = oneofakind(m)
340 var emus []Emu
341 for _, e := range m {
342 var emu Emu
343 emucache.Get(e, &emu)
344 if emu.ID == "" {
345 continue
346 }
347 emus = append(emus, emu)
348 }
349 return emus
350}
351
352var re_memes = regexp.MustCompile("meme: ?([^\n]+)")
353var re_avatar = regexp.MustCompile("avatar: ?([^\n]+)")
354
355func memetize(honk *Honk) {
356 repl := func(x string) string {
357 name := x[5:]
358 if name[0] == ' ' {
359 name = name[1:]
360 }
361 fd, err := os.Open("memes/" + name)
362 if err != nil {
363 log.Printf("no meme for %s", name)
364 return x
365 }
366 var peek [512]byte
367 n, _ := fd.Read(peek[:])
368 ct := http.DetectContentType(peek[:n])
369 fd.Close()
370
371 url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
372 fileid, err := savefile("", name, name, url, ct, false, nil)
373 if err != nil {
374 log.Printf("error saving meme: %s", err)
375 return x
376 }
377 d := &Donk{
378 FileID: fileid,
379 XID: "",
380 Name: name,
381 Media: ct,
382 URL: url,
383 Local: false,
384 }
385 honk.Donks = append(honk.Donks, d)
386 return ""
387 }
388 honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
389}
390
391var re_quickmention = regexp.MustCompile("(^|[ \n])@[[:alnum:]]+([ \n.]|$)")
392
393func quickrename(s string, userid int64) string {
394 nonstop := true
395 for nonstop {
396 nonstop = false
397 s = re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
398 prefix := ""
399 if m[0] == ' ' || m[0] == '\n' {
400 prefix = m[:1]
401 m = m[1:]
402 }
403 prefix += "@"
404 m = m[1:]
405 tail := ""
406 if last := m[len(m)-1]; last == ' ' || last == '\n' || last == '.' {
407 tail = m[len(m)-1:]
408 m = m[:len(m)-1]
409 }
410
411 xid := fullname(m, userid)
412
413 if xid != "" {
414 _, name := handles(xid)
415 if name != "" {
416 nonstop = true
417 m = name
418 }
419 }
420 return prefix + m + tail
421 })
422 }
423 return s
424}
425
426var shortnames = 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.XID] = h.Name
431 }
432 return m, true
433}, Invalidator: &honkerinvalidator})
434
435func shortname(userid int64, xid string) string {
436 var m map[string]string
437 ok := shortnames.Get(userid, &m)
438 if ok {
439 return m[xid]
440 }
441 return ""
442}
443
444var fullnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
445 honkers := gethonkers(userid)
446 m := make(map[string]string)
447 for _, h := range honkers {
448 m[h.Name] = h.XID
449 }
450 return m, true
451}, Invalidator: &honkerinvalidator})
452
453func fullname(name string, userid int64) string {
454 var m map[string]string
455 ok := fullnames.Get(userid, &m)
456 if ok {
457 return m[name]
458 }
459 return ""
460}
461
462func mentionize(s string) string {
463 fill := `<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`
464 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
465 where := gofish(m)
466 if where == "" {
467 return m
468 }
469 who := m[0 : 1+strings.IndexByte(m[1:], '@')]
470 return fmt.Sprintf(fill, html.EscapeString(where), html.EscapeString(who))
471 })
472 s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
473 return fmt.Sprintf(fill, html.EscapeString(m[1:]), html.EscapeString(m))
474 })
475 return s
476}
477
478func ontoreplacer(h string) string {
479 return fmt.Sprintf(`<a href="https://%s/o/%s">%s</a>`, serverName,
480 strings.ToLower(h[1:]), h)
481}
482
483var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
484var re_urlhost = regexp.MustCompile("https://([^/ #]+)")
485
486func originate(u string) string {
487 m := re_urlhost.FindStringSubmatch(u)
488 if len(m) > 1 {
489 return m[1]
490 }
491 return ""
492}
493
494var allhandles = cache.New(cache.Options{Filler: func(xid string) (string, bool) {
495 var handle string
496 row := stmtGetXonker.QueryRow(xid, "handle")
497 err := row.Scan(&handle)
498 if err != nil {
499 log.Printf("need to get a handle: %s", xid)
500 info, err := investigate(xid)
501 if err != nil {
502 m := re_unurl.FindStringSubmatch(xid)
503 if len(m) > 2 {
504 handle = m[2]
505 } else {
506 handle = xid
507 }
508 } else {
509 handle = info.Name
510 }
511 }
512 return handle, true
513}})
514
515// handle, handle@host
516func handles(xid string) (string, string) {
517 if xid == "" {
518 return "", ""
519 }
520 var handle string
521 allhandles.Get(xid, &handle)
522 if handle == xid {
523 return xid, xid
524 }
525 return handle, handle + "@" + originate(xid)
526}
527
528func butnottooloud(aud []string) {
529 for i, a := range aud {
530 if strings.HasSuffix(a, "/followers") {
531 aud[i] = ""
532 }
533 }
534}
535
536func loudandproud(aud []string) bool {
537 for _, a := range aud {
538 if a == thewholeworld {
539 return true
540 }
541 }
542 return false
543}
544
545func firstclass(honk *Honk) bool {
546 return honk.Audience[0] == thewholeworld
547}
548
549func oneofakind(a []string) []string {
550 seen := make(map[string]bool)
551 seen[""] = true
552 j := 0
553 for _, s := range a {
554 if !seen[s] {
555 seen[s] = true
556 a[j] = s
557 j++
558 }
559 }
560 return a[:j]
561}
562
563var ziggies = cache.New(cache.Options{Filler: func(userid int64) (*KeyInfo, bool) {
564 var user *WhatAbout
565 ok := somenumberedusers.Get(userid, &user)
566 if !ok {
567 return nil, false
568 }
569 ki := new(KeyInfo)
570 ki.keyname = user.URL + "#key"
571 ki.seckey = user.SecKey
572 return ki, true
573}})
574
575func ziggy(userid int64) *KeyInfo {
576 var ki *KeyInfo
577 ziggies.Get(userid, &ki)
578 return ki
579}
580
581var zaggies = cache.New(cache.Options{Filler: func(keyname string) (httpsig.PublicKey, bool) {
582 var data string
583 row := stmtGetXonker.QueryRow(keyname, "pubkey")
584 err := row.Scan(&data)
585 var key httpsig.PublicKey
586 if err != nil {
587 log.Printf("hitting the webs for missing pubkey: %s", keyname)
588 j, err := GetJunk(keyname)
589 if err != nil {
590 log.Printf("error getting %s pubkey: %s", keyname, err)
591 when := time.Now().UTC().Format(dbtimeformat)
592 stmtSaveXonker.Exec(keyname, "failed", "pubkey", when)
593 return key, true
594 }
595 allinjest(originate(keyname), j)
596 row = stmtGetXonker.QueryRow(keyname, "pubkey")
597 err = row.Scan(&data)
598 if err != nil {
599 log.Printf("key not found after ingesting")
600 when := time.Now().UTC().Format(dbtimeformat)
601 stmtSaveXonker.Exec(keyname, "failed", "pubkey", when)
602 return key, true
603 }
604 }
605 _, key, err = httpsig.DecodeKey(data)
606 if err != nil {
607 log.Printf("error decoding %s pubkey: %s", keyname, err)
608 return key, true
609 }
610 return key, true
611}, Limit: 512})
612
613func zaggy(keyname string) httpsig.PublicKey {
614 var key httpsig.PublicKey
615 zaggies.Get(keyname, &key)
616 return key
617}
618
619func savingthrow(keyname string) {
620 when := time.Now().UTC().Add(-30 * time.Minute).Format(dbtimeformat)
621 stmtDeleteXonker.Exec(keyname, "pubkey", when)
622 zaggies.Clear(keyname)
623}
624
625func keymatch(keyname string, actor string) string {
626 hash := strings.IndexByte(keyname, '#')
627 if hash == -1 {
628 hash = len(keyname)
629 }
630 owner := keyname[0:hash]
631 if owner == actor {
632 return originate(actor)
633 }
634 return ""
635}