diff --git a/batch/batch.go b/batch/batch.go
index f7b2ece25695f99e5dc1d2bb2b2325a1bcc5da65..a28d0bc1571f2ebfc03a09dd9d086e426286c935 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 ce002ce325089fa513926ab53741388f3cda6eed..fd762401e1e428a60026be8fa0c50b9f9e061caa 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 e86544a85b27c25e2b5a3e63872be6db951fb103..f8f59315049a50e6a232065735153b3c75c7f56a 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 0000000000000000000000000000000000000000..5b4469d107be3512cc9b70b5da8cb9d02bc0bc53
--- /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 0e7baa3375d07463af166b243d9f48e0214c6a90..03b926872355f63da4456b2efc20f945565645f7 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 f7ea79e5fd323821fe00b28b5ee28389c95b9e5f..7dba0206789274073830df090039c7cdeadcbc12 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 17aaaa88072fca7bb0817a612c3aadfdcedb4fe2..4df2ec9d54a7e3985cd696d26834d8494a3eaa67 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 36e04987e19cf3150c36817eca33c9453b2093b2..9c61b1f8a1e6c1abcb7ec36133375f94e10fedf5 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 654e98b77d3a3567b916f128bf1296d5a15b9b41..680e034e15a85f06a1771a48c24c3b7ff8a353bd 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 0d6de1fa4b802c2fb0431e9ff04c88bc5f7bb1be..ad7545c638d28ca44eb4ee61671c761057e1332f 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 578676e7338f0ac468b5ca6f903580a236e19d89..df9ce8eb761ee58697940df387b1883ae72cd629 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 c6f4813efa89d424a4a98f752fd9f8ffe2902658..311ae1c21865e8e90073215a2217246e3d6989d6 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 399ba7bacd10b79f9904e491da8f87881ba0ffd2..afef9bdbd56b5fc5df8713144597c23c4d55600e 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 edd8d08febc5d86eedea28b4c1e905809df99650..07ca4745eadb2bc4e64aeb328e27a33d07a38d9b 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 36944002cad665af8b61ea13dc26718ced8a8163..cd6d7ff7a6da310f5a8f2d4ff5057f172cf7ea80 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 52abe2e3d9ad34049e6ef0cdeb283876c03ee73d..76bb10e6050dbcfbc09cebfca955ad27d7209006 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 0dffa9bf5a5d6affe76cc8b7d3b3d3e40ea86341..8ca36086803965862a6bfd230db6aefb511675e7 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 333570373ec004d9d8b249257bc7cbb98172b8e0..456802f4b5fea7b9d2980bc785761f5de4ad07f0 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 d90291e17fcea48228996aae52af3896515ce3ac..cc795cded10b98335f16803261a9ce593eeefd69 100644
--- a/this/this.go
+++ b/this/this.go
@@ -117,5 +117,7 @@ func StartThis(login string) error {
})
}
+ ShowAlerts(true)
+
return nil
}