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 log.Printf("rejecting announce: %s", origin)
213 return true
214 }
215 }
216 if f.Actor == origin {
217 log.Printf("rejecting origin: %s", origin)
218 return true
219 }
220 }
221 return false
222}
223
224func rejectactor(userid int64, actor string) bool {
225 filts := rejectfilters(userid, actor)
226 for _, f := range filts {
227 if f.IsAnnounce {
228 continue
229 }
230 if f.Actor == actor {
231 log.Printf("rejecting actor: %s", actor)
232 return true
233 }
234 }
235 origin := originate(actor)
236 if origin == "" {
237 return false
238 }
239 filts = rejectfilters(userid, origin)
240 for _, f := range filts {
241 if f.IsAnnounce {
242 continue
243 }
244 if f.Actor == origin {
245 log.Printf("rejecting actor: %s", actor)
246 return true
247 }
248 }
249 return false
250}
251
252func stealthmode(userid int64, r *http.Request) bool {
253 agent := r.UserAgent()
254 agent = originate(agent)
255 if agent != "" {
256 fake := rejectorigin(userid, agent, false)
257 if fake {
258 log.Printf("faking 404 for %s", agent)
259 return true
260 }
261 }
262 return false
263}
264
265func matchfilter(h *Honk, f *Filter) bool {
266 return matchfilterX(h, f) != ""
267}
268
269func matchfilterX(h *Honk, f *Filter) string {
270 rv := ""
271 match := true
272 if match && f.Actor != "" {
273 match = false
274 if f.Actor == h.Honker || f.Actor == h.Oonker {
275 match = true
276 rv = f.Actor
277 }
278 if !match && (f.Actor == originate(h.Honker) ||
279 f.Actor == originate(h.Oonker) ||
280 f.Actor == originate(h.XID)) {
281 match = true
282 rv = f.Actor
283 }
284 if !match && f.IncludeAudience {
285 for _, a := range h.Audience {
286 if f.Actor == a || f.Actor == originate(a) {
287 match = true
288 rv = f.Actor
289 break
290 }
291 }
292 }
293 }
294 if match && f.IsAnnounce {
295 match = false
296 if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
297 f.AnnounceOf == originate(h.Oonker) {
298 match = true
299 rv += " announce"
300 }
301 }
302 if match && f.Text != "" {
303 match = false
304 re := f.re_text
305 m := re.FindString(h.Precis)
306 if m == "" {
307 m = re.FindString(h.Noise)
308 }
309 if m == "" {
310 for _, d := range h.Donks {
311 m = re.FindString(d.Desc)
312 if m != "" {
313 break
314 }
315 }
316 }
317 if m != "" {
318 match = true
319 rv = m
320 }
321 }
322 if match {
323 return rv
324 }
325 return ""
326}
327
328func rejectxonk(xonk *Honk) bool {
329 var m arejectmap
330 rejectcache.Get(xonk.UserID, &m)
331 filts := m[rejectAnyKey]
332 filts = append(filts, m[xonk.Honker]...)
333 filts = append(filts, m[xonk.Oonker]...)
334 for _, a := range xonk.Audience {
335 filts = append(filts, m[a]...)
336 }
337 for _, f := range filts {
338 if cause := matchfilterX(xonk, f); cause != "" {
339 log.Printf("rejecting %s because %s", xonk.XID, cause)
340 return true
341 }
342 }
343 return false
344}
345
346func skipMedia(xonk *Honk) bool {
347 filts := getfilters(xonk.UserID, filtSkipMedia)
348 for _, f := range filts {
349 if matchfilter(xonk, f) {
350 return true
351 }
352 }
353 return false
354}
355
356func unsee(userid int64, h *Honk) {
357 filts := getfilters(userid, filtCollapse)
358 for _, f := range filts {
359 if bad := matchfilterX(h, f); bad != "" {
360 if h.Precis == "" {
361 h.Precis = bad
362 }
363 h.Open = ""
364 break
365 }
366 }
367 filts = getfilters(userid, filtRewrite)
368 for _, f := range filts {
369 if matchfilter(h, f) {
370 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
371 }
372 }
373}
374
375var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
376 rows, err := stmtUntagged.Query(userid)
377 if err != nil {
378 log.Printf("error query untagged: %s", err)
379 return nil, false
380 }
381 defer rows.Close()
382 bad := make(map[string]bool)
383 for rows.Next() {
384 var xid, rid string
385 var flags int64
386 err = rows.Scan(&xid, &rid, &flags)
387 if err != nil {
388 log.Printf("error scanning untag: %s", err)
389 continue
390 }
391 if flags&flagIsUntagged != 0 {
392 bad[xid] = true
393 }
394 if bad[rid] {
395 bad[xid] = true
396 }
397 }
398 return bad, true
399}})
400
401func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
402 var badparents map[string]bool
403 untagged.GetAndLock(userid, &badparents)
404 j := 0
405 reversehonks(honks)
406 for _, h := range honks {
407 if badparents[h.RID] {
408 badparents[h.XID] = true
409 continue
410 }
411 honks[j] = h
412 j++
413 }
414 untagged.Unlock()
415 honks = honks[0:j]
416 reversehonks(honks)
417 if !withfilt {
418 return honks
419 }
420 filts := getfilters(userid, filtHide)
421 j = 0
422outer:
423 for _, h := range honks {
424 for _, f := range filts {
425 if matchfilter(h, f) {
426 continue outer
427 }
428 }
429 honks[j] = h
430 j++
431 }
432 honks = honks[0:j]
433 return honks
434}