commit 8bd76f1ccf332018688df3d37351be92ca7a11b8
parent 40952661334b9edaf04586a84dbbad9ecd5a8b89
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Mon, 5 Jun 2023 01:54:13 -0700
dynamic spam filters
Diffstat:
9 files changed, 217 insertions(+), 3 deletions(-)
diff --git a/cmd/dkf/migrations/136.sql b/cmd/dkf/migrations/136.sql
@@ -0,0 +1,9 @@
+-- +migrate Up
+CREATE TABLE IF NOT EXISTS spam_filters (
+ id INTEGER NOT NULL PRIMARY KEY,
+ filter VARCHAR(255) NOT NULL,
+ is_regex TINYINT(1) NOT NULL DEFAULT 0,
+ action INTEGER NOT NULL DEFAULT 0,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);
+
+-- +migrate Down
diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go
@@ -82,6 +82,7 @@ func Start(c *cli.Context) error {
utils.SGo(func() { xmrWatch(db) })
utils.SGo(func() { openBrowser(noBrowser, int64(port)) })
+ v1.LoadFilters(db)
v1.ChessInstance = v1.NewChess(db)
v1.BattleshipInstance = v1.NewBattleship(db)
v1.WWInstance = v1.NewWerewolf(db)
diff --git a/pkg/database/tableSpamFilters.go b/pkg/database/tableSpamFilters.go
@@ -0,0 +1,29 @@
+package database
+
+import "time"
+
+type SpamFilter struct {
+ ID int64
+ Action int64
+ Filter string
+ IsRegex bool
+ CreatedAt time.Time
+}
+
+func (d *DkfDB) GetSpamFilters() (out []SpamFilter, err error) {
+ err = d.db.Find(&out).Error
+ return
+}
+
+func (d *DkfDB) CreateOrEditSpamFilter(id int64, filter string, isRegex bool, action int64) (out SpamFilter, err error) {
+ out.ID = id
+ out.Filter = filter
+ out.IsRegex = isRegex
+ out.Action = action
+ err = d.db.Save(&out).Error
+ return
+}
+
+func (d *DkfDB) DeleteSpamFilterByID(id int64) error {
+ return d.db.Delete(SpamFilter{}, "id = ?", id).Error
+}
diff --git a/pkg/web/handlers/admin.go b/pkg/web/handlers/admin.go
@@ -3,6 +3,7 @@ package handlers
import (
dutils "dkforest/pkg/database/utils"
"dkforest/pkg/managers"
+ v1 "dkforest/pkg/web/handlers/api/v1"
"github.com/jinzhu/gorm"
"net/http"
"regexp"
@@ -18,6 +19,40 @@ import (
"github.com/skratchdot/open-golang/open"
)
+func AdminSpamFiltersHandler(c echo.Context) error {
+ db := c.Get("database").(*database.DkfDB)
+ var data adminSpamFiltersData
+ data.ActiveTab = "spamfilters"
+ data.SpamFilters, _ = db.GetSpamFilters()
+ data.SpamFiltersCount = int64(len(data.SpamFilters))
+
+ if c.Request().Method == http.MethodPost {
+ btnSubmit := c.Request().PostFormValue("btn_submit")
+ data.ID = utils.DoParseInt64(c.Request().PostFormValue("id"))
+ data.Filter = c.Request().PostFormValue("filter")
+ data.IsRegex = utils.DoParseBool(c.Request().PostFormValue("is_regex"))
+ data.Action = utils.Clamp(utils.DoParseInt64(c.Request().PostFormValue("action")), 0, 2)
+ if !utils.ValidateRuneLength(data.Filter, 1, 255) {
+ data.Error = "filter must be within 1-255 characters"
+ return c.Render(http.StatusOK, "admin.spam-filter", data)
+ }
+ if data.ID == 0 || btnSubmit == "edit" {
+ if _, err := db.CreateOrEditSpamFilter(data.ID, data.Filter, data.IsRegex, data.Action); err != nil {
+ logrus.Error(err)
+ }
+ v1.LoadFilters(db)
+ } else if btnSubmit == "delete" {
+ if err := db.DeleteSpamFilterByID(data.ID); err != nil {
+ logrus.Error(err)
+ }
+ v1.LoadFilters(db)
+ }
+ return c.Redirect(http.StatusFound, "/admin/spam-filters")
+ }
+
+ return c.Render(http.StatusOK, "admin.spam-filters", data)
+}
+
func AdminNewGistHandler(c echo.Context) error {
authUser := c.Get("authUser").(*database.User)
db := c.Get("database").(*database.DkfDB)
diff --git a/pkg/web/handlers/api/v1/spamInterceptor.go b/pkg/web/handlers/api/v1/spamInterceptor.go
@@ -14,9 +14,55 @@ import (
type SpamInterceptor struct{}
+type Filter struct {
+ IsRegex bool
+ Term string
+ Rgx *regexp.Regexp
+ Kick bool
+ Hb bool
+}
+
+var filters []Filter
+
+func LoadFilters(db *database.DkfDB) {
+ filters = make([]Filter, 0)
+ dbFilters, _ := db.GetSpamFilters()
+ for _, dbFilter := range dbFilters {
+ f := Filter{IsRegex: dbFilter.IsRegex}
+ if dbFilter.Action == 1 {
+ f.Kick = true
+ } else if dbFilter.Action == 2 {
+ f.Hb = true
+ }
+ if dbFilter.IsRegex {
+ f.Rgx = regexp.MustCompile(dbFilter.Filter)
+ } else {
+ f.Term = dbFilter.Filter
+ }
+ filters = append(filters, f)
+ }
+}
+
func (i SpamInterceptor) InterceptMsg(c *Command) {
+ lowerCaseMessage := strings.ToLower(c.message)
+ silentSelfKick := config.SilentSelfKick.Load()
+ for _, f := range filters {
+ isMatch := (f.IsRegex && f.Rgx.MatchString(c.message)) ||
+ (!f.IsRegex && strings.Contains(lowerCaseMessage, f.Term))
+ if isMatch {
+ if f.Kick {
+ _ = dutils.SelfKick(c.db, *c.authUser, silentSelfKick)
+ } else if f.Hb {
+ dutils.SelfHellBan(c.db, c.authUser)
+ return
+ }
+ c.err = ErrSpamFilterTriggered
+ return
+ }
+ }
+
if c.room.IsOfficialRoom() {
- if err := checkSpam(c.db, c.origMessage, c.authUser); err != nil {
+ if err := checkSpam(c.db, c.origMessage, lowerCaseMessage, c.authUser); err != nil {
c.err = err
return
}
@@ -35,8 +81,7 @@ func (i SpamInterceptor) InterceptMsg(c *Command) {
var ErrSpamFilterTriggered = errors.New("spam filter triggered")
-func checkSpam(db *database.DkfDB, origMessage string, authUser *database.User) error {
- lowerCaseMessage := strings.ToLower(origMessage)
+func checkSpam(db *database.DkfDB, origMessage, lowerCaseMessage string, authUser *database.User) error {
silentSelfKick := config.SilentSelfKick.Load()
// Kick retard new users
diff --git a/pkg/web/handlers/data.go b/pkg/web/handlers/data.go
@@ -880,6 +880,17 @@ type adminCreateGistData struct {
ErrorName string
}
+type adminSpamFiltersData struct {
+ ActiveTab string
+ SpamFiltersCount int64
+ SpamFilters []database.SpamFilter
+ ID int64
+ Filter string
+ IsRegex bool
+ Action int64
+ Error string
+}
+
type publicProfileData struct {
User database.User
PublicNotes database.UserPublicNote
diff --git a/pkg/web/public/views/pages/admin/index.gohtml b/pkg/web/public/views/pages/admin/index.gohtml
@@ -17,6 +17,7 @@
<a href="/admin/gists" class="list-group-item list-group-item-action{{ if eq .Data.ActiveTab "gists" }} active{{ end }}">{{ t "Gists" . }}</a>
<a href="/admin/backup" class="list-group-item list-group-item-action{{ if eq .Data.ActiveTab "backup" }} active{{ end }}">{{ t "Backup" . }}</a>
<a href="/admin/ddos" class="list-group-item list-group-item-action{{ if eq .Data.ActiveTab "ddos" }} active{{ end }}">{{ t "DDoS" . }}</a>
+ <a href="/admin/spam-filters" class="list-group-item list-group-item-action{{ if eq .Data.ActiveTab "spamfilters" }} active{{ end }}">{{ t "Spam filters" . }}</a>
<!-- <a href="/admin/update" class="list-group-item list-group-item-action{{ if eq .Data.ActiveTab "update" }} active{{ end }}">{{ t "Update" . }}</a>-->
</div>
</div>
diff --git a/pkg/web/public/views/pages/admin/spam-filters.gohtml b/pkg/web/public/views/pages/admin/spam-filters.gohtml
@@ -0,0 +1,80 @@
+{{ define "sub-content" }}
+ <div class="pb-2 mt-4 mb-4 border-bottom">
+ <div class="float-right">
+ <a href="/admin/spam-filters/new" class="btn btn-success">
+ <i class="fa fa-plus fa-fw"></i> New filter
+ </a>
+ </div>
+ <h2>{{ .Data.SpamFiltersCount | comma }} Spam filters</h2>
+</div>
+
+{{ if .Data.Error }}
+ <div class="alert alert-danger">{{ .Data.Error }}</div>
+{{ end }}
+
+<table class="table table-striped table-sm table-novpadding table-dark">
+ <thead>
+ <tr>
+ <th>Filter</th>
+ <th>Is regex</th>
+ <th>Action</th>
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{ range .Data.SpamFilters }}
+ <form method="post">
+ <input type="hidden" name="id" value="{{ .ID }}" />
+ <input type="hidden" name="csrf" value="{{ $.CSRF }}" />
+ <tr>
+ <td><input type="text" name="filter" value="{{ .Filter }}" class="form-control form-control-sm" /></td>
+ <td class="align-middle">
+ <div class="form-check form-check-1">
+ <div class="checkbox-wrapper form-check-input">
+ <input class="my-cbx" type="checkbox" name="is_regex" id="is_regex_{{ .ID }}" value="1"{{ if .IsRegex }} checked{{ end }} />
+ <label for="is_regex_{{ .ID }}" class="toggle"><span></span></label>
+ </div>
+ <label class="form-check-label" for="is_regex_{{ .ID }}">regex</label>
+ </div>
+ </td>
+ <td>
+ <select name="action" class="form-control form-control-sm">
+ <option value="1"{{ if eq .Action 1 }} selected{{ end }}>Kick</option>
+ <option value="2"{{ if eq .Action 2 }} selected{{ end }}>Hellban</option>
+ <option value="0"{{ if eq .Action 0 }} selected{{ end }}>Error</option>
+ </select>
+ </td>
+ <td class="text-right">
+ <button type="submit" class="btn btn-sm btn-primary" title="Edit spam filter" name="btn_submit" value="edit">Edit</button>
+ <button type="submit" class="btn btn-sm btn-danger" title="Delete spam filter" name="btn_submit" value="delete">X</button>
+ </td>
+ </tr>
+ </form>
+ {{ end }}
+ <form method="post">
+ <input type="hidden" name="csrf" value="{{ .CSRF }}" />
+ <tr>
+ <td><input name="filter" type="text" class="form-control form-control-sm" /></td>
+ <td class="align-middle">
+ <div class="form-check form-check-1">
+ <div class="checkbox-wrapper form-check-input">
+ <input class="my-cbx" type="checkbox" name="is_regex" id="is_regex" value="1" />
+ <label for="is_regex" class="toggle"><span></span></label>
+ </div>
+ <label class="form-check-label" for="is_regex">regex</label>
+ </div>
+ </td>
+ <td>
+ <select name="action" class="form-control form-control-sm">
+ <option value="1" selected>Kick</option>
+ <option value="2">Hellban</option>
+ <option value="0">Error</option>
+ </select>
+ </td>
+ <td class="text-right"><button type="submit" class="btn btn-success btn-sm">Add</button></td>
+ </tr>
+ </form>
+ </tbody>
+</table>
+
+{{ end }}
+\ No newline at end of file
diff --git a/pkg/web/web.go b/pkg/web/web.go
@@ -246,6 +246,8 @@ func getMainServer(db *database.DkfDB, i18nBundle *i18n.Bundle, renderer *tmp.Te
adminGroup.GET("/admin/sessions", handlers.SessionsHandler)
adminGroup.GET("/admin/backup", handlers.BackupHandler)
adminGroup.POST("/admin/backup", handlers.BackupHandler)
+ adminGroup.GET("/admin/spam-filters", handlers.AdminSpamFiltersHandler)
+ adminGroup.POST("/admin/spam-filters", handlers.AdminSpamFiltersHandler)
adminGroup.GET("/admin/ddos", handlers.DdosHandler)
adminGroup.POST("/admin/ddos", handlers.DdosHandler)
adminGroup.GET("/admin/audits", handlers.AdminAuditsHandler)