all repos — honk @ d5d1e19de1cd44b2de7efe60a525d317206a5acf

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