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 IsAnnounce bool `json:",omitempty"`
37 AnnounceOf string `json:",omitempty"`
38 Reject bool `json:",omitempty"`
39 SkipMedia bool `json:",omitempty"`
40 Hide bool `json:",omitempty"`
41 Collapse bool `json:",omitempty"`
42 Rewrite string `json:",omitempty"`
43 re_rewrite *regexp.Regexp
44 Replace string `json:",omitempty"`
45 Expiration time.Time
46 Notes string
47}
48
49type filtType uint
50
51const (
52 filtNone filtType = iota
53 filtAny
54 filtReject
55 filtSkipMedia
56 filtHide
57 filtCollapse
58 filtRewrite
59)
60
61var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"}
62
63func (ft filtType) String() string {
64 return filtNames[ft]
65}
66
67type afiltermap map[filtType][]*Filter
68
69var filtInvalidator cache.Invalidator
70var filtcache *cache.Cache
71
72func init() {
73 // resolve init loop
74 filtcache = cache.New(cache.Options{Filler: filtcachefiller, Invalidator: &filtInvalidator})
75}
76
77func filtcachefiller(userid int64) (afiltermap, bool) {
78 rows, err := stmtGetFilters.Query(userid)
79 if err != nil {
80 elog.Printf("error querying filters: %s", err)
81 return nil, false
82 }
83 defer rows.Close()
84
85 now := time.Now()
86
87 var expflush time.Time
88
89 filtmap := make(afiltermap)
90 for rows.Next() {
91 filt := new(Filter)
92 var j string
93 var filterid int64
94 err = rows.Scan(&filterid, &j)
95 if err == nil {
96 err = unjsonify(j, filt)
97 }
98 if err != nil {
99 elog.Printf("error scanning filter: %s", err)
100 continue
101 }
102 if !filt.Expiration.IsZero() {
103 if filt.Expiration.Before(now) {
104 continue
105 }
106 if expflush.IsZero() || filt.Expiration.Before(expflush) {
107 expflush = filt.Expiration
108 }
109 }
110 if t := filt.Text; t != "" && t != "." {
111 wordfront := t[0] != '#'
112 wordtail := true
113 t = "(?i:" + t + ")"
114 if wordfront {
115 t = "\\b" + t
116 }
117 if wordtail {
118 t = t + "\\b"
119 }
120 filt.re_text, err = regexp.Compile(t)
121 if err != nil {
122 elog.Printf("error compiling filter text: %s", err)
123 continue
124 }
125 }
126 if t := filt.Rewrite; t != "" {
127 wordfront := t[0] != '#'
128 wordtail := true
129 t = "(?i:" + t + ")"
130 if wordfront {
131 t = "\\b" + t
132 }
133 if wordtail {
134 t = t + "\\b"
135 }
136 filt.re_rewrite, err = regexp.Compile(t)
137 if err != nil {
138 elog.Printf("error compiling filter rewrite: %s", err)
139 continue
140 }
141 }
142 filt.ID = filterid
143 if filt.Reject {
144 filt.Actions = append(filt.Actions, filtReject)
145 filtmap[filtReject] = append(filtmap[filtReject], filt)
146 }
147 if filt.SkipMedia {
148 filt.Actions = append(filt.Actions, filtSkipMedia)
149 filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
150 }
151 if filt.Hide {
152 filt.Actions = append(filt.Actions, filtHide)
153 filtmap[filtHide] = append(filtmap[filtHide], filt)
154 }
155 if filt.Collapse {
156 filt.Actions = append(filt.Actions, filtCollapse)
157 filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
158 }
159 if filt.Rewrite != "" {
160 filt.Actions = append(filt.Actions, filtRewrite)
161 filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
162 }
163 filtmap[filtAny] = append(filtmap[filtAny], filt)
164 }
165 sorting := filtmap[filtAny]
166 sort.Slice(filtmap[filtAny], func(i, j int) bool {
167 return sorting[i].Name < sorting[j].Name
168 })
169 if !expflush.IsZero() {
170 dur := expflush.Sub(now)
171 go filtcacheclear(userid, dur)
172 }
173 return filtmap, true
174}
175
176func filtcacheclear(userid int64, dur time.Duration) {
177 time.Sleep(dur + time.Second)
178 filtInvalidator.Clear(userid)
179}
180
181func getfilters(userid int64, scope filtType) []*Filter {
182 var filtmap afiltermap
183 ok := filtcache.Get(userid, &filtmap)
184 if ok {
185 return filtmap[scope]
186 }
187 return nil
188}
189
190type arejectmap map[string][]*Filter
191
192var rejectAnyKey = "..."
193
194var rejectcache = cache.New(cache.Options{Filler: func(userid int64) (arejectmap, bool) {
195 m := make(arejectmap)
196 filts := getfilters(userid, filtReject)
197 for _, f := range filts {
198 if f.Text != "" {
199 key := rejectAnyKey
200 m[key] = append(m[key], f)
201 continue
202 }
203 if f.IsAnnounce && f.AnnounceOf != "" {
204 key := f.AnnounceOf
205 m[key] = append(m[key], f)
206 }
207 if f.Actor != "" {
208 key := f.Actor
209 m[key] = append(m[key], f)
210 }
211 }
212 return m, true
213}, Invalidator: &filtInvalidator})
214
215func rejectfilters(userid int64, name string) []*Filter {
216 var m arejectmap
217 rejectcache.Get(userid, &m)
218 return m[name]
219}
220
221func rejectorigin(userid int64, origin string, isannounce bool) bool {
222 if o := originate(origin); o != "" {
223 origin = o
224 }
225 filts := rejectfilters(userid, origin)
226 for _, f := range filts {
227 if isannounce && f.IsAnnounce {
228 if f.AnnounceOf == origin {
229 return true
230 }
231 }
232 if f.Actor == origin {
233 return true
234 }
235 }
236 return false
237}
238
239func rejectactor(userid int64, actor string) bool {
240 filts := rejectfilters(userid, actor)
241 for _, f := range filts {
242 if f.IsAnnounce {
243 continue
244 }
245 if f.Actor == actor {
246 ilog.Printf("rejecting actor: %s", actor)
247 return true
248 }
249 }
250 origin := originate(actor)
251 if origin == "" {
252 return false
253 }
254 filts = rejectfilters(userid, origin)
255 for _, f := range filts {
256 if f.IsAnnounce {
257 continue
258 }
259 if f.Actor == origin {
260 ilog.Printf("rejecting actor: %s", actor)
261 return true
262 }
263 }
264 return false
265}
266
267func stealthmode(userid int64, r *http.Request) bool {
268 agent := r.UserAgent()
269 agent = originate(agent)
270 if agent != "" {
271 fake := rejectorigin(userid, agent, false)
272 if fake {
273 ilog.Printf("faking 404 for %s", agent)
274 return true
275 }
276 }
277 return false
278}
279
280func matchfilter(h *Honk, f *Filter) bool {
281 return matchfilterX(h, f) != ""
282}
283
284func matchfilterX(h *Honk, f *Filter) string {
285 rv := ""
286 match := true
287 if match && f.Actor != "" {
288 match = false
289 if f.Actor == h.Honker || f.Actor == h.Oonker {
290 match = true
291 rv = f.Actor
292 }
293 if !match && (f.Actor == originate(h.Honker) ||
294 f.Actor == originate(h.Oonker) ||
295 f.Actor == originate(h.XID)) {
296 match = true
297 rv = f.Actor
298 }
299 if !match && f.IncludeAudience {
300 for _, a := range h.Audience {
301 if f.Actor == a || f.Actor == originate(a) {
302 match = true
303 rv = f.Actor
304 break
305 }
306 }
307 }
308 }
309 if match && f.IsAnnounce {
310 match = false
311 if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
312 f.AnnounceOf == originate(h.Oonker) {
313 match = true
314 rv += " announce"
315 }
316 }
317 if match && f.Text != "" && f.Text != "." {
318 match = false
319 re := f.re_text
320 m := re.FindString(h.Precis)
321 if m == "" {
322 m = re.FindString(h.Noise)
323 }
324 if m == "" {
325 for _, d := range h.Donks {
326 m = re.FindString(d.Desc)
327 if m != "" {
328 break
329 }
330 }
331 }
332 if m != "" {
333 match = true
334 rv = m
335 }
336 }
337 if match && f.Text == "." {
338 match = false
339 if h.Precis != "" {
340 match = true
341 rv = h.Precis
342 }
343 }
344 if match {
345 return rv
346 }
347 return ""
348}
349
350func rejectxonk(xonk *Honk) bool {
351 var m arejectmap
352 rejectcache.Get(xonk.UserID, &m)
353 filts := m[rejectAnyKey]
354 filts = append(filts, m[xonk.Honker]...)
355 filts = append(filts, m[originate(xonk.Honker)]...)
356 filts = append(filts, m[xonk.Oonker]...)
357 filts = append(filts, m[originate(xonk.Oonker)]...)
358 for _, a := range xonk.Audience {
359 filts = append(filts, m[a]...)
360 filts = append(filts, m[originate(a)]...)
361 }
362 for _, f := range filts {
363 if cause := matchfilterX(xonk, f); cause != "" {
364 ilog.Printf("rejecting %s because %s", xonk.XID, cause)
365 return true
366 }
367 }
368 return false
369}
370
371func skipMedia(xonk *Honk) bool {
372 filts := getfilters(xonk.UserID, filtSkipMedia)
373 for _, f := range filts {
374 if matchfilter(xonk, f) {
375 return true
376 }
377 }
378 return false
379}
380
381func unsee(honks []*Honk, userid int64) {
382 if userid != -1 {
383 colfilts := getfilters(userid, filtCollapse)
384 rwfilts := getfilters(userid, filtRewrite)
385 for _, h := range honks {
386 for _, f := range colfilts {
387 if bad := matchfilterX(h, f); bad != "" {
388 if h.Precis == "" {
389 h.Precis = bad
390 }
391 h.Open = ""
392 break
393 }
394 }
395 if h.Open == "open" && h.Precis == "unspecified horror" {
396 h.Precis = ""
397 }
398 for _, f := range rwfilts {
399 if matchfilter(h, f) {
400 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
401 }
402 }
403 if len(h.Noise) > 6000 && h.Open == "open" {
404 if h.Precis == "" {
405 h.Precis = "really freaking long"
406 }
407 h.Open = ""
408 }
409 }
410 } else {
411 for _, h := range honks {
412 if h.Precis != "" {
413 h.Open = ""
414 }
415 }
416 }
417}
418
419var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
420 rows, err := stmtUntagged.Query(userid)
421 if err != nil {
422 elog.Printf("error query untagged: %s", err)
423 return nil, false
424 }
425 defer rows.Close()
426 bad := make(map[string]bool)
427 for rows.Next() {
428 var xid, rid string
429 var flags int64
430 err = rows.Scan(&xid, &rid, &flags)
431 if err != nil {
432 elog.Printf("error scanning untag: %s", err)
433 continue
434 }
435 if flags&flagIsUntagged != 0 {
436 bad[xid] = true
437 }
438 if bad[rid] {
439 bad[xid] = true
440 }
441 }
442 return bad, true
443}})
444
445func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
446 var badparents map[string]bool
447 untagged.GetAndLock(userid, &badparents)
448 j := 0
449 reversehonks(honks)
450 for _, h := range honks {
451 if badparents[h.RID] {
452 badparents[h.XID] = true
453 continue
454 }
455 honks[j] = h
456 j++
457 }
458 untagged.Unlock()
459 honks = honks[0:j]
460 reversehonks(honks)
461 if !withfilt {
462 return honks
463 }
464 filts := getfilters(userid, filtHide)
465 j = 0
466outer:
467 for _, h := range honks {
468 for _, f := range filts {
469 if matchfilter(h, f) {
470 continue outer
471 }
472 }
473 honks[j] = h
474 j++
475 }
476 honks = honks[0:j]
477 return honks
478}