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 "strconv"
23 "strings"
24 "time"
25 "unicode"
26
27 "humungus.tedunangst.com/r/webs/cache"
28 "humungus.tedunangst.com/r/webs/gencache"
29 "humungus.tedunangst.com/r/webs/login"
30)
31
32type Filter struct {
33 ID int64 `json:"-"`
34 Actions []filtType `json:"-"`
35 Name string
36 Date time.Time
37 Actor string `json:",omitempty"`
38 IncludeAudience bool `json:",omitempty"`
39 OnlyUnknowns bool `json:",omitempty"`
40 Text string `json:",omitempty"`
41 re_text *regexp.Regexp
42 IsReply bool `json:",omitempty"`
43 IsAnnounce bool `json:",omitempty"`
44 AnnounceOf string `json:",omitempty"`
45 Reject bool `json:",omitempty"`
46 SkipMedia bool `json:",omitempty"`
47 Hide bool `json:",omitempty"`
48 Collapse bool `json:",omitempty"`
49 Rewrite string `json:",omitempty"`
50 re_rewrite *regexp.Regexp
51 Replace string `json:",omitempty"`
52 Expiration time.Time
53 Notes string
54}
55
56type filtType uint
57
58const (
59 filtNone filtType = iota
60 filtAny
61 filtReject
62 filtSkipMedia
63 filtHide
64 filtCollapse
65 filtRewrite
66)
67
68var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"}
69
70func (ft filtType) String() string {
71 return filtNames[ft]
72}
73
74type afiltermap map[filtType][]*Filter
75
76var filtInvalidator gencache.Invalidator[UserID]
77var filtcache *gencache.Cache[UserID, afiltermap]
78
79func init() {
80 // resolve init loop
81 filtcache = gencache.New(gencache.Options[UserID, afiltermap]{
82 Fill: filtcachefiller,
83 Invalidator: &filtInvalidator,
84 })
85}
86
87func filtcachefiller(userid UserID) (afiltermap, bool) {
88 rows, err := stmtGetFilters.Query(userid)
89 if err != nil {
90 elog.Printf("error querying filters: %s", err)
91 return nil, false
92 }
93 defer rows.Close()
94
95 now := time.Now()
96
97 var expflush time.Time
98
99 filtmap := make(afiltermap)
100 for rows.Next() {
101 filt := new(Filter)
102 var j string
103 var filterid int64
104 err = rows.Scan(&filterid, &j)
105 if err == nil {
106 err = unjsonify(j, filt)
107 }
108 if err != nil {
109 elog.Printf("error scanning filter: %s", err)
110 continue
111 }
112 if !filt.Expiration.IsZero() {
113 if filt.Expiration.Before(now) {
114 continue
115 }
116 if expflush.IsZero() || filt.Expiration.Before(expflush) {
117 dlog.Printf("filter expired: %s", filt.Name)
118 expflush = filt.Expiration
119 }
120 }
121 if t := filt.Text; t != "" && t != "." {
122 wordfront := unicode.IsLetter(rune(t[0]))
123 wordtail := unicode.IsLetter(rune(t[len(t)-1]))
124 t = "(?i:" + t + ")"
125 if wordfront {
126 t = "\\b" + t
127 }
128 if wordtail {
129 t = t + "\\b"
130 }
131 filt.re_text, err = regexp.Compile(t)
132 if err != nil {
133 elog.Printf("error compiling filter text: %s", err)
134 continue
135 }
136 }
137 if t := filt.Rewrite; t != "" {
138 wordfront := unicode.IsLetter(rune(t[0]))
139 wordtail := unicode.IsLetter(rune(t[len(t)-1]))
140 t = "(?i:" + t + ")"
141 if wordfront {
142 t = "\\b" + t
143 }
144 if wordtail {
145 t = t + "\\b"
146 }
147 filt.re_rewrite, err = regexp.Compile(t)
148 if err != nil {
149 elog.Printf("error compiling filter rewrite: %s", err)
150 continue
151 }
152 }
153 filt.ID = filterid
154 if filt.Reject {
155 filt.Actions = append(filt.Actions, filtReject)
156 filtmap[filtReject] = append(filtmap[filtReject], filt)
157 }
158 if filt.SkipMedia {
159 filt.Actions = append(filt.Actions, filtSkipMedia)
160 filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
161 }
162 if filt.Hide {
163 filt.Actions = append(filt.Actions, filtHide)
164 filtmap[filtHide] = append(filtmap[filtHide], filt)
165 }
166 if filt.Collapse {
167 filt.Actions = append(filt.Actions, filtCollapse)
168 filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
169 }
170 if filt.Rewrite != "" {
171 filt.Actions = append(filt.Actions, filtRewrite)
172 filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
173 }
174 filtmap[filtAny] = append(filtmap[filtAny], filt)
175 }
176 sorting := filtmap[filtAny]
177 sort.Slice(filtmap[filtAny], func(i, j int) bool {
178 return sorting[i].Name < sorting[j].Name
179 })
180 if !expflush.IsZero() {
181 dur := expflush.Sub(now)
182 go filtcacheclear(userid, dur)
183 }
184 return filtmap, true
185}
186
187func filtcacheclear(userid UserID, dur time.Duration) {
188 dlog.Printf("clearing filters in %s", dur.String())
189 time.Sleep(dur + time.Second)
190 filtInvalidator.Clear(userid)
191}
192
193func getfilters(userid UserID, scope filtType) []*Filter {
194 filtmap, ok := filtcache.Get(userid)
195 if ok {
196 return filtmap[scope]
197 }
198 return nil
199}
200
201type arejectmap map[string][]*Filter
202
203var rejectAnyKey = "..."
204
205var rejectcache = gencache.New(gencache.Options[UserID, arejectmap]{Fill: func(userid UserID) (arejectmap, bool) {
206 m := make(arejectmap)
207 filts := getfilters(userid, filtReject)
208 for _, f := range filts {
209 if f.Text != "" {
210 key := rejectAnyKey
211 m[key] = append(m[key], f)
212 continue
213 }
214 if f.IsAnnounce && f.AnnounceOf != "" {
215 key := f.AnnounceOf
216 m[key] = append(m[key], f)
217 }
218 if f.Actor != "" {
219 key := f.Actor
220 m[key] = append(m[key], f)
221 }
222 }
223 return m, true
224}, Invalidator: &filtInvalidator})
225
226func rejectfilters(userid UserID, name string) []*Filter {
227 m, _ := rejectcache.Get(userid)
228 return m[name]
229}
230
231func rejectorigin(userid UserID, origin string, isannounce bool) bool {
232 if o := originate(origin); o != "" {
233 origin = o
234 }
235 filts := rejectfilters(userid, origin)
236 for _, f := range filts {
237 if f.OnlyUnknowns {
238 continue
239 }
240 if isannounce && f.IsAnnounce {
241 if f.AnnounceOf == origin {
242 return true
243 }
244 }
245 if f.Actor == origin {
246 return true
247 }
248 }
249 return false
250}
251
252func rejectactor(userid UserID, actor string) bool {
253 filts := rejectfilters(userid, actor)
254 for _, f := range filts {
255 if f.IsAnnounce || f.IsReply {
256 continue
257 }
258 if f.Text != "" {
259 continue
260 }
261 ilog.Printf("rejecting actor: %s", actor)
262 return true
263 }
264 origin := originate(actor)
265 if origin == "" {
266 return false
267 }
268 filts = rejectfilters(userid, origin)
269 for _, f := range filts {
270 if f.IsAnnounce {
271 continue
272 }
273 if f.Actor == origin {
274 if f.OnlyUnknowns {
275 if unknownActor(userid, actor) {
276 ilog.Printf("rejecting unknown actor: %s", actor)
277 return true
278 }
279 continue
280 }
281 ilog.Printf("rejecting actor: %s", actor)
282 return true
283 }
284 }
285 return false
286}
287
288var knownknowns = gencache.New(gencache.Options[UserID, map[string]bool]{Fill: func(userid UserID) (map[string]bool, bool) {
289 m := make(map[string]bool)
290 honkers := gethonkers(userid)
291 for _, h := range honkers {
292 m[h.XID] = true
293 }
294 return m, true
295}, Invalidator: &honkerinvalidator})
296
297func unknownActor(userid UserID, actor string) bool {
298 knowns, _ := knownknowns.Get(userid)
299 return !knowns[actor]
300}
301
302func stealthmode(userid UserID, r *http.Request) bool {
303 agent := requestActor(r)
304 if agent != "" {
305 fake := rejectorigin(userid, agent, false)
306 if fake {
307 ilog.Printf("faking 404 for %s", agent)
308 return true
309 }
310 }
311 return false
312}
313
314func matchfilter(h *Honk, f *Filter) bool {
315 return matchfilterX(h, f) != ""
316}
317
318func matchfilterX(h *Honk, f *Filter) string {
319 rv := ""
320 match := true
321 if match && f.Actor != "" {
322 match = false
323 if f.Actor == h.Honker || f.Actor == h.Oonker {
324 match = true
325 rv = f.Actor
326 }
327 if !match && !f.OnlyUnknowns && (f.Actor == originate(h.Honker) ||
328 f.Actor == originate(h.Oonker) ||
329 f.Actor == originate(h.XID)) {
330 match = true
331 rv = f.Actor
332 }
333 if !match && f.IncludeAudience {
334 for _, a := range h.Audience {
335 if f.Actor == a || f.Actor == originate(a) {
336 match = true
337 rv = f.Actor
338 break
339 }
340 }
341 }
342 }
343 if match && f.IsReply {
344 match = false
345 if h.RID != "" {
346 match = true
347 rv += " reply"
348 }
349 }
350 if match && f.IsAnnounce {
351 match = false
352 if h.Oonker != "" {
353 if f.AnnounceOf == "" || f.AnnounceOf == h.Oonker || f.AnnounceOf == originate(h.Oonker) {
354 match = true
355 rv += " announce"
356 }
357 }
358 }
359 if match && f.Text != "" && f.Text != "." {
360 match = false
361 re := f.re_text
362 m := re.FindString(h.Precis)
363 if m == "" {
364 m = re.FindString(h.Noise)
365 }
366 if m == "" {
367 for _, d := range h.Donks {
368 m = re.FindString(d.Desc)
369 if m != "" {
370 break
371 }
372 }
373 }
374 if m != "" {
375 match = true
376 rv = m
377 }
378 }
379 if match && f.Text == "." {
380 match = false
381 if h.Precis != "" {
382 match = true
383 rv = h.Precis
384 }
385 }
386 if match {
387 return rv
388 }
389 return ""
390}
391
392func rejectxonk(xonk *Honk) bool {
393 m, _ := rejectcache.Get(xonk.UserID)
394 filts := m[rejectAnyKey]
395 filts = append(filts, m[xonk.Honker]...)
396 filts = append(filts, m[originate(xonk.Honker)]...)
397 filts = append(filts, m[xonk.Oonker]...)
398 filts = append(filts, m[originate(xonk.Oonker)]...)
399 for _, a := range xonk.Audience {
400 filts = append(filts, m[a]...)
401 filts = append(filts, m[originate(a)]...)
402 }
403 for _, f := range filts {
404 if cause := matchfilterX(xonk, f); cause != "" {
405 ilog.Printf("rejecting %s because %s", xonk.XID, cause)
406 return true
407 }
408 }
409 return false
410}
411
412func skipMedia(xonk *Honk) bool {
413 filts := getfilters(xonk.UserID, filtSkipMedia)
414 for _, f := range filts {
415 if matchfilter(xonk, f) {
416 return true
417 }
418 }
419 return false
420}
421
422func unsee(honks []*Honk, userid UserID) {
423 if userid != -1 {
424 colfilts := getfilters(userid, filtCollapse)
425 rwfilts := getfilters(userid, filtRewrite)
426 for _, h := range honks {
427 for _, f := range colfilts {
428 if bad := matchfilterX(h, f); bad != "" {
429 if h.Precis == "" {
430 h.Precis = bad
431 }
432 h.Open = ""
433 break
434 }
435 }
436 if h.Open == "open" && h.Precis == "unspecified horror" {
437 h.Precis = ""
438 }
439 for _, f := range rwfilts {
440 if matchfilter(h, f) {
441 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
442 }
443 }
444 if len(h.Noise) > 6000 && h.Open == "open" {
445 if h.Precis == "" {
446 h.Precis = "really freaking long"
447 }
448 h.Open = ""
449 }
450 }
451 } else {
452 for _, h := range honks {
453 if h.Precis != "" {
454 h.Open = ""
455 }
456 }
457 }
458}
459
460var untagged = cache.New(cache.Options{Filler: func(userid UserID) (map[string]bool, bool) {
461 rows, err := stmtUntagged.Query(userid)
462 if err != nil {
463 elog.Printf("error query untagged: %s", err)
464 return nil, false
465 }
466 defer rows.Close()
467 bad := make(map[string]bool)
468 for rows.Next() {
469 var xid, rid string
470 var flags int64
471 err = rows.Scan(&xid, &rid, &flags)
472 if err != nil {
473 elog.Printf("error scanning untag: %s", err)
474 continue
475 }
476 if flags&flagIsUntagged != 0 {
477 bad[xid] = true
478 }
479 if bad[rid] {
480 bad[xid] = true
481 }
482 }
483 return bad, true
484}})
485
486func osmosis(honks []*Honk, userid UserID, withfilt bool) []*Honk {
487 var badparents map[string]bool
488 untagged.GetAndLock(userid, &badparents)
489 j := 0
490 reversehonks(honks)
491 for _, h := range honks {
492 if badparents[h.RID] {
493 badparents[h.XID] = true
494 continue
495 }
496 honks[j] = h
497 j++
498 }
499 untagged.Unlock()
500 honks = honks[0:j]
501 reversehonks(honks)
502 if !withfilt {
503 return honks
504 }
505 filts := getfilters(userid, filtHide)
506 j = 0
507outer:
508 for _, h := range honks {
509 for _, f := range filts {
510 if matchfilter(h, f) {
511 continue outer
512 }
513 }
514 honks[j] = h
515 j++
516 }
517 honks = honks[0:j]
518 return honks
519}
520
521func savehfcs(w http.ResponseWriter, r *http.Request) {
522 userid := UserID(login.GetUserInfo(r).UserID)
523 itsok := r.FormValue("itsok")
524 if itsok == "iforgiveyou" {
525 hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0)
526 _, err := stmtDeleteFilter.Exec(userid, hfcsid)
527 if err != nil {
528 elog.Printf("error deleting filter: %s", err)
529 }
530 filtInvalidator.Clear(userid)
531 http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
532 return
533 }
534
535 filt := new(Filter)
536 filt.Name = strings.TrimSpace(r.FormValue("name"))
537 filt.Date = time.Now().UTC()
538 filt.Actor = strings.TrimSpace(r.FormValue("actor"))
539 filt.IncludeAudience = r.FormValue("incaud") == "yes"
540 filt.OnlyUnknowns = r.FormValue("unknowns") == "yes"
541 filt.Text = strings.TrimSpace(r.FormValue("filttext"))
542 filt.IsReply = r.FormValue("isreply") == "yes"
543 filt.IsAnnounce = r.FormValue("isannounce") == "yes"
544 filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof"))
545 filt.Reject = r.FormValue("doreject") == "yes"
546 filt.SkipMedia = r.FormValue("doskipmedia") == "yes"
547 filt.Hide = r.FormValue("dohide") == "yes"
548 filt.Collapse = r.FormValue("docollapse") == "yes"
549 filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite"))
550 filt.Replace = strings.TrimSpace(r.FormValue("filtreplace"))
551 if dur := parseDuration(r.FormValue("filtduration")); dur > 0 {
552 filt.Expiration = time.Now().UTC().Add(dur)
553 }
554 filt.Notes = strings.TrimSpace(r.FormValue("filtnotes"))
555
556 if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce {
557 ilog.Printf("blank filter")
558 http.Error(w, "can't save a blank filter", http.StatusInternalServerError)
559 return
560 }
561
562 j, err := jsonify(filt)
563 if err == nil {
564 _, err = stmtSaveFilter.Exec(userid, j)
565 }
566 if err != nil {
567 elog.Printf("error saving filter: %s", err)
568 }
569
570 filtInvalidator.Clear(userid)
571 http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
572}