dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit 1ce2b07cc0f5c25c4ca7a5c0e02557d245a86221
parent 61f47aff099ff72b3145519c01de732f0f11546d
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Fri,  9 Jun 2023 00:17:59 -0700

purge modal admin tool

Diffstat:
Mpkg/database/tableChatMessages.go | 23+++++++++++++++++++++++
Mpkg/web/handlers/api/v1/topBarHandler.go | 3++-
Mpkg/web/handlers/handlers.go | 3++-
Mpkg/web/handlers/interceptors/command/command.go | 12++++++++++++
Mpkg/web/handlers/interceptors/slashInterceptor.go | 49++++++++++++++++++++++++++-----------------------
Mpkg/web/handlers/streamModals/codeModal.go | 6+++---
Mpkg/web/handlers/streamModals/manager.go | 4++--
Apkg/web/handlers/streamModals/purgeModal.go | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/handlers/streamModals/streamModal.go | 2+-
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)