all repos — honk @ e54ece42b54344f4770eb41c7a8ff101477ccc69

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/htfilter"
 32	"humungus.tedunangst.com/r/webs/httpsig"
 33)
 34
 35func reverbolate(userid int64, honks []*Honk) {
 36	filt := htfilter.New()
 37	filt.Imager = replaceimg
 38	zilences := getzilences(userid)
 39	for _, h := range honks {
 40		h.What += "ed"
 41		if h.What == "tonked" {
 42			h.What = "honked back"
 43			h.Style = "subtle"
 44		}
 45		if !h.Public {
 46			h.Style += " limited"
 47		}
 48		translate(h)
 49		if h.Whofore == 2 || h.Whofore == 3 {
 50			h.URL = h.XID
 51			if h.What != "bonked" {
 52				h.Noise = re_memes.ReplaceAllString(h.Noise, "")
 53				h.Noise = mentionize(h.Noise)
 54				h.Noise = ontologize(h.Noise)
 55			}
 56			h.Username, h.Handle = handles(h.Honker)
 57		} else {
 58			_, h.Handle = handles(h.Honker)
 59			h.Username = h.Handle
 60			if len(h.Username) > 20 {
 61				h.Username = h.Username[:20] + ".."
 62			}
 63			if h.URL == "" {
 64				h.URL = h.XID
 65			}
 66		}
 67		if h.Oonker != "" {
 68			_, h.Oondle = handles(h.Oonker)
 69		}
 70		zap := make(map[*Donk]bool)
 71		h.Precis = unpucker(h.Precis)
 72		h.Noise = unpucker(h.Noise)
 73		h.Open = "open"
 74		if userid == -1 {
 75			if h.Precis != "" {
 76				h.Open = ""
 77			}
 78		} else {
 79			if badword := unsee(zilences, h.Precis, h.Noise, h.Donks); badword != "" {
 80				if h.Precis == "" {
 81					h.Precis = badword
 82				}
 83				h.Open = ""
 84			} else if h.Precis == "unspecified horror" {
 85				h.Precis = ""
 86			}
 87		}
 88		if len(h.Noise) > 4000 && h.Open == "open" {
 89			if h.Precis == "" {
 90				h.Precis = "really freaking long"
 91			}
 92			h.Open = ""
 93		}
 94
 95		h.HTPrecis, _ = filt.String(h.Precis)
 96		h.HTML, _ = filt.String(h.Noise)
 97		emuxifier := func(e string) string {
 98			for _, d := range h.Donks {
 99				if d.Name == e {
100					zap[d] = true
101					if d.Local {
102						return fmt.Sprintf(`<img class="emu" title="%s" src="/d/%s">`, d.Name, d.XID)
103					}
104				}
105			}
106			return e
107		}
108		h.HTPrecis = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTPrecis), emuxifier))
109		h.HTML = template.HTML(re_emus.ReplaceAllStringFunc(string(h.HTML), emuxifier))
110		j := 0
111		for i := 0; i < len(h.Donks); i++ {
112			if !zap[h.Donks[i]] {
113				h.Donks[j] = h.Donks[i]
114				j++
115			}
116		}
117		h.Donks = h.Donks[:j]
118	}
119}
120
121func replaceimg(node *html.Node) string {
122	src := htfilter.GetAttr(node, "src")
123	alt := htfilter.GetAttr(node, "alt")
124	//title := GetAttr(node, "title")
125	if htfilter.HasClass(node, "Emoji") && alt != "" {
126		return alt
127	}
128	alt = html.EscapeString(alt)
129	src = html.EscapeString(src)
130	d := finddonk(src)
131	if d != nil {
132		src = fmt.Sprintf("https://%s/d/%s", serverName, d.XID)
133		return fmt.Sprintf(`<img alt="%s" title="%s" src="%s">`, alt, alt, src)
134	}
135	return fmt.Sprintf(`&lt;img alt="%s" src="<a href="%s">%s<a>"&gt;`, alt, src, src)
136}
137
138func inlineimgs(node *html.Node) string {
139	src := htfilter.GetAttr(node, "src")
140	alt := htfilter.GetAttr(node, "alt")
141	//title := GetAttr(node, "title")
142	if htfilter.HasClass(node, "Emoji") && alt != "" {
143		return alt
144	}
145	alt = html.EscapeString(alt)
146	src = html.EscapeString(src)
147	if !strings.HasPrefix(src, "https://"+serverName+"/") {
148		d := savedonk(src, "image", alt, "image", true)
149		if d != nil {
150			src = fmt.Sprintf("https://%s/d/%s", serverName, d.XID)
151		}
152	}
153	log.Printf("inline img with src: %s", src)
154	return fmt.Sprintf(`<img alt="%s" title="%s" src="%s>`, alt, alt, src)
155}
156
157func translate(honk *Honk) {
158	if honk.Format == "html" {
159		return
160	}
161	noise := honk.Noise
162	if strings.HasPrefix(noise, "DZ:") {
163		idx := strings.Index(noise, "\n")
164		if idx == -1 {
165			honk.Precis = noise
166			noise = ""
167		} else {
168			honk.Precis = noise[:idx]
169			noise = noise[idx+1:]
170		}
171	}
172	honk.Precis = strings.TrimSpace(honk.Precis)
173
174	noise = strings.TrimSpace(noise)
175	noise = quickrename(noise, honk.UserID)
176	noise = obfusbreak(noise)
177
178	honk.Noise = noise
179	honk.Onts = oneofakind(ontologies(honk.Noise))
180}
181
182func unsee(zilences []*regexp.Regexp, precis string, noise string, donks []*Donk) string {
183	for _, z := range zilences {
184		if z.MatchString(precis) || z.MatchString(noise) {
185			if precis == "" {
186				w := z.String()
187				return w[6 : len(w)-3]
188			}
189			return precis
190		}
191		for _, d := range donks {
192			if z.MatchString(d.Desc) {
193				if precis == "" {
194					w := z.String()
195					return w[6 : len(w)-3]
196				}
197				return precis
198			}
199		}
200	}
201	return ""
202}
203
204func osmosis(honks []*Honk, userid int64) []*Honk {
205	zords := getzords(userid)
206	j := 0
207outer:
208	for _, h := range honks {
209		for _, z := range zords {
210			if z.MatchString(h.Precis) || z.MatchString(h.Noise) {
211				continue outer
212			}
213			for _, d := range h.Donks {
214				if z.MatchString(d.Desc) {
215					continue outer
216				}
217			}
218		}
219		honks[j] = h
220		j++
221	}
222	honks = honks[0:j]
223	return honks
224}
225
226func shortxid(xid string) string {
227	idx := strings.LastIndexByte(xid, '/')
228	if idx == -1 {
229		return xid
230	}
231	return xid[idx+1:]
232}
233
234func xfiltrate() string {
235	letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
236	var b [18]byte
237	rand.Read(b[:])
238	for i, c := range b {
239		b[i] = letters[c&63]
240	}
241	s := string(b[:])
242	return s
243}
244
245var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
246
247func ontologies(s string) []string {
248	m := re_hashes.FindAllString(s, -1)
249	j := 0
250	for _, h := range m {
251		if h[0] == '&' {
252			continue
253		}
254		if h[0] != '#' {
255			h = h[1:]
256		}
257		m[j] = h
258		j++
259	}
260	return m[:j]
261}
262
263type Mention struct {
264	who   string
265	where string
266}
267
268var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
269var re_urltions = regexp.MustCompile(`@https://\S+`)
270
271func grapevine(s string) []string {
272	var mentions []string
273	m := re_mentions.FindAllString(s, -1)
274	for i := range m {
275		where := gofish(m[i])
276		if where != "" {
277			mentions = append(mentions, where)
278		}
279	}
280	m = re_urltions.FindAllString(s, -1)
281	for i := range m {
282		mentions = append(mentions, m[i][1:])
283	}
284	return mentions
285}
286
287func bunchofgrapes(s string) []Mention {
288	m := re_mentions.FindAllString(s, -1)
289	var mentions []Mention
290	for i := range m {
291		where := gofish(m[i])
292		if where != "" {
293			mentions = append(mentions, Mention{who: m[i], where: where})
294		}
295	}
296	m = re_urltions.FindAllString(s, -1)
297	for i := range m {
298		mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
299	}
300	return mentions
301}
302
303type Emu struct {
304	ID   string
305	Name string
306}
307
308var re_link = regexp.MustCompile(`@?https?://[^\s"]+[\w/)]`)
309var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
310
311func herdofemus(noise string) []Emu {
312	m := re_emus.FindAllString(noise, -1)
313	m = oneofakind(m)
314	var emus []Emu
315	for _, e := range m {
316		fname := e[1 : len(e)-1]
317		_, err := os.Stat("emus/" + fname + ".png")
318		if err != nil {
319			continue
320		}
321		url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
322		emus = append(emus, Emu{ID: url, Name: e})
323	}
324	return emus
325}
326
327var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
328
329func memetize(honk *Honk) {
330	repl := func(x string) string {
331		name := x[5:]
332		if name[0] == ' ' {
333			name = name[1:]
334		}
335		fd, err := os.Open("memes/" + name)
336		if err != nil {
337			log.Printf("no meme for %s", name)
338			return x
339		}
340		var peek [512]byte
341		n, _ := fd.Read(peek[:])
342		ct := http.DetectContentType(peek[:n])
343		fd.Close()
344
345		url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
346		fileid, err := savefile("", name, name, url, ct, false, nil)
347		if err != nil {
348			log.Printf("error saving meme: %s", err)
349			return x
350		}
351		var d Donk
352		d.FileID = fileid
353		d.XID = ""
354		d.Name = name
355		d.Media = ct
356		d.URL = url
357		d.Local = false
358		honk.Donks = append(honk.Donks, &d)
359		return ""
360	}
361	honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
362}
363
364var re_bolder = regexp.MustCompile(`(^|\W)\*\*([\w\s,.!?':_-]+)\*\*($|\W)`)
365var re_italicer = regexp.MustCompile(`(^|\W)\*([\w\s,.!?':_-]+)\*($|\W)`)
366var re_bigcoder = regexp.MustCompile("```\n?((?s:.*?))\n?```\n?")
367var re_coder = regexp.MustCompile("`([^`]*)`")
368var re_quoter = regexp.MustCompile(`(?m:^&gt; (.*)\n?)`)
369
370func markitzero(s string) string {
371	var bigcodes []string
372	bigsaver := func(code string) string {
373		bigcodes = append(bigcodes, code)
374		return "``````"
375	}
376	s = re_bigcoder.ReplaceAllStringFunc(s, bigsaver)
377	var lilcodes []string
378	lilsaver := func(code string) string {
379		lilcodes = append(lilcodes, code)
380		return "`x`"
381	}
382	s = re_coder.ReplaceAllStringFunc(s, lilsaver)
383	s = re_bolder.ReplaceAllString(s, "$1<b>$2</b>$3")
384	s = re_italicer.ReplaceAllString(s, "$1<i>$2</i>$3")
385	s = re_quoter.ReplaceAllString(s, "<blockquote>$1</blockquote><p>")
386	lilun := func(s string) string {
387		code := lilcodes[0]
388		lilcodes = lilcodes[1:]
389		return code
390	}
391	s = re_coder.ReplaceAllStringFunc(s, lilun)
392	bigun := func(s string) string {
393		code := bigcodes[0]
394		bigcodes = bigcodes[1:]
395		return code
396	}
397	s = re_bigcoder.ReplaceAllStringFunc(s, bigun)
398	s = re_bigcoder.ReplaceAllString(s, "<pre><code>$1</code></pre><p>")
399	s = re_coder.ReplaceAllString(s, "<code>$1</code>")
400	return s
401}
402
403func obfusbreak(s string) string {
404	s = strings.TrimSpace(s)
405	s = strings.Replace(s, "\r", "", -1)
406	s = html.EscapeString(s)
407	// dammit go
408	s = strings.Replace(s, "&#39;", "'", -1)
409	linkfn := func(url string) string {
410		if url[0] == '@' {
411			return url
412		}
413		addparen := false
414		adddot := false
415		if strings.HasSuffix(url, ")") && strings.IndexByte(url, '(') == -1 {
416			url = url[:len(url)-1]
417			addparen = true
418		}
419		if strings.HasSuffix(url, ".") {
420			url = url[:len(url)-1]
421			adddot = true
422		}
423		url = fmt.Sprintf(`<a class="mention u-url" href="%s">%s</a>`, url, url)
424		if adddot {
425			url += "."
426		}
427		if addparen {
428			url += ")"
429		}
430		return url
431	}
432	s = re_link.ReplaceAllStringFunc(s, linkfn)
433
434	s = markitzero(s)
435
436	s = strings.Replace(s, "\n", "<br>", -1)
437	return s
438}
439
440var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+ ")
441
442func quickrename(s string, userid int64) string {
443	return re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
444		prefix := ""
445		if m[0] == ' ' {
446			prefix = " "
447			m = m[1:]
448		}
449		prefix += "@"
450		m = m[1:]
451		m = m[:len(m)-1]
452
453		row := stmtOneHonker.QueryRow(m, userid)
454		var xid string
455		err := row.Scan(&xid)
456		if err == nil {
457			_, name := handles(xid)
458			if name != "" {
459				m = name
460			}
461		}
462		return prefix + m + " "
463	})
464}
465
466func mentionize(s string) string {
467	s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
468		where := gofish(m)
469		if where == "" {
470			return m
471		}
472		who := m[0 : 1+strings.IndexByte(m[1:], '@')]
473		return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
474			html.EscapeString(where), html.EscapeString(who))
475	})
476	s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
477		return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
478			html.EscapeString(m[1:]), html.EscapeString(m))
479	})
480	return s
481}
482
483func ontologize(s string) string {
484	s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
485		if o[0] == '&' {
486			return o
487		}
488		p := ""
489		h := o
490		if h[0] != '#' {
491			p = h[:1]
492			h = h[1:]
493		}
494		return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
495			strings.ToLower(h[1:]), h)
496	})
497	return s
498}
499
500var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
501var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
502
503func originate(u string) string {
504	m := re_urlhost.FindStringSubmatch(u)
505	if len(m) > 1 {
506		return m[1]
507	}
508	return ""
509}
510
511var allhandles = make(map[string]string)
512var handlelock sync.Mutex
513
514// handle, handle@host
515func handles(xid string) (string, string) {
516	if xid == "" {
517		return "", ""
518	}
519	handlelock.Lock()
520	handle := allhandles[xid]
521	handlelock.Unlock()
522	if handle == "" {
523		handle = findhandle(xid)
524		handlelock.Lock()
525		allhandles[xid] = handle
526		handlelock.Unlock()
527	}
528	if handle == xid {
529		return xid, xid
530	}
531	return handle, handle + "@" + originate(xid)
532}
533
534func findhandle(xid string) string {
535	row := stmtGetXonker.QueryRow(xid, "handle")
536	var handle string
537	err := row.Scan(&handle)
538	if err != nil {
539		p, _ := investigate(xid)
540		if p == nil {
541			m := re_unurl.FindStringSubmatch(xid)
542			if len(m) > 2 {
543				handle = m[2]
544			} else {
545				handle = xid
546			}
547		} else {
548			handle = p.Handle
549		}
550		_, err = stmtSaveXonker.Exec(xid, handle, "handle")
551		if err != nil {
552			log.Printf("error saving handle: %s", err)
553		}
554	}
555	return handle
556}
557
558var handleprelock sync.Mutex
559
560func prehandle(xid string) {
561	handleprelock.Lock()
562	defer handleprelock.Unlock()
563	handles(xid)
564}
565
566func prepend(s string, x []string) []string {
567	return append([]string{s}, x...)
568}
569
570// pleroma leaks followers addressed posts to followers
571func butnottooloud(aud []string) {
572	for i, a := range aud {
573		if strings.HasSuffix(a, "/followers") {
574			aud[i] = ""
575		}
576	}
577}
578
579func keepitquiet(aud []string) bool {
580	for _, a := range aud {
581		if a == thewholeworld {
582			return false
583		}
584	}
585	return true
586}
587
588func firstclass(honk *Honk) bool {
589	return honk.Audience[0] == thewholeworld
590}
591
592func oneofakind(a []string) []string {
593	var x []string
594	for n, s := range a {
595		if s != "" {
596			x = append(x, s)
597			for i := n + 1; i < len(a); i++ {
598				if a[i] == s {
599					a[i] = ""
600				}
601			}
602		}
603	}
604	return x
605}
606
607var ziggies = make(map[string]*rsa.PrivateKey)
608var zaggies = make(map[string]*rsa.PublicKey)
609var ziggylock sync.Mutex
610
611func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
612	ziggylock.Lock()
613	key = ziggies[username]
614	ziggylock.Unlock()
615	if key == nil {
616		db := opendatabase()
617		row := db.QueryRow("select seckey from users where username = ?", username)
618		var data string
619		row.Scan(&data)
620		var err error
621		key, _, err = httpsig.DecodeKey(data)
622		if err != nil {
623			log.Printf("error decoding %s seckey: %s", username, err)
624			return
625		}
626		ziggylock.Lock()
627		ziggies[username] = key
628		ziggylock.Unlock()
629	}
630	keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
631	return
632}
633
634func zaggy(keyname string) (key *rsa.PublicKey) {
635	ziggylock.Lock()
636	key = zaggies[keyname]
637	ziggylock.Unlock()
638	if key != nil {
639		return
640	}
641	row := stmtGetXonker.QueryRow(keyname, "pubkey")
642	var data string
643	err := row.Scan(&data)
644	if err != nil {
645		log.Printf("hitting the webs for missing pubkey: %s", keyname)
646		j, err := GetJunk(keyname)
647		if err != nil {
648			log.Printf("error getting %s pubkey: %s", keyname, err)
649			return
650		}
651		keyobj, ok := j.GetMap("publicKey")
652		if ok {
653			j = keyobj
654		}
655		data, ok = j.GetString("publicKeyPem")
656		if !ok {
657			log.Printf("error finding %s pubkey", keyname)
658			return
659		}
660		_, ok = j.GetString("owner")
661		if !ok {
662			log.Printf("error finding %s pubkey owner", keyname)
663			return
664		}
665		_, key, err = httpsig.DecodeKey(data)
666		if err != nil {
667			log.Printf("error decoding %s pubkey: %s", keyname, err)
668			return
669		}
670		_, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
671		if err != nil {
672			log.Printf("error saving key: %s", err)
673		}
674	} else {
675		_, key, err = httpsig.DecodeKey(data)
676		if err != nil {
677			log.Printf("error decoding %s pubkey: %s", keyname, err)
678			return
679		}
680	}
681	ziggylock.Lock()
682	zaggies[keyname] = key
683	ziggylock.Unlock()
684	return
685}
686
687func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
688	_, err := stmtDeleteXonker.Exec(keyname, "pubkey")
689	if err != nil {
690		log.Printf("error deleting key: %s", err)
691	}
692	ziggylock.Lock()
693	delete(zaggies, keyname)
694	ziggylock.Unlock()
695	return httpsig.VerifyRequest(r, payload, zaggy)
696}
697
698var thumbbiters map[int64]map[string]bool
699var zoggles map[int64]map[string]bool
700var zordses map[int64][]*regexp.Regexp
701var zilences map[int64][]*regexp.Regexp
702var thumblock sync.Mutex
703
704func bitethethumbs() {
705	rows, err := stmtThumbBiters.Query()
706	if err != nil {
707		log.Printf("error getting thumbbiters: %s", err)
708		return
709	}
710	defer rows.Close()
711
712	thumblock.Lock()
713	defer thumblock.Unlock()
714	thumbbiters = make(map[int64]map[string]bool)
715	zoggles = make(map[int64]map[string]bool)
716	zordses = make(map[int64][]*regexp.Regexp)
717	zilences = make(map[int64][]*regexp.Regexp)
718	for rows.Next() {
719		var userid int64
720		var name, wherefore string
721		err = rows.Scan(&userid, &name, &wherefore)
722		if err != nil {
723			log.Printf("error scanning zonker: %s", err)
724			continue
725		}
726		if wherefore == "zord" || wherefore == "zilence" {
727			zord := "\\b(?i:" + name + ")\\b"
728			re, err := regexp.Compile(zord)
729			if err != nil {
730				log.Printf("error compiling zord: %s", err)
731			} else {
732				if wherefore == "zord" {
733					zordses[userid] = append(zordses[userid], re)
734				} else {
735					zilences[userid] = append(zilences[userid], re)
736				}
737			}
738		}
739		if wherefore == "zoggle" {
740			m := zoggles[userid]
741			if m == nil {
742				m = make(map[string]bool)
743				zoggles[userid] = m
744			}
745			m[name] = true
746		}
747		if wherefore == "zonker" || wherefore == "zomain" {
748			m := thumbbiters[userid]
749			if m == nil {
750				m = make(map[string]bool)
751				thumbbiters[userid] = m
752			}
753			m[name] = true
754		}
755	}
756}
757
758func getzords(userid int64) []*regexp.Regexp {
759	thumblock.Lock()
760	defer thumblock.Unlock()
761	return zordses[userid]
762}
763
764func getzilences(userid int64) []*regexp.Regexp {
765	thumblock.Lock()
766	defer thumblock.Unlock()
767	return zilences[userid]
768}
769
770func thoudostbitethythumb(userid int64, who []string, objid string) bool {
771	thumblock.Lock()
772	biters := thumbbiters[userid]
773	thumblock.Unlock()
774	objwhere := originate(objid)
775	if objwhere != "" && biters[objwhere] {
776		log.Printf("thumbbiter: %s", objid)
777		return true
778	}
779	for _, w := range who {
780		if biters[w] {
781			log.Printf("thumbbiter: %s", w)
782			return true
783		}
784		where := originate(w)
785		if where != "" {
786			if biters[where] {
787				log.Printf("thumbbiter: %s", w)
788				return true
789			}
790		}
791	}
792	return false
793}
794
795func stealthmode(userid int64, r *http.Request) bool {
796	agent := r.UserAgent()
797	agent = originate(agent)
798	addr := r.Header.Get("X-Forwarded-For")
799	thumblock.Lock()
800	biters := thumbbiters[userid]
801	thumblock.Unlock()
802	fake := (agent != "" && biters[agent]) || (addr != "" && biters[addr])
803	if fake {
804		log.Printf("faking 404 for %s from %s", agent, addr)
805	}
806	return fake
807}
808
809func keymatch(keyname string, actor string) string {
810	hash := strings.IndexByte(keyname, '#')
811	if hash == -1 {
812		hash = len(keyname)
813	}
814	owner := keyname[0:hash]
815	if owner == actor {
816		return originate(actor)
817	}
818	return ""
819}