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}