all repos — honk @ fa6917a048b9c40caa63396291e74da2ca144da8

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 := getfilters(userid, filtCollapse)
 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); 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 shortxid(xid string) string {
183	idx := strings.LastIndexByte(xid, '/')
184	if idx == -1 {
185		return xid
186	}
187	return xid[idx+1:]
188}
189
190func xfiltrate() string {
191	letters := "BCDFGHJKLMNPQRSTVWXYZbcdfghjklmnpqrstvwxyz1234567891234567891234"
192	var b [18]byte
193	rand.Read(b[:])
194	for i, c := range b {
195		b[i] = letters[c&63]
196	}
197	s := string(b[:])
198	return s
199}
200
201var re_hashes = regexp.MustCompile(`(?:^| )#[[:alnum:]][[:alnum:]_-]*`)
202
203func ontologies(s string) []string {
204	m := re_hashes.FindAllString(s, -1)
205	j := 0
206	for _, h := range m {
207		if h[0] == '&' {
208			continue
209		}
210		if h[0] != '#' {
211			h = h[1:]
212		}
213		m[j] = h
214		j++
215	}
216	return m[:j]
217}
218
219type Mention struct {
220	who   string
221	where string
222}
223
224var re_mentions = regexp.MustCompile(`@[[:alnum:]._-]+@[[:alnum:].-]*[[:alnum:]]`)
225var re_urltions = regexp.MustCompile(`@https://\S+`)
226
227func grapevine(s string) []string {
228	var mentions []string
229	m := re_mentions.FindAllString(s, -1)
230	for i := range m {
231		where := gofish(m[i])
232		if where != "" {
233			mentions = append(mentions, where)
234		}
235	}
236	m = re_urltions.FindAllString(s, -1)
237	for i := range m {
238		mentions = append(mentions, m[i][1:])
239	}
240	return mentions
241}
242
243func bunchofgrapes(s string) []Mention {
244	m := re_mentions.FindAllString(s, -1)
245	var mentions []Mention
246	for i := range m {
247		where := gofish(m[i])
248		if where != "" {
249			mentions = append(mentions, Mention{who: m[i], where: where})
250		}
251	}
252	m = re_urltions.FindAllString(s, -1)
253	for i := range m {
254		mentions = append(mentions, Mention{who: m[i][1:], where: m[i][1:]})
255	}
256	return mentions
257}
258
259type Emu struct {
260	ID   string
261	Name string
262}
263
264var re_link = regexp.MustCompile(`@?https?://[^\s"]+[\w/)]`)
265var re_emus = regexp.MustCompile(`:[[:alnum:]_-]+:`)
266
267func herdofemus(noise string) []Emu {
268	m := re_emus.FindAllString(noise, -1)
269	m = oneofakind(m)
270	var emus []Emu
271	for _, e := range m {
272		fname := e[1 : len(e)-1]
273		_, err := os.Stat("emus/" + fname + ".png")
274		if err != nil {
275			continue
276		}
277		url := fmt.Sprintf("https://%s/emu/%s.png", serverName, fname)
278		emus = append(emus, Emu{ID: url, Name: e})
279	}
280	return emus
281}
282
283var re_memes = regexp.MustCompile("meme: ?([[:alnum:]_.-]+)")
284
285func memetize(honk *Honk) {
286	repl := func(x string) string {
287		name := x[5:]
288		if name[0] == ' ' {
289			name = name[1:]
290		}
291		fd, err := os.Open("memes/" + name)
292		if err != nil {
293			log.Printf("no meme for %s", name)
294			return x
295		}
296		var peek [512]byte
297		n, _ := fd.Read(peek[:])
298		ct := http.DetectContentType(peek[:n])
299		fd.Close()
300
301		url := fmt.Sprintf("https://%s/meme/%s", serverName, name)
302		fileid, err := savefile("", name, name, url, ct, false, nil)
303		if err != nil {
304			log.Printf("error saving meme: %s", err)
305			return x
306		}
307		var d Donk
308		d.FileID = fileid
309		d.XID = ""
310		d.Name = name
311		d.Media = ct
312		d.URL = url
313		d.Local = false
314		honk.Donks = append(honk.Donks, &d)
315		return ""
316	}
317	honk.Noise = re_memes.ReplaceAllStringFunc(honk.Noise, repl)
318}
319
320var re_bolder = regexp.MustCompile(`(^|\W)\*\*([\w\s,.!?':_-]+)\*\*($|\W)`)
321var re_italicer = regexp.MustCompile(`(^|\W)\*([\w\s,.!?':_-]+)\*($|\W)`)
322var re_bigcoder = regexp.MustCompile("```\n?((?s:.*?))\n?```\n?")
323var re_coder = regexp.MustCompile("`([^`]*)`")
324var re_quoter = regexp.MustCompile(`(?m:^&gt; (.*)\n?)`)
325
326func markitzero(s string) string {
327	var bigcodes []string
328	bigsaver := func(code string) string {
329		bigcodes = append(bigcodes, code)
330		return "``````"
331	}
332	s = re_bigcoder.ReplaceAllStringFunc(s, bigsaver)
333	var lilcodes []string
334	lilsaver := func(code string) string {
335		lilcodes = append(lilcodes, code)
336		return "`x`"
337	}
338	s = re_coder.ReplaceAllStringFunc(s, lilsaver)
339	s = re_bolder.ReplaceAllString(s, "$1<b>$2</b>$3")
340	s = re_italicer.ReplaceAllString(s, "$1<i>$2</i>$3")
341	s = re_quoter.ReplaceAllString(s, "<blockquote>$1</blockquote><p>")
342	lilun := func(s string) string {
343		code := lilcodes[0]
344		lilcodes = lilcodes[1:]
345		return code
346	}
347	s = re_coder.ReplaceAllStringFunc(s, lilun)
348	bigun := func(s string) string {
349		code := bigcodes[0]
350		bigcodes = bigcodes[1:]
351		return code
352	}
353	s = re_bigcoder.ReplaceAllStringFunc(s, bigun)
354	s = re_bigcoder.ReplaceAllString(s, "<pre><code>$1</code></pre><p>")
355	s = re_coder.ReplaceAllString(s, "<code>$1</code>")
356	return s
357}
358
359func obfusbreak(s string) string {
360	s = strings.TrimSpace(s)
361	s = strings.Replace(s, "\r", "", -1)
362	s = html.EscapeString(s)
363	// dammit go
364	s = strings.Replace(s, "&#39;", "'", -1)
365	linkfn := func(url string) string {
366		if url[0] == '@' {
367			return url
368		}
369		addparen := false
370		adddot := false
371		if strings.HasSuffix(url, ")") && strings.IndexByte(url, '(') == -1 {
372			url = url[:len(url)-1]
373			addparen = true
374		}
375		if strings.HasSuffix(url, ".") {
376			url = url[:len(url)-1]
377			adddot = true
378		}
379		url = fmt.Sprintf(`<a class="mention u-url" href="%s">%s</a>`, url, url)
380		if adddot {
381			url += "."
382		}
383		if addparen {
384			url += ")"
385		}
386		return url
387	}
388	s = re_link.ReplaceAllStringFunc(s, linkfn)
389
390	s = markitzero(s)
391
392	s = strings.Replace(s, "\n", "<br>", -1)
393	return s
394}
395
396var re_quickmention = regexp.MustCompile("(^| )@[[:alnum:]]+ ")
397
398func quickrename(s string, userid int64) string {
399	return re_quickmention.ReplaceAllStringFunc(s, func(m string) string {
400		prefix := ""
401		if m[0] == ' ' {
402			prefix = " "
403			m = m[1:]
404		}
405		prefix += "@"
406		m = m[1:]
407		m = m[:len(m)-1]
408
409		row := stmtOneHonker.QueryRow(m, userid)
410		var xid string
411		err := row.Scan(&xid)
412		if err == nil {
413			_, name := handles(xid)
414			if name != "" {
415				m = name
416			}
417		}
418		return prefix + m + " "
419	})
420}
421
422func mentionize(s string) string {
423	s = re_mentions.ReplaceAllStringFunc(s, func(m string) string {
424		where := gofish(m)
425		if where == "" {
426			return m
427		}
428		who := m[0 : 1+strings.IndexByte(m[1:], '@')]
429		return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
430			html.EscapeString(where), html.EscapeString(who))
431	})
432	s = re_urltions.ReplaceAllStringFunc(s, func(m string) string {
433		return fmt.Sprintf(`<span class="h-card"><a class="u-url mention" href="%s">%s</a></span>`,
434			html.EscapeString(m[1:]), html.EscapeString(m))
435	})
436	return s
437}
438
439func ontologize(s string) string {
440	s = re_hashes.ReplaceAllStringFunc(s, func(o string) string {
441		if o[0] == '&' {
442			return o
443		}
444		p := ""
445		h := o
446		if h[0] != '#' {
447			p = h[:1]
448			h = h[1:]
449		}
450		return fmt.Sprintf(`%s<a class="mention u-url" href="https://%s/o/%s">%s</a>`, p, serverName,
451			strings.ToLower(h[1:]), h)
452	})
453	return s
454}
455
456var re_unurl = regexp.MustCompile("https://([^/]+).*/([^/]+)")
457var re_urlhost = regexp.MustCompile("https://([^/ ]+)")
458
459func originate(u string) string {
460	m := re_urlhost.FindStringSubmatch(u)
461	if len(m) > 1 {
462		return m[1]
463	}
464	return ""
465}
466
467var allhandles = make(map[string]string)
468var handlelock sync.Mutex
469
470// handle, handle@host
471func handles(xid string) (string, string) {
472	if xid == "" {
473		return "", ""
474	}
475	handlelock.Lock()
476	handle := allhandles[xid]
477	handlelock.Unlock()
478	if handle == "" {
479		handle = findhandle(xid)
480		handlelock.Lock()
481		allhandles[xid] = handle
482		handlelock.Unlock()
483	}
484	if handle == xid {
485		return xid, xid
486	}
487	return handle, handle + "@" + originate(xid)
488}
489
490func findhandle(xid string) string {
491	row := stmtGetXonker.QueryRow(xid, "handle")
492	var handle string
493	err := row.Scan(&handle)
494	if err != nil {
495		p, _ := investigate(xid)
496		if p == nil {
497			m := re_unurl.FindStringSubmatch(xid)
498			if len(m) > 2 {
499				handle = m[2]
500			} else {
501				handle = xid
502			}
503		} else {
504			handle = p.Handle
505		}
506		_, err = stmtSaveXonker.Exec(xid, handle, "handle")
507		if err != nil {
508			log.Printf("error saving handle: %s", err)
509		}
510	}
511	return handle
512}
513
514var handleprelock sync.Mutex
515
516func prehandle(xid string) {
517	handleprelock.Lock()
518	defer handleprelock.Unlock()
519	handles(xid)
520}
521
522func prepend(s string, x []string) []string {
523	return append([]string{s}, x...)
524}
525
526// pleroma leaks followers addressed posts to followers
527func butnottooloud(aud []string) {
528	for i, a := range aud {
529		if strings.HasSuffix(a, "/followers") {
530			aud[i] = ""
531		}
532	}
533}
534
535func keepitquiet(aud []string) bool {
536	for _, a := range aud {
537		if a == thewholeworld {
538			return false
539		}
540	}
541	return true
542}
543
544func firstclass(honk *Honk) bool {
545	return honk.Audience[0] == thewholeworld
546}
547
548func oneofakind(a []string) []string {
549	var x []string
550	for n, s := range a {
551		if s != "" {
552			x = append(x, s)
553			for i := n + 1; i < len(a); i++ {
554				if a[i] == s {
555					a[i] = ""
556				}
557			}
558		}
559	}
560	return x
561}
562
563var ziggies = make(map[string]*rsa.PrivateKey)
564var zaggies = make(map[string]*rsa.PublicKey)
565var ziggylock sync.Mutex
566
567func ziggy(username string) (keyname string, key *rsa.PrivateKey) {
568	ziggylock.Lock()
569	key = ziggies[username]
570	ziggylock.Unlock()
571	if key == nil {
572		db := opendatabase()
573		row := db.QueryRow("select seckey from users where username = ?", username)
574		var data string
575		row.Scan(&data)
576		var err error
577		key, _, err = httpsig.DecodeKey(data)
578		if err != nil {
579			log.Printf("error decoding %s seckey: %s", username, err)
580			return
581		}
582		ziggylock.Lock()
583		ziggies[username] = key
584		ziggylock.Unlock()
585	}
586	keyname = fmt.Sprintf("https://%s/%s/%s#key", serverName, userSep, username)
587	return
588}
589
590func zaggy(keyname string) (key *rsa.PublicKey) {
591	ziggylock.Lock()
592	key = zaggies[keyname]
593	ziggylock.Unlock()
594	if key != nil {
595		return
596	}
597	row := stmtGetXonker.QueryRow(keyname, "pubkey")
598	var data string
599	err := row.Scan(&data)
600	if err != nil {
601		log.Printf("hitting the webs for missing pubkey: %s", keyname)
602		j, err := GetJunk(keyname)
603		if err != nil {
604			log.Printf("error getting %s pubkey: %s", keyname, err)
605			return
606		}
607		keyobj, ok := j.GetMap("publicKey")
608		if ok {
609			j = keyobj
610		}
611		data, ok = j.GetString("publicKeyPem")
612		if !ok {
613			log.Printf("error finding %s pubkey", keyname)
614			return
615		}
616		_, ok = j.GetString("owner")
617		if !ok {
618			log.Printf("error finding %s pubkey owner", keyname)
619			return
620		}
621		_, key, err = httpsig.DecodeKey(data)
622		if err != nil {
623			log.Printf("error decoding %s pubkey: %s", keyname, err)
624			return
625		}
626		_, err = stmtSaveXonker.Exec(keyname, data, "pubkey")
627		if err != nil {
628			log.Printf("error saving key: %s", err)
629		}
630	} else {
631		_, key, err = httpsig.DecodeKey(data)
632		if err != nil {
633			log.Printf("error decoding %s pubkey: %s", keyname, err)
634			return
635		}
636	}
637	ziggylock.Lock()
638	zaggies[keyname] = key
639	ziggylock.Unlock()
640	return
641}
642
643func makeitworksomehowwithoutregardforkeycontinuity(keyname string, r *http.Request, payload []byte) (string, error) {
644	_, err := stmtDeleteXonker.Exec(keyname, "pubkey")
645	if err != nil {
646		log.Printf("error deleting key: %s", err)
647	}
648	ziggylock.Lock()
649	delete(zaggies, keyname)
650	ziggylock.Unlock()
651	return httpsig.VerifyRequest(r, payload, zaggy)
652}
653
654func keymatch(keyname string, actor string) string {
655	hash := strings.IndexByte(keyname, '#')
656	if hash == -1 {
657		hash = len(keyname)
658	}
659	owner := keyname[0:hash]
660	if owner == actor {
661		return originate(actor)
662	}
663	return ""
664}