all repos — honk @ a20e95b0574a454fd51195657e01bb3784ff59ee

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}
382
383var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
384
385var emucache = cache.New(cache.Options{Filler: func(ename string) (Emu, bool) {
386	fname := ename[1 : len(ename)-1]
387	_, err := os.Stat(dataDir + "/emus/" + fname + ".png")
388	if err != nil {
389		return Emu{Name: ename, ID: ""}, true
390	}
391	url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
392	return Emu{ID: url, Name: ename}, true
393}, Duration: 10 * time.Second})
394
395func herdofemus(noise string) []Emu {
396	m := re_emus.FindAllString(noise, -1)
397	m = oneofakind(m)
398	var emus []Emu
399	for _, e := range m {
400		var emu Emu
401		emucache.Get(e, &emu)
402		if emu.ID == "" {
403			continue
404		}
405		emus = append(emus, emu)
406	}
407	return emus
408}
409
410var re_memes = regexp.MustCompile("meme: ?([^\n]+)")
411var re_avatar = regexp.MustCompile("avatar: ?([^\n]+)")
412
413func memetize(honk *Honk) {
414	repl := func(x string) string {
415		name := x[5:]
416		if name[0] == ' ' {
417			name = name[1:]
418		}
419		fd, err := os.Open(dataDir + "/memes/" + name)
420		if err != nil {
421			ilog.Printf("no meme for %s", name)
422			return x
423		}
424		var peek [512]byte
425		n, _ := fd.Read(peek[:])
426		ct := http.DetectContentType(peek[:n])
427		fd.Close()
428
429		url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
430		fileid, err := savefile(name, name, url, ct, false, nil)
431		if err != nil {
432			elog.Printf("error saving meme: %s", err)
433			return x
434		}
435		d := &Donk{
436			FileID: fileid,
437			Name:   name,
438			Media:  ct,
439			URL:    url,
440			Local:  false,
441		}
442		honk.Donks = append(honk.Donks, d)
443		return ""
444	}
445	honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
446}
447
448var re_quickmention = regexp.MustCompile("(^|[ \n])@[[:alnum:]]+([ \n.]|$)")
449
450func quickrename(s string, userid int64) string {
451	nonstop := true
452	for nonstop {
453		nonstop = false
454		s = re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
455			prefix := ""
456			if m[0] == ' ' || m[0] == '\n' {
457				prefix = m[:1]
458				m = m[1:]
459			}
460			prefix += "@"
461			m = m[1:]
462			tail := ""
463			if last := m[len(m)-1]; last == ' ' || last == '\n' || last == '.' {
464				tail = m[len(m)-1:]
465				m = m[:len(m)-1]
466			}
467
468			xid := fullname(m, userid)
469
470			if xid != "" {
471				_, name := handles(xid)
472				if name != "" {
473					nonstop = true
474					m = name
475				}
476			}
477			return prefix + m + tail
478		})
479	}
480	return s
481}
482
483var shortnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
484	honkers := gethonkers(userid)
485	m := make(map[string]string)
486	for _, h := range honkers {
487		m[h.XID] = h.Name
488	}
489	return m, true
490}, Invalidator: &honkerinvalidator})
491
492func shortname(userid int64, xid string) string {
493	var m map[string]string
494	ok := shortnames.Get(userid, &m)
495	if ok {
496		return m[xid]
497	}
498	return ""
499}
500
501var fullnames = cache.New(cache.Options{Filler: func(userid int64) (map[string]string, bool) {
502	honkers := gethonkers(userid)
503	m := make(map[string]string)
504	for _, h := range honkers {
505		m[h.Name] = h.XID
506	}
507	return m, true
508}, Invalidator: &honkerinvalidator})
509
510func fullname(name string, userid int64) string {
511	var m map[string]string
512	ok := fullnames.Get(userid, &m)
513	if ok {
514		return m[name]
515	}
516	return ""
517}
518
519func attoreplacer(m string) string {
520	fill := `<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`
521	where := gofish(m)
522	if where == "" {
523		return m
524	}
525	who := m[0 : 1+strings.IndexByte(m[1:], '@')]
526	return fmt.Sprintf(fill, html.EscapeString(where), html.EscapeString(who))
527}
528
529func ontoreplacer(h string) string {
530	return fmt.Sprintf(`<a href="https://%s/o/%s">%s</a>`, serverName,
531		strings.ToLower(h[1:]), h)
532}
533
534var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
535var re_urlhost = regexp.MustCompile("https://([^/ #)]+)")
536
537func originate(u string) string {
538	m := re_urlhost.FindStringSubmatch(u)
539	if len(m) > 1 {
540		return m[1]
541	}
542	return ""
543}
544
545var allhandles = cache.New(cache.Options{Filler: func(xid string) (string, bool) {
546	handle := getxonker(xid, "handle")
547	if handle == "" {
548		dlog.Printf("need to get a handle: %s", xid)
549		info, err := investigate(xid)
550		if err != nil {
551			m := re_unurl.FindStringSubmatch(xid)
552			if len(m) > 2 {
553				handle = m[2]
554			} else {
555				handle = xid
556			}
557		} else {
558			handle = info.Name
559		}
560	}
561	return handle, true
562}})
563
564// handle, handle@host
565func handles(xid string) (string, string) {
566	if xid == "" || xid == thewholeworld || strings.HasSuffix(xid, "/followers") {
567		return "", ""
568	}
569	var handle string
570	allhandles.Get(xid, &handle)
571	if handle == xid {
572		return xid, xid
573	}
574	return handle, handle + "@" + originate(xid)
575}
576
577func butnottooloud(aud []string) {
578	for i, a := range aud {
579		if strings.HasSuffix(a, "/followers") {
580			aud[i] = ""
581		}
582	}
583}
584
585func loudandproud(aud []string) bool {
586	for _, a := range aud {
587		if a == thewholeworld {
588			return true
589		}
590	}
591	return false
592}
593
594func firstclass(honk *Honk) bool {
595	return honk.Audience[0] == thewholeworld
596}
597
598func oneofakind(a []string) []string {
599	seen := make(map[string]bool)
600	seen[""] = true
601	j := 0
602	for _, s := range a {
603		if !seen[s] {
604			seen[s] = true
605			a[j] = s
606			j++
607		}
608	}
609	return a[:j]
610}
611
612var ziggies = cache.New(cache.Options{Filler: func(userid int64) (*KeyInfo, bool) {
613	var user *WhatAbout
614	ok := somenumberedusers.Get(userid, &user)
615	if !ok {
616		return nil, false
617	}
618	ki := new(KeyInfo)
619	ki.keyname = user.URL + "#key"
620	ki.seckey = user.SecKey
621	return ki, true
622}})
623
624func ziggy(userid int64) *KeyInfo {
625	var ki *KeyInfo
626	ziggies.Get(userid, &ki)
627	return ki
628}
629
630var zaggies = cache.New(cache.Options{Filler: func(keyname string) (httpsig.PublicKey, bool) {
631	data := getxonker(keyname, "pubkey")
632	if data == "" {
633		var key httpsig.PublicKey
634		dlog.Printf("hitting the webs for missing pubkey: %s", keyname)
635		j, err := GetJunk(keyname)
636		if err != nil {
637			ilog.Printf("error getting %s pubkey: %s", keyname, err)
638			when := time.Now().UTC().Format(dbtimeformat)
639			stmtSaveXonker.Exec(keyname, "failed", "pubkey", when)
640			return key, true
641		}
642		allinjest(originate(keyname), j)
643		data = getxonker(keyname, "pubkey")
644		if data == "" {
645			ilog.Printf("key not found after ingesting")
646			when := time.Now().UTC().Format(dbtimeformat)
647			stmtSaveXonker.Exec(keyname, "failed", "pubkey", when)
648			return key, true
649		}
650	}
651	_, key, err := httpsig.DecodeKey(data)
652	if err != nil {
653		ilog.Printf("error decoding %s pubkey: %s", keyname, err)
654		return key, true
655	}
656	return key, true
657}, Limit: 512})
658
659func zaggy(keyname string) (httpsig.PublicKey, error) {
660	var key httpsig.PublicKey
661	zaggies.Get(keyname, &key)
662	return key, nil
663}
664
665func savingthrow(keyname string) {
666	when := time.Now().Add(-30 * time.Minute).UTC().Format(dbtimeformat)
667	stmtDeleteXonker.Exec(keyname, "pubkey", when)
668	zaggies.Clear(keyname)
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}