all repos — honk @ 65f175d7add19e465731ca834796695ecbd9b65d

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