all repos — honk @ 00d1e9979b1bcf03e0146bce91270b89e822d8be

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