diff --git a/conference_ops.go b/conference_ops.go index 5da201e..140529b 100644 --- a/conference_ops.go +++ b/conference_ops.go @@ -21,7 +21,9 @@ import ( "strings" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/ui" + log "github.com/sirupsen/logrus" ) 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) } +/* 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) { if ctxt.CurrentUser().IsAnon { ctxt.SetRC(http.StatusForbidden) @@ -456,6 +466,80 @@ func MoveMessageForm(ctxt ui.AmContext) (string, any, error) { 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. * Parameters: * ctxt - The AmContext for the request. diff --git a/database/post.go b/database/post.go index f1e73d4..455275f 100644 --- a/database/post.go +++ b/database/post.go @@ -410,6 +410,110 @@ func (p *PostHeader) Nuke(ctx context.Context, u *User, ipaddr string) error { 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. * Parameters: * ctx - Standard Go context value. diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index 2c4c7d3..6c79a9b 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -43,7 +43,7 @@ _(italicized items can be deferred)_ - ~~Post Scribble~~ - ~~Post Nuke~~ - ~~Post Filter User~~ - - Post Move + - ~~Post Move~~ - Post Publish - Manage Communities on communities sidebox - ~~Conference Hotlist sidebox~~ diff --git a/main.go b/main.go index 28bfd11..a77f800 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ func setupEcho() *echo.Echo { opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage)) opsGroup.GET("/nuke/:msg", ui.AmWrap(NukeMessage)) opsGroup.GET("/move/:msg", ui.AmWrap(MoveMessageForm)) + opsGroup.POST("/move/:msg", ui.AmWrap(MoveMessage)) opsGroup.GET("/manage", ui.AmWrap(TopicManage)) opsGroup.GET("/subscribe", ui.AmWrap(TopicSetSubscribe)) opsGroup.GET("/rmbozo/:uid", ui.AmWrap(TopicRemoveBozo)) diff --git a/ui/views/move_message.jet b/ui/views/move_message.jet index beeb93a..ae3651c 100644 --- a/ui/views/move_message.jet +++ b/ui/views/move_message.jet @@ -22,7 +22,7 @@ -
+