hooked up "nuke message" and finished defining the message box
This commit is contained in:
@@ -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"><%s></span>,
|
||||
originally composed by <span class="font-bold text-red-600"><%s></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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"><Playground.129.16></span>,
|
||||
originally composed by <span class="font-bold text-red-600"><erbo></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
@@ -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 *}
|
||||
|
||||
Reference in New Issue
Block a user