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) > 6000 && 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 = markitzero(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_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+ ")
320
321func quickrename(s string, userid int64) string {
322 return re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
323 prefix := ""
324 if m[0] == ' ' {
325 prefix = " "
326 m = m[1:]
327 }
328 prefix += "@"
329 m = m[1:]
330 m = m[:len(m)-1]
331
332 row := stmtOneHonker.QueryRow(m, userid)
333 var xid string
334 err := row.Scan(&xid)
335 if err == nil {
336 _, name := handles(xid)
337 if name != "" {
338 m = name
339 }
340 }
341 return prefix + m + " "
342 })
343}
344
345func mentionize(s string) string {
346 s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
347 where := gofish(m)
348 if where == "" {
349 return m
350 }
351 who := m[0 : 1+strings.IndexByte(m[1:], '@')]
352 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
353 html.EscapeString(where), html.EscapeString(who))
354 })
355 s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
356 return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
357 html.EscapeString(m[1:]), html.EscapeString(m))
358 })
359 return s
360}
361
362func ontologize(s string) string {
363 s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
364 if o[0] == '&' {
365 return o
366 }
367 p := ""
368 h := o
369 if h[0] != '#' {
370 p = h[:1]
371 h = h[1:]
372 }
373 return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
374 strings.ToLower(h[1:]), h)
375 })
376 return s
377}
378
379var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
380var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
381
382func originate(u string) string {
383 m := re_urlhost.FindStringSubmatch(u)
384 if len(m) > 1 {
385 return m[1]
386 }
387 return ""
388}
389
390var allhandles = make(map[string]string)
391var handlelock sync.Mutex
392
393// handle, handle@host
394func handles(xid string) (string, string) {
395 if xid == "" {
396 return "", ""
397 }
398 handlelock.Lock()
399 handle := allhandles[xid]
400 handlelock.Unlock()
401 if handle == "" {
402 handle = findhandle(xid)
403 handlelock.Lock()
404 allhandles[xid] = handle
405 handlelock.Unlock()
406 }
407 if handle == xid {
408 return xid, xid
409 }
410 return handle, handle + "@" + originate(xid)
411}
412
413func findhandle(xid string) string {
414 row := stmtGetXonker.QueryRow(xid, "handle")
415 var handle string
416 err := row.Scan(&handle)
417 if err != nil {
418 p, _ := investigate(xid)
419 if p == nil {
420 m := re_unurl.FindStringSubmatch(xid)
421 if len(m) > 2 {
422 handle = m[2]
423 } else {
424 handle = xid
425 }
426 } else {
427 handle = p.Handle
428 }
429 _, err = stmtSaveXonker.Exec(xid, handle, "handle")
430 if err != nil {
431 log.Printf("error saving handle: %s", err)
432 }
433 }
434 return handle
435}
436
437var handleprelock sync.Mutex
438
439func prehandle(xid string) {
440 handleprelock.Lock()
441 defer handleprelock.Unlock()
442 handles(xid)
443}
444
445func prepend(s string, x []string) []string {
446 return append([]string{s}, x...)
447}
448
449// pleroma leaks followers addressed posts to followers
450func butnottooloud(aud []string) {
451 for i, a := range aud {
452 if strings.HasSuffix(a, "/followers") {
453 aud[i] = ""
454 }
455 }
456}
457
458func keepitquiet(aud []string) bool {
459 for _, a := range aud {
460 if a == thewholeworld {
461 return false
462 }
463 }
464 return true
465}
466
467func firstclass(honk *Honk) bool {
468 return honk.Audience[0] == thewholeworld
469}
470
471func oneofakind(a []string) []string {
472 var x []string
473 for n, s := range a {
474 if s != "" {
475 x = append(x, s)
476 for i := n + 1; i < len(a); i++ {
477 if a[i] == s {
478 a[i] = ""
479 }
480 }
481 }
482 }
483 return x
484}
485
486var ziggies = make(map[string]*rsa.PrivateKey)
487var zaggies = make(map[string]*rsa.PublicKey)
488var ziggylock sync.Mutex
489
490func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
491 ziggylock.Lock()
492 key = ziggies[username]
493 ziggylock.Unlock()
494 if key == nil {
495 db := opendatabase()
496 row := db.QueryRow("select seckey from users where username = ?", username)
497 var data string
498 row.Scan(&data)
499 var err error
500 key, _, err = httpsig.DecodeKey(data)
501 if err != nil {
502 log.Printf("error decoding %s seckey: %s", username, err)
503 return
504 }
505 ziggylock.Lock()
506 ziggies[username] = key
507 ziggylock.Unlock()
508 }
509 keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
510 return
511}
512
513func zaggy(keyname string) (key *rsa.PublicKey) {
514 ziggylock.Lock()
515 key = zaggies[keyname]
516 ziggylock.Unlock()
517 if key != nil {
518 return
519 }
520 row := stmtGetXonker.QueryRow(keyname, "pubkey")
521 var data string
522 err := row.Scan(&data)
523 if err != nil {
524 log.Printf("hitting the webs for missing pubkey: %s", keyname)
525 j, err := GetJunk(keyname)
526 if err != nil {
527 log.Printf("error getting %s pubkey: %s", keyname, err)
528 return
529 }
530 keyobj, ok := j.GetMap("publicKey")
531 if ok {
532 j = keyobj
533 }
534 data, ok = j.GetString("publicKeyPem")
535 if !ok {
536 log.Printf("error finding %s pubkey", keyname)
537 return
538 }
539 _, ok = j.GetString("owner")
540 if !ok {
541 log.Printf("error finding %s pubkey owner", keyname)
542 return
543 }
544 _, key, err = httpsig.DecodeKey(data)
545 if err != nil {
546 log.Printf("error decoding %s pubkey: %s", keyname, err)
547 return
548 }
549 _, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
550 if err != nil {
551 log.Printf("error saving key: %s", err)
552 }
553 } else {
554 _, key, err = httpsig.DecodeKey(data)
555 if err != nil {
556 log.Printf("error decoding %s pubkey: %s", keyname, err)
557 return
558 }
559 }
560 ziggylock.Lock()
561 zaggies[keyname] = key
562 ziggylock.Unlock()
563 return
564}
565
566func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
567 _, err := stmtDeleteXonker.Exec(keyname, "pubkey")
568 if err != nil {
569 log.Printf("error deleting key: %s", err)
570 }
571 ziggylock.Lock()
572 delete(zaggies, keyname)
573 ziggylock.Unlock()
574 return httpsig.VerifyRequest(r, payload, zaggy)
575}
576
577func keymatch(keyname string, actor string) string {
578 hash := strings.IndexByte(keyname, '#')
579 if hash == -1 {
580 hash = len(keyname)
581 }
582 owner := keyname[0:hash]
583 if owner == actor {
584 return originate(actor)
585 }
586 return ""
587}