landed "move message" functionality

This commit is contained in:
2026-01-29 22:38:27 -07:00
parent 8341a6e4d4
commit 84d734578b
5 changed files with 191 additions and 2 deletions
+84
View File
@@ -21,7 +21,9 @@ import (
"strings" "strings"
"git.erbosoft.com/amy/amsterdam/database" "git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/email"
"git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/ui"
log "github.com/sirupsen/logrus"
) )
var ENOPERM error = errors.New("you are not permitted to perform this operation") var ENOPERM error = errors.New("you are not permitted to perform this operation")
@@ -406,6 +408,14 @@ func NukeMessage(ctxt ui.AmContext) (string, any, error) {
return mbox.Render(ctxt) return mbox.Render(ctxt)
} }
/* MoveMessageForm displays the form for moving a message..
* 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 MoveMessageForm(ctxt ui.AmContext) (string, any, error) { func MoveMessageForm(ctxt ui.AmContext) (string, any, error) {
if ctxt.CurrentUser().IsAnon { if ctxt.CurrentUser().IsAnon {
ctxt.SetRC(http.StatusForbidden) ctxt.SetRC(http.StatusForbidden)
@@ -456,6 +466,80 @@ func MoveMessageForm(ctxt ui.AmContext) (string, any, error) {
return "framed_template", "move_message.jet", nil return "framed_template", "move_message.jet", nil
} }
/* MoveMessage moves a message to a different 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 MoveMessage(ctxt ui.AmContext) (string, any, error) {
if ctxt.CurrentUser().IsAnon {
ctxt.SetRC(http.StatusForbidden)
return ui.ErrorPage(ctxt, ENOPERM)
}
comm := ctxt.CurrentCommunity()
conf := ctxt.GetScratch("currentConference").(*database.Conference)
myLevel := ctxt.GetScratch("levelInConference").(uint16)
topic := ctxt.GetScratch("currentTopic").(*database.Topic)
msgNum, err := strconv.Atoi(ctxt.URLParam("msg"))
if err != nil {
return ui.ErrorPage(ctxt, err)
}
hdrs, err := database.AmGetPostRange(ctxt.Ctx(), topic, int32(msgNum), int32(msgNum))
if err != nil {
return ui.ErrorPage(ctxt, err)
} else if len(hdrs) != 1 {
return ui.ErrorPage(ctxt, errors.New("internal error getting post reference"))
}
if ctxt.FormFieldIsSet("cancel") {
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", comm.Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num), nil
}
if !conf.TestPermission("Conference.Nuke", myLevel) || !conf.TestPermission("Conference.Post", myLevel) || topic.TopMessage == 0 {
ctxt.SetRC(http.StatusForbidden)
return ui.ErrorPage(ctxt, ENOPERM)
}
targetId, err := ctxt.FormFieldInt("target")
if err != nil {
return ui.ErrorPage(ctxt, err)
}
target, err := database.AmGetTopic(ctxt.Ctx(), int32(targetId))
if err != nil {
return ui.ErrorPage(ctxt, err)
}
// Move the topic!
err = hdrs[0].MoveTo(ctxt.Ctx(), target, ctxt.CurrentUser(), ctxt.RemoteIP())
if err != nil {
return ui.ErrorPage(ctxt, err)
}
// Now, we need to send this post to whoever subscribed to the NEW topic. But it's tricky because we don't have
// all the information that we'd have if the post was just posted. Spool off any database operations in the task function.
subs, err := target.GetSubscribers(ctxt.Ctx())
if err != nil {
log.Errorf("unable to deliver message to subscribers: %v", err)
} else if len(subs) > 0 {
// kick off a task to compose E-mails and deliver them to everyone
alias := ctxt.GetScratch("currentAlias").(string)
ipaddr := ctxt.RemoteIP()
poster := ctxt.CurrentUser() // N.B.: only used for E-mail headers
hdr := hdrs[0]
ampool.Submit(func(ctx context.Context) {
var postText string
postText, err = hdr.Text(ctx)
if err == nil {
email.AmDeliverSubscription(ctx, comm, conf, alias, target, poster, hdr, postText, subs, ipaddr)
} else {
log.Errorf("unable to start AmDeliverSubscription - %v", err)
}
})
}
return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number), nil
}
/* TopicManage displays the "manage topic" page. /* TopicManage displays the "manage topic" page.
* Parameters: * Parameters:
* ctxt - The AmContext for the request. * ctxt - The AmContext for the request.
+104
View File
@@ -410,6 +410,110 @@ func (p *PostHeader) Nuke(ctx context.Context, u *User, ipaddr string) error {
return nil return nil
} }
// MoveTo moves this message to a new topic.
func (p *PostHeader) MoveTo(ctx context.Context, target *Topic, u *User, ipaddr string) error {
if target.TopicId == p.TopicId {
return nil // this is a no-op
}
if p.ScribbleDate != nil && p.ScribbleUid != nil {
return errors.New("cannot move a scribbled message")
}
oldTopic, err := AmGetTopic(ctx, p.TopicId)
if err != nil {
return err
}
if oldTopic.ConfId != target.ConfId {
return errors.New("target topic must be in the same conference")
}
if oldTopic.TopMessage == 0 {
return errors.New("cannot move the only message out of a conference")
}
conf, err := AmGetConference(ctx, oldTopic.ConfId)
if err != nil {
return err
}
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, posts WRITE, topicsettings WRITE;")
defer func() {
if unlock {
tx.ExecContext(ctx, "UNLOCK TABLES;")
}
}()
// Adjust post record in the database to make it part of the new topic.
_, err = tx.ExecContext(ctx, "UPDATE posts SET parent = 0, topicid = ?, num = ? WHERE postid = ?", target.TopicId, target.TopMessage+1, p.PostId)
if err != nil {
return err
}
// Adjust the topic values in the database to reflect that it has a new post.
_, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = top_message + 1, lastupdate = NOW() WHERE topicid = ?", target.TopicId)
if err != nil {
return err
}
// Read back the last update.
row := tx.QueryRowContext(ctx, "SELECT lastupdate FROM topics WHERE topicid = ?", target.TopicId)
var lastUpdate time.Time
err = row.Scan(&lastUpdate)
if err != nil {
return err
}
// Now we have to renumber the posts in the OLD topic just as if the old post was nuked.
_, err = tx.ExecContext(ctx, "UPDATE posts SET num = num - 1 WHERE topicid = ? AND num > ?", p.TopicId, p.Num)
if err == nil {
_, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = top_message - 1 WHERE topicid = ?", p.TopicId)
if err == nil {
_, err = tx.ExecContext(ctx, "UPDATE posts SET parent = ? WHERE parent = ?", p.Parent, p.PostId)
if err == nil {
_, err = tx.ExecContext(ctx, "UPDATE topicsettings SET last_message = ? WHERE topicid = ? AND last_message > ?",
oldTopic.TopMessage-1, p.TopicId, oldTopic.TopMessage-1)
}
}
}
if err != nil {
return err
}
// Touch the "update" in the conference.
err = conf.TouchUpdate(ctx, tx, lastUpdate)
if err != nil {
return err
}
// Unlock tables and commit.
tx.ExecContext(ctx, "UNLOCK TABLES;")
unlock = false
if err = tx.Commit(); err != nil {
return err
}
success = true
// Now patch the data structures we have.
p.Parent = 0
p.TopicId = target.TopicId
p.Num = target.TopMessage + 1
target.TopMessage++
target.LastUpdate = lastUpdate
// And audit the result.
ar = AmNewAudit(AuditConferenceMoveMessage, u.Uid, ipaddr, fmt.Sprintf("conf=%d,post=%d", conf.ConfId, p.PostId),
fmt.Sprintf("fromTopic=%d", oldTopic.TopicId), fmt.Sprintf("toTopic=%d", target.TopicId))
return nil
}
/* AmGetPost gets a single post from the database by ID. /* AmGetPost gets a single post from the database by ID.
* Parameters: * Parameters:
* ctx - Standard Go context value. * ctx - Standard Go context value.
+1 -1
View File
@@ -43,7 +43,7 @@ _(italicized items can be deferred)_
- ~~Post Scribble~~ - ~~Post Scribble~~
- ~~Post Nuke~~ - ~~Post Nuke~~
- ~~Post Filter User~~ - ~~Post Filter User~~
- Post Move - ~~Post Move~~
- Post Publish - Post Publish
- Manage Communities on communities sidebox - Manage Communities on communities sidebox
- ~~Conference Hotlist sidebox~~ - ~~Conference Hotlist sidebox~~
+1
View File
@@ -114,6 +114,7 @@ func setupEcho() *echo.Echo {
opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage)) opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage))
opsGroup.GET("/nuke/:msg", ui.AmWrap(NukeMessage)) opsGroup.GET("/nuke/:msg", ui.AmWrap(NukeMessage))
opsGroup.GET("/move/:msg", ui.AmWrap(MoveMessageForm)) opsGroup.GET("/move/:msg", ui.AmWrap(MoveMessageForm))
opsGroup.POST("/move/:msg", ui.AmWrap(MoveMessage))
opsGroup.GET("/manage", ui.AmWrap(TopicManage)) opsGroup.GET("/manage", ui.AmWrap(TopicManage))
opsGroup.GET("/subscribe", ui.AmWrap(TopicSetSubscribe)) opsGroup.GET("/subscribe", ui.AmWrap(TopicSetSubscribe))
opsGroup.GET("/rmbozo/:uid", ui.AmWrap(TopicRemoveBozo)) opsGroup.GET("/rmbozo/:uid", ui.AmWrap(TopicRemoveBozo))
+1 -1
View File
@@ -22,7 +22,7 @@
</div> </div>
<!-- Topic Selection Form --> <!-- Topic Selection Form -->
<form method="POST" action="{{ formLink }}"> <form method="POST" class="max-w-6xl" action="{{ formLink }}">
<div class="bg-gray-50 p-6 rounded-lg space-y-4"> <div class="bg-gray-50 p-6 rounded-lg space-y-4">
<label for="target" class="block text-black text-sm font-bold mb-2">Move to topic:</label> <label for="target" class="block text-black text-sm font-bold mb-2">Move to topic:</label>
<select id="target" name="target" size="1" <select id="target" name="target" size="1"