all repos — honk @ 8f557f8374725f7df73656083cd92d45d907896a

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 filt.Text != "" {
112			filt.re_text, err = regexp.Compile("\\b(?i:" + filt.Text + ")\\b")
113			if err != nil {
114				log.Printf("error compiling filter text: %s", err)
115				continue
116			}
117		}
118		if filt.Rewrite != "" {
119			filt.re_rewrite, err = regexp.Compile("\\b(?i:" + filt.Rewrite + ")\\b")
120			if err != nil {
121				log.Printf("error compiling filter rewrite: %s", err)
122				continue
123			}
124		}
125		filt.ID = filterid
126		if filt.Reject {
127			filt.Actions = append(filt.Actions, filtReject)
128			filtmap[filtReject] = append(filtmap[filtReject], filt)
129		}
130		if filt.SkipMedia {
131			filt.Actions = append(filt.Actions, filtSkipMedia)
132			filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
133		}
134		if filt.Hide {
135			filt.Actions = append(filt.Actions, filtHide)
136			filtmap[filtHide] = append(filtmap[filtHide], filt)
137		}
138		if filt.Collapse {
139			filt.Actions = append(filt.Actions, filtCollapse)
140			filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
141		}
142		if filt.Rewrite != "" {
143			filt.Actions = append(filt.Actions, filtRewrite)
144			filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
145		}
146		filtmap[filtAny] = append(filtmap[filtAny], filt)
147	}
148	sorting := filtmap[filtAny]
149	sort.Slice(filtmap[filtAny], func(i, j int) bool {
150		return sorting[i].Name < sorting[j].Name
151	})
152	if !expflush.IsZero() {
153		dur := expflush.Sub(now)
154		go filtcacheclear(userid, dur)
155	}
156	return filtmap, true
157}
158
159func filtcacheclear(userid int64, dur time.Duration) {
160	time.Sleep(dur + time.Second)
161	filtInvalidator.Clear(userid)
162}
163
164func getfilters(userid int64, scope filtType) []*Filter {
165	var filtmap afiltermap
166	ok := filtcache.Get(userid, &filtmap)
167	if ok {
168		return filtmap[scope]
169	}
170	return nil
171}
172
173type arejectmap map[string][]*Filter
174
175var rejectAnyKey = "..."
176
177var rejectcache = cache.New(cache.Options{Filler: func(userid int64) (arejectmap, bool) {
178	m := make(arejectmap)
179	filts := getfilters(userid, filtReject)
180	for _, f := range filts {
181		if f.Text != "" {
182			key := rejectAnyKey
183			m[key] = append(m[key], f)
184			continue
185		}
186		if f.IsAnnounce && f.AnnounceOf != "" {
187			key := f.AnnounceOf
188			m[key] = append(m[key], f)
189		}
190		if f.Actor != "" {
191			key := f.Actor
192			m[key] = append(m[key], f)
193		}
194	}
195	return m, true
196}, Invalidator: &filtInvalidator})
197
198func rejectfilters(userid int64, name string) []*Filter {
199	var m arejectmap
200	rejectcache.Get(userid, &m)
201	return m[name]
202}
203
204func rejectorigin(userid int64, origin string, isannounce bool) bool {
205	if o := originate(origin); o != "" {
206		origin = o
207	}
208	filts := rejectfilters(userid, origin)
209	for _, f := range filts {
210		if isannounce && f.IsAnnounce {
211			if f.AnnounceOf == origin {
212				return true
213			}
214		}
215		if f.Actor == origin {
216			return true
217		}
218	}
219	return false
220}
221
222func rejectactor(userid int64, actor string) bool {
223	filts := rejectfilters(userid, actor)
224	for _, f := range filts {
225		if f.IsAnnounce {
226			continue
227		}
228		if f.Actor == actor {
229			log.Printf("rejecting actor: %s", actor)
230			return true
231		}
232	}
233	origin := originate(actor)
234	if origin == "" {
235		return false
236	}
237	filts = rejectfilters(userid, origin)
238	for _, f := range filts {
239		if f.IsAnnounce {
240			continue
241		}
242		if f.Actor == origin {
243			log.Printf("rejecting actor: %s", actor)
244			return true
245		}
246	}
247	return false
248}
249
250func stealthmode(userid int64, r *http.Request) bool {
251	agent := r.UserAgent()
252	agent = originate(agent)
253	if agent != "" {
254		fake := rejectorigin(userid, agent, false)
255		if fake {
256			log.Printf("faking 404 for %s", agent)
257			return true
258		}
259	}
260	return false
261}
262
263func matchfilter(h *Honk, f *Filter) bool {
264	return matchfilterX(h, f) != ""
265}
266
267func matchfilterX(h *Honk, f *Filter) string {
268	rv := ""
269	match := true
270	if match && f.Actor != "" {
271		match = false
272		if f.Actor == h.Honker || f.Actor == h.Oonker {
273			match = true
274			rv = f.Actor
275		}
276		if !match && (f.Actor == originate(h.Honker) ||
277			f.Actor == originate(h.Oonker) ||
278			f.Actor == originate(h.XID)) {
279			match = true
280			rv = f.Actor
281		}
282		if !match && f.IncludeAudience {
283			for _, a := range h.Audience {
284				if f.Actor == a || f.Actor == originate(a) {
285					match = true
286					rv = f.Actor
287					break
288				}
289			}
290		}
291	}
292	if match && f.IsAnnounce {
293		match = false
294		if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
295			f.AnnounceOf == originate(h.Oonker) {
296			match = true
297			rv += " announce"
298		}
299	}
300	if match && f.Text != "" {
301		match = false
302		re := f.re_text
303		m := re.FindString(h.Precis)
304		if m == "" {
305			m = re.FindString(h.Noise)
306		}
307		if m == "" {
308			for _, d := range h.Donks {
309				m = re.FindString(d.Desc)
310				if m != "" {
311					break
312				}
313			}
314		}
315		if m != "" {
316			match = true
317			rv = m
318		}
319	}
320	if match {
321		return rv
322	}
323	return ""
324}
325
326func rejectxonk(xonk *Honk) bool {
327	var m arejectmap
328	rejectcache.Get(xonk.UserID, &m)
329	filts := m[rejectAnyKey]
330	filts = append(filts, m[xonk.Honker]...)
331	filts = append(filts, m[originate(xonk.Honker)]...)
332	filts = append(filts, m[xonk.Oonker]...)
333	filts = append(filts, m[originate(xonk.Oonker)]...)
334	for _, a := range xonk.Audience {
335		filts = append(filts, m[a]...)
336		filts = append(filts, m[originate(a)]...)
337	}
338	for _, f := range filts {
339		if cause := matchfilterX(xonk, f); cause != "" {
340			log.Printf("rejecting %s because %s", xonk.XID, cause)
341			return true
342		}
343	}
344	return false
345}
346
347func skipMedia(xonk *Honk) bool {
348	filts := getfilters(xonk.UserID, filtSkipMedia)
349	for _, f := range filts {
350		if matchfilter(xonk, f) {
351			return true
352		}
353	}
354	return false
355}
356
357func unsee(honks []*Honk, userid int64) {
358	if userid != -1 {
359		colfilts := getfilters(userid, filtCollapse)
360		rwfilts := getfilters(userid, filtRewrite)
361		for _, h := range honks {
362			for _, f := range colfilts {
363				if bad := matchfilterX(h, f); bad != "" {
364					if h.Precis == "" {
365						h.Precis = bad
366					}
367					h.Open = ""
368					break
369				}
370			}
371			if h.Open == "open" && h.Precis == "unspecified horror" {
372				h.Precis = ""
373			}
374			for _, f := range rwfilts {
375				if matchfilter(h, f) {
376					h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
377				}
378			}
379			if len(h.Noise) > 6000 && h.Open == "open" {
380				if h.Precis == "" {
381					h.Precis = "really freaking long"
382				}
383				h.Open = ""
384			}
385		}
386	}
387}
388
389var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
390	rows, err := stmtUntagged.Query(userid)
391	if err != nil {
392		log.Printf("error query untagged: %s", err)
393		return nil, false
394	}
395	defer rows.Close()
396	bad := make(map[string]bool)
397	for rows.Next() {
398		var xid, rid string
399		var flags int64
400		err = rows.Scan(&xid, &rid, &flags)
401		if err != nil {
402			log.Printf("error scanning untag: %s", err)
403			continue
404		}
405		if flags&flagIsUntagged != 0 {
406			bad[xid] = true
407		}
408		if bad[rid] {
409			bad[xid] = true
410		}
411	}
412	return bad, true
413}})
414
415func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
416	var badparents map[string]bool
417	untagged.GetAndLock(userid, &badparents)
418	j := 0
419	reversehonks(honks)
420	for _, h := range honks {
421		if badparents[h.RID] {
422			badparents[h.XID] = true
423			continue
424		}
425		honks[j] = h
426		j++
427	}
428	untagged.Unlock()
429	honks = honks[0:j]
430	reversehonks(honks)
431	if !withfilt {
432		return honks
433	}
434	filts := getfilters(userid, filtHide)
435	j = 0
436outer:
437	for _, h := range honks {
438		for _, f := range filts {
439			if matchfilter(h, f) {
440				continue outer
441			}
442		}
443		honks[j] = h
444		j++
445	}
446	honks = honks[0:j]
447	return honks
448}