commit 1ce2b07cc0f5c25c4ca7a5c0e02557d245a86221
parent 61f47aff099ff72b3145519c01de732f0f11546d
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Fri, 9 Jun 2023 00:17:59 -0700
purge modal admin tool
Diffstat:
9 files changed, 219 insertions(+), 31 deletions(-)
diff --git a/pkg/database/tableChatMessages.go b/pkg/database/tableChatMessages.go
@@ -466,6 +466,29 @@ func (d *DkfDB) DeleteUserHbChatMessages(userID UserID) error {
return d.db.Where("user_id = ? AND is_hellbanned = 1", userID).Delete(&ChatMessage{}).Error
}
+func (d *DkfDB) DeleteUserChatMessagesOpt(userID UserID, hbOnly bool, secs int64) error {
+ q := d.db.Debug().Where("user_id = ?", userID)
+ if secs > 0 {
+ switch secs {
+ case 300:
+ q = q.Where("created_at > datetime('now', '-300 Second', 'localtime')")
+ case 3600:
+ q = q.Where("created_at > datetime('now', '-3600 Second', 'localtime')")
+ case 21600:
+ q = q.Where("created_at > datetime('now', '-21600 Second', 'localtime')")
+ case 43200:
+ q = q.Where("created_at > datetime('now', '-43200 Second', 'localtime')")
+ case 86400:
+ q = q.Where("created_at > datetime('now', '-86400 Second', 'localtime')")
+ }
+ }
+ if hbOnly {
+ q = q.Where("is_hellbanned = 1")
+ }
+ err := q.Delete(&ChatMessage{}).Error
+ return err
+}
+
func (d *DkfDB) DeleteOldChatMessages() {
rooms, _ := d.GetOfficialChatRooms()
for _, room := range rooms {
diff --git a/pkg/web/handlers/api/v1/topBarHandler.go b/pkg/web/handlers/api/v1/topBarHandler.go
@@ -198,6 +198,7 @@ func ChatTopBarHandler(c echo.Context) error {
interceptors.UploadInterceptor{},
interceptors.SlashInterceptor{},
streamModals.CodeModal{},
+ streamModals.PurgeModal{},
interceptors.MsgInterceptor{},
}
for _, interceptor := range interceptorsArr {
@@ -216,7 +217,7 @@ func handleCmdError(err error, ctx echo.Context, data chatTopBarData, redirectUR
return ctx.Redirect(http.StatusFound, redirectURL)
} else if err == command.ErrStop {
return ctx.Render(http.StatusOK, "chat-top-bar", data)
- } else if serr, ok := err.(*interceptors.ErrSuccess); ok {
+ } else if serr, ok := err.(*command.ErrSuccess); ok {
data.Success = serr.Error()
return ctx.Render(http.StatusOK, "chat-top-bar", data)
}
diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go
@@ -5127,6 +5127,7 @@ func ChatStreamMessagesHandler(c echo.Context) error {
// Register modals and send the css for them
modalsManager := streamModals.NewModalsManager()
modalsManager.Register(streamModals.NewCodeModal(authUser.ID, room))
+ modalsManager.Register(streamModals.NewPurgeModal(authUser.ID, room))
send(modalsManager.Css())
data.ReadMarker, _ = db.GetUserReadMarker(authUser.ID, room.ID)
@@ -5255,7 +5256,7 @@ Loop:
return nil
}
- if modalsManager.Handle(authUser, topic, csrf, msgTyp, send) {
+ if modalsManager.Handle(db, authUser, topic, csrf, msgTyp, send) {
c.Response().Flush()
continue
}
diff --git a/pkg/web/handlers/interceptors/command/command.go b/pkg/web/handlers/interceptors/command/command.go
@@ -12,6 +12,18 @@ import (
var ErrRedirect = errors.New("redirect")
var ErrStop = errors.New("stop")
+type ErrSuccess struct {
+ msg string
+}
+
+func NewErrSuccess(msg string) *ErrSuccess {
+ return &ErrSuccess{msg: msg}
+}
+
+func (e ErrSuccess) Error() string {
+ return e.msg
+}
+
const (
RedirectPmQP = "pm"
RedirectEditQP = "e"
diff --git a/pkg/web/handlers/interceptors/slashInterceptor.go b/pkg/web/handlers/interceptors/slashInterceptor.go
@@ -25,18 +25,6 @@ import (
"time"
)
-type ErrSuccess struct {
- msg string
-}
-
-func NewErrSuccess(msg string) *ErrSuccess {
- return &ErrSuccess{msg: msg}
-}
-
-func (e ErrSuccess) Error() string {
- return e.msg
-}
-
// SlashInterceptor handle all forward slash commands.
//
// If by the end of this function, the c.err is set, it will trigger
@@ -155,6 +143,7 @@ func handleAdminCmd(c *command.Command) (handled bool) {
return handleSystemCmd(c) ||
handleSetChatRoomExternalLink(c) ||
handlePurge(c) ||
+ handlePurgeCmd(c) ||
handleRename(c) ||
handleNewMeme(c) ||
handleRenameMeme(c) ||
@@ -764,9 +753,9 @@ func handleToggleReadOnlyCmd(c *command.Command) (handled bool) {
c.Room.ReadOnly = !c.Room.ReadOnly
c.Room.DoSave(c.DB)
if c.Room.ReadOnly {
- c.Err = NewErrSuccess("room is now read-only")
+ c.Err = command.NewErrSuccess("room is now read-only")
} else {
- c.Err = NewErrSuccess("room is no longer read-only")
+ c.Err = command.NewErrSuccess("room is no longer read-only")
}
return true
}
@@ -1166,9 +1155,9 @@ func handleTogglePmBlacklistedUser(c *command.Command) (handled bool) {
return true
}
if c.DB.ToggleBlacklistedUser(c.AuthUser.ID, user.ID) {
- c.Err = NewErrSuccess("added to blacklist")
+ c.Err = command.NewErrSuccess("added to blacklist")
} else {
- c.Err = NewErrSuccess("removed from blacklist")
+ c.Err = command.NewErrSuccess("removed from blacklist")
}
return true
}
@@ -1184,9 +1173,9 @@ func handleTogglePmWhitelistedUser(c *command.Command) (handled bool) {
return true
}
if c.DB.ToggleWhitelistedUser(c.AuthUser.ID, user.ID) {
- c.Err = NewErrSuccess("added to whitelist")
+ c.Err = command.NewErrSuccess("added to whitelist")
} else {
- c.Err = NewErrSuccess("removed from whitelist")
+ c.Err = command.NewErrSuccess("removed from whitelist")
}
return true
}
@@ -1213,7 +1202,7 @@ func handleChessCmd(c *command.Command) (handled bool) {
c.Err = err
return true
}
- c.Err = NewErrSuccess("chess game created")
+ c.Err = command.NewErrSuccess("chess game created")
return true
}
return
@@ -1257,7 +1246,7 @@ func handleInboxCmd(c *command.Command) (handled bool) {
c.DB.CreateInboxMessage(html, c.Room.ID, c.AuthUser.ID, toUser.ID, true, false, nil)
c.DataMessage = "/inbox " + string(username) + " "
- c.Err = NewErrSuccess("inbox sent")
+ c.Err = command.NewErrSuccess("inbox sent")
return true
} else if strings.HasPrefix(c.Message, "/inbox ") {
@@ -1530,7 +1519,7 @@ func handleDateCmd(c *command.Command) (handled bool) {
func handleSuccessCmd(c *command.Command) (handled bool) {
if c.Message == "/success" {
- c.Err = NewErrSuccess("success message")
+ c.Err = command.NewErrSuccess("success message")
return true
}
return
@@ -1575,6 +1564,20 @@ func handleSetChatRoomExternalLink(c *command.Command) (handled bool) {
return
}
+func handlePurgeCmd(c *command.Command) (handled bool) {
+ if c.Message == "/purge" {
+ c.Err = command.ErrRedirect
+ if !c.AuthUser.UseStream {
+ c.Err = errors.New("only work on stream version of this chat")
+ return true
+ }
+ payload := database.ChatMessageType{}
+ streamModals.PurgeModal{}.Show(c.AuthUser.ID, c.Room.ID, payload)
+ return true
+ }
+ return
+}
+
func handlePurge(c *command.Command) (handled bool) {
if m := purgeRgx.FindStringSubmatch(c.Message); len(m) == 3 {
isHB := m[1] == " -hb"
@@ -1667,7 +1670,7 @@ func handleRemoveMeme(c *command.Command) (handled bool) {
c.Err = err
return true
}
- c.Err = NewErrSuccess("meme removed")
+ c.Err = command.NewErrSuccess("meme removed")
return true
}
return
@@ -1684,7 +1687,7 @@ func handleRenameMeme(c *command.Command) (handled bool) {
}
meme.Slug = newSlug
meme.DoSave(c.DB)
- c.Err = NewErrSuccess("meme renamed")
+ c.Err = command.NewErrSuccess("meme renamed")
return true
}
return
diff --git a/pkg/web/handlers/streamModals/codeModal.go b/pkg/web/handlers/streamModals/codeModal.go
@@ -52,7 +52,7 @@ func (m *CodeModal) Css() string {
return getCss()
}
-func (m *CodeModal) Handle(authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool {
+func (m *CodeModal) Handle(db *database.DkfDB, authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool {
if topic == m.topics[0] {
send(getCodeModalHTML(m.idx, m.room.Name, csrf, msgTyp, authUser.SyntaxHighlightCode))
return true
@@ -81,7 +81,7 @@ func getCss() string {
font-size: 18px;
height: 23px;
border: 1px solid #850000;
- border-radius: 0 0 0 5px;
+ border-radius: 0 5px 0 5px;
cursor: pointer;
}
.code-modal .header .cancel:hover {
@@ -145,7 +145,7 @@ func (_ CodeModal) InterceptMsg(cmd *command.Command) {
}
CodeModal{}.Hide(cmd.AuthUser.ID, cmd.Room.ID)
-
+
if !isValidLang(lang) {
lang = ""
}
diff --git a/pkg/web/handlers/streamModals/manager.go b/pkg/web/handlers/streamModals/manager.go
@@ -36,10 +36,10 @@ func (m *ModalsManager) Topics() (out []string) {
}
// Handle returns after the first modal that handle a specific topic
-func (m *ModalsManager) Handle(authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool {
+func (m *ModalsManager) Handle(db *database.DkfDB, authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool {
for _, modal := range m.modals {
if utils.InArr(topic, modal.Topics()) {
- if modal.Handle(authUser, topic, csrf, msgTyp, send) {
+ if modal.Handle(db, authUser, topic, csrf, msgTyp, send) {
return true
}
}
diff --git a/pkg/web/handlers/streamModals/purgeModal.go b/pkg/web/handlers/streamModals/purgeModal.go
@@ -0,0 +1,148 @@
+package streamModals
+
+import (
+ "bytes"
+ "dkforest/pkg/database"
+ "dkforest/pkg/utils"
+ "dkforest/pkg/web/handlers/interceptors/command"
+ "fmt"
+ "html/template"
+ "strconv"
+ "strings"
+)
+
+const purgeModalName = "purge"
+
+type PurgeModal struct {
+ StreamModal
+}
+
+func (m PurgeModal) Show(userID database.UserID, roomID database.RoomID, payload database.ChatMessageType) {
+ database.MsgPubSub.Pub(m.showTopic(purgeModalName, userID, roomID), payload)
+}
+
+func (m PurgeModal) Hide(userID database.UserID, roomID database.RoomID) {
+ database.MsgPubSub.Pub(m.hideTopic(purgeModalName, userID, roomID), database.ChatMessageType{})
+}
+
+func NewPurgeModal(userID database.UserID, room database.ChatRoom) *PurgeModal {
+ m := &PurgeModal{StreamModal{name: purgeModalName, userID: userID, room: room}}
+ m.topics = append(m.topics, m.showTopic(purgeModalName, userID, room.ID), m.hideTopic(purgeModalName, userID, room.ID))
+ return m
+}
+
+func (m *PurgeModal) Css() string {
+ return getPurgeModalCss()
+}
+
+func (m *PurgeModal) Handle(db *database.DkfDB, authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool {
+ if topic == m.topics[0] {
+ send(getPurgeModalHTML(db, m.idx, m.room.Name, csrf, msgTyp))
+ return true
+
+ } else if topic == m.topics[1] {
+ send(`<style>.purge-modal-` + strconv.Itoa(m.idx) + `{display:none;}</style>`)
+ m.idx++
+ return true
+ }
+
+ return false
+}
+
+func getPurgeModalCss() string {
+ return strings.Join(strings.Split(`
+.purge-modal {
+ display: block;
+ width: 400px;
+ left: calc(50% - 200px - (185px/2));
+ height: 100px;
+ position: fixed; top: 0;
+ background-color: gray; z-index: 999; border-radius: 5px;
+}
+ .purge-modal .header { position: absolute; top: 0; right: 0; }
+ .purge-modal .header .cancel {
+ border: 1px solid gray;
+ background-color: #ff7070;
+ color: #850000;
+ font-size: 18px;
+ height: 23px;
+ border: 1px solid #850000;
+ border-radius: 0 5px 0 5px;
+ cursor: pointer;
+ }
+ .purge-modal .header .cancel:hover {
+ background-color: #ff6767;
+ }
+ .purge-modal .wrapper { position: absolute; top: 25px; left: 10px; right: 10px; bottom: 30px; }
+ .purge-modal .wrapper textarea { width: 100%; height: 100%; color: #fff; background-color: rgba(79,79,79,1); border: 1px solid rgba(90,90,90,1); }
+ .purge-modal .controls { position: absolute; left: 10px; right: 10px; bottom: 5px; }`, "\n"), " ")
+}
+
+func getPurgeModalHTML(db *database.DkfDB, purgeModalIdx int, roomName, csrf string, msgTyp database.ChatMessageType) string {
+ htmlTmpl := `<div class="purge-modal purge-modal-{{ .PurgeModalIdx }}">
+<form method="post" target="iframe1" action="/api/v1/chat/top-bar/{{ .RoomName }}">
+ <input type="hidden" name="csrf" value="{{ .CSRF }}" />
+ <input type="hidden" name="sender" value="purgeModal" />
+ <div class=wrapper>
+ <input type="text" name="username" placeholder="username" autocomplete="off" autocapitalize="none" />
+ <select name="typ">
+ <option value="all">All messages</option>
+ <option value="hb">HB messages</option>
+ </select>
+ <select name="delta">
+ <option value="300">5 minutes</option>
+ <option value="3600" selected>1 hour</option>
+ <option value="21600">6 hour</option>
+ <option value="43200">12 hour</option>
+ <option value="86400">24 hour</option>
+ </select>
+ </div>
+ <div class="controls">
+ <button type="submit">purge</button>
+ </div>
+ <div class="header">
+ <button class="cancel" type="submit" name="btn_cancel" value="1">×</button>
+ </div>
+</form>
+</div>`
+ data := map[string]any{
+ "CSRF": csrf,
+ "RoomName": roomName,
+ "PurgeModalIdx": purgeModalIdx,
+ }
+ var buf bytes.Buffer
+ _ = utils.Must(template.New("").Parse(htmlTmpl)).Execute(&buf, data)
+ return buf.String()
+}
+
+func (_ PurgeModal) InterceptMsg(cmd *command.Command) {
+ sender := cmd.C.Request().PostFormValue("sender")
+ btnCancel := cmd.C.Request().PostFormValue("btn_cancel")
+ delta := utils.DoParseInt64(cmd.C.Request().PostFormValue("delta"))
+ username := database.Username(cmd.C.Request().PostFormValue("username"))
+ typ := cmd.C.Request().PostFormValue("typ")
+
+ if sender != "purgeModal" {
+ return
+ }
+
+ PurgeModal{}.Hide(cmd.AuthUser.ID, cmd.Room.ID)
+
+ if btnCancel == "1" {
+ cmd.Err = command.ErrRedirect
+ return
+ }
+
+ user, err := cmd.DB.GetUserByUsername(username)
+ if err != nil {
+ cmd.Err = err
+ return
+ }
+ cmd.DB.NewAudit(*cmd.AuthUser, fmt.Sprintf("purge %s #%d", user.Username, user.ID))
+ _ = cmd.DB.DeleteUserChatMessagesOpt(user.ID, typ == "hb", delta)
+
+ database.MsgPubSub.Pub(database.RefreshTopic, database.ChatMessageType{Typ: database.ForceRefresh})
+
+ cmd.Err = command.NewErrSuccess(string(user.Username) + " purged")
+ return
+}
diff --git a/pkg/web/handlers/streamModals/streamModal.go b/pkg/web/handlers/streamModals/streamModal.go
@@ -8,7 +8,7 @@ type IStreamModal interface {
// Topics returns all the topics the modal is interested in
Topics() []string
// Handle a stream message
- Handle(authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool
+ Handle(db *database.DkfDB, authUser *database.User, topic, csrf string, msgTyp database.ChatMessageType, send func(string)) bool
// Implement interceptor
Show(database.UserID, database.RoomID, database.ChatMessageType)
Hide(database.UserID, database.RoomID)