all repos — honk @ 2a20b1671d9f032ffffb1023dd104e2e7e4f31ec

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	"path"
 28	"regexp"
 29	"strconv"
 30	"strings"
 31	"sync"
 32	"time"
 33
 34	"github.com/dustin/go-humanize"
 35	"golang.org/x/net/html"
 36	"humungus.tedunangst.com/r/webs/gencache"
 37	"humungus.tedunangst.com/r/webs/htfilter"
 38	"humungus.tedunangst.com/r/webs/httpsig"
 39	"humungus.tedunangst.com/r/webs/login"
 40	"humungus.tedunangst.com/r/webs/mz"
 41	"humungus.tedunangst.com/r/webs/templates"
 42)
 43
 44var allowedclasses = make(map[string]bool)
 45
 46func init() {
 47	allowedclasses["kw"] = true
 48	allowedclasses["bi"] = true
 49	allowedclasses["st"] = true
 50	allowedclasses["nm"] = true
 51	allowedclasses["tp"] = true
 52	allowedclasses["op"] = true
 53	allowedclasses["cm"] = true
 54	allowedclasses["al"] = true
 55	allowedclasses["dl"] = true
 56}
 57
 58var relingo = make(map[string]string)
 59
 60func loadLingo() {
 61	for _, l := range []string{"honked", "bonked", "honked back", "qonked", "evented"} {
 62		v := l
 63		k := "lingo-" + strings.ReplaceAll(l, " ", "")
 64		getconfig(k, &v)
 65		relingo[l] = v
 66	}
 67}
 68
 69func prettifydate(d time.Time) string {
 70	var customMags = []humanize.RelTimeMagnitude{
 71		{time.Second, "now", time.Second},
 72		{2 * time.Second, "1s %s", 1},
 73		{time.Minute, "%ds %s", time.Second},
 74		{2 * time.Minute, "1m %s", 1},
 75		{time.Hour, "%dm %s", time.Minute},
 76		{2 * time.Hour, "1h %s", 1},
 77		{humanize.Day, "%dh %s", time.Hour},
 78		{2 * humanize.Day, "1d %s", 1},
 79		{humanize.Week, "%dd %s", humanize.Day},
 80		{2 * humanize.Week, "1w %s", 1},
 81		{humanize.Month, "%dw %s", humanize.Week},
 82	}
 83
 84	since := time.Since(d)
 85	// More than a month, return the actual date.
 86	if since.Hours() > 730 {
 87		return d.Format("02 Jan 2006 15:04")
 88	}
 89
 90	return humanize.CustomRelTime(d, time.Now(), "", "from now", customMags)
 91}
 92func reverbolate(userid UserID, honks []*Honk) {
 93	var handlers sync.WaitGroup
 94	user, _ := somenumberedusers.Get(userid)
 95	for i := range honks {
 96		h := honks[i]
 97		// idk where else to put this
 98		h.DatePretty = prettifydate(h.Date)
 99		h.What += "ed"
100		if h.What == "honked" && h.RID != "" {
101			h.What = "honked back"
102			h.Style += " subtle"
103		}
104		if !h.Public {
105			h.Style += " limited"
106		}
107		if h.Whofore == 1 {
108			h.Style += " atme"
109		}
110		translate(h)
111		local := false
112		if h.Whofore == 2 || h.Whofore == 3 {
113			local = true
114		}
115		if local && h.What != "bonked" {
116			h.Noise = re_memes.ReplaceAllString(h.Noise, "")
117		}
118		handlers.Add(1)
119		go func() {
120			h.Username, h.Handle = handles(h.Honker)
121			if !local {
122				short := shortname(userid, h.Honker)
123				if short != "" {
124					h.Username = short
125				} else {
126					h.Username = h.Handle
127					if len(h.Username) > 20 {
128						h.Username = h.Username[:20] + ".."
129					}
130				}
131			}
132			if user != nil {
133				hset := []string{}
134				if h.Honker != user.URL {
135					hset = append(hset, "@"+h.Handle)
136				}
137				if user.Options.MentionAll {
138					for _, a := range h.Audience {
139						if a == h.Honker || a == user.URL {
140							continue
141						}
142						_, hand := handles(a)
143						if hand != "" {
144							hand = "@" + hand
145							hset = append(hset, hand)
146						}
147					}
148				}
149				h.Handles = strings.Join(hset, " ")
150			}
151			if h.URL == "" {
152				h.URL = h.XID
153			}
154			if h.Oonker != "" {
155				_, h.Oondle = handles(h.Oonker)
156			}
157			handlers.Done()
158		}()
159		h.Precis = demoji(h.Precis)
160		h.Noise = demoji(h.Noise)
161		h.Open = "open"
162		var misto string
163		for _, m := range h.Mentions {
164			if m.Where != h.Honker && !m.IsPresent(h.Noise) {
165				misto += string(templates.Sprintf(" <a href=\"%sh?xid=%s\">%s</a>", serverPrefix, url.QueryEscape(m.Where), m.Who))
166			}
167		}
168		var mistag string
169		for _, o := range h.Onts {
170			if !OntIsPresent(o, h.Noise) {
171				mistag += " " + o
172			}
173		}
174		if len(misto) > 0 || len(mistag) > 0 {
175			if len(misto) > 0 {
176				misto = fmt.Sprintf("(%s)<p>", misto[1:])
177			}
178			if len(mistag) > 0 {
179				mistag = fmt.Sprintf("<p>(%s)", mistag[1:])
180			}
181			h.Noise = misto + h.Noise + mistag
182		}
183
184		zap := make(map[string]bool)
185		{
186			var htf htfilter.Filter
187			htf.Imager = replaceimgsand(zap, false, h)
188			htf.SpanClasses = allowedclasses
189			htf.BaseURL, _ = url.Parse(h.XID)
190			emuxifier := func(e string) string {
191				for _, d := range h.Donks {
192					if d.Name == e {
193						zap[d.XID] = true
194						if d.Local {
195							return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
196						}
197					}
198				}
199				if local && h.What != "bonked" {
200					emu, _ := emucache.Get(e)
201					if emu != nil {
202						return fmt.Sprintf(`<img class="emu" title="%s" src="%s">`, emu.Name, emu.ID)
203					}
204				}
205				return e
206			}
207			htf.FilterText = func(w io.Writer, data string) {
208				data = htfilter.EscapeText(data)
209				data = re_emus.ReplaceAllStringFunc(data, emuxifier)
210				io.WriteString(w, data)
211			}
212			if user != nil {
213				htf.RetargetLink = func(href string) string {
214					h2 := strings.ReplaceAll(href, "/@", "/users/")
215					for _, m := range h.Mentions {
216						if h2 == m.Where || href == m.Where {
217							return "/h?xid=" + url.QueryEscape(m.Where)
218						}
219					}
220					return href
221				}
222			}
223			p, _ := htf.String(h.Precis)
224			n, _ := htf.String(h.Noise)
225			h.Precis = string(p)
226			h.Noise = string(n)
227		}
228		j := 0
229		for i := 0; i < len(h.Donks); i++ {
230			if !zap[h.Donks[i].XID] {
231				h.Donks[j] = h.Donks[i]
232				j++
233			}
234		}
235		h.Donks = h.Donks[:j]
236	}
237
238	unsee(honks, userid)
239
240	for _, h := range honks {
241		renderflags(h)
242
243		h.HTPrecis = template.HTML(h.Precis)
244		h.HTML = template.HTML(h.Noise)
245		if redo := relingo[h.What]; redo != "" {
246			h.What = redo
247		}
248	}
249}
250
251func replaceimgsand(zap map[string]bool, absolute bool, honk *Honk) func(node *html.Node) string {
252	return func(node *html.Node) string {
253		src := htfilter.GetAttr(node, "src")
254		alt := htfilter.GetAttr(node, "alt")
255		//title := GetAttr(node, "title")
256		if htfilter.HasClass(node, "Emoji") && alt != "" {
257			return alt
258		}
259		base := path.Base(src)
260		didx, _ := strconv.Atoi(base)
261		var d *Donk
262		if strings.HasPrefix(src, serverPrefix) && didx > 0 && didx <= len(honk.Donks) {
263			d = honk.Donks[didx-1]
264		} else {
265			d = finddonk(src)
266		}
267		if d != nil {
268			zap[d.XID] = true
269			base := ""
270			if absolute {
271				base = serverURL("")
272			}
273			return string(templates.Sprintf(`<img alt="%s" title="%s" src="%s/d/%s">`, alt, alt, base, d.XID))
274		}
275		return string(templates.Sprintf(`&lt;img alt="%s" src="<a href="%s">%s</a>"&gt;`, alt, src, src))
276	}
277}
278
279func translatechonk(ch *Chonk) {
280	noise := ch.Noise
281	if ch.Format == "markdown" {
282		var marker mz.Marker
283		noise = marker.Mark(noise)
284	}
285	var htf htfilter.Filter
286	htf.SpanClasses = allowedclasses
287	htf.BaseURL, _ = url.Parse(ch.XID)
288	ch.HTML, _ = htf.String(noise)
289}
290
291func filterchonk(ch *Chonk) {
292	translatechonk(ch)
293
294	noise := string(ch.HTML)
295
296	local := originate(ch.XID) == serverName
297
298	zap := make(map[string]bool)
299	emuxifier := func(e string) string {
300		for _, d := range ch.Donks {
301			if d.Name == e {
302				zap[d.XID] = true
303				if d.Local {
304					return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
305				}
306			}
307		}
308		if local {
309			emu, _ := emucache.Get(e)
310			if emu != nil {
311				return fmt.Sprintf(`<img class="emu" title="%s" src="%s">`, emu.Name, emu.ID)
312			}
313		}
314		return e
315	}
316	noise = re_emus.ReplaceAllStringFunc(noise, emuxifier)
317	j := 0
318	for i := 0; i < len(ch.Donks); i++ {
319		if !zap[ch.Donks[i].XID] {
320			ch.Donks[j] = ch.Donks[i]
321			j++
322		}
323	}
324	ch.Donks = ch.Donks[:j]
325
326	if strings.HasPrefix(noise, "<p>") {
327		noise = noise[3:]
328	}
329	ch.HTML = template.HTML(noise)
330	if short := shortname(ch.UserID, ch.Who); short != "" {
331		ch.Handle = short
332	} else {
333		ch.Handle, _ = handles(ch.Who)
334	}
335
336}
337
338func inlineimgsfor(honk *Honk) func(node *html.Node) string {
339	return func(node *html.Node) string {
340		src := htfilter.GetAttr(node, "src")
341		alt := htfilter.GetAttr(node, "alt")
342		base := path.Base(src)
343		didx, _ := strconv.Atoi(base)
344		if strings.HasPrefix(src, serverPrefix) && didx > 0 && didx <= len(honk.Donks) {
345			dlog.Printf("skipping inline image %s", src)
346			return ""
347		}
348		d := savedonk(src, "image", alt, "image", true)
349		if d != nil {
350			honk.Donks = append(honk.Donks, d)
351		}
352		dlog.Printf("inline img with src: %s", src)
353		return ""
354	}
355}
356
357func imaginate(honk *Honk) {
358	var htf htfilter.Filter
359	htf.Imager = inlineimgsfor(honk)
360	htf.BaseURL, _ = url.Parse(honk.XID)
361	htf.String(honk.Noise)
362}
363
364var re_dangerous = regexp.MustCompile("^[a-zA-Z]{2}:")
365
366func precipitate(honk *Honk) {
367	noise := honk.Noise
368	if re_dangerous.MatchString(noise) {
369		idx := strings.Index(noise, "\n")
370		if idx == -1 {
371			honk.Precis = noise
372			noise = ""
373		} else {
374			honk.Precis = noise[:idx]
375			noise = noise[idx+1:]
376		}
377		var marker mz.Marker
378		marker.Short = true
379		honk.Precis = marker.Mark(strings.TrimSpace(honk.Precis))
380		honk.Noise = noise
381	}
382}
383
384func translate(honk *Honk) {
385	if honk.Format == "html" {
386		return
387	}
388	noise := honk.Noise
389
390	var marker mz.Marker
391	marker.HashLinker = ontoreplacer
392	marker.AtLinker = attoreplacer
393	marker.AllowImages = true
394	noise = strings.TrimSpace(noise)
395	noise = marker.Mark(noise)
396	honk.Noise = noise
397	honk.Onts = append(honk.Onts, marker.HashTags...)
398	honk.Mentions = bunchofgrapes(marker.Mentions)
399	for _, t := range oneofakind(strings.Split(honk.Onties, " ")) {
400		if t[0] != '#' {
401			t = "#" + t
402		}
403		honk.Onts = append(honk.Onts, t)
404	}
405	honk.Onts = oneofakind(honk.Onts)
406	honk.Mentions = append(honk.Mentions, bunchofgrapes(oneofakind(strings.Split(honk.SeeAlso, " ")))...)
407}
408
409func redoimages(honk *Honk) {
410	zap := make(map[string]bool)
411	{
412		var htf htfilter.Filter
413		htf.Imager = replaceimgsand(zap, true, honk)
414		htf.SpanClasses = allowedclasses
415		htf.BaseURL, _ = url.Parse(honk.XID)
416		p, _ := htf.String(honk.Precis)
417		n, _ := htf.String(honk.Noise)
418		honk.Precis = string(p)
419		honk.Noise = string(n)
420	}
421	j := 0
422	for i := 0; i < len(honk.Donks); i++ {
423		if !zap[honk.Donks[i].XID] {
424			honk.Donks[j] = honk.Donks[i]
425			j++
426		}
427	}
428	honk.Donks = honk.Donks[:j]
429
430	honk.Noise = re_memes.ReplaceAllString(honk.Noise, "")
431	honk.Noise = strings.Replace(honk.Noise, "<a href=", "<a class=\"mention u-url\" href=", -1)
432}
433
434func xcelerate(b []byte) string {
435	letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
436	for i, c := range b {
437		b[i] = letters[c&63]
438	}
439	s := string(b)
440	return s
441}
442
443func shortxid(xid string) string {
444	h := sha512.New512_256()
445	io.WriteString(h, xid)
446	return xcelerate(h.Sum(nil)[:20])
447}
448
449func xfiltrate() string {
450	var b [18]byte
451	rand.Read(b[:])
452	return xcelerate(b[:])
453}
454
455func grapevine(mentions []Mention) []string {
456	var s []string
457	for _, m := range mentions {
458		s = append(s, m.Where)
459	}
460	return s
461}
462
463func bunchofgrapes(m []string) []Mention {
464	var mentions []Mention
465	for i := range m {
466		who := m[i]
467		if strings.HasPrefix(who, "@https://") {
468			mentions = append(mentions, Mention{Who: who, Where: who[1:]})
469			continue
470		}
471		where := gofish(who)
472		if where != "" {
473			mentions = append(mentions, Mention{Who: who, Where: where})
474		}
475	}
476	return mentions
477}
478
479type Emu struct {
480	ID   string
481	Name string
482	Type string
483}
484
485var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
486
487var emucache = gencache.New(gencache.Options[string, *Emu]{Fill: func(ename string) (*Emu, bool) {
488	fname := ename[1 : len(ename)-1]
489	exts := []string{".png", ".gif"}
490	for _, ext := range exts {
491		_, err := os.Stat(dataDir + "/emus/" + fname + ext)
492		if err != nil {
493			continue
494		}
495		url := serverURL("/emu/%s%s", fname, ext)
496		if develMode {
497			url = fmt.Sprintf("/emu/%s%s", fname, ext)
498		}
499		return &Emu{ID: url, Name: ename, Type: "image/" + ext[1:]}, true
500	}
501	return nil, true
502}, Duration: 10 * time.Second})
503
504func herdofemus(noise string) []*Emu {
505	m := re_emus.FindAllString(noise, -1)
506	m = oneofakind(m)
507	var emus []*Emu
508	for _, e := range m {
509		emu, _ := emucache.Get(e)
510		if emu == nil {
511			continue
512		}
513		emus = append(emus, emu)
514	}
515	return emus
516}
517
518var re_memes = regexp.MustCompile("meme: ?([^\n]+)")
519var re_avatar = regexp.MustCompile("avatar: ?([^\n]+)")
520var re_banner = regexp.MustCompile("banner: ?([^\n]+)")
521var re_convoy = regexp.MustCompile("convoy: ?([^\n]+)")
522var re_convalidate = regexp.MustCompile("^(https?|tag|data):")
523
524func memetize(honk *Honk) {
525	repl := func(x string) string {
526		name := x[5:]
527		if name[0] == ' ' {
528			name = name[1:]
529		}
530		fd, err := os.Open(dataDir + "/memes/" + name)
531		if err != nil {
532			ilog.Printf("no meme for %s", name)
533			return x
534		}
535		var peek [512]byte
536		n, _ := fd.Read(peek[:])
537		ct := http.DetectContentType(peek[:n])
538		fd.Close()
539
540		url := serverURL("/meme/%s", name)
541		fileid, err := savefile(name, name, url, ct, false, nil, nil)
542		if err != nil {
543			elog.Printf("error saving meme: %s", err)
544			return x
545		}
546		d := &Donk{
547			FileID: fileid,
548			Name:   name,
549			Media:  ct,
550			URL:    url,
551			Local:  false,
552		}
553		honk.Donks = append(honk.Donks, d)
554		return ""
555	}
556	honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
557}
558
559var re_quickmention = regexp.MustCompile("(^|[ \n])@[[:alnum:]_]+([ \n:;.,']|$)")
560
561func quickrename(s string, userid UserID) string {
562	nonstop := true
563	for nonstop {
564		nonstop = false
565		s = re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
566			prefix := ""
567			if m[0] == ' ' || m[0] == '\n' {
568				prefix = m[:1]
569				m = m[1:]
570			}
571			prefix += "@"
572			m = m[1:]
573			tail := ""
574			if last := m[len(m)-1]; last == ' ' || last == '\n' ||
575				last == ':' || last == ';' ||
576				last == '.' || last == ',' || last == '\'' {
577				tail = m[len(m)-1:]
578				m = m[:len(m)-1]
579			}
580
581			xid := fullname(m, userid)
582
583			if xid != "" {
584				_, name := handles(xid)
585				if name != "" {
586					nonstop = true
587					m = name
588				}
589			}
590			return prefix + m + tail
591		})
592	}
593	return s
594}
595
596var shortnames = gencache.New(gencache.Options[UserID, map[string]string]{Fill: func(userid UserID) (map[string]string, bool) {
597	honkers := gethonkers(userid)
598	m := make(map[string]string)
599	for _, h := range honkers {
600		m[h.XID] = h.Name
601	}
602	return m, true
603}, Invalidator: &honkerinvalidator})
604
605func shortname(userid UserID, xid string) string {
606	m, ok := shortnames.Get(userid)
607	if ok {
608		return m[xid]
609	}
610	return ""
611}
612
613var fullnames = gencache.New(gencache.Options[UserID, map[string]string]{Fill: func(userid UserID) (map[string]string, bool) {
614	honkers := gethonkers(userid)
615	m := make(map[string]string)
616	for _, h := range honkers {
617		m[h.Name] = h.XID
618	}
619	return m, true
620}, Invalidator: &honkerinvalidator})
621
622func fullname(name string, userid UserID) string {
623	m, ok := fullnames.Get(userid)
624	if ok {
625		return m[name]
626	}
627	return ""
628}
629
630func attoreplacer(m string) string {
631	fill := `<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`
632	if strings.HasPrefix(m, "@https://") {
633		return fmt.Sprintf(fill, html.EscapeString(m[1:]), html.EscapeString(m))
634	}
635	where := gofish(m)
636	if where == "" {
637		return m
638	}
639	who := m[0 : 1+strings.IndexByte(m[1:], '@')]
640	return fmt.Sprintf(fill, html.EscapeString(where), html.EscapeString(who))
641}
642
643func ontoreplacer(h string) string {
644	return fmt.Sprintf(`<a class="mention hashtag" href="%s">%s</a>`,
645		serverURL("/o/%s", strings.ToLower(h[1:])), h)
646}
647
648var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
649var re_urlhost = regexp.MustCompile("https://([^/ #)?]+)")
650
651func originate(u string) string {
652	m := re_urlhost.FindStringSubmatch(u)
653	if len(m) > 1 {
654		return m[1]
655	}
656	return ""
657}
658
659var allhandles = gencache.New(gencache.Options[string, string]{Fill: func(xid string) (string, bool) {
660	handle := getxonker(xid, "handle")
661	if handle == "" {
662		dlog.Printf("need to get a handle: %s", xid)
663		info, err := investigate(xid)
664		if err != nil {
665			m := re_unurl.FindStringSubmatch(xid)
666			if len(m) > 2 {
667				handle = m[2]
668			} else {
669				handle = xid
670			}
671		} else {
672			handle = info.Name
673		}
674	}
675	return handle, true
676}})
677
678// handle, handle@host
679func handles(xid string) (string, string) {
680	if xid == "" || xid == thewholeworld || strings.HasSuffix(xid, "/followers") {
681		return "", ""
682	}
683	handle, _ := allhandles.Get(xid)
684	if handle == xid {
685		return xid, xid
686	}
687	return handle, handle + "@" + originate(xid)
688}
689
690func butnottooloud(aud []string) {
691	for i, a := range aud {
692		if strings.HasSuffix(a, "/followers") {
693			aud[i] = ""
694		}
695	}
696}
697
698func loudandproud(aud []string) bool {
699	for _, a := range aud {
700		if a == thewholeworld {
701			return true
702		}
703	}
704	return false
705}
706
707func firstclass(honk *Honk) bool {
708	return honk.Audience[0] == thewholeworld
709}
710
711func oneofakind(a []string) []string {
712	seen := make(map[string]bool)
713	seen[""] = true
714	j := 0
715	for _, s := range a {
716		if !seen[s] {
717			seen[s] = true
718			a[j] = s
719			j++
720		}
721	}
722	if j < len(a)/2 {
723		rv := make([]string, j)
724		copy(rv, a[:j])
725		return rv
726	}
727	return a[:j]
728}
729
730var ziggies = gencache.New(gencache.Options[UserID, *KeyInfo]{Fill: func(userid UserID) (*KeyInfo, bool) {
731	user, ok := somenumberedusers.Get(userid)
732	if !ok {
733		return nil, false
734	}
735	ki := new(KeyInfo)
736	ki.keyname = user.URL + "#key"
737	ki.seckey = user.SecKey
738	return ki, true
739}})
740
741func ziggy(userid UserID) *KeyInfo {
742	ki, _ := ziggies.Get(userid)
743	return ki
744}
745
746var zaggies = gencache.New(gencache.Options[string, httpsig.PublicKey]{Fill: func(keyname string) (httpsig.PublicKey, bool) {
747	data := getxonker(keyname, "pubkey")
748	if data == "" {
749		dlog.Printf("hitting the webs for missing pubkey: %s", keyname)
750		j, err := GetJunk(readyLuserOne, keyname)
751		if err != nil {
752			ilog.Printf("error getting %s pubkey: %s", keyname, err)
753			when := time.Now().UTC().Format(dbtimeformat)
754			stmtSaveXonker.Exec(keyname, "failed", "pubkey", when)
755			return httpsig.PublicKey{}, true
756		}
757		allinjest(originate(keyname), j)
758		data = getxonker(keyname, "pubkey")
759		if data == "" {
760			ilog.Printf("key not found after ingesting")
761			when := time.Now().UTC().Format(dbtimeformat)
762			stmtSaveXonker.Exec(keyname, "failed", "pubkey", when)
763			return httpsig.PublicKey{}, true
764		}
765	}
766	if data == "failed" {
767		ilog.Printf("lookup previously failed key %s", keyname)
768		return httpsig.PublicKey{}, true
769	}
770	_, key, err := httpsig.DecodeKey(data)
771	if err != nil {
772		ilog.Printf("error decoding %s pubkey: %s", keyname, err)
773		return key, true
774	}
775	return key, true
776}, Limit: 512})
777
778func zaggy(keyname string) (httpsig.PublicKey, error) {
779	key, _ := zaggies.Get(keyname)
780	return key, nil
781}
782
783func savingthrow(keyname string) {
784	when := time.Now().Add(-30 * time.Minute).UTC().Format(dbtimeformat)
785	stmtDeleteXonker.Exec(keyname, "pubkey", when)
786	zaggies.Clear(keyname)
787}
788
789func keymatch(keyname string, actor string) string {
790	origin := originate(actor)
791	if origin == originate(keyname) {
792		return origin
793	}
794	return ""
795}
796
797func avatator(n string, uinfo *login.UserInfo) []byte {
798	var a []byte
799	if isurl(n) {
800		if uinfo != nil {
801			j, err := GetJunkFast(UserID(uinfo.UserID), n)
802			if err != nil {
803				dlog.Println("avatating: getting junk:", err)
804				a = avatateautogen(n)
805			}
806			pfpurl, _ := j.GetString("icon", "url")
807			res, err := http.Get(pfpurl)
808			if err != nil {
809				dlog.Println("avatating: getting pfp url:", err)
810				a = avatateautogen(n)
811			} else {
812				defer res.Body.Close()
813
814				pfpbytes, err := io.ReadAll(res.Body)
815				if err != nil {
816					dlog.Println("avatating: bruh shits clapped:", err)
817					a = avatateautogen(n)
818				}
819				a = pfpbytes
820			}
821		} else {
822			a = avatateautogen(n)
823		}
824	} else {
825		a = avatateautogen(n)
826	}
827
828	return a
829}
830
831func xfildate() string {
832	var b [21]byte
833	rand.Read(b[:])
834	now := time.Now().Unix() / 60 / 60 / 24
835	b[2] = byte(now & 63)
836	b[1] = byte((now / 64) & 63)
837	b[0] = byte((now / 64 / 64) & 63)
838	return xcelerate(b[:])
839}