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