all repos — honk @ 2326db38dbee7da1bf861752eca2f297aeb62115

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