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
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 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 log.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 log.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 filt.Text != "" {
112 filt.re_text, err = regexp.Compile("\\b(?i:" + filt.Text + ")\\b")
113 if err != nil {
114 log.Printf("error compiling filter text: %s", err)
115 continue
116 }
117 }
118 if filt.Rewrite != "" {
119 filt.re_rewrite, err = regexp.Compile("\\b(?i:" + filt.Rewrite + ")\\b")
120 if err != nil {
121 log.Printf("error compiling filter rewrite: %s", err)
122 continue
123 }
124 }
125 filt.ID = filterid
126 if filt.Reject {
127 filt.Actions = append(filt.Actions, filtReject)
128 filtmap[filtReject] = append(filtmap[filtReject], filt)
129 }
130 if filt.SkipMedia {
131 filt.Actions = append(filt.Actions, filtSkipMedia)
132 filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
133 }
134 if filt.Hide {
135 filt.Actions = append(filt.Actions, filtHide)
136 filtmap[filtHide] = append(filtmap[filtHide], filt)
137 }
138 if filt.Collapse {
139 filt.Actions = append(filt.Actions, filtCollapse)
140 filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
141 }
142 if filt.Rewrite != "" {
143 filt.Actions = append(filt.Actions, filtRewrite)
144 filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
145 }
146 filtmap[filtAny] = append(filtmap[filtAny], filt)
147 }
148 sorting := filtmap[filtAny]
149 sort.Slice(filtmap[filtAny], func(i, j int) bool {
150 return sorting[i].Name < sorting[j].Name
151 })
152 if !expflush.IsZero() {
153 dur := expflush.Sub(now)
154 go filtcacheclear(userid, dur)
155 }
156 return filtmap, true
157}
158
159func filtcacheclear(userid int64, dur time.Duration) {
160 time.Sleep(dur + time.Second)
161 filtInvalidator.Clear(userid)
162}
163
164func getfilters(userid int64, scope filtType) []*Filter {
165 var filtmap afiltermap
166 ok := filtcache.Get(userid, &filtmap)
167 if ok {
168 return filtmap[scope]
169 }
170 return nil
171}
172
173type arejectmap map[string][]*Filter
174
175var rejectAnyKey = "..."
176
177var rejectcache = cache.New(cache.Options{Filler: func(userid int64) (arejectmap, bool) {
178 m := make(arejectmap)
179 filts := getfilters(userid, filtReject)
180 for _, f := range filts {
181 if f.Text != "" {
182 key := rejectAnyKey
183 m[key] = append(m[key], f)
184 continue
185 }
186 if f.IsAnnounce && f.AnnounceOf != "" {
187 key := f.AnnounceOf
188 m[key] = append(m[key], f)
189 }
190 if f.Actor != "" {
191 key := f.Actor
192 m[key] = append(m[key], f)
193 }
194 }
195 return m, true
196}, Invalidator: &filtInvalidator})
197
198func rejectfilters(userid int64, name string) []*Filter {
199 var m arejectmap
200 rejectcache.Get(userid, &m)
201 return m[name]
202}
203
204func rejectorigin(userid int64, origin string, isannounce bool) bool {
205 if o := originate(origin); o != "" {
206 origin = o
207 }
208 filts := rejectfilters(userid, origin)
209 for _, f := range filts {
210 if isannounce && f.IsAnnounce {
211 if f.AnnounceOf == origin {
212 return true
213 }
214 }
215 if f.Actor == origin {
216 return true
217 }
218 }
219 return false
220}
221
222func rejectactor(userid int64, actor string) bool {
223 filts := rejectfilters(userid, actor)
224 for _, f := range filts {
225 if f.IsAnnounce {
226 continue
227 }
228 if f.Actor == actor {
229 log.Printf("rejecting actor: %s", actor)
230 return true
231 }
232 }
233 origin := originate(actor)
234 if origin == "" {
235 return false
236 }
237 filts = rejectfilters(userid, origin)
238 for _, f := range filts {
239 if f.IsAnnounce {
240 continue
241 }
242 if f.Actor == origin {
243 log.Printf("rejecting actor: %s", actor)
244 return true
245 }
246 }
247 return false
248}
249
250func stealthmode(userid int64, r *http.Request) bool {
251 agent := r.UserAgent()
252 agent = originate(agent)
253 if agent != "" {
254 fake := rejectorigin(userid, agent, false)
255 if fake {
256 log.Printf("faking 404 for %s", agent)
257 return true
258 }
259 }
260 return false
261}
262
263func matchfilter(h *Honk, f *Filter) bool {
264 return matchfilterX(h, f) != ""
265}
266
267func matchfilterX(h *Honk, f *Filter) string {
268 rv := ""
269 match := true
270 if match && f.Actor != "" {
271 match = false
272 if f.Actor == h.Honker || f.Actor == h.Oonker {
273 match = true
274 rv = f.Actor
275 }
276 if !match && (f.Actor == originate(h.Honker) ||
277 f.Actor == originate(h.Oonker) ||
278 f.Actor == originate(h.XID)) {
279 match = true
280 rv = f.Actor
281 }
282 if !match && f.IncludeAudience {
283 for _, a := range h.Audience {
284 if f.Actor == a || f.Actor == originate(a) {
285 match = true
286 rv = f.Actor
287 break
288 }
289 }
290 }
291 }
292 if match && f.IsAnnounce {
293 match = false
294 if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
295 f.AnnounceOf == originate(h.Oonker) {
296 match = true
297 rv += " announce"
298 }
299 }
300 if match && f.Text != "" {
301 match = false
302 re := f.re_text
303 m := re.FindString(h.Precis)
304 if m == "" {
305 m = re.FindString(h.Noise)
306 }
307 if m == "" {
308 for _, d := range h.Donks {
309 m = re.FindString(d.Desc)
310 if m != "" {
311 break
312 }
313 }
314 }
315 if m != "" {
316 match = true
317 rv = m
318 }
319 }
320 if match {
321 return rv
322 }
323 return ""
324}
325
326func rejectxonk(xonk *Honk) bool {
327 var m arejectmap
328 rejectcache.Get(xonk.UserID, &m)
329 filts := m[rejectAnyKey]
330 filts = append(filts, m[xonk.Honker]...)
331 filts = append(filts, m[originate(xonk.Honker)]...)
332 filts = append(filts, m[xonk.Oonker]...)
333 filts = append(filts, m[originate(xonk.Oonker)]...)
334 for _, a := range xonk.Audience {
335 filts = append(filts, m[a]...)
336 filts = append(filts, m[originate(a)]...)
337 }
338 for _, f := range filts {
339 if cause := matchfilterX(xonk, f); cause != "" {
340 log.Printf("rejecting %s because %s", xonk.XID, cause)
341 return true
342 }
343 }
344 return false
345}
346
347func skipMedia(xonk *Honk) bool {
348 filts := getfilters(xonk.UserID, filtSkipMedia)
349 for _, f := range filts {
350 if matchfilter(xonk, f) {
351 return true
352 }
353 }
354 return false
355}
356
357func unsee(honks []*Honk, userid int64) {
358 if userid != -1 {
359 colfilts := getfilters(userid, filtCollapse)
360 rwfilts := getfilters(userid, filtRewrite)
361 for _, h := range honks {
362 for _, f := range colfilts {
363 if bad := matchfilterX(h, f); bad != "" {
364 if h.Precis == "" {
365 h.Precis = bad
366 }
367 h.Open = ""
368 break
369 }
370 }
371 if h.Open == "open" && h.Precis == "unspecified horror" {
372 h.Precis = ""
373 }
374 for _, f := range rwfilts {
375 if matchfilter(h, f) {
376 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
377 }
378 }
379 if len(h.Noise) > 6000 && h.Open == "open" {
380 if h.Precis == "" {
381 h.Precis = "really freaking long"
382 }
383 h.Open = ""
384 }
385 }
386 }
387}
388
389var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
390 rows, err := stmtUntagged.Query(userid)
391 if err != nil {
392 log.Printf("error query untagged: %s", err)
393 return nil, false
394 }
395 defer rows.Close()
396 bad := make(map[string]bool)
397 for rows.Next() {
398 var xid, rid string
399 var flags int64
400 err = rows.Scan(&xid, &rid, &flags)
401 if err != nil {
402 log.Printf("error scanning untag: %s", err)
403 continue
404 }
405 if flags&flagIsUntagged != 0 {
406 bad[xid] = true
407 }
408 if bad[rid] {
409 bad[xid] = true
410 }
411 }
412 return bad, true
413}})
414
415func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
416 var badparents map[string]bool
417 untagged.GetAndLock(userid, &badparents)
418 j := 0
419 reversehonks(honks)
420 for _, h := range honks {
421 if badparents[h.RID] {
422 badparents[h.XID] = true
423 continue
424 }
425 honks[j] = h
426 j++
427 }
428 untagged.Unlock()
429 honks = honks[0:j]
430 reversehonks(honks)
431 if !withfilt {
432 return honks
433 }
434 filts := getfilters(userid, filtHide)
435 j = 0
436outer:
437 for _, h := range honks {
438 for _, f := range filts {
439 if matchfilter(h, f) {
440 continue outer
441 }
442 }
443 honks[j] = h
444 j++
445 }
446 honks = honks[0:j]
447 return honks
448}