all repos — honk @ 870e50b65c6de4ec7460f0379ed4a33212484a41

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		d := savedonk(src, "image", alt, "image", true)
174		if d != nil {
175			honk.Donks = append(honk.Donks, d)
176		}
177		log.Printf("inline img with src: %s", src)
178		return ""
179	}
180}
181
182func imaginate(honk *Honk) {
183	imgfilt := htfilter.New()
184	imgfilt.Imager = inlineimgsfor(honk)
185	imgfilt.String(honk.Noise)
186}
187
188func translate(honk *Honk, redoimages bool) {
189	if honk.Format == "html" {
190		return
191	}
192	noise := honk.Noise
193	if strings.HasPrefix(noise, "DZ:") {
194		idx := strings.Index(noise, "\n")
195		if idx == -1 {
196			honk.Precis = noise
197			noise = ""
198		} else {
199			honk.Precis = noise[:idx]
200			noise = noise[idx+1:]
201		}
202	}
203	honk.Precis = strings.TrimSpace(honk.Precis)
204
205	noise = strings.TrimSpace(noise)
206	noise = quickrename(noise, honk.UserID)
207	noise = markitzero(noise)
208	honk.Noise = noise
209	honk.Onts = oneofakind(ontologies(honk.Noise))
210
211	if redoimages {
212		zap := make(map[string]bool)
213		{
214			filt := htfilter.New()
215			filt.Imager = replaceimgsand(zap, true)
216			filt.SpanClasses = allowedclasses
217			p, _ := filt.String(honk.Precis)
218			n, _ := filt.String(honk.Noise)
219			honk.Precis = string(p)
220			honk.Noise = string(n)
221		}
222		j := 0
223		for i := 0; i < len(honk.Donks); i++ {
224			if !zap[honk.Donks[i].XID] {
225				honk.Donks[j] = honk.Donks[i]
226				j++
227			}
228		}
229		honk.Donks = honk.Donks[:j]
230	}
231}
232
233func shortxid(xid string) string {
234	idx := strings.LastIndexByte(xid, '/')
235	if idx == -1 {
236		return xid
237	}
238	return xid[idx+1:]
239}
240
241func xfiltrate() string {
242	letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
243	var b [18]byte
244	rand.Read(b[:])
245	for i, c := range b {
246		b[i] = letters[c&63]
247	}
248	s := string(b[:])
249	return s
250}
251
252var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
253
254func ontologies(s string) []string {
255	m := re_hashes.FindAllString(s, -1)
256	j := 0
257	for _, h := range m {
258		if h[0] == '&' {
259			continue
260		}
261		if h[0] != '#' {
262			h = h[1:]
263		}
264		m[j] = h
265		j++
266	}
267	return m[:j]
268}
269
270type Mention struct {
271	who   string
272	where string
273}
274
275var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
276var re_urltions = regexp.MustCompile(`@https://\S+`)
277
278func grapevine(s string) []string {
279	var mentions []string
280	m := re_mentions.FindAllString(s, -1)
281	for i := range m {
282		where := gofish(m[i])
283		if where != "" {
284			mentions = append(mentions, where)
285		}
286	}
287	m = re_urltions.FindAllString(s, -1)
288	for i := range m {
289		mentions = append(mentions, m[i][1:])
290	}
291	return mentions
292}
293
294func bunchofgrapes(s string) []Mention {
295	m := re_mentions.FindAllString(s, -1)
296	var mentions []Mention
297	for i := range m {
298		where := gofish(m[i])
299		if where != "" {
300			mentions = append(mentions, Mention{who: m[i], where: where})
301		}
302	}
303	m = re_urltions.FindAllString(s, -1)
304	for i := range m {
305		mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
306	}
307	return mentions
308}
309
310type Emu struct {
311	ID   string
312	Name string
313}
314
315var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
316
317func herdofemus(noise string) []Emu {
318	m := re_emus.FindAllString(noise, -1)
319	m = oneofakind(m)
320	var emus []Emu
321	for _, e := range m {
322		fname := e[1 : len(e)-1]
323		_, err := os.Stat("emus/" + fname + ".png")
324		if err != nil {
325			continue
326		}
327		url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
328		emus = append(emus, Emu{ID: url, Name: e})
329	}
330	return emus
331}
332
333var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
334
335func memetize(honk *Honk) {
336	repl := func(x string) string {
337		name := x[5:]
338		if name[0] == ' ' {
339			name = name[1:]
340		}
341		fd, err := os.Open("memes/" + name)
342		if err != nil {
343			log.Printf("no meme for %s", name)
344			return x
345		}
346		var peek [512]byte
347		n, _ := fd.Read(peek[:])
348		ct := http.DetectContentType(peek[:n])
349		fd.Close()
350
351		url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
352		fileid, err := savefile("", name, name, url, ct, false, nil)
353		if err != nil {
354			log.Printf("error saving meme: %s", err)
355			return x
356		}
357		d := &Donk{
358			FileID: fileid,
359			XID:    "",
360			Name:   name,
361			Media:  ct,
362			URL:    url,
363			Local:  false,
364		}
365		honk.Donks = append(honk.Donks, d)
366		return ""
367	}
368	honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
369}
370
371var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+( |$)")
372
373func quickrename(s string, userid int64) string {
374	nonstop := true
375	for nonstop {
376		nonstop = false
377		s = re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
378			prefix := ""
379			if m[0] == ' ' {
380				prefix = " "
381				m = m[1:]
382			}
383			prefix += "@"
384			m = m[1:]
385			if m[len(m)-1] == ' ' {
386				m = m[:len(m)-1]
387			}
388
389			xid := fullname(m, userid)
390
391			if xid != "" {
392				_, name := handles(xid)
393				if name != "" {
394					nonstop = true
395					m = name
396				}
397			}
398			return prefix + m + " "
399		})
400	}
401	return s
402}
403
404var shortnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
405	honkers := gethonkers(userid)
406	m := make(map[string]string)
407	for _, h := range honkers {
408		m[h.XID] = h.Name
409	}
410	return m, true
411}, Invalidator: &honkerinvalidator})
412
413func shortname(userid int64, xid string) string {
414	var m map[string]string
415	ok := shortnames.Get(userid, &m)
416	if ok {
417		return m[xid]
418	}
419	return ""
420}
421
422var fullnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
423	honkers := gethonkers(userid)
424	m := make(map[string]string)
425	for _, h := range honkers {
426		m[h.Name] = h.XID
427	}
428	return m, true
429}, Invalidator: &honkerinvalidator})
430
431func fullname(name string, userid int64) string {
432	var m map[string]string
433	ok := fullnames.Get(userid, &m)
434	if ok {
435		return m[name]
436	}
437	return ""
438}
439
440func mentionize(s string) string {
441	s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
442		where := gofish(m)
443		if where == "" {
444			return m
445		}
446		who := m[0 : 1+strings.IndexByte(m[1:], '@')]
447		return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
448			html.EscapeString(where), html.EscapeString(who))
449	})
450	s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
451		return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
452			html.EscapeString(m[1:]), html.EscapeString(m))
453	})
454	return s
455}
456
457func ontologize(s string) string {
458	s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
459		if o[0] == '&' {
460			return o
461		}
462		p := ""
463		h := o
464		if h[0] != '#' {
465			p = h[:1]
466			h = h[1:]
467		}
468		return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
469			strings.ToLower(h[1:]), h)
470	})
471	return s
472}
473
474var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
475var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
476
477func originate(u string) string {
478	m := re_urlhost.FindStringSubmatch(u)
479	if len(m) > 1 {
480		return m[1]
481	}
482	return ""
483}
484
485var allhandles = make(map[string]string)
486var handlelock sync.Mutex
487
488// handle, handle@host
489func handles(xid string) (string, string) {
490	if xid == "" {
491		return "", ""
492	}
493	handlelock.Lock()
494	handle := allhandles[xid]
495	handlelock.Unlock()
496	if handle == "" {
497		handle = findhandle(xid)
498		handlelock.Lock()
499		allhandles[xid] = handle
500		handlelock.Unlock()
501	}
502	if handle == xid {
503		return xid, xid
504	}
505	return handle, handle + "@" + originate(xid)
506}
507
508func findhandle(xid string) string {
509	row := stmtGetXonker.QueryRow(xid, "handle")
510	var handle string
511	err := row.Scan(&handle)
512	if err != nil {
513		p, _ := investigate(xid)
514		if p == nil {
515			m := re_unurl.FindStringSubmatch(xid)
516			if len(m) > 2 {
517				handle = m[2]
518			} else {
519				handle = xid
520			}
521		} else {
522			handle = p.Handle
523		}
524		_, err = stmtSaveXonker.Exec(xid, handle, "handle")
525		if err != nil {
526			log.Printf("error saving handle: %s", err)
527		}
528	}
529	return handle
530}
531
532var handleprelock sync.Mutex
533
534func prehandle(xid string) {
535	handleprelock.Lock()
536	defer handleprelock.Unlock()
537	handles(xid)
538}
539
540func prepend(s string, x []string) []string {
541	return append([]string{s}, x...)
542}
543
544// pleroma leaks followers addressed posts to followers
545func butnottooloud(aud []string) {
546	for i, a := range aud {
547		if strings.HasSuffix(a, "/followers") {
548			aud[i] = ""
549		}
550	}
551}
552
553func keepitquiet(aud []string) bool {
554	for _, a := range aud {
555		if a == thewholeworld {
556			return false
557		}
558	}
559	return true
560}
561
562func firstclass(honk *Honk) bool {
563	return honk.Audience[0] == thewholeworld
564}
565
566func oneofakind(a []string) []string {
567	seen := make(map[string]bool)
568	seen[""] = true
569	j := 0
570	for _, s := range a {
571		if !seen[s] {
572			seen[s] = true
573			a[j] = s
574			j++
575		}
576	}
577	return a[:j]
578}
579
580var ziggies = make(map[string]*rsa.PrivateKey)
581var zaggies = make(map[string]*rsa.PublicKey)
582var ziggylock sync.Mutex
583
584func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
585	ziggylock.Lock()
586	key = ziggies[username]
587	ziggylock.Unlock()
588	if key == nil {
589		db := opendatabase()
590		row := db.QueryRow("select seckey from users where username = ?", username)
591		var data string
592		row.Scan(&data)
593		var err error
594		key, _, err = httpsig.DecodeKey(data)
595		if err != nil {
596			log.Printf("error decoding %s seckey: %s", username, err)
597			return
598		}
599		ziggylock.Lock()
600		ziggies[username] = key
601		ziggylock.Unlock()
602	}
603	keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
604	return
605}
606
607func zaggy(keyname string) (key *rsa.PublicKey) {
608	ziggylock.Lock()
609	key = zaggies[keyname]
610	ziggylock.Unlock()
611	if key != nil {
612		return
613	}
614	row := stmtGetXonker.QueryRow(keyname, "pubkey")
615	var data string
616	err := row.Scan(&data)
617	if err != nil {
618		log.Printf("hitting the webs for missing pubkey: %s", keyname)
619		j, err := GetJunk(keyname)
620		if err != nil {
621			log.Printf("error getting %s pubkey: %s", keyname, err)
622			return
623		}
624		keyobj, ok := j.GetMap("publicKey")
625		if ok {
626			j = keyobj
627		}
628		data, ok = j.GetString("publicKeyPem")
629		if !ok {
630			log.Printf("error finding %s pubkey", keyname)
631			return
632		}
633		_, ok = j.GetString("owner")
634		if !ok {
635			log.Printf("error finding %s pubkey owner", keyname)
636			return
637		}
638		_, key, err = httpsig.DecodeKey(data)
639		if err != nil {
640			log.Printf("error decoding %s pubkey: %s", keyname, err)
641			return
642		}
643		_, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
644		if err != nil {
645			log.Printf("error saving key: %s", err)
646		}
647	} else {
648		_, key, err = httpsig.DecodeKey(data)
649		if err != nil {
650			log.Printf("error decoding %s pubkey: %s", keyname, err)
651			return
652		}
653	}
654	ziggylock.Lock()
655	zaggies[keyname] = key
656	ziggylock.Unlock()
657	return
658}
659
660func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
661	_, err := stmtDeleteXonker.Exec(keyname, "pubkey")
662	if err != nil {
663		log.Printf("error deleting key: %s", err)
664	}
665	ziggylock.Lock()
666	delete(zaggies, keyname)
667	ziggylock.Unlock()
668	return httpsig.VerifyRequest(r, payload, zaggy)
669}
670
671func keymatch(keyname string, actor string) string {
672	hash := strings.IndexByte(keyname, '#')
673	if hash == -1 {
674		hash = len(keyname)
675	}
676	owner := keyname[0:hash]
677	if owner == actor {
678		return originate(actor)
679	}
680	return ""
681}