all repos — honk @ 295ec2fa9271db6aac4d03e276a153c5aff25c16

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