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