add mailer and update control fields

This commit is contained in:
Sky Johnson 2025-08-27 09:10:40 -05:00
parent 2e3a977530
commit fc30d04ccb
4 changed files with 176 additions and 92 deletions

View File

@ -1,9 +0,0 @@
{
"id": 1,
"world_size": 200,
"open": 1,
"admin_email": "",
"class_1_name": "Mage",
"class_2_name": "Warrior",
"class_3_name": "Paladin"
}

View File

@ -15,13 +15,15 @@ var (
// Control represents the game control settings
type Control struct {
ID int `json:"id"`
WorldSize int `json:"world_size"`
Open int `json:"open"`
AdminEmail string `json:"admin_email"`
Class1Name string `json:"class_1_name"`
Class2Name string `json:"class_2_name"`
Class3Name string `json:"class_3_name"`
EmailMode string `json:"email_mode"` // "file" or "smtp"
EmailFilePath string `json:"email_file_path"` // path for file mode
SMTPHost string `json:"smtp_host"`
SMTPPort string `json:"smtp_port"`
SMTPUsername string `json:"smtp_username"`
SMTPPassword string `json:"smtp_password"`
}
// Init loads control settings from the specified JSON file
@ -43,17 +45,13 @@ func Init(jsonFile string) error {
if ctrl.WorldSize == 0 {
ctrl.WorldSize = defaults.WorldSize
}
if ctrl.Class1Name == "" {
ctrl.Class1Name = defaults.Class1Name
if ctrl.EmailMode == "" {
ctrl.EmailMode = defaults.EmailMode
}
if ctrl.Class2Name == "" {
ctrl.Class2Name = defaults.Class2Name
}
if ctrl.Class3Name == "" {
ctrl.Class3Name = defaults.Class3Name
if ctrl.EmailFilePath == "" {
ctrl.EmailFilePath = defaults.EmailFilePath
}
ctrl.ID = 1 // Ensure singleton ID
global = &ctrl
} else {
// Create default control settings if file doesn't exist
@ -95,13 +93,15 @@ func Save() error {
// New creates a new Control with sensible defaults
func New() *Control {
return &Control{
ID: 1, // Singleton
WorldSize: 200,
Open: 1,
AdminEmail: "",
Class1Name: "Mage",
Class2Name: "Warrior",
Class3Name: "Paladin",
EmailMode: "file",
EmailFilePath: "emails.txt",
SMTPHost: "",
SMTPPort: "587",
SMTPUsername: "",
SMTPPassword: "",
}
}
@ -120,7 +120,6 @@ func Set(control *Control) error {
mu.Lock()
defer mu.Unlock()
control.ID = 1 // Ensure it's always ID 1 (singleton)
if err := control.Validate(); err != nil {
return err
}
@ -171,14 +170,19 @@ func (c *Control) Validate() error {
if c.Open != 0 && c.Open != 1 {
return fmt.Errorf("Open must be 0 or 1")
}
if c.Class1Name == "" {
return fmt.Errorf("Class1Name cannot be empty")
if c.EmailMode != "file" && c.EmailMode != "smtp" {
return fmt.Errorf("EmailMode must be 'file' or 'smtp'")
}
if c.Class2Name == "" {
return fmt.Errorf("Class2Name cannot be empty")
if c.EmailMode == "smtp" {
if c.SMTPHost == "" {
return fmt.Errorf("SMTPHost required when EmailMode is 'smtp'")
}
if c.SMTPUsername == "" {
return fmt.Errorf("SMTPUsername required when EmailMode is 'smtp'")
}
if c.SMTPPassword == "" {
return fmt.Errorf("SMTPPassword required when EmailMode is 'smtp'")
}
if c.Class3Name == "" {
return fmt.Errorf("Class3Name cannot be empty")
}
return nil
}
@ -199,62 +203,19 @@ func SetOpen(open bool) error {
})
}
// GetClassNames returns all class names as a slice
func (c *Control) GetClassNames() []string {
classes := make([]string, 0, 3)
if c.Class1Name != "" {
classes = append(classes, c.Class1Name)
}
if c.Class2Name != "" {
classes = append(classes, c.Class2Name)
}
if c.Class3Name != "" {
classes = append(classes, c.Class3Name)
}
return classes
}
// GetClassName returns the name of a specific class (1-3)
func (c *Control) GetClassName(classNum int) string {
switch classNum {
case 1:
return c.Class1Name
case 2:
return c.Class2Name
case 3:
return c.Class3Name
default:
return ""
}
}
// IsValidClassName returns true if the given name matches one of the configured classes
func (c *Control) IsValidClassName(name string) bool {
if name == "" {
return false
}
return name == c.Class1Name || name == c.Class2Name || name == c.Class3Name
}
// GetClassNumber returns the class number (1-3) for a given class name, or 0 if not found
func (c *Control) GetClassNumber(name string) int {
if name == c.Class1Name && name != "" {
return 1
}
if name == c.Class2Name && name != "" {
return 2
}
if name == c.Class3Name && name != "" {
return 3
}
return 0
}
// HasAdminEmail returns true if an admin email is configured
func (c *Control) HasAdminEmail() bool {
return c.AdminEmail != ""
}
// IsEmailConfigured returns true if email is properly configured
func (c *Control) IsEmailConfigured() bool {
if c.EmailMode == "file" {
return c.EmailFilePath != ""
}
return c.SMTPHost != "" && c.SMTPUsername != "" && c.SMTPPassword != ""
}
// IsWorldSizeValid returns true if the world size is within reasonable bounds
func (c *Control) IsWorldSizeValid() bool {
return c.WorldSize > 0 && c.WorldSize <= 10000

View File

@ -0,0 +1,132 @@
package email
import (
"fmt"
"net/smtp"
"os"
"strings"
"sync"
"time"
)
var (
global *Emailer
mu sync.RWMutex
)
type Emailer struct {
mode string
filePath string
host string
port string
username string
password string
}
type Email struct {
From string
To []string
Subject string
Body string
}
// Init initializes the global emailer with the provided settings
func Init(mode, filePath, host, port, username, password string) error {
mu.Lock()
defer mu.Unlock()
emailer := &Emailer{
mode: mode,
filePath: filePath,
host: host,
port: port,
username: username,
password: password,
}
if err := emailer.validate(); err != nil {
return err
}
global = emailer
return nil
}
// Get returns the global emailer instance (thread-safe)
func Get() *Emailer {
mu.RLock()
defer mu.RUnlock()
if global == nil {
panic("email not initialized - call Init first")
}
return global
}
// Send sends an email using the configured method (thread-safe)
func Send(email Email) error {
emailer := Get()
mu.RLock()
defer mu.RUnlock()
switch emailer.mode {
case "file":
return emailer.sendToFile(email)
case "smtp":
return emailer.sendViaSMTP(email)
default:
return fmt.Errorf("invalid email mode: %s", emailer.mode)
}
}
func (e *Emailer) validate() error {
if e.mode != "file" && e.mode != "smtp" {
return fmt.Errorf("mode must be 'file' or 'smtp'")
}
if e.mode == "smtp" {
if e.host == "" || e.username == "" || e.password == "" {
return fmt.Errorf("smtp mode requires host, username, and password")
}
}
if e.mode == "file" && e.filePath == "" {
return fmt.Errorf("file mode requires filePath")
}
return nil
}
func (e *Emailer) sendToFile(email Email) error {
content := fmt.Sprintf(`--- EMAIL ---
Time: %s
From: %s
To: %s
Subject: %s
%s
--- END EMAIL ---
`, time.Now().Format(time.RFC3339), email.From, strings.Join(email.To, ", "), email.Subject, email.Body)
f, err := os.OpenFile(e.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(content)
return err
}
func (e *Emailer) sendViaSMTP(email Email) error {
auth := smtp.PlainAuth("", e.username, e.password, e.host)
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s",
email.From,
strings.Join(email.To, ", "),
email.Subject,
email.Body,
)
addr := e.host + ":" + e.port
return smtp.SendMail(addr, auth, email.From, email.To, []byte(msg))
}

View File

@ -836,7 +836,7 @@ func (t *Template) getStructField(obj any, fieldName string) any {
}
rv := reflect.ValueOf(obj)
if rv.Kind() == reflect.Ptr {
if rv.Kind() == reflect.Pointer {
if rv.IsNil() {
return nil
}
@ -976,7 +976,7 @@ func (t *Template) isTruthy(value any) bool {
switch rv.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
return rv.Len() > 0
case reflect.Ptr:
case reflect.Pointer:
return !rv.IsNil()
}
return true