Commit 287bda5b authored by Kevin Lyda's avatar Kevin Lyda
Browse files

Clean up mail subsystem

parent 63d657e2
Loading
Loading
Loading
Loading
+7 −8
Original line number Diff line number Diff line
@@ -98,15 +98,14 @@ func ActionMail(cmd *dclish.Command) error {
		subject = cmd.Flags["/SUBJECT"].Value
	}

	// Validate all recipients first.
	recipients := make([]string, len(cmd.Args))
	for i, arg := range cmd.Args {
		login := strings.ToUpper(arg)
		user, err := this.Q.GetUser(ctx, login)
		if err != nil || user.Login != login {
			return fmt.Errorf("user '%s' not found", arg)
		}
		recipients[i] = login
	// Resolve and validate all recipients first (supports @ADMINS and @MODS).
	var recipients []string
	for _, arg := range cmd.Args {
		expanded, err := resolveMailRecipients(ctx, strings.ToUpper(arg))
		if err != nil {
			return err
		}
		recipients = append(recipients, expanded...)
	}

	if subject == "" {
+67 −14
Original line number Diff line number Diff line
package repl

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"
@@ -21,14 +23,63 @@ var currentMailID int64
// ErrExitMail is a sentinel error to exit the mail sub-app REPL.
var ErrExitMail = errors.New("exit mail")

// resolveMailRecipients expands a recipient argument to a list of logins.
// Supports @ADMINS and @MODS group aliases in addition to individual logins.
func resolveMailRecipients(ctx context.Context, arg string) ([]string, error) {
	switch arg {
	case "@ADMINS":
		logins, err := this.Q.GetAdmins(ctx)
		if err != nil {
			return nil, err
		}
		if len(logins) == 0 {
			return nil, errors.New("no admins found")
		}
		return logins, nil
	case "@MODS":
		logins, err := this.Q.GetModerators(ctx)
		if err != nil {
			return nil, err
		}
		if len(logins) == 0 {
			return nil, errors.New("no moderators found")
		}
		return logins, nil
	default:
		user, err := this.Q.GetUser(ctx, arg)
		if err != nil || user.Login != arg {
			return nil, fmt.Errorf("user '%s' not found", arg)
		}
		return []string{arg}, nil
	}
}

// ActionMailHelp handles the HELP command in the mail sub-app.
func ActionMailHelp(cmd *dclish.Command) error {
	if len(cmd.Args) == 0 {
		fmt.Printf("%s\n", mailHelpMap["HELP"])
		return nil
	}
	pager.Pager(findHelp(mailHelpMap, cmd.Args, cmd.Args[0]))
	return nil
}

// completeMailHelpTopics returns the list of mail help topics for tab completion.
func completeMailHelpTopics() []string {
	topics := make([]string, 0, len(mailHelpMap))
	for topic := range mailHelpMap {
		topics = append(topics, topic)
	}
	sort.Strings(topics)
	return topics
}

// ActionMailSend handles the SEND command in the mail sub-app.
func ActionMailSend(cmd *dclish.Command) error {
	ctx := storage.Context()
	recipient := strings.ToUpper(cmd.Args[0])

	user, err := this.Q.GetUser(ctx, recipient)
	if err != nil || user.Login != recipient {
		return fmt.Errorf("user '%s' not found", recipient)
	recipients, err := resolveMailRecipients(ctx, strings.ToUpper(cmd.Args[0]))
	if err != nil {
		return err
	}

	subject := ""
@@ -53,6 +104,7 @@ func ActionMailSend(cmd *dclish.Command) error {
		return err
	}

	for _, recipient := range recipients {
		err = this.Q.CreateMail(ctx, storage.CreateMailParams{
			FromLogin: this.User.Login,
			ToLogin:   recipient,
@@ -63,6 +115,7 @@ func ActionMailSend(cmd *dclish.Command) error {
			return err
		}
		fmt.Printf("Mail sent to %s.\n", recipient)
	}
	return nil
}

+55 −5
Original line number Diff line number Diff line
package repl

import "git.lyda.ie/pp/bulletin/dclish"
import (
	"fmt"
	"sort"
	"strings"

	"git.lyda.ie/pp/bulletin/dclish"
)

var mailHelpMap = map[string]string{}

var mailCommands = dclish.Commands{
	"SEND": {
		Description: `Sends a private mail message to a user.
		Description: `Sends a private mail message to a user or group.

  Format:
    SEND recipient`,
    SEND recipient

  The recipient may be a login name, @ADMINS (all admins), or @MODS
  (all moderators).`,
		MinArgs: 1,
		MaxArgs: 1,
		Action:  ActionMailSend,
@@ -30,11 +41,11 @@ first unread message.
		MaxArgs: 1,
		Action:  ActionMailRead,
	},
	"LIST": {
	"DIRECTORY": {
		Description: `Lists your inbox showing message ID, sender, date, and subject.

  Format:
    LIST`,
    DIRECTORY`,
		Action: ActionMailList,
		Flags: dclish.Flags{
			"/UNREAD": {
@@ -65,6 +76,15 @@ first unread message.
		Description: `Displays the previous mail message.`,
		Action:      ActionMailBack,
	},
	"HELP": {
		Description: `Displays help for mail commands.

  Format:
    HELP [command]`,
		MaxArgs:   1,
		Action:    ActionMailHelp,
		Completer: completeMailHelpTopics,
	},
	"EXIT": {
		Description: `Returns to the BULLETIN prompt.`,
		Action:      ActionMailExit,
@@ -74,3 +94,33 @@ first unread message.
		Action:      ActionMailExit,
	},
}

func init() {
	generateHelp(mailHelpMap, mailCommands)

	// Append a list of topics to the HELP entry.
	topics := make([]string, 0, len(mailHelpMap))
	maxlen := 0
	for topic := range mailHelpMap {
		if len(topic) > maxlen {
			maxlen = len(topic)
		}
		topics = append(topics, topic)
	}
	maxlen += 2
	sort.Strings(topics)

	buf := &strings.Builder{}
	linelen := 2
	fmt.Fprint(buf, "\n\nThe following commands are available for more help\n\n  ")
	for _, topic := range topics {
		linelen += maxlen
		if linelen > 78 {
			fmt.Fprint(buf, "\n  ")
			linelen = maxlen + 2
		}
		fmt.Fprintf(buf, "%-*s", maxlen, topic)
	}
	fmt.Fprint(buf, "\n")
	mailHelpMap["HELP"] += buf.String()
}
+8 −0
Original line number Diff line number Diff line
@@ -78,3 +78,11 @@ SELECT login, last_login FROM users WHERE last_login >= ? ORDER BY login;
-- GetLastLoginByLoginSince gets the login and last_login time for a user since a date.
-- name: GetLastLoginByLoginSince :one
SELECT login, last_login FROM users WHERE login = ? AND last_login >= ?;

-- GetAdmins gets all admin logins.
-- name: GetAdmins :many
SELECT login FROM users WHERE admin = 1 ORDER BY login;

-- GetModerators gets all moderator logins.
-- name: GetModerators :many
SELECT login FROM users WHERE moderator = 1 ORDER BY login;
+60 −0
Original line number Diff line number Diff line
@@ -57,6 +57,36 @@ func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error)
	return i, err
}

const getAdmins = `-- name: GetAdmins :many
SELECT login FROM users WHERE admin = 1 ORDER BY login
`

// GetAdmins gets all admin logins.
//
//	SELECT login FROM users WHERE admin = 1 ORDER BY login
func (q *Queries) GetAdmins(ctx context.Context) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, getAdmins)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var login string
		if err := rows.Scan(&login); err != nil {
			return nil, err
		}
		items = append(items, login)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getLastLogin = `-- name: GetLastLogin :many
SELECT login, last_login FROM users ORDER BY login
`
@@ -202,6 +232,36 @@ func (q *Queries) GetLastLoginSince(ctx context.Context, lastLogin time.Time) ([
	return items, nil
}

const getModerators = `-- name: GetModerators :many
SELECT login FROM users WHERE moderator = 1 ORDER BY login
`

// GetModerators gets all moderator logins.
//
//	SELECT login FROM users WHERE moderator = 1 ORDER BY login
func (q *Queries) GetModerators(ctx context.Context) ([]string, error) {
	rows, err := q.db.QueryContext(ctx, getModerators)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var items []string
	for rows.Next() {
		var login string
		if err := rows.Scan(&login); err != nil {
			return nil, err
		}
		items = append(items, login)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

const getUser = `-- name: GetUser :one
SELECT login, name, admin, moderator, disabled, prompt, signature, last_activity, last_login, create_at, update_at, suspended FROM users WHERE login = ?
`