all repos — honk @ f3455b672ccb316c3b770b68c59a74830e0d0803

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