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 "log"
20 "net/http"
21 "regexp"
22 "sort"
23 "time"
24
25 "humungus.tedunangst.com/r/webs/cache"
26)
27
28type Filter struct {
29 ID int64 `json:"-"`
30 Actions []filtType `json:"-"`
31 Name string
32 Date time.Time
33 Actor string `json:",omitempty"`
34 IncludeAudience bool `json:",omitempty"`
35 Text string `json:",omitempty"`
36 re_text *regexp.Regexp
37 IsAnnounce bool `json:",omitempty"`
38 AnnounceOf string `json:",omitempty"`
39 Reject bool `json:",omitempty"`
40 SkipMedia bool `json:",omitempty"`
41 Hide bool `json:",omitempty"`
42 Collapse bool `json:",omitempty"`
43 Rewrite string `json:",omitempty"`
44 re_rewrite *regexp.Regexp
45 Replace string `json:",omitempty"`
46 Expiration time.Time
47 Notes string
48}
49
50type filtType uint
51
52const (
53 filtNone filtType = iota
54 filtAny
55 filtReject
56 filtSkipMedia
57 filtHide
58 filtCollapse
59 filtRewrite
60)
61
62var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"}
63
64func (ft filtType) String() string {
65 return filtNames[ft]
66}
67
68type afiltermap map[filtType][]*Filter
69
70var filtcache *cache.Cache
71
72func init() {
73 // resolve init loop
74 filtcache = cache.New(cache.Options{Filler: filtcachefiller})
75}
76
77func filtcachefiller(userid int64) (afiltermap, bool) {
78 rows, err := stmtGetFilters.Query(userid)
79 if err != nil {
80 log.Printf("error querying filters: %s", err)
81 return nil, false
82 }
83 defer rows.Close()
84
85 now := time.Now()
86
87 var expflush time.Time
88
89 filtmap := make(afiltermap)
90 for rows.Next() {
91 filt := new(Filter)
92 var j string
93 var filterid int64
94 err = rows.Scan(&filterid, &j)
95 if err == nil {
96 err = unjsonify(j, filt)
97 }
98 if err != nil {
99 log.Printf("error scanning filter: %s", err)
100 continue
101 }
102 if !filt.Expiration.IsZero() {
103 if filt.Expiration.Before(now) {
104 continue
105 }
106 if expflush.IsZero() || filt.Expiration.Before(expflush) {
107 expflush = filt.Expiration
108 }
109 }
110 if filt.Text != "" {
111 filt.re_text, err = regexp.Compile("\\b(?i:" + filt.Text + ")\\b")
112 if err != nil {
113 log.Printf("error compiling filter text: %s", err)
114 continue
115 }
116 }
117 if filt.Rewrite != "" {
118 filt.re_rewrite, err = regexp.Compile("\\b(?i:" + filt.Rewrite + ")\\b")
119 if err != nil {
120 log.Printf("error compiling filter rewrite: %s", err)
121 continue
122 }
123 }
124 filt.ID = filterid
125 if filt.Reject {
126 filt.Actions = append(filt.Actions, filtReject)
127 filtmap[filtReject] = append(filtmap[filtReject], filt)
128 }
129 if filt.SkipMedia {
130 filt.Actions = append(filt.Actions, filtSkipMedia)
131 filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
132 }
133 if filt.Hide {
134 filt.Actions = append(filt.Actions, filtHide)
135 filtmap[filtHide] = append(filtmap[filtHide], filt)
136 }
137 if filt.Collapse {
138 filt.Actions = append(filt.Actions, filtCollapse)
139 filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
140 }
141 if filt.Rewrite != "" {
142 filt.Actions = append(filt.Actions, filtRewrite)
143 filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
144 }
145 filtmap[filtAny] = append(filtmap[filtAny], filt)
146 }
147 sorting := filtmap[filtAny]
148 sort.Slice(filtmap[filtAny], func(i, j int) bool {
149 return sorting[i].Name < sorting[j].Name
150 })
151 if !expflush.IsZero() {
152 dur := expflush.Sub(now)
153 go filtcacheclear(userid, dur)
154 }
155 return filtmap, true
156}
157
158func filtcacheclear(userid int64, dur time.Duration) {
159 time.Sleep(dur + time.Second)
160 filtcache.Clear(userid)
161}
162
163func getfilters(userid int64, scope filtType) []*Filter {
164 var filtmap afiltermap
165 ok := filtcache.Get(userid, &filtmap)
166 if ok {
167 return filtmap[scope]
168 }
169 return nil
170}
171
172func rejectorigin(userid int64, origin string, isannounce bool) bool {
173 if o := originate(origin); o != "" {
174 origin = o
175 }
176 filts := getfilters(userid, filtReject)
177 for _, f := range filts {
178 if f.Text != "" {
179 continue
180 }
181 if f.IsAnnounce {
182 if !isannounce {
183 continue
184 }
185 if f.AnnounceOf == origin {
186 log.Printf("rejecting announce: %s", origin)
187 return true
188 }
189 }
190 if f.Actor == origin {
191 log.Printf("rejecting origin: %s", origin)
192 return true
193 }
194 }
195 return false
196}
197
198func rejectactor(userid int64, actor string) bool {
199 origin := originate(actor)
200 filts := getfilters(userid, filtReject)
201 for _, f := range filts {
202 if f.IsAnnounce || f.Text != "" {
203 continue
204 }
205 if f.Actor == actor || (origin != "" && f.Actor == origin) {
206 log.Printf("rejecting actor: %s", actor)
207 return true
208 }
209 }
210 return false
211}
212
213func stealthmode(userid int64, r *http.Request) bool {
214 agent := r.UserAgent()
215 agent = originate(agent)
216 if agent != "" {
217 fake := rejectorigin(userid, agent, false)
218 if fake {
219 log.Printf("faking 404 for %s", agent)
220 return true
221 }
222 }
223 return false
224}
225
226func matchfilter(h *Honk, f *Filter) bool {
227 return matchfilterX(h, f) != ""
228}
229
230func matchfilterX(h *Honk, f *Filter) string {
231 rv := ""
232 match := true
233 if match && f.Actor != "" {
234 match = false
235 if f.Actor == h.Honker || f.Actor == h.Oonker {
236 match = true
237 rv = f.Actor
238 }
239 if !match && (f.Actor == originate(h.Honker) ||
240 f.Actor == originate(h.Oonker) ||
241 f.Actor == originate(h.XID)) {
242 match = true
243 rv = f.Actor
244 }
245 if !match && f.IncludeAudience {
246 for _, a := range h.Audience {
247 if f.Actor == a || f.Actor == originate(a) {
248 match = true
249 rv = f.Actor
250 break
251 }
252 }
253 }
254 }
255 if match && f.IsAnnounce {
256 match = false
257 if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
258 f.AnnounceOf == originate(h.Oonker) {
259 match = true
260 rv += " announce"
261 }
262 }
263 if match && f.Text != "" {
264 match = false
265 re := f.re_text
266 m := re.FindString(h.Precis)
267 if m == "" {
268 m = re.FindString(h.Noise)
269 }
270 if m == "" {
271 for _, d := range h.Donks {
272 m = re.FindString(d.Desc)
273 if m != "" {
274 break
275 }
276 }
277 }
278 if m != "" {
279 match = true
280 rv = m
281 }
282 }
283 if match {
284 return rv
285 }
286 return ""
287}
288
289func rejectxonk(xonk *Honk) bool {
290 filts := getfilters(xonk.UserID, filtReject)
291 for _, f := range filts {
292 if matchfilter(xonk, f) {
293 log.Printf("rejecting %s because %s", xonk.XID, f.Actor)
294 return true
295 }
296 }
297 return false
298}
299
300func skipMedia(xonk *Honk) bool {
301 filts := getfilters(xonk.UserID, filtSkipMedia)
302 for _, f := range filts {
303 if matchfilter(xonk, f) {
304 return true
305 }
306 }
307 return false
308}
309
310func unsee(userid int64, h *Honk) {
311 filts := getfilters(userid, filtCollapse)
312 for _, f := range filts {
313 if bad := matchfilterX(h, f); bad != "" {
314 if h.Precis == "" {
315 h.Precis = bad
316 }
317 h.Open = ""
318 break
319 }
320 }
321 filts = getfilters(userid, filtRewrite)
322 for _, f := range filts {
323 if matchfilter(h, f) {
324 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
325 }
326 }
327}
328
329var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
330 rows, err := stmtUntagged.Query(userid)
331 if err != nil {
332 log.Printf("error query untagged: %s", err)
333 return nil, false
334 }
335 defer rows.Close()
336 bad := make(map[string]bool)
337 for rows.Next() {
338 var xid, rid string
339 var flags int64
340 err = rows.Scan(&xid, &rid, &flags)
341 if err != nil {
342 log.Printf("error scanning untag: %s", err)
343 continue
344 }
345 if flags&flagIsUntagged != 0 {
346 bad[xid] = true
347 }
348 if bad[rid] {
349 bad[xid] = true
350 }
351 }
352 return bad, true
353}})
354
355func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
356 var badparents map[string]bool
357 untagged.GetAndLock(userid, &badparents)
358 j := 0
359 reversehonks(honks)
360 for _, h := range honks {
361 if badparents[h.RID] {
362 badparents[h.XID] = true
363 continue
364 }
365 honks[j] = h
366 j++
367 }
368 untagged.Unlock()
369 honks = honks[0:j]
370 reversehonks(honks)
371 if !withfilt {
372 return honks
373 }
374 filts := getfilters(userid, filtHide)
375 j = 0
376outer:
377 for _, h := range honks {
378 for _, f := range filts {
379 if matchfilter(h, f) {
380 continue outer
381 }
382 }
383 honks[j] = h
384 j++
385 }
386 honks = honks[0:j]
387 return honks
388}