From cce4b201fc795fe7d67efd064040ad9e596d0b14 Mon Sep 17 00:00:00 2001 From: Kevin Lyda <kevin@lyda.ie> Date: Fri, 23 May 2025 21:40:04 +0100 Subject: [PATCH] All commands have stubs of some sort --- batch/batch.go | 15 +++-- folders/messages.go | 2 +- repl/command.go | 5 ++ repl/mail.go | 38 ++++++++++++ repl/messages.go | 8 +-- repl/repl.go | 22 +++---- repl/show.go | 4 +- storage/broadcast.sql.go | 2 +- storage/display.go | 29 ++++++++- storage/messages.sql.go | 21 ++++--- storage/migrations/1_create_table.up.sql | 6 +- storage/queries/broadcast.sql | 2 +- storage/queries/messages.sql | 13 ++-- storage/queries/seed.sql | 2 +- storage/queries/users.sql | 5 +- storage/seed.sql.go | 2 +- storage/users.sql.go | 15 ++--- this/alert.go | 75 ++++++++++++++++++++---- this/this.go | 2 + 19 files changed, 195 insertions(+), 73 deletions(-) create mode 100644 repl/mail.go diff --git a/batch/batch.go b/batch/batch.go index f7b2ece..a28d0bc 100644 --- a/batch/batch.go +++ b/batch/batch.go @@ -83,11 +83,16 @@ To complete the installation, please enter the login, name and ssh key for the first user.`) system := storage.System{} system.Name, err = ask.GetLine("Enter system name: ") + system.Name = strings.ToUpper(system.Name) ask.CheckErr(err) - system.DefaultExpire, err = ask.GetInt64("Enter default expiry in days: ") - ask.CheckErr(err) - system.ExpireLimit, err = ask.GetInt64("Enter expiration limit in days: ") - ask.CheckErr(err) + system.DefaultExpire, err = ask.GetInt64("Enter default expiry in days (180): ") + if err != nil { + system.DefaultExpire = 180 + } + system.ExpireLimit, err = ask.GetInt64("Enter expiration limit in days (365): ") + if err != nil { + system.ExpireLimit = 365 + } err = q.SetSystem(ctx, storage.SetSystemParams{ Name: system.Name, DefaultExpire: system.DefaultExpire, @@ -133,7 +138,7 @@ the first user.`) Subject: seedMsgs[i].Subject, Message: seedMsgs[i].Message, CreateAt: seedMsgs[i].Date, - Expiration: time.Now(), + Expiration: time.Now().UTC(), })) } diff --git a/folders/messages.go b/folders/messages.go index ce002ce..fd76240 100644 --- a/folders/messages.go +++ b/folders/messages.go @@ -23,7 +23,7 @@ func CreateMessage(author, subject, message, folder string, permanent, shutdown } else { days = min(days, sysdef.ExpireLimit) } - exp := time.Now().AddDate(0, 0, int(days)) + exp := time.Now().AddDate(0, 0, int(days)).UTC() expiration = &exp } // TODO: replace _ with rows and check. diff --git a/repl/command.go b/repl/command.go index e86544a..f8f5931 100644 --- a/repl/command.go +++ b/repl/command.go @@ -550,6 +550,7 @@ triple quotes. I.e. a network address of the form xxx%"address" must be specified as xxx%"""address""".`, MinArgs: 1, MaxArgs: 10, + Action: ActionMail, Flags: dclish.Flags{ "/SUBJECT": { Description: `/SUBJECT=text @@ -801,6 +802,7 @@ If you wish to use another method for sending the mail, define BULL_MAILER to point to a command procedure. This procedure will then be executed in place of MAIL, and the parameters passed to it are the username and subject of the message.`, + Action: ActionRespond, Flags: dclish.Flags{ "/CC": { Description: `/CC=user[s] @@ -1397,6 +1399,7 @@ In order to apply this to a specific folder, first select the folder Format: SET SHOWNEW`, + Action: ActionSetShowNew, Flags: dclish.Flags{ "/ALL": { Description: ` Specifies that the SET SHOWNEW option is the default for all users for @@ -1422,6 +1425,7 @@ In order to apply this to a specific folder, first select the folder Format: SET NOSHOWNEW`, + Action: ActionSetNoShowNew, Flags: dclish.Flags{ "/ALL": { Description: ` Specifies that the SET NOSHOWNEW option is the default for all users @@ -1498,6 +1502,7 @@ the SELECT command, information about that folder is shown. "NEW": { Description: `Shows folders which have new unread messages for which BRIEF or READNEW have been set.`, + Action: ActionShowNew, }, "PRIVILEGES": { Description: `Shows the privileges necessary to use privileged commands. Also shows diff --git a/repl/mail.go b/repl/mail.go new file mode 100644 index 0000000..5b4469d --- /dev/null +++ b/repl/mail.go @@ -0,0 +1,38 @@ +package repl + +import ( + "fmt" + + "git.lyda.ie/kevin/bulletin/dclish" +) + +/* + +Instead of making MAIL and FORWARD the same, why not have "MAIL" enter a mail +mode where you can read mails. + +Alternatively, make "mail" just a folder like any other except set to private and make the owner that user. Maybe have a leading colon that CREATE can't use. + +Or... just get rid of the mail bit. + +Or... tie it into actual smtp mail. + +*/ + +// ActionForward handles the `FORWARD` command. +func ActionForward(_ *dclish.Command) error { + fmt.Println("ERROR: mail system is yet TODO.") + return nil +} + +// ActionMail handles the `MAIL` command. +func ActionMail(_ *dclish.Command) error { + fmt.Println("ERROR: mail system is yet TODO.") + return nil +} + +// ActionRespond handles the `RESPOND` command. +func ActionRespond(_ *dclish.Command) error { + fmt.Println("ERROR: mail system is yet TODO.") + return nil +} diff --git a/repl/messages.go b/repl/messages.go index 0e7baa3..03b9268 100644 --- a/repl/messages.go +++ b/repl/messages.go @@ -92,7 +92,7 @@ func ActionAdd(cmd *dclish.Command) error { if err != nil { optExpiration = nil } - exp := time.Now().AddDate(0, 0, days) + exp := time.Now().AddDate(0, 0, days).UTC() optExpiration = &exp } else { optExpiration = &exp @@ -524,12 +524,6 @@ func ActionReply(cmd *dclish.Command) error { return nil } -// ActionForward handles the `FORWARD` command. -func ActionForward(cmd *dclish.Command) error { - fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description) - return nil -} - // ActionSeen handles the `SEEN` command. func ActionSeen(cmd *dclish.Command) error { // TODO: review help. diff --git a/repl/repl.go b/repl/repl.go index f7ea79e..7dba020 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "strings" "unicode" "git.lyda.ie/kevin/bulletin/this" @@ -31,30 +32,23 @@ func Loop() error { } defer rl.Close() - // TODO: Remove once commands are implemented. - unimplemented := 0 - total := len(commands) - fmt.Print("Missing") + missing := []string{} for c := range commands { if commands[c].Action == nil { - fmt.Printf(" [%s]", c) - unimplemented++ + missing = append(missing, c) } if len(commands[c].Commands) > 0 { - total-- - unimplemented-- for subc := range commands[c].Commands { if commands[c].Commands[subc].Action == nil { - fmt.Printf(" [%s %s]", c, subc) - unimplemented++ + missing = append(missing, c+" "+subc) } } - total += len(commands[c].Commands) } } - fmt.Printf("\nTODO: %d out of %d commands still to be implemented.\n", - unimplemented, total) - // TODO: END + if len(missing) != 0 { + fmt.Printf("ERROR: some commands lack actions: %s.\n", + strings.Join(missing, ", ")) + } for { this.ShowBroadcast() diff --git a/repl/show.go b/repl/show.go index 17aaaa8..4df2ec9 100644 --- a/repl/show.go +++ b/repl/show.go @@ -15,8 +15,8 @@ import ( ) // ActionShow handles the `SHOW` command. -func ActionShow(cmd *dclish.Command) error { - fmt.Println(cmd.Description) +func ActionShow(_ *dclish.Command) error { + this.ShowAlerts(false) return nil } diff --git a/storage/broadcast.sql.go b/storage/broadcast.sql.go index 36e0498..9c61b1f 100644 --- a/storage/broadcast.sql.go +++ b/storage/broadcast.sql.go @@ -70,7 +70,7 @@ func (q *Queries) GetBroadcasts(ctx context.Context, createAt time.Time) ([]Broa } const reapBroadcasts = `-- name: ReapBroadcasts :exec -DELETE FROM broadcast WHERE julianday(create_at) > 3 +DELETE FROM broadcast WHERE julianday(current_timestamp) - julianday(create_at) > 3 ` func (q *Queries) ReapBroadcasts(ctx context.Context) error { diff --git a/storage/display.go b/storage/display.go index 654e98b..680e034 100644 --- a/storage/display.go +++ b/storage/display.go @@ -8,7 +8,8 @@ import ( // Alert values. const ( - AlertNone int64 = iota + AlertDoNotCare int64 = iota + AlertNone AlertBrief AlertReadNew AlertShowNew @@ -17,9 +18,24 @@ const ( AlertShowNewPerm ) +// EffectiveAlert returns the effective alert from a user and folder alert level. +func EffectiveAlert(user, folder int64) int64 { + // Folder permanent alert is set - it wins. + if folder > 4 { + return folder - 3 + } + // User doesn't care. + if user == AlertDoNotCare { + return folder + } + // User's choice of alert level wins. + return user +} + // AlertString translates an alert number to a string. func AlertString(alert int64) string { var as = []string{ + "Not Set", "None", "Brief", "ReadNew", @@ -34,6 +50,7 @@ func AlertString(alert int64) string { // FolderAlertString translates an alert number to a string. func FolderAlertString(alert int64) string { var as = []string{ + "Not Set", "None", "Brief", "ReadNew", @@ -74,7 +91,15 @@ func (m *Message) OneLine(expire bool) string { t = m.CreateAt } ts := t.Format("2006-01-02 15:04:05") - return fmt.Sprintf("%4d %-43.43s %-12.12s %-10s\n", m.ID, m.Subject, m.Author, ts) + return fmt.Sprintf("%4d %-43.43s %-12.12s %-10s\n", + m.ID, m.Subject, m.Author, ts) +} + +// AlertLine renders a message in a line for a SHOWNEW setting. +func (m *Message) AlertLine() string { + ts := m.CreateAt.Format("2006-01-02 15:04:05") + return fmt.Sprintf("%-25.25s %4d %-23.23s %-12.12s %-10s\n", + m.Folder, m.ID, m.Subject, m.Author, ts) } // String displays a user (mainly used for debugging). diff --git a/storage/messages.sql.go b/storage/messages.sql.go index 0d6de1f..ad7545c 100644 --- a/storage/messages.sql.go +++ b/storage/messages.sql.go @@ -52,28 +52,31 @@ func (q *Queries) DeleteAllMessages(ctx context.Context, folder string) error { } const getAlertMessages = `-- name: GetAlertMessages :many -SELECT f.alert AS folder_alert, fc.alert AS user_alert, m.id, m.folder, m.author, m.subject, m.message, m.permanent, m.system, m.shutdown, m.expiration, m.create_at, m.update_at FROM messages AS m - LEFT JOIN folder_configs AS fc ON m.folder = fc.folder - LEFT JOIN folders AS f ON m.folder = f.name +SELECT CAST(f.alert AS INT) AS folder_alert, + CAST(COALESCE(fc.alert, 0) AS INT) AS user_alert, + m.id, m.folder, m.author, m.subject, m.message, m.permanent, m.system, m.shutdown, m.expiration, m.create_at, m.update_at + FROM messages AS m + LEFT OUTER JOIN folders AS f ON m.folder = f.name + LEFT OUTER JOIN folder_configs AS fc ON f.name = fc.folder AND fc.login = ? WHERE m.create_at >= ? - AND fc.login = ? - AND (f.alert > 3 OR fc.alert > 0 OR (f.alert > 0 AND fc.alert IS NULL)) + AND (f.alert > 4 OR fc.alert > 1 + OR (f.alert > 1 AND COALESCE(fc.alert, 0) != 1)) ORDER BY m.folder, m.id ` type GetAlertMessagesParams struct { - CreateAt time.Time Login string + CreateAt time.Time } type GetAlertMessagesRow struct { - FolderAlert sql.NullInt64 - UserAlert sql.NullInt64 + FolderAlert int64 + UserAlert int64 Message Message } func (q *Queries) GetAlertMessages(ctx context.Context, arg GetAlertMessagesParams) ([]GetAlertMessagesRow, error) { - rows, err := q.db.QueryContext(ctx, getAlertMessages, arg.CreateAt, arg.Login) + rows, err := q.db.QueryContext(ctx, getAlertMessages, arg.Login, arg.CreateAt) if err != nil { return nil, err } diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql index 578676e..df9ce8e 100644 --- a/storage/migrations/1_create_table.up.sql +++ b/storage/migrations/1_create_table.up.sql @@ -36,8 +36,8 @@ END; CREATE TABLE folders ( name VARCHAR(25) PRIMARY KEY NOT NULL, always INT DEFAULT 0 NOT NULL, - --- 0=no, 1(4)=brief(p), 2(5)=readnew(p), 3(6)=shownew(p) - alert INT DEFAULT 0 NOT NULL, + --- 0=don't care, 1=no, 2(5)=brief(p), 3(6)=readnew(p), 4(7)=shownew(p) + alert INT DEFAULT 1 NOT NULL, description VARCHAR(53) DEFAULT 0 NOT NULL, owner VARCHAR(12) NOT NULL, notify INT DEFAULT 0 NOT NULL, @@ -157,7 +157,7 @@ CREATE TABLE folder_configs ( folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, always INT DEFAULT 0 NOT NULL, - --- 0=no, 1=brief, 2=readnew, 3=shownew + --- 0=don't care, 1=no, 2=brief, 3=readnew, 4=shownew alert INT DEFAULT 0 NOT NULL, PRIMARY KEY (login, folder) ) WITHOUT ROWID; diff --git a/storage/queries/broadcast.sql b/storage/queries/broadcast.sql index c6f4813..311ae1c 100644 --- a/storage/queries/broadcast.sql +++ b/storage/queries/broadcast.sql @@ -14,4 +14,4 @@ UPDATE users SET last_activity = ? WHERE login = ?; DELETE FROM broadcast; -- name: ReapBroadcasts :exec -DELETE FROM broadcast WHERE julianday(create_at) > 3; +DELETE FROM broadcast WHERE julianday(current_timestamp) - julianday(create_at) > 3; diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql index 399ba7b..afef9bd 100644 --- a/storage/queries/messages.sql +++ b/storage/queries/messages.sql @@ -110,10 +110,13 @@ UPDATE messages SET SELECT * FROM messages WHERE create_at >= ? AND shutdown = 1; -- name: GetAlertMessages :many -SELECT f.alert AS folder_alert, fc.alert AS user_alert, sqlc.embed(m) FROM messages AS m - LEFT JOIN folder_configs AS fc ON m.folder = fc.folder - LEFT JOIN folders AS f ON m.folder = f.name +SELECT CAST(f.alert AS INT) AS folder_alert, + CAST(COALESCE(fc.alert, 0) AS INT) AS user_alert, + sqlc.embed(m) + FROM messages AS m + LEFT OUTER JOIN folders AS f ON m.folder = f.name + LEFT OUTER JOIN folder_configs AS fc ON f.name = fc.folder AND fc.login = ? WHERE m.create_at >= ? - AND fc.login = ? - AND (f.alert > 3 OR fc.alert > 0 OR (f.alert > 0 AND fc.alert IS NULL)) + AND (f.alert > 4 OR fc.alert > 1 + OR (f.alert > 1 AND COALESCE(fc.alert, 0) != 1)) ORDER BY m.folder, m.id; diff --git a/storage/queries/seed.sql b/storage/queries/seed.sql index edd8d08..07ca474 100644 --- a/storage/queries/seed.sql +++ b/storage/queries/seed.sql @@ -4,7 +4,7 @@ INSERT INTO users (login, name, admin, moderator) -- name: SeedFolderGeneral :exec INSERT INTO folders (name, owner, description, system, alert) - VALUES ('GENERAL', 'SYSTEM', 'Default general bulletin folder.', 1, 1); + VALUES ('GENERAL', 'SYSTEM', 'Default general bulletin folder.', 1, 2); -- name: SeedCreateMessage :exec INSERT INTO messages diff --git a/storage/queries/users.sql b/storage/queries/users.sql index 3694400..cd6d7ff 100644 --- a/storage/queries/users.sql +++ b/storage/queries/users.sql @@ -2,7 +2,10 @@ SELECT * FROM users WHERE login = ?; -- name: AddUser :one -INSERT INTO users (login, name, admin, disabled, last_login) VALUES (?, ?, ?, ?, ?) +INSERT INTO users + (login, name, admin, disabled, last_activity, last_login) + VALUES + (?, ?, ?, ?, '1970-01-01', '1970-01-01') RETURNING *; -- name: IsUserAdmin :one diff --git a/storage/seed.sql.go b/storage/seed.sql.go index 52abe2e..76bb10e 100644 --- a/storage/seed.sql.go +++ b/storage/seed.sql.go @@ -41,7 +41,7 @@ func (q *Queries) SeedCreateMessage(ctx context.Context, arg SeedCreateMessagePa const seedFolderGeneral = `-- name: SeedFolderGeneral :exec INSERT INTO folders (name, owner, description, system, alert) - VALUES ('GENERAL', 'SYSTEM', 'Default general bulletin folder.', 1, 1) + VALUES ('GENERAL', 'SYSTEM', 'Default general bulletin folder.', 1, 2) ` func (q *Queries) SeedFolderGeneral(ctx context.Context) error { diff --git a/storage/users.sql.go b/storage/users.sql.go index 0dffa9b..8ca3608 100644 --- a/storage/users.sql.go +++ b/storage/users.sql.go @@ -11,16 +11,18 @@ import ( ) const addUser = `-- name: AddUser :one -INSERT INTO users (login, name, admin, disabled, last_login) VALUES (?, ?, ?, ?, ?) +INSERT INTO users + (login, name, admin, disabled, last_activity, last_login) + VALUES + (?, ?, ?, ?, '1970-01-01', '1970-01-01') RETURNING login, name, admin, moderator, disabled, prompt, signature, last_activity, last_login, create_at, update_at ` type AddUserParams struct { - Login string - Name string - Admin int64 - Disabled int64 - LastLogin time.Time + Login string + Name string + Admin int64 + Disabled int64 } func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error) { @@ -29,7 +31,6 @@ func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error) arg.Name, arg.Admin, arg.Disabled, - arg.LastLogin, ) var i User err := row.Scan( diff --git a/this/alert.go b/this/alert.go index 3335703..456802f 100644 --- a/this/alert.go +++ b/this/alert.go @@ -2,15 +2,18 @@ package this import ( "fmt" + "sort" + "strings" "time" + "git.lyda.ie/kevin/bulletin/pager" "git.lyda.ie/kevin/bulletin/storage" ) -// ShowBroadcast print broadcast messages. +// ShowBroadcast print broadcast and shutdown messages. func ShowBroadcast() { ctx := storage.Context() - User.LastActivity = time.Now() + User.LastActivity = time.Now().UTC() msgs, _ := Q.GetBroadcasts(ctx, User.LastActivity) if len(msgs) > 0 { fmt.Println("BROADCAST MSG START") @@ -19,22 +22,68 @@ func ShowBroadcast() { } fmt.Println("BROADCAST MSG END") } + Q.UpdateLastActivity(ctx, storage.UpdateLastActivityParams{ + LastActivity: User.LastActivity, + Login: User.Login, + }) + Q.ReapBroadcasts(ctx) +} - alerts, _ := Q.GetAlertMessages(ctx, storage.GetAlertMessagesParams{ +// ShowAlerts print alert messages. +func ShowAlerts(doReadNew bool) { + ctx := storage.Context() + alerts, err := Q.GetAlertMessages(ctx, storage.GetAlertMessagesParams{ CreateAt: User.LastActivity, Login: User.Name, }) - if len(alerts) > 0 { - // TODO: fix printing. - fmt.Println("ALERT MSG START") - for i := range alerts { - fmt.Printf("%s", alerts[i].Message.OneLine(false)) + if err != nil { + fmt.Printf("ERROR: failed to get alerts: %s.\n", err) + } + briefs := map[string]int{} + shownew := &strings.Builder{} + readnew := &strings.Builder{} + for i := range alerts { + switch storage.EffectiveAlert(alerts[i].UserAlert, alerts[i].FolderAlert) { + case storage.AlertBrief: + briefs[alerts[i].Message.Folder]++ + case storage.AlertShowNew: + fmt.Fprintf(shownew, "%s", alerts[i].Message.AlertLine()) + case storage.AlertReadNew: + if doReadNew { + Q.SetMessageSeen(ctx, storage.SetMessageSeenParams{ + Login: User.Login, + Folder: Folder.Name, + Msgid: alerts[i].Message.ID, + }) + fmt.Fprintf(readnew, "%s\n", alerts[i].Message.String()) + } } - fmt.Println("ALERT MSG END") } - Q.UpdateLastActivity(ctx, storage.UpdateLastActivityParams{ - LastActivity: User.LastActivity, - Login: User.Login, - }) + activity := false + if len(briefs) > 0 { + activity = true + keys := make([]string, len(briefs)) + i := 0 + for key := range briefs { + keys[i] = key + i++ + } + sort.Strings(keys) + for _, key := range keys { + fmt.Printf("There are %d new messages in %s\n", briefs[key], key) + } + } + if shownew.Len() > 0 { + activity = true + fmt.Println(shownew.String()) + } + if readnew.Len() > 0 { + activity = true + pager.Pager(readnew.String()) + } + if !activity { + fmt.Println("No new activity found.") + } + } diff --git a/this/this.go b/this/this.go index d90291e..cc795cd 100644 --- a/this/this.go +++ b/this/this.go @@ -117,5 +117,7 @@ func StartThis(login string) error { }) } + ShowAlerts(true) + return nil } -- GitLab