all repos — honk @ c41f4d26d3d13ab1d38eb62614033e1b805ace21

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