diff --git a/conference_ops.go b/conference_ops.go index 05dbb6f..43ac483 100644 --- a/conference_ops.go +++ b/conference_ops.go @@ -273,6 +273,48 @@ func StickTopic(ctxt ui.AmContext) (string, any, error) { return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number), nil } +/* DeleteTopic deletes the current topic. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func DeleteTopic(ctxt ui.AmContext) (string, any, error) { + if ctxt.CurrentUser().IsAnon { + ctxt.SetRC(http.StatusForbidden) + return ui.ErrorPage(ctxt, ENOPERM) + } + conf := ctxt.GetScratch("currentConference").(*database.Conference) + myLevel := ctxt.GetScratch("levelInConference").(uint16) + topic := ctxt.GetScratch("currentTopic").(*database.Topic) + if !conf.TestPermission("Conference.Nuke", myLevel) { + ctxt.SetRC(http.StatusForbidden) + return ui.ErrorPage(ctxt, ENOPERM) + } + + // Load the message box, and, if we have a valid "yes," then perform the delete + mbox, err := ui.AmLoadMessageBox("deleteTopic") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + if mbox.Validate(ctxt, "yes") { + err := topic.Delete(ctxt.Ctx(), ctxt.CurrentUser(), ctxt.RemoteIP(), ampool) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + return "redirect", fmt.Sprintf("/comm/%s/conf/%s", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias")), nil + } + + // Set up to display the message box. + mbox.SetMessage(fmt.Sprintf(`You are about to detele the topic "%s" + from the "%s" conference!`, topic.Name, conf.Name)) + mbox.SetLink("no", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number)) + mbox.SetLink("yes", fmt.Sprintf("/comm/%s/conf/%s/op/%d/delete", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number)) + return mbox.Render(ctxt) +} + /* HideMessage hides or shows a topic message. * Parameters: * ctxt - The AmContext for the request. diff --git a/database/topic.go b/database/topic.go index f95abb9..5dc88d5 100644 --- a/database/topic.go +++ b/database/topic.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "git.erbosoft.com/amy/amsterdam/util" "github.com/jmoiron/sqlx" log "github.com/sirupsen/logrus" ) @@ -289,6 +290,116 @@ func (t *Topic) GetSubscribers(ctx context.Context) ([]int32, error) { return rc, err } +// backgroundPurgeTopic removes all posts from a topic that's been deleted. +func backgroundPurgeTopic(ctx context.Context, topicid int32) error { + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + // Get some stats on the posts we have to remove. + row := tx.QueryRowContext(ctx, "SELECT MAX(postid) FROM posts WHERE topicid = ?", topicid) + var postMax int32 + err := row.Scan(&postMax) + if err != nil { + return err + } + + // Perform wholesale deletes on auxiliary tables. + _, err = tx.ExecContext(ctx, "DELETE FROM postdata WHERE postid IN (SELECT postid FROM posts WHERE topicid = ? AND postid <= ?)", topicid, postMax) + if err == nil { + _, err = tx.ExecContext(ctx, "DELETE FROM postattach WHERE postid IN (SELECT postid FROM posts WHERE topicid = ? AND postid <= ?)", topicid, postMax) + if err == nil { + _, err = tx.ExecContext(ctx, "DELETE FROM postdogear WHERE postid IN (SELECT postid FROM posts WHERE topicid = ? AND postid <= ?)", topicid, postMax) + if err == nil { + _, err = tx.ExecContext(ctx, "DELETE FROM postpublish WHERE postid IN (SELECT postid FROM posts WHERE topicid = ? AND postid <= ?)", topicid, postMax) + } + } + } + if err != nil { + return err + } + + // Now delete from the main posts table. + _, err = tx.ExecContext(ctx, "DELETE FROM posts WHERE topicid = ? AND postid <= ?", topicid, postMax) + if err != nil { + return err + } + if err = tx.Commit(); err != nil { + return err + } + success = true + return nil +} + +// Delete deletes this topic. +func (t *Topic) Delete(ctx context.Context, u *User, ipaddr string, background *util.WorkerPool) error { + var ar *AuditRecord = nil + defer func() { + AmStoreAudit(ar) + }() + success := false + tx := amdb.MustBegin() + defer func() { + if !success { + tx.Rollback() + } + }() + + unlock := true + tx.ExecContext(ctx, "LOCK TABLES confs WRITE, topics WRITE, topicsettings WRITE, topicbozo WRITE;") + defer func() { + if unlock { + tx.ExecContext(ctx, "UNLOCK TABLES;") + } + }() + + conf, err := AmGetConference(ctx, t.ConfId) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, "DELETE FROM topics WHERE topicid = ?", t.TopicId) + if err == nil { + _, err = tx.ExecContext(ctx, "DELETE FROM topicsettings WHERE topicid = ?", t.TopicId) + if err == nil { + _, err = tx.ExecContext(ctx, "DELETE FROM topicbozo WHERE topicid = ?", t.TopicId) + } + } + if err != nil { + return err + } + + err = conf.TouchUpdate(ctx, tx, time.Now()) + if err != nil { + return err + } + tx.ExecContext(ctx, "UNLOCK TABLES;") + unlock = false + if err = tx.Commit(); err != nil { + return err + } + success = true + + // create audit record + ar = AmNewAudit(AuditConferenceDeleteTopic, u.Uid, ipaddr, fmt.Sprintf("confid=%d", conf.ConfId), + fmt.Sprintf("topic=%d", t.TopicId)) + + // Spin off a background task to finish deleting this topic. + myTopicId := t.TopicId + background.Submit(func(ctx context.Context) { + err := backgroundPurgeTopic(ctx, myTopicId) + if err != nil { + log.Errorf("backgroundTopicPurge FAILED with %v", err) + } + }) + + return nil +} + // TopicSettings contains per-user settings for topics, including the "last read" message pointer. type TopicSettings struct { TopicId int32 `db:"topicid"` // unique ID of the topic diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index c987559..1b73b59 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -39,7 +39,7 @@ _(italicized items can be deferred)_ - ~~Stick/Unstick~~ - ~~Freeze/Unfreeze~~ - ~~Archive/Unarchive~~ - - Delete + - ~~Delete~~ - ~~Post Scribble~~ - ~~Post Nuke~~ - ~~Post Filter User~~ diff --git a/main.go b/main.go index cb2896a..24119f2 100644 --- a/main.go +++ b/main.go @@ -113,6 +113,7 @@ func setupEcho() *echo.Echo { opsGroup.GET("/freeze", ui.AmWrap(FreezeTopic)) opsGroup.GET("/archive", ui.AmWrap(ArchiveTopic)) opsGroup.GET("/stick", ui.AmWrap(StickTopic)) + opsGroup.GET("/delete", ui.AmWrap(DeleteTopic)) opsGroup.GET("/hide/:msg", ui.AmWrap(HideMessage)) opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage)) opsGroup.GET("/nuke/:msg", ui.AmWrap(NukeMessage)) diff --git a/ui/messagedefs.yaml b/ui/messagedefs.yaml index f06cd75..71f38f9 100644 --- a/ui/messagedefs.yaml +++ b/ui/messagedefs.yaml @@ -31,3 +31,27 @@ messagedefs: tone: "green" icon: "✗" text: "No, Cancel" + - id: "deleteTopic" + title: "Delete Topic" + tone: "red" + destructive: true + message: "You are about to delete a topic!" + warningIcon: "💣" + warningLines: + - text: "Warning: This action cannot be undone!" + bold: true + - text: "Deleting this topic will permanently remove it from the system." + bold: false + buttons: + - id: "yes" + link: "placeholder" + confirm: true + tone: "red" + icon: "✓" + text: "Yes, Delete It" + - id: "no" + link: "placeholder" + confirm: false + tone: "green" + icon: "✗" + text: "No, Cancel" diff --git a/ui/views/posts.jet b/ui/views/posts.jet index f4e4c37..416adc2 100644 --- a/ui/views/posts.jet +++ b/ui/views/posts.jet @@ -61,7 +61,7 @@ {{ end }} {{ if canDelete }} - Delete Topic {{ end }}