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