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 != "" {
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 != "" {
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 {
338 return rv
339 }
340 return ""
341}
342
343func rejectxonk(xonk *Honk) bool {
344 var m arejectmap
345 rejectcache.Get(xonk.UserID, &m)
346 filts := m[rejectAnyKey]
347 filts = append(filts, m[xonk.Honker]...)
348 filts = append(filts, m[originate(xonk.Honker)]...)
349 filts = append(filts, m[xonk.Oonker]...)
350 filts = append(filts, m[originate(xonk.Oonker)]...)
351 for _, a := range xonk.Audience {
352 filts = append(filts, m[a]...)
353 filts = append(filts, m[originate(a)]...)
354 }
355 for _, f := range filts {
356 if cause := matchfilterX(xonk, f); cause != "" {
357 ilog.Printf("rejecting %s because %s", xonk.XID, cause)
358 return true
359 }
360 }
361 return false
362}
363
364func skipMedia(xonk *Honk) bool {
365 filts := getfilters(xonk.UserID, filtSkipMedia)
366 for _, f := range filts {
367 if matchfilter(xonk, f) {
368 return true
369 }
370 }
371 return false
372}
373
374func unsee(honks []*Honk, userid int64) {
375 if userid != -1 {
376 colfilts := getfilters(userid, filtCollapse)
377 rwfilts := getfilters(userid, filtRewrite)
378 for _, h := range honks {
379 for _, f := range colfilts {
380 if bad := matchfilterX(h, f); bad != "" {
381 if h.Precis == "" {
382 h.Precis = bad
383 }
384 h.Open = ""
385 break
386 }
387 }
388 if h.Open == "open" && h.Precis == "unspecified horror" {
389 h.Precis = ""
390 }
391 for _, f := range rwfilts {
392 if matchfilter(h, f) {
393 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
394 }
395 }
396 if len(h.Noise) > 6000 && h.Open == "open" {
397 if h.Precis == "" {
398 h.Precis = "really freaking long"
399 }
400 h.Open = ""
401 }
402 }
403 }
404}
405
406var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
407 rows, err := stmtUntagged.Query(userid)
408 if err != nil {
409 elog.Printf("error query untagged: %s", err)
410 return nil, false
411 }
412 defer rows.Close()
413 bad := make(map[string]bool)
414 for rows.Next() {
415 var xid, rid string
416 var flags int64
417 err = rows.Scan(&xid, &rid, &flags)
418 if err != nil {
419 elog.Printf("error scanning untag: %s", err)
420 continue
421 }
422 if flags&flagIsUntagged != 0 {
423 bad[xid] = true
424 }
425 if bad[rid] {
426 bad[xid] = true
427 }
428 }
429 return bad, true
430}})
431
432func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
433 var badparents map[string]bool
434 untagged.GetAndLock(userid, &badparents)
435 j := 0
436 reversehonks(honks)
437 for _, h := range honks {
438 if badparents[h.RID] {
439 badparents[h.XID] = true
440 continue
441 }
442 honks[j] = h
443 j++
444 }
445 untagged.Unlock()
446 honks = honks[0:j]
447 reversehonks(honks)
448 if !withfilt {
449 return honks
450 }
451 filts := getfilters(userid, filtHide)
452 j = 0
453outer:
454 for _, h := range honks {
455 for _, f := range filts {
456 if matchfilter(h, f) {
457 continue outer
458 }
459 }
460 honks[j] = h
461 j++
462 }
463 honks = honks[0:j]
464 return honks
465}