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