all repos — honk @ 778afecc80653b23afadeaa3cc9b9a42c0b8dbb3

my fork of honk

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