hooked up "nuke message" and finished defining the message box

This commit is contained in:
2026-01-28 18:59:38 -07:00
parent ef47631b24
commit b59e15a48e
7 changed files with 217 additions and 10 deletions
+61
View File
@@ -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 <span class="font-mono font-bold text-red-600">&lt;%s&gt;</span>,
originally composed by <span class="font-bold text-red-600">&lt;%s&gt;</span>!`, 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.
+41 -2
View File
@@ -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
}
+1 -1
View File
@@ -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
+1
View File
@@ -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
+108
View File
@@ -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"`
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")
}
+3 -5
View File
@@ -13,7 +13,7 @@
<!-- Dialog Header -->
<div class="bg-{{ tone }}-600 px-6 py-4">
<h1 class="text-white text-2xl font-bold text-center flex items-center justify-center gap-3">
{{ if destructive }}span class="text-3xl">⚠️</span>{{ end }}
{{ if destructive }}<span class="text-3xl">⚠️</span>{{ end }}
{{ amsterdam_pageTitle }}
{{ if destructive }}<span class="text-3xl">⚠️</span>{{ end }}
</h1>
@@ -23,9 +23,7 @@
<div class="px-8 py-8">
<div class="text-center mb-8">
<p class="text-gray-800 text-lg leading-relaxed">
{{ message }}
{* You are about to nuke message <span class="font-mono font-bold text-red-600">&lt;Playground.129.16&gt;</span>,
originally composed by <span class="font-bold text-red-600">&lt;erbo&gt;</span>! *}
{{ message | raw }}
</p>
{{ if destructive }}
<p class="text-gray-800 text-lg font-bold mt-4">Are you sure you want to do this?</p>
@@ -53,7 +51,7 @@
<!-- Action Buttons -->
<div class="flex gap-4 justify-center">
{{ range i, bt := buttons }}
<a href="{{ bt.Link }}"
<a href="{{ buttonLinks[i] }}"
class="bg-{{ bt.Tone }}-600 hover:bg-{{ bt.Tone }}-700 text-white font-bold px-8 py-3 rounded-lg text-lg transition-colors shadow-lg hover:shadow-xl flex items-center gap-2">
<span class="text-xl">{{ bt.Icon }}</span>
{{ bt.Text }}
+1 -1
View File
@@ -134,7 +134,7 @@
class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm font-medium transition-colors whitespace-nowrap">Filter User</a>
{{ end }}
{{ if canNuke }}
<a href="/TODO"
<a href="{{ topicListLink }}/op/{{ topicNum }}/nuke/{{ p.Num }}"
class="bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm font-medium transition-colors whitespace-nowrap">Nuke</a>
{{ end }}
{{ if false }}{* TODO *}