all repos — honk @ dbfc63833e116c41e86a6e39760626844afb54c4

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	"time"
 23	"unicode"
 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	IsReply         bool   `json:",omitempty"`
 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		elog.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			elog.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 != "" && 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				elog.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				elog.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			ilog.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			ilog.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			ilog.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.IsReply {
312		match = false
313		if h.RID != "" {
314			match = true
315			rv += " reply"
316		}
317	}
318	if match && f.IsAnnounce {
319		match = false
320		if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
321			f.AnnounceOf == originate(h.Oonker) {
322			match = true
323			rv += " announce"
324		}
325	}
326	if match && f.Text != "" && f.Text != "." {
327		match = false
328		re := f.re_text
329		m := re.FindString(h.Precis)
330		if m == "" {
331			m = re.FindString(h.Noise)
332		}
333		if m == "" {
334			for _, d := range h.Donks {
335				m = re.FindString(d.Desc)
336				if m != "" {
337					break
338				}
339			}
340		}
341		if m != "" {
342			match = true
343			rv = m
344		}
345	}
346	if match && f.Text == "." {
347		match = false
348		if h.Precis != "" {
349			match = true
350			rv = h.Precis
351		}
352	}
353	if match {
354		return rv
355	}
356	return ""
357}
358
359func rejectxonk(xonk *Honk) bool {
360	var m arejectmap
361	rejectcache.Get(xonk.UserID, &m)
362	filts := m[rejectAnyKey]
363	filts = append(filts, m[xonk.Honker]...)
364	filts = append(filts, m[originate(xonk.Honker)]...)
365	filts = append(filts, m[xonk.Oonker]...)
366	filts = append(filts, m[originate(xonk.Oonker)]...)
367	for _, a := range xonk.Audience {
368		filts = append(filts, m[a]...)
369		filts = append(filts, m[originate(a)]...)
370	}
371	for _, f := range filts {
372		if cause := matchfilterX(xonk, f); cause != "" {
373			ilog.Printf("rejecting %s because %s", xonk.XID, cause)
374			return true
375		}
376	}
377	return false
378}
379
380func skipMedia(xonk *Honk) bool {
381	filts := getfilters(xonk.UserID, filtSkipMedia)
382	for _, f := range filts {
383		if matchfilter(xonk, f) {
384			return true
385		}
386	}
387	return false
388}
389
390func unsee(honks []*Honk, userid int64) {
391	if userid != -1 {
392		colfilts := getfilters(userid, filtCollapse)
393		rwfilts := getfilters(userid, filtRewrite)
394		for _, h := range honks {
395			for _, f := range colfilts {
396				if bad := matchfilterX(h, f); bad != "" {
397					if h.Precis == "" {
398						h.Precis = bad
399					}
400					h.Open = ""
401					break
402				}
403			}
404			if h.Open == "open" && h.Precis == "unspecified horror" {
405				h.Precis = ""
406			}
407			for _, f := range rwfilts {
408				if matchfilter(h, f) {
409					h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
410				}
411			}
412			if len(h.Noise) > 6000 && h.Open == "open" {
413				if h.Precis == "" {
414					h.Precis = "really freaking long"
415				}
416				h.Open = ""
417			}
418		}
419	} else {
420		for _, h := range honks {
421			if h.Precis != "" {
422				h.Open = ""
423			}
424		}
425	}
426}
427
428var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
429	rows, err := stmtUntagged.Query(userid)
430	if err != nil {
431		elog.Printf("error query untagged: %s", err)
432		return nil, false
433	}
434	defer rows.Close()
435	bad := make(map[string]bool)
436	for rows.Next() {
437		var xid, rid string
438		var flags int64
439		err = rows.Scan(&xid, &rid, &flags)
440		if err != nil {
441			elog.Printf("error scanning untag: %s", err)
442			continue
443		}
444		if flags&flagIsUntagged != 0 {
445			bad[xid] = true
446		}
447		if bad[rid] {
448			bad[xid] = true
449		}
450	}
451	return bad, true
452}})
453
454func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
455	var badparents map[string]bool
456	untagged.GetAndLock(userid, &badparents)
457	j := 0
458	reversehonks(honks)
459	for _, h := range honks {
460		if badparents[h.RID] {
461			badparents[h.XID] = true
462			continue
463		}
464		honks[j] = h
465		j++
466	}
467	untagged.Unlock()
468	honks = honks[0:j]
469	reversehonks(honks)
470	if !withfilt {
471		return honks
472	}
473	filts := getfilters(userid, filtHide)
474	j = 0
475outer:
476	for _, h := range honks {
477		for _, f := range filts {
478			if matchfilter(h, f) {
479				continue outer
480			}
481		}
482		honks[j] = h
483		j++
484	}
485	honks = honks[0:j]
486	return honks
487}