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
|
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.
|
/* TopicManage displays the "manage topic" page.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctxt - The AmContext for the request.
|
* ctxt - The AmContext for the request.
|
||||||
|
|||||||
+41
-2
@@ -57,6 +57,11 @@ const (
|
|||||||
// ErrNoPostData is returned if post data is missing.
|
// ErrNoPostData is returned if post data is missing.
|
||||||
var ErrNoPostData = errors.New("no post data")
|
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.
|
// IsScribbled returns true if the post has been scribbled, false if not.
|
||||||
func (p *PostHeader) IsScribbled() bool {
|
func (p *PostHeader) IsScribbled() bool {
|
||||||
return p.ScribbleUid != nil && p.ScribbleDate != nil
|
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
|
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.
|
// SetHidden sets the "hidden" flag on a post.
|
||||||
func (p *PostHeader) SetHidden(ctx context.Context, u *User, flag bool, ipaddr string) error {
|
func (p *PostHeader) SetHidden(ctx context.Context, u *User, flag bool, ipaddr string) error {
|
||||||
var ar *AuditRecord = nil
|
var ar *AuditRecord = nil
|
||||||
@@ -326,7 +354,7 @@ func (p *PostHeader) Nuke(ctx context.Context, u *User, ipaddr string) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
unlock := true
|
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() {
|
defer func() {
|
||||||
if unlock {
|
if unlock {
|
||||||
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
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 {
|
if _, err = tx.ExecContext(ctx, "UPDATE posts SET num = (num - 1) WHERE topicid = ? AND num > ?", p.TopicId, p.Num); err != nil {
|
||||||
return err
|
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
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ _(italicized items can be deferred)_
|
|||||||
- Archive/Unarchive
|
- Archive/Unarchive
|
||||||
- Delete
|
- Delete
|
||||||
- ~~Post Scribble~~
|
- ~~Post Scribble~~
|
||||||
- Post Nuke
|
- ~~Post Nuke~~
|
||||||
- Post Filter User
|
- Post Filter User
|
||||||
- Post Move
|
- Post Move
|
||||||
- Post Publish
|
- Post Publish
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ func setupEcho() *echo.Echo {
|
|||||||
opsGroup.GET("/hide", ui.AmWrap(HideTopic))
|
opsGroup.GET("/hide", ui.AmWrap(HideTopic))
|
||||||
opsGroup.GET("/hide/:msg", ui.AmWrap(HideMessage))
|
opsGroup.GET("/hide/:msg", ui.AmWrap(HideMessage))
|
||||||
opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage))
|
opsGroup.GET("/scribble/:msg", ui.AmWrap(ScribbleMessage))
|
||||||
|
opsGroup.GET("/nuke/:msg", ui.AmWrap(NukeMessage))
|
||||||
opsGroup.GET("/manage", ui.AmWrap(TopicManage))
|
opsGroup.GET("/manage", ui.AmWrap(TopicManage))
|
||||||
|
|
||||||
return e
|
return e
|
||||||
|
|||||||
+109
-1
@@ -10,8 +10,14 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,11 +47,13 @@ type MessageBoxDefinition struct {
|
|||||||
WarningIcon string `yaml:"warningIcon"`
|
WarningIcon string `yaml:"warningIcon"`
|
||||||
WarningLines []MBoxWarningLine `yaml:"warningLines"`
|
WarningLines []MBoxWarningLine `yaml:"warningLines"`
|
||||||
Buttons []MBoxButton `yaml:"buttons"`
|
Buttons []MBoxButton `yaml:"buttons"`
|
||||||
|
useConfirm bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageBoxDefs is the top-level structure for defining message boxes.
|
// MessageBoxDefs is the top-level structure for defining message boxes.
|
||||||
type MessageBoxDefs struct {
|
type MessageBoxDefs struct {
|
||||||
D []MessageBoxDefinition `yaml:"messagedefs"`
|
D []MessageBoxDefinition `yaml:"messagedefs"`
|
||||||
|
table map[string]*MessageBoxDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed messagedefs.yaml
|
//go:embed messagedefs.yaml
|
||||||
@@ -59,4 +67,104 @@ func init() {
|
|||||||
if err := yaml.Unmarshal(initMessageData, &messageBoxDefs); err != nil {
|
if err := yaml.Unmarshal(initMessageData, &messageBoxDefs); err != nil {
|
||||||
panic(err) // can't happen
|
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 -->
|
<!-- Dialog Header -->
|
||||||
<div class="bg-{{ tone }}-600 px-6 py-4">
|
<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">
|
<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 }}
|
{{ amsterdam_pageTitle }}
|
||||||
{{ if destructive }}<span class="text-3xl">⚠️</span>{{ end }}
|
{{ if destructive }}<span class="text-3xl">⚠️</span>{{ end }}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -23,9 +23,7 @@
|
|||||||
<div class="px-8 py-8">
|
<div class="px-8 py-8">
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<p class="text-gray-800 text-lg leading-relaxed">
|
<p class="text-gray-800 text-lg leading-relaxed">
|
||||||
{{ message }}
|
{{ message | raw }}
|
||||||
{* 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>! *}
|
|
||||||
</p>
|
</p>
|
||||||
{{ if destructive }}
|
{{ if destructive }}
|
||||||
<p class="text-gray-800 text-lg font-bold mt-4">Are you sure you want to do this?</p>
|
<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 -->
|
<!-- Action Buttons -->
|
||||||
<div class="flex gap-4 justify-center">
|
<div class="flex gap-4 justify-center">
|
||||||
{{ range i, bt := buttons }}
|
{{ 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">
|
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>
|
<span class="text-xl">{{ bt.Icon }}</span>
|
||||||
{{ bt.Text }}
|
{{ 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>
|
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 }}
|
{{ end }}
|
||||||
{{ if canNuke }}
|
{{ 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>
|
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 }}
|
{{ end }}
|
||||||
{{ if false }}{* TODO *}
|
{{ if false }}{* TODO *}
|
||||||
|
|||||||
Reference in New Issue
Block a user