filter option to match unknown actors.
Ted Unangst tedu@tedunangst.com
Tue, 19 Dec 2023 12:38:10 -0500
5 files changed,
90 insertions(+),
58 deletions(-)
M
docs/changelog.txt
→
docs/changelog.txt
@@ -2,6 +2,8 @@ changelog
### next ++ Filter option to match unknown actors. + + Update some dependencies. + Watch local.css for changes.
M
docs/hfcs.1
→
docs/hfcs.1
@@ -44,10 +44,8 @@ .Fa Ar actor
property. .It Ar include audience Previous match is applied against -.Fa to -and -.Fa cc -fields as well. +.It Ar only unknowns +Previous (domain) match is applied only to unknown actors. .It Ar text Regular expression match against the post .Fa content .@@ -97,6 +95,9 @@ that can be matched against and will appear if the post is collapsed.
.Pp An optional expiration may be specified as a duration. XdYhZm for X days, Y hours, and Z minutes. +.Sh EXAMPLES +A rudimentary spam filter may be constructed with where set to mastodon.social, +checking the only unknowns option, with an action of reject. .Sh SEE ALSO .Xr honk 1 .Sh CAVEATS
M
hfcs.go
→
hfcs.go
@@ -19,10 +19,13 @@ import (
"net/http" "regexp" "sort" + "strconv" + "strings" "time" "unicode" "humungus.tedunangst.com/r/webs/cache" + "humungus.tedunangst.com/r/webs/login" ) type Filter struct {@@ -32,6 +35,7 @@ Name string
Date time.Time Actor string `json:",omitempty"` IncludeAudience bool `json:",omitempty"` + OnlyUnknowns bool `json:",omitempty"` Text string `json:",omitempty"` re_text *regexp.Regexp IsReply bool `json:",omitempty"`@@ -226,6 +230,9 @@ origin = o
} filts := rejectfilters(userid, origin) for _, f := range filts { + if f.OnlyUnknowns { + continue + } if isannounce && f.IsAnnounce { if f.AnnounceOf == origin { return true@@ -259,6 +266,10 @@ if f.IsAnnounce {
continue } if f.Actor == origin { + if f.OnlyUnknowns && unknownActor(userid, actor) { + ilog.Printf("rejecting unknown actor: %s", actor) + return true + } ilog.Printf("rejecting actor: %s", actor) return true }@@ -266,6 +277,21 @@ }
return false } +var knownknowns = cache.New(cache.Options{Filler: func(userid int64) (map[string]bool, bool) { + m := make(map[string]bool) + honkers := gethonkers(userid) + for _, h := range honkers { + m[h.XID] = true + } + return m, true +}, Invalidator: &honkerinvalidator}) + +func unknownActor(userid int64, actor string) bool { + var knowns map[string]bool + knownknowns.Get(userid, &knowns) + return !knowns[actor] +} + func stealthmode(userid int64, r *http.Request) bool { agent := r.UserAgent() agent = originate(agent)@@ -292,7 +318,7 @@ if f.Actor == h.Honker || f.Actor == h.Oonker {
match = true rv = f.Actor } - if !match && (f.Actor == originate(h.Honker) || + if !match && !f.OnlyUnknowns && (f.Actor == originate(h.Honker) || f.Actor == originate(h.Oonker) || f.Actor == originate(h.XID)) { match = true@@ -485,3 +511,56 @@ }
honks = honks[0:j] return honks } + +func savehfcs(w http.ResponseWriter, r *http.Request) { + userinfo := login.GetUserInfo(r) + itsok := r.FormValue("itsok") + if itsok == "iforgiveyou" { + hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0) + _, err := stmtDeleteFilter.Exec(userinfo.UserID, hfcsid) + if err != nil { + elog.Printf("error deleting filter: %s", err) + } + filtInvalidator.Clear(userinfo.UserID) + http.Redirect(w, r, "/hfcs", http.StatusSeeOther) + return + } + + filt := new(Filter) + filt.Name = strings.TrimSpace(r.FormValue("name")) + filt.Date = time.Now().UTC() + filt.Actor = strings.TrimSpace(r.FormValue("actor")) + filt.IncludeAudience = r.FormValue("incaud") == "yes" + filt.OnlyUnknowns = r.FormValue("unknowns") == "yes" + filt.Text = strings.TrimSpace(r.FormValue("filttext")) + filt.IsReply = r.FormValue("isreply") == "yes" + filt.IsAnnounce = r.FormValue("isannounce") == "yes" + filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof")) + filt.Reject = r.FormValue("doreject") == "yes" + filt.SkipMedia = r.FormValue("doskipmedia") == "yes" + filt.Hide = r.FormValue("dohide") == "yes" + filt.Collapse = r.FormValue("docollapse") == "yes" + filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite")) + filt.Replace = strings.TrimSpace(r.FormValue("filtreplace")) + if dur := parseDuration(r.FormValue("filtduration")); dur > 0 { + filt.Expiration = time.Now().UTC().Add(dur) + } + filt.Notes = strings.TrimSpace(r.FormValue("filtnotes")) + + if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce { + ilog.Printf("blank filter") + http.Error(w, "can't save a blank filter", http.StatusInternalServerError) + return + } + + j, err := jsonify(filt) + if err == nil { + _, err = stmtSaveFilter.Exec(userinfo.UserID, j) + } + if err != nil { + elog.Printf("error saving filter: %s", err) + } + + filtInvalidator.Clear(userinfo.UserID) + http.Redirect(w, r, "/hfcs", http.StatusSeeOther) +}
M
views/hfcs.html
→
views/hfcs.html
@@ -18,6 +18,8 @@ <p><label for="actor">who or where:</label><br>
<input tabindex=1 type="text" name="actor" value="" autocomplete=off> <p><span><label class=button for="incaud">include audience: <input tabindex=1 type="checkbox" id="incaud" name="incaud" value="yes"><span></span></label></span> +<span><label class=button for="unknowns">only unknowns: +<input tabindex=1 type="checkbox" id="unknowns" name="unknowns" value="yes"><span></span></label></span> <p><label for="filttext">text matches:</label><br> <input tabindex=1 type="text" name="filttext" value="" autocomplete=off> <p><span><label class=button for="isreply">is reply:@@ -55,7 +57,7 @@ <section class="honk">
<p>Name: {{ .Name }} {{ with .Notes }}<p>Notes: {{ . }}{{ end }} <p>Date: {{ .Date.Format "2006-01-02" }} -{{ with .Actor }}<p>Who: {{ . }}{{ end }} {{ with .IncludeAudience }} (inclusive) {{ end }} +{{ with .Actor }}<p>Who: {{ . }}{{ end }}{{ if .IncludeAudience }} (inclusive){{ end }}{{ if .OnlyUnknowns }} (unknowns){{ end }} {{ if .IsReply }}<p>Reply: y{{ end }} {{ if .IsAnnounce }}<p>Announce: {{ .AnnounceOf }}{{ end }} {{ with .Text }}<p>Text: {{ . }}{{ end }}
M
web.go
→
web.go
@@ -2234,58 +2234,6 @@ elog.Print(err)
} } -func savehfcs(w http.ResponseWriter, r *http.Request) { - userinfo := login.GetUserInfo(r) - itsok := r.FormValue("itsok") - if itsok == "iforgiveyou" { - hfcsid, _ := strconv.ParseInt(r.FormValue("hfcsid"), 10, 0) - _, err := stmtDeleteFilter.Exec(userinfo.UserID, hfcsid) - if err != nil { - elog.Printf("error deleting filter: %s", err) - } - filtInvalidator.Clear(userinfo.UserID) - http.Redirect(w, r, "/hfcs", http.StatusSeeOther) - return - } - - filt := new(Filter) - filt.Name = strings.TrimSpace(r.FormValue("name")) - filt.Date = time.Now().UTC() - filt.Actor = strings.TrimSpace(r.FormValue("actor")) - filt.IncludeAudience = r.FormValue("incaud") == "yes" - filt.Text = strings.TrimSpace(r.FormValue("filttext")) - filt.IsReply = r.FormValue("isreply") == "yes" - filt.IsAnnounce = r.FormValue("isannounce") == "yes" - filt.AnnounceOf = strings.TrimSpace(r.FormValue("announceof")) - filt.Reject = r.FormValue("doreject") == "yes" - filt.SkipMedia = r.FormValue("doskipmedia") == "yes" - filt.Hide = r.FormValue("dohide") == "yes" - filt.Collapse = r.FormValue("docollapse") == "yes" - filt.Rewrite = strings.TrimSpace(r.FormValue("filtrewrite")) - filt.Replace = strings.TrimSpace(r.FormValue("filtreplace")) - if dur := parseDuration(r.FormValue("filtduration")); dur > 0 { - filt.Expiration = time.Now().UTC().Add(dur) - } - filt.Notes = strings.TrimSpace(r.FormValue("filtnotes")) - - if filt.Actor == "" && filt.Text == "" && !filt.IsAnnounce { - ilog.Printf("blank filter") - http.Error(w, "can't save a blank filter", http.StatusInternalServerError) - return - } - - j, err := jsonify(filt) - if err == nil { - _, err = stmtSaveFilter.Exec(userinfo.UserID, j) - } - if err != nil { - elog.Printf("error saving filter: %s", err) - } - - filtInvalidator.Clear(userinfo.UserID) - http.Redirect(w, r, "/hfcs", http.StatusSeeOther) -} - func accountpage(w http.ResponseWriter, r *http.Request) { u := login.GetUserInfo(r) user, _ := butwhatabout(u.Username)