Loading repl/mail.go +7 −8 Original line number Diff line number Diff line Loading @@ -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 == "" { Loading repl/mail_actions.go +67 −14 Original line number Diff line number Diff line package repl import ( "context" "errors" "fmt" "sort" "strconv" "strings" "time" Loading @@ -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 := "" Loading @@ -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, Loading @@ -63,6 +115,7 @@ func ActionMailSend(cmd *dclish.Command) error { return err } fmt.Printf("Mail sent to %s.\n", recipient) } return nil } Loading repl/mail_commands.go +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, Loading @@ -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": { Loading Loading @@ -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, Loading @@ -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() } storage/queries/users.sql +8 −0 Original line number Diff line number Diff line Loading @@ -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; storage/users.sql.go +60 −0 Original line number Diff line number Diff line Loading @@ -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 ` Loading Loading @@ -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 = ? ` Loading Loading
repl/mail.go +7 −8 Original line number Diff line number Diff line Loading @@ -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 == "" { Loading
repl/mail_actions.go +67 −14 Original line number Diff line number Diff line package repl import ( "context" "errors" "fmt" "sort" "strconv" "strings" "time" Loading @@ -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 := "" Loading @@ -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, Loading @@ -63,6 +115,7 @@ func ActionMailSend(cmd *dclish.Command) error { return err } fmt.Printf("Mail sent to %s.\n", recipient) } return nil } Loading
repl/mail_commands.go +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, Loading @@ -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": { Loading Loading @@ -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, Loading @@ -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() }
storage/queries/users.sql +8 −0 Original line number Diff line number Diff line Loading @@ -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;
storage/users.sql.go +60 −0 Original line number Diff line number Diff line Loading @@ -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 ` Loading Loading @@ -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 = ? ` Loading