From b59e15a48eb3610f5bac198419d516b0cb2a9d4d Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Wed, 28 Jan 2026 18:59:38 -0700 Subject: [PATCH] hooked up "nuke message" and finished defining the message box --- conference_ops.go | 61 ++++++++++++++++++++++ database/post.go | 43 +++++++++++++++- docs/MISSINGFUNCS.md | 2 +- main.go | 1 + ui/messagebox.go | 110 +++++++++++++++++++++++++++++++++++++++- ui/views/messagebox.jet | 8 ++- ui/views/posts.jet | 2 +- 7 files changed, 217 insertions(+), 10 deletions(-) diff --git a/conference_ops.go b/conference_ops.go index 1633a69..a33b710 100644 --- a/conference_ops.go +++ b/conference_ops.go @@ -276,6 +276,67 @@ func ScribbleMessage(ctxt ui.AmContext) (string, any, error) { return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num), nil } +/* NukeMessage nukes (deletes entirely) a topic 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 NukeMessage(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) + 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 !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 nuke! + mbox, err := ui.AmLoadMessageBox("nuke") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + if mbox.Validate(ctxt, "yes") { + // do the nuking! + err := hdrs[0].Nuke(ctxt.Ctx(), ctxt.CurrentUser(), ctxt.RemoteIP()) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + return "redirect", fmt.Sprintf("/comm/%s/conf/%s/r/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number), nil + } + + // Set up to display the message box. + link, err := hdrs[0].Link(ctxt.Ctx(), "community") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + creator, err := hdrs[0].Creator(ctxt.Ctx()) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + mbox.SetMessage(fmt.Sprintf(`You are about to nuke message <%s>, + originally composed by <%s>!`, link, creator.Username)) + mbox.SetLink("no", fmt.Sprintf("/comm/%s/conf/%s/r/%d?r=%d&ac=1", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num)) + mbox.SetLink("yes", fmt.Sprintf("/comm/%s/conf/%s/op/%d/nuke/%d", ctxt.CurrentCommunity().Alias, ctxt.GetScratch("currentAlias"), topic.Number, hdrs[0].Num)) + return mbox.Render(ctxt) +} + /* TopicManage displays the "manage topic" page. * Parameters: * ctxt - The AmContext for the request. diff --git a/database/post.go b/database/post.go index edec40a..f1e73d4 100644 --- a/database/post.go +++ b/database/post.go @@ -57,6 +57,11 @@ const ( // ErrNoPostData is returned if post data is missing. var ErrNoPostData = errors.New("no post data") +// Creator returns the creator of the post. +func (p *PostHeader) Creator(ctx context.Context) (*User, error) { + return AmGetUser(ctx, p.CreatorUid) +} + // IsScribbled returns true if the post has been scribbled, false if not. func (p *PostHeader) IsScribbled() bool { return p.ScribbleUid != nil && p.ScribbleDate != nil @@ -219,6 +224,29 @@ func (p *PostHeader) Text(ctx context.Context) (string, error) { return *dbdata[0].Data, nil } +// Link returns a link string to this post. +func (p *PostHeader) Link(ctx context.Context, scope string) (string, error) { + if scope == "topic" { + return fmt.Sprintf("%d", p.Num), nil + } + if scope == "conference" || scope == "community" || scope == "global" { + topic, err := AmGetTopic(ctx, p.TopicId) + if err != nil { + return "", err + } + parent, err := topic.Link(ctx, scope) + if err != nil { + return "", err + } + if strings.HasSuffix(parent, ".") { + return fmt.Sprintf("%s%d", parent, p.Num), nil + } else { + return fmt.Sprintf("%s.%d", parent, p.Num), nil + } + } + return "", errors.New("invalid scope") +} + // SetHidden sets the "hidden" flag on a post. func (p *PostHeader) SetHidden(ctx context.Context, u *User, flag bool, ipaddr string) error { var ar *AuditRecord = nil @@ -326,7 +354,7 @@ func (p *PostHeader) Nuke(ctx context.Context, u *User, ipaddr string) error { } }() unlock := true - tx.ExecContext(ctx, "LOCK TABLES posts WRITE, postdata WRITE, postattach WRITE, postdogear WRITE, postpublish WRITE, topics WRITE;") + tx.ExecContext(ctx, "LOCK TABLES posts WRITE, postdata WRITE, postattach WRITE, postdogear WRITE, postpublish WRITE, topics WRITE, topicsettings WRITE;") defer func() { if unlock { tx.ExecContext(ctx, "UNLOCK TABLES;") @@ -355,8 +383,19 @@ func (p *PostHeader) Nuke(ctx context.Context, u *User, ipaddr string) error { if _, err = tx.ExecContext(ctx, "UPDATE posts SET num = (num - 1) WHERE topicid = ? AND num > ?", p.TopicId, p.Num); err != nil { return err } + row := tx.QueryRowContext(ctx, "SELECT top_message FROM topics WHERE topicid = ?", p.TopicId) // Renumber phase 2 - reset the top message in this topic - if _, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = (top_message - 1) WHERE topicid = ?", p.TopicId); err != nil { + var topMessage int32 + if err = row.Scan(&topMessage); err != nil { + return err + } + topMessage-- + if _, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = ? WHERE topicid = ?", topMessage, p.TopicId); err != nil { + return err + } + // Renumber phase 3 - adjust the last message in all settings for that topic + if _, err = tx.ExecContext(ctx, "UPDATE topicsettings SET last_message = ? WHERE topicid = ? AND last_message > ?", + topMessage, p.TopicId, topMessage); err != nil { return err } diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index d8878d4..122046d 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -41,7 +41,7 @@ _(italicized items can be deferred)_ - Archive/Unarchive - Delete - ~~Post Scribble~~ - - Post Nuke + - ~~Post Nuke~~ - Post Filter User - Post Move - Post Publish diff --git a/main.go b/main.go index 796c850..82dac35 100644 --- a/main.go +++ b/main.go @@ -109,6 +109,7 @@ func setupEcho() *echo.Echo { opsGroup.GET("/hide", ui.AmWrap(HideTopic)) opsGroup.GET("/hide/:msg", ui.AmWrap(HideMessage)) opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage)) + opsGroup.GET("/nuke/:msg", ui.AmWrap(NukeMessage)) opsGroup.GET("/manage", ui.AmWrap(TopicManage)) return e diff --git a/ui/messagebox.go b/ui/messagebox.go index 684f948..2288a56 100644 --- a/ui/messagebox.go +++ b/ui/messagebox.go @@ -10,8 +10,14 @@ package ui import ( + "crypto/sha1" _ "embed" + "encoding/hex" + "errors" + "fmt" + "strings" + "git.erbosoft.com/amy/amsterdam/util" "gopkg.in/yaml.v3" ) @@ -41,11 +47,13 @@ type MessageBoxDefinition struct { WarningIcon string `yaml:"warningIcon"` WarningLines []MBoxWarningLine `yaml:"warningLines"` Buttons []MBoxButton `yaml:"buttons"` + useConfirm bool } // MessageBoxDefs is the top-level structure for defining message boxes. type MessageBoxDefs struct { - D []MessageBoxDefinition `yaml:"messagedefs"` + D []MessageBoxDefinition `yaml:"messagedefs"` + table map[string]*MessageBoxDefinition } //go:embed messagedefs.yaml @@ -59,4 +67,104 @@ func init() { if err := yaml.Unmarshal(initMessageData, &messageBoxDefs); err != nil { panic(err) // can't happen } + messageBoxDefs.table = make(map[string]*MessageBoxDefinition) + for i, def := range messageBoxDefs.D { + messageBoxDefs.table[def.Id] = &(messageBoxDefs.D[i]) + messageBoxDefs.D[i].useConfirm = false + for _, b := range messageBoxDefs.D[i].Buttons { + if b.Confirm { + messageBoxDefs.D[i].useConfirm = true + break + } + } + } +} + +// MessageBox is the structure for a working message box. +type MessageBox struct { + def *MessageBoxDefinition + message string + buttonLinks []string +} + +// SetMessage sets the actual message inside the message box. +func (mb *MessageBox) SetMessage(t string) { + mb.message = t +} + +// SetLink sets the link for a specific button in the box. +func (mb *MessageBox) SetLink(id, link string) { + for i := range mb.def.Buttons { + if mb.def.Buttons[i].Id == id { + mb.buttonLinks[i] = link + break + } + } +} + +// Render sets up to render the message box. +func (mb *MessageBox) Render(ctxt AmContext) (string, any, error) { + blinks := mb.buttonLinks + if mb.def.useConfirm { + nonce := util.GenerateRandomAuthString() + blinks = make([]string, len(mb.buttonLinks)) + for i := range mb.buttonLinks { + if mb.def.Buttons[i].Confirm { + hasher := sha1.New() + hasher.Write([]byte(mb.def.Buttons[i].Id)) + confirmString := hex.EncodeToString(hasher.Sum([]byte(nonce))) + if strings.Contains(mb.buttonLinks[i], "?") { + blinks[i] = fmt.Sprintf("%s&confirm=%s", mb.buttonLinks[i], confirmString) + } else { + blinks[i] = fmt.Sprintf("%s?confirm=%s", mb.buttonLinks[i], confirmString) + } + } else { + blinks[i] = mb.buttonLinks[i] + } + } + ctxt.SetSession("mbconfirm."+mb.def.Id, nonce) + } + ctxt.VarMap().Set("amsterdam_pageTitle", mb.def.Title) + ctxt.VarMap().Set("tone", mb.def.Tone) + ctxt.VarMap().Set("destructive", mb.def.Destructive) + ctxt.VarMap().Set("message", mb.message) + ctxt.VarMap().Set("useWarning", len(mb.def.WarningIcon) > 0 && len(mb.def.WarningLines) > 0) + ctxt.VarMap().Set("warningIcon", mb.def.WarningIcon) + ctxt.VarMap().Set("warningLines", mb.def.WarningLines) + ctxt.VarMap().Set("buttons", mb.def.Buttons) + ctxt.VarMap().Set("buttonLinks", blinks) + return "framed_template", "messagebox.jet", nil +} + +// Validate validates that the correct button was clicked by verifying the confirmation parameter. +func (mb *MessageBox) Validate(ctxt AmContext, buttonid string) bool { + var nonceAny any + nonceAny = ctxt.GetSession("mbconfirm." + mb.def.Id) + ctxt.SetSession("mbconfirm."+mb.def.Id, "") + if nonce, ok := nonceAny.(string); ok { + confirm := ctxt.Parameter("confirm") + hasher := sha1.New() + hasher.Write([]byte(buttonid)) + confirmString := hex.EncodeToString(hasher.Sum([]byte(nonce))) + if confirm == confirmString { + return true + } + } + return false +} + +// AmLoadMessageBox loads a message box structure by ID. +func AmLoadMessageBox(id string) (*MessageBox, error) { + if def, ok := messageBoxDefs.table[id]; ok { + mbox := MessageBox{ + def: def, + message: def.Message, + buttonLinks: make([]string, len(def.Buttons)), + } + for i := range def.Buttons { + mbox.buttonLinks[i] = def.Buttons[i].Link + } + return &mbox, nil + } + return nil, errors.New("message box not found") } diff --git a/ui/views/messagebox.jet b/ui/views/messagebox.jet index 527628c..ffb2558 100644 --- a/ui/views/messagebox.jet +++ b/ui/views/messagebox.jet @@ -13,7 +13,7 @@

- {{ if destructive }}span class="text-3xl">⚠️{{ end }} + {{ if destructive }}⚠️{{ end }} {{ amsterdam_pageTitle }} {{ if destructive }}⚠️{{ end }}

@@ -23,9 +23,7 @@

- {{ message }} - {* You are about to nuke message <Playground.129.16>, - originally composed by <erbo>! *} + {{ message | raw }}

{{ if destructive }}

Are you sure you want to do this?

@@ -53,7 +51,7 @@