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