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}