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 }}