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 || f.IsReply {
254 continue
255 }
256 if f.Text != "" {
257 continue
258 }
259 ilog.Printf("rejecting actor: %s", actor)
260 return true
261 }
262 origin := originate(actor)
263 if origin == "" {
264 return false
265 }
266 filts = rejectfilters(userid, origin)
267 for _, f := range filts {
268 if f.IsAnnounce {
269 continue
270 }
271 if f.Actor == origin {
272 if f.OnlyUnknowns {
273 if unknownActor(userid, actor) {
274 ilog.Printf("rejecting unknown actor: %s", actor)
275 return true
276 }
277 continue
278 }
279 ilog.Printf("rejecting actor: %s", actor)
280 return true
281 }
282 }
283 return false
284}
285
286var knownknowns = gencache.New(gencache.Options[UserID, map[string]bool]{Fill: func(userid UserID) (map[string]bool, bool) {
287 m := make(map[string]bool)
288 honkers := gethonkers(userid)
289 for _, h := range honkers {
290 m[h.XID] = true
291 }
292 return m, true
293}, Invalidator: &honkerinvalidator})
294
295func unknownActor(userid UserID, actor string) bool {
296 knowns, _ := knownknowns.Get(userid)
297 return !knowns[actor]
298}
299
300func stealthmode(userid UserID, r *http.Request) bool {
301 agent := requestActor(r)
302 if agent != "" {
303 fake := rejectorigin(userid, agent, false)
304 if fake {
305 ilog.Printf("faking 404 for %s", agent)
306 return true
307 }
308 }
309 return false
310}
311
312func matchfilter(h *Honk, f *Filter) bool {
313 return matchfilterX(h, f) != ""
314}
315
316func matchfilterX(h *Honk, f *Filter) string {
317 rv := ""
318 match := true
319 if match && f.Actor != "" {
320 match = false
321 if f.Actor == h.Honker || f.Actor == h.Oonker {
322 match = true
323 rv = f.Actor
324 }
325 if !match && !f.OnlyUnknowns && (f.Actor == originate(h.Honker) ||
326 f.Actor == originate(h.Oonker) ||
327 f.Actor == originate(h.XID)) {
328 match = true
329 rv = f.Actor
330 }
331 if !match && f.IncludeAudience {
332 for _, a := range h.Audience {
333 if f.Actor == a || f.Actor == originate(a) {
334 match = true
335 rv = f.Actor
336 break
337 }
338 }
339 }
340 }
341 if match && f.IsReply {
342 match = false
343 if h.RID != "" {
344 match = true
345 rv += " reply"
346 }
347 }
348 if match && f.IsAnnounce {
349 match = false
350 if h.Oonker != "" {
351 if f.AnnounceOf == "" || f.AnnounceOf == h.Oonker || f.AnnounceOf == originate(h.Oonker) {
352 match = true
353 rv += " announce"
354 }
355 }
356 }
357 if match && f.Text != "" && f.Text != "." {
358 match = false
359 re := f.re_text
360 m := re.FindString(h.Precis)
361 if m == "" {
362 m = re.FindString(h.Noise)
363 }
364 if m == "" {
365 for _, d := range h.Donks {
366 m = re.FindString(d.Desc)
367 if m != "" {
368 break
369 }
370 }
371 }
372 if m != "" {
373 match = true
374 rv = m
375 }
376 }
377 if match && f.Text == "." {
378 match = false
379 if h.Precis != "" {
380 match = true
381 rv = h.Precis
382 }
383 }
384 if match {
385 return rv
386 }
387 return ""
388}
389
390func rejectxonk(xonk *Honk) bool {
391 m, _ := rejectcache.Get(xonk.UserID)
392 filts := m[rejectAnyKey]
393 filts = append(filts, m[xonk.Honker]...)
394 filts = append(filts, m[originate(xonk.Honker)]...)
395 filts = append(filts, m[xonk.Oonker]...)
396 filts = append(filts, m[originate(xonk.Oonker)]...)
397 for _, a := range xonk.Audience {
398 filts = append(filts, m[a]...)
399 filts = append(filts, m[originate(a)]...)
400 }
401 for _, f := range filts {
402 if cause := matchfilterX(xonk, f); cause != "" {
403 ilog.Printf("rejecting %s because %s", xonk.XID, cause)
404 return true
405 }
406 }
407 return false
408}
409
410func skipMedia(xonk *Honk) bool {
411 filts := getfilters(xonk.UserID, filtSkipMedia)
412 for _, f := range filts {
413 if matchfilter(xonk, f) {
414 return true
415 }
416 }
417 return false
418}
419
420func unsee(honks []*Honk, userid UserID) {
421 if userid != -1 {
422 colfilts := getfilters(userid, filtCollapse)
423 rwfilts := getfilters(userid, filtRewrite)
424 for _, h := range honks {
425 for _, f := range colfilts {
426 if bad := matchfilterX(h, f); bad != "" {
427 if h.Precis == "" {
428 h.Precis = bad
429 }
430 h.Open = ""
431 break
432 }
433 }
434 if h.Open == "open" && h.Precis == "unspecified horror" {
435 h.Precis = ""
436 }
437 for _, f := range rwfilts {
438 if matchfilter(h, f) {
439 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
440 }
441 }
442 if len(h.Noise) > 6000 && h.Open == "open" {
443 if h.Precis == "" {
444 h.Precis = "really freaking long"
445 }
446 h.Open = ""
447 }
448 }
449 } else {
450 for _, h := range honks {
451 if h.Precis != "" {
452 h.Open = ""
453 }
454 }
455 }
456}
457
458var untagged = cache.New(cache.Options{Filler: func(userid UserID) (map[string]bool, bool) {
459 rows, err := stmtUntagged.Query(userid)
460 if err != nil {
461 elog.Printf("error query untagged: %s", err)
462 return nil, false
463 }
464 defer rows.Close()
465 bad := make(map[string]bool)
466 for rows.Next() {
467 var xid, rid string
468 var flags int64
469 err = rows.Scan(&xid, &rid, &flags)
470 if err != nil {
471 elog.Printf("error scanning untag: %s", err)
472 continue
473 }
474 if flags&flagIsUntagged != 0 {
475 bad[xid] = true
476 }
477 if bad[rid] {
478 bad[xid] = true
479 }
480 }
481 return bad, true
482}})
483
484func osmosis(honks []*Honk, userid UserID, withfilt bool) []*Honk {
485 var badparents map[string]bool
486 untagged.GetAndLock(userid, &badparents)
487 j := 0
488 reversehonks(honks)
489 for _, h := range honks {
490 if badparents[h.RID] {
491 badparents[h.XID] = true
492 continue
493 }
494 honks[j] = h
495 j++
496 }
497 untagged.Unlock()
498 honks = honks[0:j]
499 reversehonks(honks)
500 if !withfilt {
501 return honks
502 }
503 filts := getfilters(userid, filtHide)
504 j = 0
505outer:
506 for _, h := range honks {
507 for _, f := range filts {
508 if matchfilter(h, f) {
509 continue outer
510 }
511 }
512 honks[j] = h
513 j++
514 }
515 honks = honks[0:j]
516 return honks
517}
518
519func savehfcs(w http.ResponseWriter, r *http.Request) {
520 userid := UserID(login.GetUserInfo(r).UserID)
521 itsok := r.FormValue("itsok")
522 if itsok == "iforgiveyou" {
523 hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0)
524 _, err := stmtDeleteFilter.Exec(userid, hfcsid)
525 if err != nil {
526 elog.Printf("error deleting filter: %s", err)
527 }
528 filtInvalidator.Clear(userid)
529 http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
530 return
531 }
532
533 filt := new(Filter)
534 filt.Name = strings.TrimSpace(r.FormValue("name"))
535 filt.Date = time.Now().UTC()
536 filt.Actor = strings.TrimSpace(r.FormValue("actor"))
537 filt.IncludeAudience = r.FormValue("incaud") == "yes"
538 filt.OnlyUnknowns = r.FormValue("unknowns") == "yes"
539 filt.Text = strings.TrimSpace(r.FormValue("filttext"))
540 filt.IsReply = r.FormValue("isreply") == "yes"
541 filt.IsAnnounce = r.FormValue("isannounce") == "yes"
542 filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof"))
543 filt.Reject = r.FormValue("doreject") == "yes"
544 filt.SkipMedia = r.FormValue("doskipmedia") == "yes"
545 filt.Hide = r.FormValue("dohide") == "yes"
546 filt.Collapse = r.FormValue("docollapse") == "yes"
547 filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite"))
548 filt.Replace = strings.TrimSpace(r.FormValue("filtreplace"))
549 if dur := parseDuration(r.FormValue("filtduration")); dur > 0 {
550 filt.Expiration = time.Now().UTC().Add(dur)
551 }
552 filt.Notes = strings.TrimSpace(r.FormValue("filtnotes"))
553
554 if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce {
555 ilog.Printf("blank filter")
556 http.Error(w, "can't save a blank filter", http.StatusInternalServerError)
557 return
558 }
559
560 j, err := jsonify(filt)
561 if err == nil {
562 _, err = stmtSaveFilter.Exec(userid, j)
563 }
564 if err != nil {
565 elog.Printf("error saving filter: %s", err)
566 }
567
568 filtInvalidator.Clear(userid)
569 http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
570}