From 59351d54b821f0adff1acc22221e10f5f7d46e83 Mon Sep 17 00:00:00 2001 From: Kevin Lyda <kevin@lyda.ie> Date: Wed, 21 May 2025 09:09:58 +0100 Subject: [PATCH] Initial broadcast support Also implemented two more SET commands --- repl/command.go | 2 + repl/repl.go | 1 + repl/set.go | 20 +++-- storage/broadcast.sql.go | 93 ++++++++++++++++++++++++ storage/display.go | 13 ++++ storage/migrations/1_create_table.up.sql | 27 ++++--- storage/models.go | 26 ++++--- storage/queries/broadcast.sql | 17 +++++ storage/queries/system.sql | 5 ++ storage/standard.sql.go | 3 +- storage/system.sql.go | 28 +++++++ storage/users.sql.go | 6 +- this/broadcast.go | 26 +++++++ this/this.go | 2 + 14 files changed, 243 insertions(+), 26 deletions(-) create mode 100644 storage/broadcast.sql.go create mode 100644 storage/queries/broadcast.sql create mode 100644 storage/queries/system.sql create mode 100644 storage/system.sql.go create mode 100644 this/broadcast.go diff --git a/repl/command.go b/repl/command.go index 206c523..e6c4488 100644 --- a/repl/command.go +++ b/repl/command.go @@ -1254,6 +1254,7 @@ no default expiration date will be present. The latter should never be specified for a folder or else the messages will disappear.`, MinArgs: 1, MaxArgs: 1, + Action: ActionSetDefaultExpire, }, "EXPIRE_LIMIT": { Description: `Specifies expiration limit that is allowed for messages. Non-privileged @@ -1268,6 +1269,7 @@ The command SHOW FOLDER/FULL will show the expiration limit, if one exists. (NOTE: SHOW FOLDER/FULL is a privileged command.)`, MinArgs: 1, MaxArgs: 1, + Action: ActionSetExpireLimit, }, "FOLDER": { Description: `Select a folder of messages. Identical to the SELECT command. See help diff --git a/repl/repl.go b/repl/repl.go index 6f3e236..f7ea79e 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -57,6 +57,7 @@ func Loop() error { // TODO: END for { + this.ShowBroadcast() line, err := rl.Readline() if err != nil { if err.Error() == "Interrupt" { diff --git a/repl/set.go b/repl/set.go index fc7bc80..ae87712 100644 --- a/repl/set.go +++ b/repl/set.go @@ -4,6 +4,7 @@ package repl import ( "errors" "fmt" + "strconv" "strings" "git.lyda.ie/kevin/bulletin/dclish" @@ -67,15 +68,24 @@ func ActionSetNobrief(_ *dclish.Command) error { } // ActionSetDefaultExpire handles the `SET DEFAULT_EXPIRE` command. -func ActionSetDefaultExpire(_ *dclish.Command) error { - fmt.Println("TODO: implement ActionSetDefaultExpire.") - return nil +func ActionSetDefaultExpire(cmd *dclish.Command) error { + value, err := strconv.ParseInt(cmd.Args[0], 10, 64) + if err != nil { + return err + } + ctx := storage.Context() + return this.Q.UpdateDefaultExpire(ctx, value) } // ActionSetExpireLimit handles the `SET EXPIRE_LIMIT` command. -func ActionSetExpireLimit(_ *dclish.Command) error { +func ActionSetExpireLimit(cmd *dclish.Command) error { fmt.Println("TODO: implement ActionSetExpireLimit.") - return nil + value, err := strconv.ParseInt(cmd.Args[0], 10, 64) + if err != nil { + return err + } + ctx := storage.Context() + return this.Q.UpdateExpireLimit(ctx, value) } // ActionSetFolder handles the `SET FOLDER` command. This selects a folder. diff --git a/storage/broadcast.sql.go b/storage/broadcast.sql.go new file mode 100644 index 0000000..550d63e --- /dev/null +++ b/storage/broadcast.sql.go @@ -0,0 +1,93 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: broadcast.sql + +package storage + +import ( + "context" + "time" +) + +const clearBroadcasts = `-- name: ClearBroadcasts :exec +DELETE FROM broadcast +` + +func (q *Queries) ClearBroadcasts(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, clearBroadcasts) + return err +} + +const createBroadcast = `-- name: CreateBroadcast :exec +INSERT INTO broadcast + (author, bell, message) + VALUES + (?, ?, ?) +` + +type CreateBroadcastParams struct { + Author string + Bell int64 + Message string +} + +func (q *Queries) CreateBroadcast(ctx context.Context, arg CreateBroadcastParams) error { + _, err := q.db.ExecContext(ctx, createBroadcast, arg.Author, arg.Bell, arg.Message) + return err +} + +const getBroadcasts = `-- name: GetBroadcasts :many +SELECT author, bell, message, create_at FROM broadcast WHERE create_at > ? ORDER BY create_at +` + +func (q *Queries) GetBroadcasts(ctx context.Context, createAt time.Time) ([]Broadcast, error) { + rows, err := q.db.QueryContext(ctx, getBroadcasts, createAt) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Broadcast + for rows.Next() { + var i Broadcast + if err := rows.Scan( + &i.Author, + &i.Bell, + &i.Message, + &i.CreateAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const reapBroadcasts = `-- name: ReapBroadcasts :exec +DELETE FROM broadcast WHERE julianday(create_at) > 3 +` + +func (q *Queries) ReapBroadcasts(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, reapBroadcasts) + return err +} + +const updateLastBroadcast = `-- name: UpdateLastBroadcast :exec +UPDATE users SET last_broadcast = ? WHERE login = ? +` + +type UpdateLastBroadcastParams struct { + LastBroadcast time.Time + Login string +} + +func (q *Queries) UpdateLastBroadcast(ctx context.Context, arg UpdateLastBroadcastParams) error { + _, err := q.db.ExecContext(ctx, updateLastBroadcast, arg.LastBroadcast, arg.Login) + return err +} diff --git a/storage/display.go b/storage/display.go index 05f904d..b995426 100644 --- a/storage/display.go +++ b/storage/display.go @@ -61,3 +61,16 @@ func (f Folder) String() string { f.Expire, f.Visibility) } + +// String displays a folder (mainly used for debugging). +func (b Broadcast) String() string { + bell := "" + if b.Bell == 1 { + bell = "\a\a" + } + return fmt.Sprintf("%sFrom: %s %s\n%s\n", + bell, + b.Author, + b.CreateAt.Format("06-01-02 15:04:05"), + b.Message) +} diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql index acec6f8..2f066f8 100644 --- a/storage/migrations/1_create_table.up.sql +++ b/storage/migrations/1_create_table.up.sql @@ -1,13 +1,14 @@ CREATE TABLE users ( - login VARCHAR(12) PRIMARY KEY NOT NULL, - name VARCHAR(53) NOT NULL, - admin INT DEFAULT 0 NOT NULL, - moderator INT DEFAULT 0 NOT NULL, - alert INT DEFAULT 0 NOT NULL, --- 0=no, 1=brief, 2=readnew - disabled INT DEFAULT 0 NOT NULL, - last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + login VARCHAR(12) PRIMARY KEY NOT NULL, + name VARCHAR(53) NOT NULL, + admin INT DEFAULT 0 NOT NULL, + moderator INT DEFAULT 0 NOT NULL, + alert INT DEFAULT 0 NOT NULL, --- 0=no, 1=brief, 2=readnew + disabled INT DEFAULT 0 NOT NULL, + last_broadcast TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ) WITHOUT ROWID; CREATE TRIGGER users_after_update_update_at @@ -182,3 +183,11 @@ CREATE TABLE system ( default_expire INT DEFAULT -1 NOT NULL, expire_limit INT DEFAULT -1 NOT NULL ); + +--- Broadcast messages. +CREATE TABLE broadcast ( + author VARCHAR(12) PRIMARY KEY NOT NULL, + bell INT DEFAULT 0 NOT NULL, + message TEXT NOT NULL, + create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL +); diff --git a/storage/models.go b/storage/models.go index d0089ad..1292885 100644 --- a/storage/models.go +++ b/storage/models.go @@ -8,6 +8,13 @@ import ( "time" ) +type Broadcast struct { + Author string + Bell int64 + Message string + CreateAt time.Time +} + type Folder struct { Name string Always int64 @@ -75,13 +82,14 @@ type System struct { } type User struct { - Login string - Name string - Admin int64 - Moderator int64 - Alert int64 - Disabled int64 - LastLogin time.Time - CreateAt time.Time - UpdateAt time.Time + Login string + Name string + Admin int64 + Moderator int64 + Alert int64 + Disabled int64 + LastBroadcast time.Time + LastLogin time.Time + CreateAt time.Time + UpdateAt time.Time } diff --git a/storage/queries/broadcast.sql b/storage/queries/broadcast.sql new file mode 100644 index 0000000..4b5dceb --- /dev/null +++ b/storage/queries/broadcast.sql @@ -0,0 +1,17 @@ +-- name: CreateBroadcast :exec +INSERT INTO broadcast + (author, bell, message) + VALUES + (?, ?, ?); + +-- name: GetBroadcasts :many +SELECT * FROM broadcast WHERE create_at > ? ORDER BY create_at; + +-- name: UpdateLastBroadcast :exec +UPDATE users SET last_broadcast = ? WHERE login = ?; + +-- name: ClearBroadcasts :exec +DELETE FROM broadcast; + +-- name: ReapBroadcasts :exec +DELETE FROM broadcast WHERE julianday(create_at) > 3; diff --git a/storage/queries/system.sql b/storage/queries/system.sql new file mode 100644 index 0000000..1d901a6 --- /dev/null +++ b/storage/queries/system.sql @@ -0,0 +1,5 @@ +-- name: UpdateDefaultExpire :exec +UPDATE system SET default_expire = ? WHERE rowid = 1; + +-- name: UpdateExpireLimit :exec +UPDATE system SET expire_limit = ? WHERE rowid = 1; diff --git a/storage/standard.sql.go b/storage/standard.sql.go index f6c4615..01d0e14 100644 --- a/storage/standard.sql.go +++ b/storage/standard.sql.go @@ -660,7 +660,7 @@ func (q *Queries) ListSeen(ctx context.Context) ([]Seen, error) { } const listUsers = `-- name: ListUsers :many -SELECT login, name, admin, moderator, alert, disabled, last_login, create_at, update_at FROM users +SELECT login, name, admin, moderator, alert, disabled, last_broadcast, last_login, create_at, update_at FROM users ` func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { @@ -679,6 +679,7 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { &i.Moderator, &i.Alert, &i.Disabled, + &i.LastBroadcast, &i.LastLogin, &i.CreateAt, &i.UpdateAt, diff --git a/storage/system.sql.go b/storage/system.sql.go new file mode 100644 index 0000000..920642d --- /dev/null +++ b/storage/system.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: system.sql + +package storage + +import ( + "context" +) + +const updateDefaultExpire = `-- name: UpdateDefaultExpire :exec +UPDATE system SET default_expire = ? WHERE rowid = 1 +` + +func (q *Queries) UpdateDefaultExpire(ctx context.Context, defaultExpire int64) error { + _, err := q.db.ExecContext(ctx, updateDefaultExpire, defaultExpire) + return err +} + +const updateExpireLimit = `-- name: UpdateExpireLimit :exec +UPDATE system SET expire_limit = ? WHERE rowid = 1 +` + +func (q *Queries) UpdateExpireLimit(ctx context.Context, expireLimit int64) error { + _, err := q.db.ExecContext(ctx, updateExpireLimit, expireLimit) + return err +} diff --git a/storage/users.sql.go b/storage/users.sql.go index 75f923c..613b4c1 100644 --- a/storage/users.sql.go +++ b/storage/users.sql.go @@ -12,7 +12,7 @@ import ( const addUser = `-- name: AddUser :one INSERT INTO users (login, name, admin, disabled, last_login) VALUES (?, ?, ?, ?, ?) -RETURNING login, name, admin, moderator, alert, disabled, last_login, create_at, update_at +RETURNING login, name, admin, moderator, alert, disabled, last_broadcast, last_login, create_at, update_at ` type AddUserParams struct { @@ -39,6 +39,7 @@ func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error) &i.Moderator, &i.Alert, &i.Disabled, + &i.LastBroadcast, &i.LastLogin, &i.CreateAt, &i.UpdateAt, @@ -127,7 +128,7 @@ func (q *Queries) GetLastLoginByLogin(ctx context.Context, login string) (GetLas } const getUser = `-- name: GetUser :one -SELECT login, name, admin, moderator, alert, disabled, last_login, create_at, update_at FROM users WHERE login = ? +SELECT login, name, admin, moderator, alert, disabled, last_broadcast, last_login, create_at, update_at FROM users WHERE login = ? ` func (q *Queries) GetUser(ctx context.Context, login string) (User, error) { @@ -140,6 +141,7 @@ func (q *Queries) GetUser(ctx context.Context, login string) (User, error) { &i.Moderator, &i.Alert, &i.Disabled, + &i.LastBroadcast, &i.LastLogin, &i.CreateAt, &i.UpdateAt, diff --git a/this/broadcast.go b/this/broadcast.go new file mode 100644 index 0000000..5682ab7 --- /dev/null +++ b/this/broadcast.go @@ -0,0 +1,26 @@ +package this + +import ( + "fmt" + "time" + + "git.lyda.ie/kevin/bulletin/storage" +) + +// ShowBroadcast print broadcast messages. +func ShowBroadcast() { + ctx := storage.Context() + User.LastBroadcast = time.Now() + msgs, _ := Q.GetBroadcasts(ctx, User.LastBroadcast) + if len(msgs) > 0 { + fmt.Println("BROADCAST MSG START") + for i := range msgs { + fmt.Printf("\n%s\n", msgs[i]) + } + fmt.Println("BROADCAST MSG END") + Q.UpdateLastBroadcast(ctx, storage.UpdateLastBroadcastParams{ + LastBroadcast: User.LastBroadcast, + Login: User.Login, + }) + } +} diff --git a/this/this.go b/this/this.go index 20f44b2..7cc73fa 100644 --- a/this/this.go +++ b/this/this.go @@ -100,10 +100,12 @@ func StartThis(login string) error { if User.Disabled == 1 { return errors.New("User is disabled") } + Folder, err = Q.GetFolder(ctx, "GENERAL") if err != nil { return err } + ReadFirstCall = true MsgID, err = Q.NextMsgid(ctx, storage.NextMsgidParams{ Folder: Folder.Name, -- GitLab