// // Copyright (c) 2019 Ted Unangst // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. package main import ( "net/http" "regexp" "sort" "time" "unicode" "humungus.tedunangst.com/r/webs/cache" ) type Filter struct { ID int64 `json:"-"` Actions []filtType `json:"-"` Name string Date time.Time Actor string `json:",omitempty"` IncludeAudience bool `json:",omitempty"` Text string `json:",omitempty"` re_text *regexp.Regexp IsReply bool `json:",omitempty"` IsAnnounce bool `json:",omitempty"` AnnounceOf string `json:",omitempty"` Reject bool `json:",omitempty"` SkipMedia bool `json:",omitempty"` Hide bool `json:",omitempty"` Collapse bool `json:",omitempty"` Rewrite string `json:",omitempty"` re_rewrite *regexp.Regexp Replace string `json:",omitempty"` Expiration time.Time Notes string } type filtType uint const ( filtNone filtType = iota filtAny filtReject filtSkipMedia filtHide filtCollapse filtRewrite ) var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"} func (ft filtType) String() string { return filtNames[ft] } type afiltermap map[filtType][]*Filter var filtInvalidator cache.Invalidator var filtcache *cache.Cache func init() { // resolve init loop filtcache = cache.New(cache.Options{Filler: filtcachefiller, Invalidator: &filtInvalidator}) } func filtcachefiller(userid int64) (afiltermap, bool) { rows, err := stmtGetFilters.Query(userid) if err != nil { elog.Printf("error querying filters: %s", err) return nil, false } defer rows.Close() now := time.Now() var expflush time.Time filtmap := make(afiltermap) for rows.Next() { filt := new(Filter) var j string var filterid int64 err = rows.Scan(&filterid, &j) if err == nil { err = unjsonify(j, filt) } if err != nil { elog.Printf("error scanning filter: %s", err) continue } if !filt.Expiration.IsZero() { if filt.Expiration.Before(now) { continue } if expflush.IsZero() || filt.Expiration.Before(expflush) { expflush = filt.Expiration } } if t := filt.Text; t != "" && t != "." { wordfront := unicode.IsLetter(rune(t[0])) wordtail := unicode.IsLetter(rune(t[len(t)-1])) t = "(?i:" + t + ")" if wordfront { t = "\\b" + t } if wordtail { t = t + "\\b" } filt.re_text, err = regexp.Compile(t) if err != nil { elog.Printf("error compiling filter text: %s", err) continue } } if t := filt.Rewrite; t != "" { wordfront := unicode.IsLetter(rune(t[0])) wordtail := unicode.IsLetter(rune(t[len(t)-1])) t = "(?i:" + t + ")" if wordfront { t = "\\b" + t } if wordtail { t = t + "\\b" } filt.re_rewrite, err = regexp.Compile(t) if err != nil { elog.Printf("error compiling filter rewrite: %s", err) continue } } filt.ID = filterid if filt.Reject { filt.Actions = append(filt.Actions, filtReject) filtmap[filtReject] = append(filtmap[filtReject], filt) } if filt.SkipMedia { filt.Actions = append(filt.Actions, filtSkipMedia) filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt) } if filt.Hide { filt.Actions = append(filt.Actions, filtHide) filtmap[filtHide] = append(filtmap[filtHide], filt) } if filt.Collapse { filt.Actions = append(filt.Actions, filtCollapse) filtmap[filtCollapse] = append(filtmap[filtCollapse], filt) } if filt.Rewrite != "" { filt.Actions = append(filt.Actions, filtRewrite) filtmap[filtRewrite] = append(filtmap[filtRewrite], filt) } filtmap[filtAny] = append(filtmap[filtAny], filt) } sorting := filtmap[filtAny] sort.Slice(filtmap[filtAny], func(i, j int) bool { return sorting[i].Name < sorting[j].Name }) if !expflush.IsZero() { dur := expflush.Sub(now) go filtcacheclear(userid, dur) } return filtmap, true } func filtcacheclear(userid int64, dur time.Duration) { time.Sleep(dur + time.Second) filtInvalidator.Clear(userid) } func getfilters(userid int64, scope filtType) []*Filter { var filtmap afiltermap ok := filtcache.Get(userid, &filtmap) if ok { return filtmap[scope] } return nil } type arejectmap map[string][]*Filter var rejectAnyKey = "..." var rejectcache = cache.New(cache.Options{Filler: func(userid int64) (arejectmap, bool) { m := make(arejectmap) filts := getfilters(userid, filtReject) for _, f := range filts { if f.Text != "" { key := rejectAnyKey m[key] = append(m[key], f) continue } if f.IsAnnounce && f.AnnounceOf != "" { key := f.AnnounceOf m[key] = append(m[key], f) } if f.Actor != "" { key := f.Actor m[key] = append(m[key], f) } } return m, true }, Invalidator: &filtInvalidator}) func rejectfilters(userid int64, name string) []*Filter { var m arejectmap rejectcache.Get(userid, &m) return m[name] } func rejectorigin(userid int64, origin string, isannounce bool) bool { if o := originate(origin); o != "" { origin = o } filts := rejectfilters(userid, origin) for _, f := range filts { if isannounce && f.IsAnnounce { if f.AnnounceOf == origin { return true } } if f.Actor == origin { return true } } return false } func rejectactor(userid int64, actor string) bool { filts := rejectfilters(userid, actor) for _, f := range filts { if f.IsAnnounce { continue } if f.Actor == actor { ilog.Printf("rejecting actor: %s", actor) return true } } origin := originate(actor) if origin == "" { return false } filts = rejectfilters(userid, origin) for _, f := range filts { if f.IsAnnounce { continue } if f.Actor == origin { ilog.Printf("rejecting actor: %s", actor) return true } } return false } func stealthmode(userid int64, r *http.Request) bool { agent := r.UserAgent() agent = originate(agent) if agent != "" { fake := rejectorigin(userid, agent, false) if fake { ilog.Printf("faking 404 for %s", agent) return true } } return false } func matchfilter(h *Honk, f *Filter) bool { return matchfilterX(h, f) != "" } func matchfilterX(h *Honk, f *Filter) string { rv := "" match := true if match && f.Actor != "" { match = false if f.Actor == h.Honker || f.Actor == h.Oonker { match = true rv = f.Actor } if !match && (f.Actor == originate(h.Honker) || f.Actor == originate(h.Oonker) || f.Actor == originate(h.XID)) { match = true rv = f.Actor } if !match && f.IncludeAudience { for _, a := range h.Audience { if f.Actor == a || f.Actor == originate(a) { match = true rv = f.Actor break } } } } if match && f.IsReply { match = false if h.RID != "" { match = true rv += " reply" } } if match && f.IsAnnounce { match = false if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker || f.AnnounceOf == originate(h.Oonker) { match = true rv += " announce" } } if match && f.Text != "" && f.Text != "." { match = false re := f.re_text m := re.FindString(h.Precis) if m == "" { m = re.FindString(h.Noise) } if m == "" { for _, d := range h.Donks { m = re.FindString(d.Desc) if m != "" { break } } } if m != "" { match = true rv = m } } if match && f.Text == "." { match = false if h.Precis != "" { match = true rv = h.Precis } } if match { return rv } return "" } func rejectxonk(xonk *Honk) bool { var m arejectmap rejectcache.Get(xonk.UserID, &m) filts := m[rejectAnyKey] filts = append(filts, m[xonk.Honker]...) filts = append(filts, m[originate(xonk.Honker)]...) filts = append(filts, m[xonk.Oonker]...) filts = append(filts, m[originate(xonk.Oonker)]...) for _, a := range xonk.Audience { filts = append(filts, m[a]...) filts = append(filts, m[originate(a)]...) } for _, f := range filts { if cause := matchfilterX(xonk, f); cause != "" { ilog.Printf("rejecting %s because %s", xonk.XID, cause) return true } } return false } func skipMedia(xonk *Honk) bool { filts := getfilters(xonk.UserID, filtSkipMedia) for _, f := range filts { if matchfilter(xonk, f) { return true } } return false } func unsee(honks []*Honk, userid int64) { if userid != -1 { colfilts := getfilters(userid, filtCollapse) rwfilts := getfilters(userid, filtRewrite) for _, h := range honks { for _, f := range colfilts { if bad := matchfilterX(h, f); bad != "" { if h.Precis == "" { h.Precis = bad } h.Open = "" break } } if h.Open == "open" && h.Precis == "unspecified horror" { h.Precis = "" } for _, f := range rwfilts { if matchfilter(h, f) { h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace) } } if len(h.Noise) > 6000 && h.Open == "open" { if h.Precis == "" { h.Precis = "really freaking long" } h.Open = "" } } } else { for _, h := range honks { if h.Precis != "" { h.Open = "" } } } } var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) { rows, err := stmtUntagged.Query(userid) if err != nil { elog.Printf("error query untagged: %s", err) return nil, false } defer rows.Close() bad := make(map[string]bool) for rows.Next() { var xid, rid string var flags int64 err = rows.Scan(&xid, &rid, &flags) if err != nil { elog.Printf("error scanning untag: %s", err) continue } if flags&flagIsUntagged != 0 { bad[xid] = true } if bad[rid] { bad[xid] = true } } return bad, true }}) func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk { var badparents map[string]bool untagged.GetAndLock(userid, &badparents) j := 0 reversehonks(honks) for _, h := range honks { if badparents[h.RID] { badparents[h.XID] = true continue } honks[j] = h j++ } untagged.Unlock() honks = honks[0:j] reversehonks(honks) if !withfilt { return honks } filts := getfilters(userid, filtHide) j = 0 outer: for _, h := range honks { for _, f := range filts { if matchfilter(h, f) { continue outer } } honks[j] = h j++ } honks = honks[0:j] return honks }