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/login"
29)
30
31type Filter struct {
32 ID int64 `json:"-"`
33 Actions []filtType `json:"-"`
34 Name string
35 Date time.Time
36 Actor string `json:",omitempty"`
37 IncludeAudience bool `json:",omitempty"`
38 OnlyUnknowns bool `json:",omitempty"`
39 Text string `json:",omitempty"`
40 re_text *regexp.Regexp
41 IsReply bool `json:",omitempty"`
42 IsAnnounce bool `json:",omitempty"`
43 AnnounceOf string `json:",omitempty"`
44 Reject bool `json:",omitempty"`
45 SkipMedia bool `json:",omitempty"`
46 Hide bool `json:",omitempty"`
47 Collapse bool `json:",omitempty"`
48 Rewrite string `json:",omitempty"`
49 re_rewrite *regexp.Regexp
50 Replace string `json:",omitempty"`
51 Expiration time.Time
52 Notes string
53}
54
55type filtType uint
56
57const (
58 filtNone filtType = iota
59 filtAny
60 filtReject
61 filtSkipMedia
62 filtHide
63 filtCollapse
64 filtRewrite
65)
66
67var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"}
68
69func (ft filtType) String() string {
70 return filtNames[ft]
71}
72
73type afiltermap map[filtType][]*Filter
74
75var filtInvalidator cache.Invalidator
76var filtcache *cache.Cache
77
78func init() {
79 // resolve init loop
80 filtcache = cache.New(cache.Options{Filler: filtcachefiller, Invalidator: &filtInvalidator})
81}
82
83func filtcachefiller(userid int64) (afiltermap, bool) {
84 rows, err := stmtGetFilters.Query(userid)
85 if err != nil {
86 elog.Printf("error querying filters: %s", err)
87 return nil, false
88 }
89 defer rows.Close()
90
91 now := time.Now()
92
93 var expflush time.Time
94
95 filtmap := make(afiltermap)
96 for rows.Next() {
97 filt := new(Filter)
98 var j string
99 var filterid int64
100 err = rows.Scan(&filterid, &j)
101 if err == nil {
102 err = unjsonify(j, filt)
103 }
104 if err != nil {
105 elog.Printf("error scanning filter: %s", err)
106 continue
107 }
108 if !filt.Expiration.IsZero() {
109 if filt.Expiration.Before(now) {
110 continue
111 }
112 if expflush.IsZero() || filt.Expiration.Before(expflush) {
113 expflush = filt.Expiration
114 }
115 }
116 if t := filt.Text; t != "" && t != "." {
117 wordfront := unicode.IsLetter(rune(t[0]))
118 wordtail := unicode.IsLetter(rune(t[len(t)-1]))
119 t = "(?i:" + t + ")"
120 if wordfront {
121 t = "\\b" + t
122 }
123 if wordtail {
124 t = t + "\\b"
125 }
126 filt.re_text, err = regexp.Compile(t)
127 if err != nil {
128 elog.Printf("error compiling filter text: %s", err)
129 continue
130 }
131 }
132 if t := filt.Rewrite; t != "" {
133 wordfront := unicode.IsLetter(rune(t[0]))
134 wordtail := unicode.IsLetter(rune(t[len(t)-1]))
135 t = "(?i:" + t + ")"
136 if wordfront {
137 t = "\\b" + t
138 }
139 if wordtail {
140 t = t + "\\b"
141 }
142 filt.re_rewrite, err = regexp.Compile(t)
143 if err != nil {
144 elog.Printf("error compiling filter rewrite: %s", err)
145 continue
146 }
147 }
148 filt.ID = filterid
149 if filt.Reject {
150 filt.Actions = append(filt.Actions, filtReject)
151 filtmap[filtReject] = append(filtmap[filtReject], filt)
152 }
153 if filt.SkipMedia {
154 filt.Actions = append(filt.Actions, filtSkipMedia)
155 filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
156 }
157 if filt.Hide {
158 filt.Actions = append(filt.Actions, filtHide)
159 filtmap[filtHide] = append(filtmap[filtHide], filt)
160 }
161 if filt.Collapse {
162 filt.Actions = append(filt.Actions, filtCollapse)
163 filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
164 }
165 if filt.Rewrite != "" {
166 filt.Actions = append(filt.Actions, filtRewrite)
167 filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
168 }
169 filtmap[filtAny] = append(filtmap[filtAny], filt)
170 }
171 sorting := filtmap[filtAny]
172 sort.Slice(filtmap[filtAny], func(i, j int) bool {
173 return sorting[i].Name < sorting[j].Name
174 })
175 if !expflush.IsZero() {
176 dur := expflush.Sub(now)
177 go filtcacheclear(userid, dur)
178 }
179 return filtmap, true
180}
181
182func filtcacheclear(userid int64, dur time.Duration) {
183 time.Sleep(dur + time.Second)
184 filtInvalidator.Clear(userid)
185}
186
187func getfilters(userid int64, scope filtType) []*Filter {
188 var filtmap afiltermap
189 ok := filtcache.Get(userid, &filtmap)
190 if ok {
191 return filtmap[scope]
192 }
193 return nil
194}
195
196type arejectmap map[string][]*Filter
197
198var rejectAnyKey = "..."
199
200var rejectcache = cache.New(cache.Options{Filler: func(userid int64) (arejectmap, bool) {
201 m := make(arejectmap)
202 filts := getfilters(userid, filtReject)
203 for _, f := range filts {
204 if f.Text != "" {
205 key := rejectAnyKey
206 m[key] = append(m[key], f)
207 continue
208 }
209 if f.IsAnnounce && f.AnnounceOf != "" {
210 key := f.AnnounceOf
211 m[key] = append(m[key], f)
212 }
213 if f.Actor != "" {
214 key := f.Actor
215 m[key] = append(m[key], f)
216 }
217 }
218 return m, true
219}, Invalidator: &filtInvalidator})
220
221func rejectfilters(userid int64, name string) []*Filter {
222 var m arejectmap
223 rejectcache.Get(userid, &m)
224 return m[name]
225}
226
227func rejectorigin(userid int64, origin string, isannounce bool) bool {
228 if o := originate(origin); o != "" {
229 origin = o
230 }
231 filts := rejectfilters(userid, origin)
232 for _, f := range filts {
233 if f.OnlyUnknowns {
234 continue
235 }
236 if isannounce && f.IsAnnounce {
237 if f.AnnounceOf == origin {
238 return true
239 }
240 }
241 if f.Actor == origin {
242 return true
243 }
244 }
245 return false
246}
247
248func rejectactor(userid int64, actor string) bool {
249 filts := rejectfilters(userid, actor)
250 for _, f := range filts {
251 if f.IsAnnounce {
252 continue
253 }
254 if f.Actor == actor {
255 ilog.Printf("rejecting actor: %s", actor)
256 return true
257 }
258 }
259 origin := originate(actor)
260 if origin == "" {
261 return false
262 }
263 filts = rejectfilters(userid, origin)
264 for _, f := range filts {
265 if f.IsAnnounce {
266 continue
267 }
268 if f.Actor == origin {
269 if f.OnlyUnknowns {
270 if unknownActor(userid, actor) {
271 ilog.Printf("rejecting unknown actor: %s", actor)
272 return true
273 }
274 continue
275 }
276 ilog.Printf("rejecting actor: %s", actor)
277 return true
278 }
279 }
280 return false
281}
282
283var knownknowns = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
284 m := make(map[string]bool)
285 honkers := gethonkers(userid)
286 for _, h := range honkers {
287 m[h.XID] = true
288 }
289 return m, true
290}, Invalidator: &honkerinvalidator})
291
292func unknownActor(userid int64, actor string) bool {
293 var knowns map[string]bool
294 knownknowns.Get(userid, &knowns)
295 return !knowns[actor]
296}
297
298func stealthmode(userid int64, r *http.Request) bool {
299 agent := r.UserAgent()
300 agent = originate(agent)
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 (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
350 f.AnnounceOf == originate(h.Oonker) {
351 match = true
352 rv += " announce"
353 }
354 }
355 if match && f.Text != "" && f.Text != "." {
356 match = false
357 re := f.re_text
358 m := re.FindString(h.Precis)
359 if m == "" {
360 m = re.FindString(h.Noise)
361 }
362 if m == "" {
363 for _, d := range h.Donks {
364 m = re.FindString(d.Desc)
365 if m != "" {
366 break
367 }
368 }
369 }
370 if m != "" {
371 match = true
372 rv = m
373 }
374 }
375 if match && f.Text == "." {
376 match = false
377 if h.Precis != "" {
378 match = true
379 rv = h.Precis
380 }
381 }
382 if match {
383 return rv
384 }
385 return ""
386}
387
388func rejectxonk(xonk *Honk) bool {
389 var m arejectmap
390 rejectcache.Get(xonk.UserID, &m)
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 int64) {
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 int64) (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 int64, 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 userinfo := login.GetUserInfo(r)
520 itsok := r.FormValue("itsok")
521 if itsok == "iforgiveyou" {
522 hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0)
523 _, err := stmtDeleteFilter.Exec(userinfo.UserID, hfcsid)
524 if err != nil {
525 elog.Printf("error deleting filter: %s", err)
526 }
527 filtInvalidator.Clear(userinfo.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(userinfo.UserID, j)
562 }
563 if err != nil {
564 elog.Printf("error saving filter: %s", err)
565 }
566
567 filtInvalidator.Clear(userinfo.UserID)
568 http.Redirect(w, r, "/hfcs", http.StatusSeeOther)
569}