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}
48
49type filtType uint
50
51const (
52 filtNone filtType = iota
53 filtAny
54 filtReject
55 filtSkipMedia
56 filtHide
57 filtCollapse
58 filtRewrite
59)
60
61var filtNames = []string{"None", "Any", "Reject", "SkipMedia", "Hide", "Collapse", "Rewrite"}
62
63func (ft filtType) String() string {
64 return filtNames[ft]
65}
66
67type afiltermap map[filtType][]*Filter
68
69var filtcache *cache.Cache
70
71func init() {
72 // resolve init loop
73 filtcache = cache.New(cache.Options{Filler: filtcachefiller})
74}
75
76func filtcachefiller(userid int64) (afiltermap, bool) {
77 rows, err := stmtGetFilters.Query(userid)
78 if err != nil {
79 log.Printf("error querying filters: %s", err)
80 return nil, false
81 }
82 defer rows.Close()
83
84 now := time.Now()
85
86 var expflush time.Time
87
88 filtmap := make(afiltermap)
89 for rows.Next() {
90 filt := new(Filter)
91 var j string
92 var filterid int64
93 err = rows.Scan(&filterid, &j)
94 if err == nil {
95 err = unjsonify(j, filt)
96 }
97 if err != nil {
98 log.Printf("error scanning filter: %s", err)
99 continue
100 }
101 if !filt.Expiration.IsZero() {
102 if filt.Expiration.Before(now) {
103 continue
104 }
105 if expflush.IsZero() || filt.Expiration.Before(expflush) {
106 expflush = filt.Expiration
107 }
108 }
109 if filt.Text != "" {
110 filt.re_text, err = regexp.Compile("\\b(?i:" + filt.Text + ")\\b")
111 if err != nil {
112 log.Printf("error compiling filter text: %s", err)
113 continue
114 }
115 }
116 if filt.Rewrite != "" {
117 filt.re_rewrite, err = regexp.Compile("\\b(?i:" + filt.Rewrite + ")\\b")
118 if err != nil {
119 log.Printf("error compiling filter rewrite: %s", err)
120 continue
121 }
122 }
123 filt.ID = filterid
124 if filt.Reject {
125 filt.Actions = append(filt.Actions, filtReject)
126 filtmap[filtReject] = append(filtmap[filtReject], filt)
127 }
128 if filt.SkipMedia {
129 filt.Actions = append(filt.Actions, filtSkipMedia)
130 filtmap[filtSkipMedia] = append(filtmap[filtSkipMedia], filt)
131 }
132 if filt.Hide {
133 filt.Actions = append(filt.Actions, filtHide)
134 filtmap[filtHide] = append(filtmap[filtHide], filt)
135 }
136 if filt.Collapse {
137 filt.Actions = append(filt.Actions, filtCollapse)
138 filtmap[filtCollapse] = append(filtmap[filtCollapse], filt)
139 }
140 if filt.Rewrite != "" {
141 filt.Actions = append(filt.Actions, filtRewrite)
142 filtmap[filtRewrite] = append(filtmap[filtRewrite], filt)
143 }
144 filtmap[filtAny] = append(filtmap[filtAny], filt)
145 }
146 sorting := filtmap[filtAny]
147 sort.Slice(filtmap[filtAny], func(i, j int) bool {
148 return sorting[i].Name < sorting[j].Name
149 })
150 if !expflush.IsZero() {
151 dur := expflush.Sub(now)
152 go filtcacheclear(userid, dur)
153 }
154 return filtmap, true
155}
156
157func filtcacheclear(userid int64, dur time.Duration) {
158 time.Sleep(dur + time.Second)
159 filtcache.Clear(userid)
160}
161
162func getfilters(userid int64, scope filtType) []*Filter {
163 var filtmap afiltermap
164 ok := filtcache.Get(userid, &filtmap)
165 if ok {
166 return filtmap[scope]
167 }
168 return nil
169}
170
171func rejectorigin(userid int64, origin string) bool {
172 if o := originate(origin); o != "" {
173 origin = o
174 }
175 filts := getfilters(userid, filtReject)
176 for _, f := range filts {
177 if f.IsAnnounce || f.Text != "" {
178 continue
179 }
180 if f.Actor == origin {
181 log.Printf("rejecting origin: %s", origin)
182 return true
183 }
184 }
185 return false
186}
187
188func rejectactor(userid int64, actor string) bool {
189 origin := originate(actor)
190 filts := getfilters(userid, filtReject)
191 for _, f := range filts {
192 if f.IsAnnounce || f.Text != "" {
193 continue
194 }
195 if f.Actor == actor || (origin != "" && f.Actor == origin) {
196 log.Printf("rejecting actor: %s", actor)
197 return true
198 }
199 }
200 return false
201}
202
203func stealthmode(userid int64, r *http.Request) bool {
204 agent := r.UserAgent()
205 agent = originate(agent)
206 if agent != "" {
207 fake := rejectorigin(userid, agent)
208 if fake {
209 log.Printf("faking 404 for %s", agent)
210 return true
211 }
212 }
213 return false
214}
215
216func matchfilter(h *Honk, f *Filter) bool {
217 match := true
218 if match && f.Actor != "" {
219 match = false
220 if f.Actor == h.Honker || f.Actor == h.Oonker {
221 match = true
222 }
223 if !match && (f.Actor == originate(h.Honker) ||
224 f.Actor == originate(h.Oonker) ||
225 f.Actor == originate(h.XID)) {
226 match = true
227 }
228 if !match && f.IncludeAudience {
229 for _, a := range h.Audience {
230 if f.Actor == a || f.Actor == originate(a) {
231 match = true
232 break
233 }
234 }
235 }
236 }
237 if match && f.IsAnnounce {
238 match = false
239 if (f.AnnounceOf == "" && h.Oonker != "") || f.AnnounceOf == h.Oonker ||
240 f.AnnounceOf == originate(h.Oonker) {
241 match = true
242 }
243 }
244 if match && f.Text != "" {
245 match = false
246 re := f.re_text
247 if re.MatchString(h.Noise) || re.MatchString(h.Precis) {
248 match = true
249 }
250 if !match {
251 for _, d := range h.Donks {
252 if re.MatchString(d.Desc) {
253 match = true
254 }
255 }
256 }
257 }
258 return match
259}
260
261func rejectxonk(xonk *Honk) bool {
262 filts := getfilters(xonk.UserID, filtReject)
263 for _, f := range filts {
264 if matchfilter(xonk, f) {
265 log.Printf("rejecting %s because %s", xonk.XID, f.Actor)
266 return true
267 }
268 }
269 return false
270}
271
272func skipMedia(xonk *Honk) bool {
273 filts := getfilters(xonk.UserID, filtSkipMedia)
274 for _, f := range filts {
275 if matchfilter(xonk, f) {
276 return true
277 }
278 }
279 return false
280}
281
282func unsee(userid int64, h *Honk) {
283 filts := getfilters(userid, filtCollapse)
284 for _, f := range filts {
285 if matchfilter(h, f) {
286 bad := f.Text
287 if f.Actor != "" {
288 bad = f.Actor
289 }
290 if h.Precis == "" {
291 h.Precis = bad
292 }
293 h.Open = ""
294 break
295 }
296 }
297 filts = getfilters(userid, filtRewrite)
298 for _, f := range filts {
299 if matchfilter(h, f) {
300 h.Noise = f.re_rewrite.ReplaceAllString(h.Noise, f.Replace)
301 }
302 }
303}
304
305func osmosis(honks []*Honk, userid int64) []*Honk {
306 filts := getfilters(userid, filtHide)
307 j := 0
308outer:
309 for _, h := range honks {
310 for _, f := range filts {
311 if matchfilter(h, f) {
312 continue outer
313 }
314 }
315 honks[j] = h
316 j++
317 }
318 honks = honks[0:j]
319 return honks
320}