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