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