Files
amsterdam/ui/messagebox.go
T

204 lines
6.0 KiB
Go

/*
* Amsterdam Web Communities System
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* SPDX-License-Identifier: MPL-2.0
*/
// Package ui holds the support for the Amsterdam user interface, wrapping Echo and Jet templates.
package ui
import (
"crypto/sha1"
_ "embed"
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/util"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
// MBoxWarningLine defines a single warning line in a message box.
type MBoxWarningLine struct {
Text string `yaml:"text"`
Bold bool `yaml:"bold"`
}
// MBoxButton defines a single button on a message box.
type MBoxButton struct {
Id string `yaml:"id"`
Link string `yaml:"link"`
Confirm bool `yaml:"confirm"`
Tone string `yaml:"tone"`
Icon string `yaml:"icon"`
Text string `yaml:"text"`
}
// MessageBoxDefinition defines a single message box resource.
type MessageBoxDefinition struct {
Id string `yaml:"id"`
Title string `yaml:"title"`
Tone string `yaml:"tone"`
Destructive bool `yaml:"destructive"`
Message string `yaml:"message"`
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
var initMessageData []byte
// messageBoxDefs is the master repository for message box data.
var messageBoxDefs MessageBoxDefs
// init loads and binds the message box definitions.
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
}
}
}
}
// setupMessageBoxes loads external message box definitions.
func setupMessageBoxes() {
mbfile := config.GlobalConfig.ExPath(config.GlobalConfig.Resources.ExternalMessageDefinitions)
if mbfile != "" {
b, err := os.ReadFile(mbfile)
if err == nil {
mb := new(MessageBoxDefs)
err = yaml.Unmarshal(b, mb)
if err == nil {
for i, def := range mb.D {
messageBoxDefs.table[def.Id] = &(mb.D[i])
mb.D[i].useConfirm = false
for _, b := range mb.D[i].Buttons {
if b.Confirm {
mb.D[i].useConfirm = true
break
}
}
}
} else {
log.Errorf("cannot parse external message definition file %s, ignored (%v)", mbfile, err)
}
} else {
log.Errorf("cannot read external message definition file %s, ignored (%v)", mbfile, err)
}
}
}
// 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) {
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.SetFrameTitle(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", "messagebox.jet"
}
// 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")
}