all repos — honk @ fff5bd19908188b5b0ef05bd960c95530724dc6b

my fork of honk

hfcs.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	"net/http"
 20	"regexp"
 21	"sort"
 22	"strconv"
 23	"strings"
 24	"time"
 25	"unicode"
 26
 27	"humungus.tedunangst.com/r/webs/cache"
 28	"humungus.tedunangst.com/r/webs/gencache"
 29	"humungus.tedunangst.com/r/webs/login"
 30)
 31
 32type Filter struct {
 33	ID              int64      `json:"-"`
 34	Actions         []filtType `json:"-"`
 35	Name            string
 36	Date            time.Time
 37	Actor           string `json:",omitempty"`
 38	IncludeAudience bool   `json:",omitempty"`
 39	OnlyUnknowns    bool   `json:",omitempty"`
 40	Text            string `json:",omitempty"`
 41	re_text         *regexp.Regexp
 42	IsReply         bool   `json:",omitempty"`
 43	IsAnnounce      bool   `json:",omitempty"`
 44	AnnounceOf      string `json:",omitempty"`
 45	Reject          bool   `json:",omitempty"`
 46	SkipMedia       bool   `json:",omitempty"`
 47	Hide            bool   `json:",omitempty"`
 48	Collapse        bool   `json:",omitempty"`
 49	Rewrite         string `json:",omitempty"`
 50	re_rewrite      *regexp.Regexp
 51	Replace         string `json:",omitempty"`
 52	Expiration      time.Time
 53	Notes           string
 54}
 55
 56type filtType uint
 57
 58const (
 59	filtNone filtType = iota
 60	filtAny
 61	filtReject
 62	filtSkipMedia
 63	filtHide
 64	filtCollapse
 65	filtRewrite
 66)
 67
 68var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"}
 69
 70func (ft filtType) String() string {
 71	return filtNames[ft]
 72}
 73
 74type afiltermap map[filtType][]*Filter
 75
 76var filtInvalidator gencache.Invalidator[int64]
 77var filtcache *gencache.Cache[int64, afiltermap]
 78
 79func init() {
 80	// resolve init loop
 81	filtcache = gencache.New(gencache.Options[int64, afiltermap]{
 82		Fill:        filtcachefiller,
 83		Invalidator: &filtInvalidator,
 84	})
 85}
 86
 87func filtcachefiller(userid int64) (afiltermap, bool) {
 88	rows, err := stmtGetFilters.Query(userid)
 89	if err != nil {
 90		elog.Printf("error querying filters: %s", err)
 91		return nil, false
 92	}
 93	defer rows.Close()
 94
 95	now := time.Now()
 96
 97	var expflush time.Time
 98
 99	filtmap := make(afiltermap)
100	for rows.Next() {
101		filt := new(Filter)
102		var j string
103		var filterid int64
104		err = rows.Scan(&filterid, &j)
105		if err == nil {
106			err = unjsonify(j, filt)
107		}
108		if err != nil {
109			elog.Printf("error scanning filter: %s", err)
110			continue
111		}
112		if !filt.Expiration.IsZero() {
113			if filt.Expiration.Before(now) {
114				continue
115			}
116			if expflush.IsZero() || filt.Expiration.Before(expflush) {
117				expflush = filt.Expiration
118			}
119		}
120		if t := filt.Text; t != "" && t != "." {
121			wordfront := unicode.IsLetter(rune(t[0]))
122			wordtail := unicode.IsLetter(rune(t[len(t)-1]))
123			t = "(?i:" + t + ")"
124			if wordfront {
125				t = "\\b" + t
126			}
127			if wordtail {
128				t = t + "\\b"
129			}
130			filt.re_text, err = regexp.Compile(t)
131			if err != nil {
132				elog.Printf("error compiling filter text: %s", err)
133				continue
134			}
135		}
136		if t := filt.Rewrite; t != "" {
137			wordfront := unicode.IsLetter(rune(t[0]))
138			wordtail := unicode.IsLetter(rune(t[len(t)-1]))
139			t = "(?i:" + t + ")"
140			if wordfront {
141				t = "\\b" + t
142			}
143			if wordtail {
144				t = t + "\\b"
145			}
146			filt.re_rewrite, err = regexp.Compile(t)
147			if err != nil {
148				elog.Printf("error compiling filter rewrite: %s", err)
149				continue
150			}
151		}
152		filt.ID = filterid
153		if filt.Reject {
154			filt.Actions = append(filt.Actions, filtReject)
155			filtmap[filtReject] = append(filtmap[filtReject], filt)
156		}
157		if filt.SkipMedia {
158			filt.Actions = append(filt.Actions, filtSkipMedia)
159			filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
160		}
161		if filt.Hide {
162			filt.Actions = append(filt.Actions, filtHide)
163			filtmap[filtHide] = append(filtmap[filtHide], filt)
164		}
165		if filt.Collapse {
166			filt.Actions = append(filt.Actions, filtCollapse)
167			filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
168		}
169		if filt.Rewrite != "" {
170			filt.Actions = append(filt.Actions, filtRewrite)
171			filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
172		}
173		filtmap[filtAny] = append(filtmap[filtAny], filt)
174	}
175	sorting := filtmap[filtAny]
176	sort.Slice(filtmap[filtAny], func(i, j int) bool {
177		return sorting[i].Name < sorting[j].Name
178	})
179	if !expflush.IsZero() {
180		dur := expflush.Sub(now)
181		go filtcacheclear(userid, dur)
182	}
183	return filtmap, true
184}
185
186func filtcacheclear(userid int64, dur time.Duration) {
187	time.Sleep(dur + time.Second)
188	filtInvalidator.Clear(userid)
189}
190
191func getfilters(userid int64, scope filtType) []*Filter {
192	filtmap, ok := filtcache.Get(userid)
193	if ok {
194		return filtmap[scope]
195	}
196	return nil
197}
198
199type arejectmap map[string][]*Filter
200
201var rejectAnyKey = "..."
202
203var rejectcache = gencache.New(gencache.Options[int64, arejectmap]{Fill: func(userid int64) (arejectmap, bool) {
204	m := make(arejectmap)
205	filts := getfilters(userid, filtReject)
206	for _, f := range filts {
207		if f.Text != "" {
208			key := rejectAnyKey
209			m[key] = append(m[key], f)
210			continue
211		}
212		if f.IsAnnounce && f.AnnounceOf != "" {
213			key := f.AnnounceOf
214			m[key] = append(m[key], f)
215		}
216		if f.Actor != "" {
217			key := f.Actor
218			m[key] = append(m[key], f)
219		}
220	}
221	return m, true
222}, Invalidator: &filtInvalidator})
223
224func rejectfilters(userid int64, name string) []*Filter {
225	m, _ := rejectcache.Get(userid)
226	return m[name]
227}
228
229func rejectorigin(userid int64, origin string, isannounce bool) bool {
230	if o := originate(origin); o != "" {
231		origin = o
232	}
233	filts := rejectfilters(userid, origin)
234	for _, f := range filts {
235		if f.OnlyUnknowns {
236			continue
237		}
238		if isannounce && f.IsAnnounce {
239			if f.AnnounceOf == origin {
240				return true
241			}
242		}
243		if f.Actor == origin {
244			return true
245		}
246	}
247	return false
248}
249
250func rejectactor(userid int64, actor string) bool {
251	filts := rejectfilters(userid, actor)
252	for _, f := range filts {
253		if f.IsAnnounce {
254			continue
255		}
256		if f.Actor == actor {
257			ilog.Printf("rejecting actor: %s", actor)
258			return true
259		}
260	}
261	origin := originate(actor)
262	if origin == "" {
263		return false
264	}
265	filts = rejectfilters(userid, origin)
266	for _, f := range filts {
267		if f.IsAnnounce {
268			continue
269		}
270		if f.Actor == origin {
271			if f.OnlyUnknowns {
272				if unknownActor(userid, actor) {
273					ilog.Printf("rejecting unknown actor: %s", actor)
274					return true
275				}
276				continue
277			}
278			ilog.Printf("rejecting actor: %s", actor)
279			return true
280		}
281	}
282	return false
283}
284
285var knownknowns = gencache.New(gencache.Options[int64, map[string]bool]{Fill: func(userid int64) (map[string]bool, bool) {
286	m := make(map[string]bool)
287	honkers := gethonkers(userid)
288	for _, h := range honkers {
289		m[h.XID] = true
290	}
291	return m, true
292}, Invalidator: &honkerinvalidator})
293
294func unknownActor(userid int64, actor string) bool {
295	knowns, _ := knownknowns.Get(userid)
296	return !knowns[actor]
297}
298
299func stealthmode(userid int64, r *http.Request) bool {
300	agent := requestActor(r)
301	if agent != "" {
302		fake := rejectorigin(userid, agent, false)
303		if fake {
304			ilog.Printf("faking 404 for %s", agent)
305			return true
306		}
307	}
308	return false
309}
310
311func matchfilter(h *Honk, f *Filter) bool {
312	return matchfilterX(h, f) != ""
313}
314
315func matchfilterX(h *Honk, f *Filter) string {
316	rv := ""
317	match := true
318	if match && f.Actor != "" {
319		match = false
320		if f.Actor == h.Honker || f.Actor == h.Oonker {
321			match = true
322			rv = f.Actor
323		}
324		if !match && !f.OnlyUnknowns && (f.Actor == originate(h.Honker) ||
325			f.Actor == originate(h.Oonker) ||
326			f.Actor == originate(h.XID)) {
327			match = true
328			rv = f.Actor
329		}
330		if !match && f.IncludeAudience {
331			for _, a := range h.Audience {
332				if f.Actor == a || f.Actor == originate(a) {
333					match = true
334					rv = f.Actor
335					break
336				}
337			}
338		}
339	}
340	if match && f.IsReply {
341		match = false
342		if h.RID != "" {
343			match = true
344			rv += " reply"
345		}
346	}
347	if match && f.IsAnnounce {
348		match = false
349		if h.Oonker != "" {
350			if f.AnnounceOf == "" || f.AnnounceOf == h.Oonker || f.AnnounceOf == originate(h.Oonker) {
351				match = true
352				rv += " announce"
353			}
354		}
355	}
356	if match && f.Text != "" && f.Text != "." {
357		match = false
358		re := f.re_text
359		m := re.FindString(h.Precis)
360		if m == "" {
361			m = re.FindString(h.Noise)
362		}
363		if m == "" {
364			for _, d := range h.Donks {
365				m = re.FindString(d.Desc)
366				if m != "" {
367					break
368				}
369			}
370		}
371		if m != "" {
372			match = true
373			rv = m
374		}
375	}
376	if match && f.Text == "." {
377		match = false
378		if h.Precis != "" {
379			match = true
380			rv = h.Precis
381		}
382	}
383	if match {
384		return rv
385	}
386	return ""
387}
388
389func rejectxonk(xonk *Honk) bool {
390	m, _ := rejectcache.Get(xonk.UserID)
391	filts := m[rejectAnyKey]
392	filts = append(filts, m[xonk.Honker]...)
393	filts = append(filts, m[originate(xonk.Honker)]...)
394	filts = append(filts, m[xonk.Oonker]...)
395	filts = append(filts, m[originate(xonk.Oonker)]...)
396	for _, a := range xonk.Audience {
397		filts = append(filts, m[a]...)
398		filts = append(filts, m[originate(a)]...)
399	}
400	for _, f := range filts {
401		if cause := matchfilterX(xonk, f); cause != "" {
402			ilog.Printf("rejecting %s because %s", xonk.XID, cause)
403			return true
404		}
405	}
406	return false
407}
408
409func skipMedia(xonk *Honk) bool {
410	filts := getfilters(xonk.UserID, filtSkipMedia)
411	for _, f := range filts {
412		if matchfilter(xonk, f) {
413			return true
414		}
415	}
416	return false
417}
418
419func unsee(honks []*Honk, userid int64) {
420	if userid != -1 {
421		colfilts := getfilters(userid, filtCollapse)
422		rwfilts := getfilters(userid, filtRewrite)
423		for _, h := range honks {
424			for _, f := range colfilts {
425				if bad := matchfilterX(h, f); bad != "" {
426					if h.Precis == "" {
427						h.Precis = bad
428					}
429					h.Open = ""
430					break
431				}
432			}
433			if h.Open == "open" && h.Precis == "unspecified horror" {
434				h.Precis = ""
435			}
436			for _, f := range rwfilts {
437				if matchfilter(h, f) {
438					h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
439				}
440			}
441			if len(h.Noise) > 6000 && h.Open == "open" {
442				if h.Precis == "" {
443					h.Precis = "really freaking long"
444				}
445				h.Open = ""
446			}
447		}
448	} else {
449		for _, h := range honks {
450			if h.Precis != "" {
451				h.Open = ""
452			}
453		}
454	}
455}
456
457var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
458	rows, err := stmtUntagged.Query(userid)
459	if err != nil {
460		elog.Printf("error query untagged: %s", err)
461		return nil, false
462	}
463	defer rows.Close()
464	bad := make(map[string]bool)
465	for rows.Next() {
466		var xid, rid string
467		var flags int64
468		err = rows.Scan(&xid, &rid, &flags)
469		if err != nil {
470			elog.Printf("error scanning untag: %s", err)
471			continue
472		}
473		if flags&flagIsUntagged != 0 {
474			bad[xid] = true
475		}
476		if bad[rid] {
477			bad[xid] = true
478		}
479	}
480	return bad, true
481}})
482
483func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
484	var badparents map[string]bool
485	untagged.GetAndLock(userid, &badparents)
486	j := 0
487	reversehonks(honks)
488	for _, h := range honks {
489		if badparents[h.RID] {
490			badparents[h.XID] = true
491			continue
492		}
493		honks[j] = h
494		j++
495	}
496	untagged.Unlock()
497	honks = honks[0:j]
498	reversehonks(honks)
499	if !withfilt {
500		return honks
501	}
502	filts := getfilters(userid, filtHide)
503	j = 0
504outer:
505	for _, h := range honks {
506		for _, f := range filts {
507			if matchfilter(h, f) {
508				continue outer
509			}
510		}
511		honks[j] = h
512		j++
513	}
514	honks = honks[0:j]
515	return honks
516}
517
518func savehfcs(w http.ResponseWriter, r *http.Request) {
519	userinfo := login.GetUserInfo(r)
520	itsok := r.FormValue("itsok")
521	if itsok == "iforgiveyou" {
522		hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0)
523		_, err := stmtDeleteFilter.Exec(userinfo.UserID, hfcsid)
524		if err != nil {
525			elog.Printf("error deleting filter: %s", err)
526		}
527		filtInvalidator.Clear(userinfo.UserID)
528		http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
529		return
530	}
531
532	filt := new(Filter)
533	filt.Name = strings.TrimSpace(r.FormValue("name"))
534	filt.Date = time.Now().UTC()
535	filt.Actor = strings.TrimSpace(r.FormValue("actor"))
536	filt.IncludeAudience = r.FormValue("incaud") == "yes"
537	filt.OnlyUnknowns = r.FormValue("unknowns") == "yes"
538	filt.Text = strings.TrimSpace(r.FormValue("filttext"))
539	filt.IsReply = r.FormValue("isreply") == "yes"
540	filt.IsAnnounce = r.FormValue("isannounce") == "yes"
541	filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof"))
542	filt.Reject = r.FormValue("doreject") == "yes"
543	filt.SkipMedia = r.FormValue("doskipmedia") == "yes"
544	filt.Hide = r.FormValue("dohide") == "yes"
545	filt.Collapse = r.FormValue("docollapse") == "yes"
546	filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite"))
547	filt.Replace = strings.TrimSpace(r.FormValue("filtreplace"))
548	if dur := parseDuration(r.FormValue("filtduration")); dur > 0 {
549		filt.Expiration = time.Now().UTC().Add(dur)
550	}
551	filt.Notes = strings.TrimSpace(r.FormValue("filtnotes"))
552
553	if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce {
554		ilog.Printf("blank filter")
555		http.Error(w, "can't save a blank filter", http.StatusInternalServerError)
556		return
557	}
558
559	j, err := jsonify(filt)
560	if err == nil {
561		_, err = stmtSaveFilter.Exec(userinfo.UserID, j)
562	}
563	if err != nil {
564		elog.Printf("error saving filter: %s", err)
565	}
566
567	filtInvalidator.Clear(userinfo.UserID)
568	http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
569}