all repos — honk @ 124af7a636026c1dfb7a255eab0465711c1f1415

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