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