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) bool {
173 if o := originate(origin); o != "" {
174 origin = o
175 }
176 filts := getfilters(userid, filtReject)
177 for _, f := range filts {
178 if f.IsAnnounce || f.Text != "" {
179 continue
180 }
181 if f.Actor == origin {
182 log.Printf("rejecting origin: %s", origin)
183 return true
184 }
185 }
186 return false
187}
188
189func rejectactor(userid int64, actor string) bool {
190 origin := originate(actor)
191 filts := getfilters(userid, filtReject)
192 for _, f := range filts {
193 if f.IsAnnounce || f.Text != "" {
194 continue
195 }
196 if f.Actor == actor || (origin != "" && f.Actor == origin) {
197 log.Printf("rejecting actor: %s", actor)
198 return true
199 }
200 }
201 return false
202}
203
204func stealthmode(userid int64, r *http.Request) bool {
205 agent := r.UserAgent()
206 agent = originate(agent)
207 if agent != "" {
208 fake := rejectorigin(userid, agent)
209 if fake {
210 log.Printf("faking 404 for %s", agent)
211 return true
212 }
213 }
214 return false
215}
216
217func matchfilter(h *Honk, f *Filter) bool {
218 return matchfilterX(h, f) != ""
219}
220
221func matchfilterX(h *Honk, f *Filter) string {
222 rv := ""
223 match := true
224 if match && f.Actor != "" {
225 match = false
226 if f.Actor == h.Honker || f.Actor == h.Oonker {
227 match = true
228 rv = f.Actor
229 }
230 if !match && (f.Actor == originate(h.Honker) ||
231 f.Actor == originate(h.Oonker) ||
232 f.Actor == originate(h.XID)) {
233 match = true
234 rv = f.Actor
235 }
236 if !match && f.IncludeAudience {
237 for _, a := range h.Audience {
238 if f.Actor == a || f.Actor == originate(a) {
239 match = true
240 rv = f.Actor
241 break
242 }
243 }
244 }
245 }
246 if match && f.IsAnnounce {
247 match = false
248 if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
249 f.AnnounceOf == originate(h.Oonker) {
250 match = true
251 rv += " announce"
252 }
253 }
254 if match && f.Text != "" {
255 match = false
256 re := f.re_text
257 m := re.FindString(h.Precis)
258 if m == "" {
259 m = re.FindString(h.Noise)
260 }
261 if m == "" {
262 for _, d := range h.Donks {
263 m = re.FindString(d.Desc)
264 if m != "" {
265 break
266 }
267 }
268 }
269 if m != "" {
270 match = true
271 rv = m
272 }
273 }
274 if match {
275 return rv
276 }
277 return ""
278}
279
280func rejectxonk(xonk *Honk) bool {
281 filts := getfilters(xonk.UserID, filtReject)
282 for _, f := range filts {
283 if matchfilter(xonk, f) {
284 log.Printf("rejecting %s because %s", xonk.XID, f.Actor)
285 return true
286 }
287 }
288 return false
289}
290
291func skipMedia(xonk *Honk) bool {
292 filts := getfilters(xonk.UserID, filtSkipMedia)
293 for _, f := range filts {
294 if matchfilter(xonk, f) {
295 return true
296 }
297 }
298 return false
299}
300
301func unsee(userid int64, h *Honk) {
302 filts := getfilters(userid, filtCollapse)
303 for _, f := range filts {
304 if bad := matchfilterX(h, f); bad != "" {
305 if h.Precis == "" {
306 h.Precis = bad
307 }
308 h.Open = ""
309 break
310 }
311 }
312 filts = getfilters(userid, filtRewrite)
313 for _, f := range filts {
314 if matchfilter(h, f) {
315 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
316 }
317 }
318}
319
320var untagged = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) {
321 rows, err := stmtUntagged.Query(userid)
322 if err != nil {
323 log.Printf("error query untagged: %s", err)
324 return nil, false
325 }
326 defer rows.Close()
327 bad := make(map[string]bool)
328 for rows.Next() {
329 var xid, rid string
330 var flags int64
331 err = rows.Scan(&xid, &rid, &flags)
332 if err != nil {
333 log.Printf("error scanning untag: %s", err)
334 continue
335 }
336 if flags&flagIsUntagged != 0 {
337 bad[xid] = true
338 }
339 if bad[rid] {
340 bad[xid] = true
341 }
342 }
343 return bad, true
344}})
345
346func osmosis(honks []*Honk, userid int64, withfilt bool) []*Honk {
347 var badparents map[string]bool
348 untagged.GetAndLock(userid, &badparents)
349 j := 0
350 reversehonks(honks)
351 for _, h := range honks {
352 if badparents[h.RID] {
353 badparents[h.XID] = true
354 continue
355 }
356 honks[j] = h
357 j++
358 }
359 untagged.Unlock()
360 honks = honks[0:j]
361 reversehonks(honks)
362 if !withfilt {
363 return honks
364 }
365 filts := getfilters(userid, filtHide)
366 j = 0
367outer:
368 for _, h := range honks {
369 for _, f := range filts {
370 if matchfilter(h, f) {
371 continue outer
372 }
373 }
374 honks[j] = h
375 j++
376 }
377 honks = honks[0:j]
378 return honks
379}