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