all repos — honk @ 2c0beeb1c725a6abed0be9c2d80456cef071204a

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