From 79925858ae856d46ef58e5cf6e6a1490b59c34ac Mon Sep 17 00:00:00 2001 From: Kevin Lyda <kevin@lyda.ie> Date: Thu, 15 May 2025 18:55:39 +0100 Subject: [PATCH] Add DELETE command; this.Folder fix The `this.Folder` var is now a `storage.Folder`. Implemented the `DELETE` command. --- NOTES.md | 7 +++-- folders/folders.go | 16 +++++------ folders/messages.go | 25 ++++++++++++++-- repl/command.go | 18 ++---------- repl/folders.go | 6 ++-- repl/messages.go | 56 +++++++++++++++++++++++++++++------- repl/set.go | 6 ++-- storage/folders.sql.go | 44 ++++++++++++++++++++++------ storage/messages.sql.go | 9 ++++++ storage/queries/folders.sql | 4 +-- storage/queries/messages.sql | 3 ++ this/this.go | 13 +++++---- 12 files changed, 147 insertions(+), 60 deletions(-) diff --git a/NOTES.md b/NOTES.md index 46182bb..94e264d 100644 --- a/NOTES.md +++ b/NOTES.md @@ -29,15 +29,15 @@ repl.commands? * Run [godoc](http://localhost:6060/) and then review where the help text is lacking. * ~~Move to a storage layer.~~ - * this.Folder should be a storage.Folder. * Implement each command. * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~ * Messages: ~~ADD~~, ~~CURRENT~~, ~~DIRECTORY~~, ~~BACK~~, CHANGE, - ~~FIRST~~, ~~NEXT~~, ~~READ~~, DELETE + ~~FIRST~~, ~~NEXT~~, ~~READ~~, ~~DELETE~~ * Messages edit: CHANGE, REPLY, FORWARD * Moving messages: COPY, MOVE * Compound commands: SET and SHOW - make HELP work for them. * Mail: MAIL, FORWARD, + * Run this.Skew.Safe() before... each command? each write? * Handle broadcast messages - have bulletin watch a directory and display files from it. Then have them delete the file if it's older than 5 minutes (allow for failure) @@ -71,7 +71,8 @@ Done: * ~~expire~~ * ~~Add a pager~~ * ~~SHOW VERSION~~ - * Check db version; notify user if it changes; refuse to write to db if it has. + * ~~Check db version; notify user if it changes; refuse to write to db if it has.~~ + * ~~this.Folder should be a storage.Folder.~~ ## Module links diff --git a/folders/folders.go b/folders/folders.go index d34bfef..7f2245c 100644 --- a/folders/folders.go +++ b/folders/folders.go @@ -10,19 +10,19 @@ import ( ) // ValidFolder validates the folder name for this user. -func ValidFolder(folder string) (string, error) { +func ValidFolder(folder string) (storage.Folder, error) { if strings.Contains(folder, "%") { - return "", errors.New("Folder name cannot contain a %") + return storage.Folder{}, errors.New("Folder name cannot contain a %") } correct := FindFolder(folder) - if correct == "" { - return "", errors.New("Unable to select the folder") + if correct.Name == "" { + return storage.Folder{}, errors.New("Unable to select the folder") } - if !IsFolderAccess(correct, this.User.Login) { + if !IsFolderAccess(correct.Name, this.User.Login) { // TODO: Should be: // WRITE(6,'('' You are not allowed to access folder.'')') // WRITE(6,'('' See '',A,'' if you wish to access folder.'')') - return "", errors.New("Unable to select the folder") + return storage.Folder{}, errors.New("Unable to select the folder") } return correct, nil } @@ -77,10 +77,10 @@ func ListFolder() ([]storage.ListFolderRow, error) { } // FindFolder finds a folder based on the prefix. -func FindFolder(name string) string { +func FindFolder(name string) storage.Folder { ctx := storage.Context() folder, _ := this.Q.FindFolderExact(ctx, name) - if folder != "" { + if folder.Name != "" { return folder } folder, _ = this.Q.FindFolderPrefix(ctx, name) diff --git a/folders/messages.go b/folders/messages.go index 6eeb8eb..d05b8e4 100644 --- a/folders/messages.go +++ b/folders/messages.go @@ -67,7 +67,7 @@ func MarkSeen(msgids []int64) error { for _, msgid := range msgids { this.Q.SetMessageSeen(ctx, storage.SetMessageSeenParams{ Login: this.User.Login, - Folder: this.Folder, + Folder: this.Folder.Name, Msgid: msgid, }) } @@ -81,7 +81,7 @@ func MarkUnseen(msgids []int64) error { for _, msgid := range msgids { this.Q.UnsetMessageSeen(ctx, storage.UnsetMessageSeenParams{ Login: this.User.Login, - Folder: this.Folder, + Folder: this.Folder.Name, Msgid: msgid, }) } @@ -137,3 +137,24 @@ func ListMessages(folder string) ([]storage.Message, error) { rows, err := this.Q.ListMessages(ctx, folder) return rows, err } + +// DeleteMessages deletes a list of messages. +func DeleteMessages(msgids []int64) error { + ctx := storage.Context() + + for _, msgid := range msgids { + this.Q.DeleteMessage(ctx, storage.DeleteMessageParams{ + Folder: this.Folder.Name, + ID: msgid, + }) + } + return nil +} + +// DeleteAllMessages deletes all messages in a folder. +func DeleteAllMessages() error { + ctx := storage.Context() + + this.Q.DeleteAllMessages(ctx, this.Folder.Name) + return nil +} diff --git a/repl/command.go b/repl/command.go index 83167df..8b63bd6 100644 --- a/repl/command.go +++ b/repl/command.go @@ -396,8 +396,7 @@ of the message again, you can enter the CURRENT command. message is deleted. Only the original owner or a privileged user can delete a message. Note that the message is not deleted immediately, but its expiration is set 15 minutes in the future. This is to allow a user -to recover the message using the UNDELETE command. If you want the -message deleted immediately, use the /IMMEDIATE qualifier. +to recover the message using the UNDELETE command. Format: DELETE [message_number][-message_number1] @@ -410,26 +409,13 @@ specified if the folder is remote. The key words CURRENT and LAST can also be specified in the range in, place of an actual number, i.e. CURRENT-LAST, 1-CURRENT, etc.`, MaxArgs: 1, + Action: ActionDelete, Flags: dclish.Flags{ "/ALL": { Description: ` Specifies to delete all the messages in the folder. Note: This will not work for remote folders. Only one message can be deleted from a remote folder at a time.`, }, - "/IMMEDIATE": { - Description: ` Specifies that the message is to be deleted immediately.`, - }, - "/SUBJECT": { - Description: `/SUBJECT=subject - - Specifies the subject of the bulletin to be deleted at a remote DECNET - node. The DECNET node must be specified with the /NODE qualifier. The - specified subject need not be the exact subject of the message. It can - be a substring of the subject. This is in case you have forgotten the - exact subject that was specified. Case is not critical either. You - will be notified if the deletion was successful.`, - OptArg: true, - }, }, }, "DIRECTORY": { diff --git a/repl/folders.go b/repl/folders.go index da4b0a9..8c4b29d 100644 --- a/repl/folders.go +++ b/repl/folders.go @@ -101,12 +101,12 @@ func ActionSelect(cmd *dclish.Command) error { return errors.New("Folder name cannot contain a %") } folder := folders.FindFolder(cmd.Args[0]) - if folder == "" { + if folder.Name == "" { return errors.New("Unable to select the folder") } - if folders.IsFolderAccess(folder, this.User.Login) { + if folders.IsFolderAccess(folder.Name, this.User.Login) { this.Folder = folder - fmt.Printf("Folder has been set to '%s'.\n", folder) + fmt.Printf("Folder has been set to '%s'.\n", folder.Name) return nil } // TODO: Should be: diff --git a/repl/messages.go b/repl/messages.go index 9e5f223..3368838 100644 --- a/repl/messages.go +++ b/repl/messages.go @@ -31,7 +31,7 @@ func ActionDirectory(cmd *dclish.Command) error { } this.Folder = folder } - msgs, err := folders.ListMessages(this.Folder) + msgs, err := folders.ListMessages(this.Folder.Name) if err != nil { return err } @@ -125,7 +125,7 @@ func ActionAdd(cmd *dclish.Command) error { fmt.Printf("TODO: optSystem is not yet implemented - you set it to %d\n", optSystem) if len(optFolder) == 0 { - optFolder = []string{this.Folder} + optFolder = []string{this.Folder.Name} } // TODO: check if folders exist. if optSubject == "" { @@ -150,7 +150,7 @@ func ActionAdd(cmd *dclish.Command) error { // ActionCurrent handles the `CURRENT` command. func ActionCurrent(_ *dclish.Command) error { // TODO: handle flags. - msg, err := folders.ReadMessage(this.User.Login, this.Folder, this.MsgID) + msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, this.MsgID) if err != nil { return err } @@ -165,12 +165,12 @@ func ActionCurrent(_ *dclish.Command) error { // ActionBack handles the `BACK` command. func ActionBack(_ *dclish.Command) error { // TODO: handle flags. - msgid := folders.PrevMsgid(this.User.Login, this.Folder, this.MsgID) + msgid := folders.PrevMsgid(this.User.Login, this.Folder.Name, this.MsgID) if msgid == 0 { fmt.Println("No previous messages") return nil } - msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) + msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } @@ -189,7 +189,7 @@ func ActionChange(cmd *dclish.Command) error { // ActionFirst handles the `FIRST` command. func ActionFirst(_ *dclish.Command) error { - msgid := folders.FirstMessage(this.Folder) + msgid := folders.FirstMessage(this.Folder.Name) if msgid == 0 { fmt.Println("No messages in folder") return nil @@ -201,12 +201,12 @@ func ActionFirst(_ *dclish.Command) error { // ActionNext handles the `NEXT` command. func ActionNext(_ *dclish.Command) error { // TODO: handle flags. - msgid := folders.NextMsgid(this.User.Login, this.Folder, this.MsgID) + msgid := folders.NextMsgid(this.User.Login, this.Folder.Name, this.MsgID) if msgid == 0 { fmt.Println("No next messages") return nil } - msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) + msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } @@ -228,14 +228,14 @@ func ActionRead(cmd *dclish.Command) error { return err } } - msg, err := folders.ReadMessage(this.User.Login, this.Folder, msgid) + msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid) if err != nil { return err } // TODO: pager needs to report if the whole message was read // and only increment if not. pager.Pager(msg.String()) - msgid = folders.NextMsgid(this.User.Login, this.Folder, msgid) + msgid = folders.NextMsgid(this.User.Login, this.Folder.Name, msgid) this.MsgID = max(this.MsgID, msgid) return nil } @@ -285,3 +285,39 @@ func ActionUnseen(cmd *dclish.Command) error { } return nil } + +// ActionDelete handles the `DELETE` command. +func ActionDelete(cmd *dclish.Command) error { + // TODO: Follow permissions. + var err error + + all := false + if cmd.Flags["/ALL"].Value == "true" { + all = true + } + if all { + if len(cmd.Args) == 1 { + fmt.Println("ERROR: Can't specify both message numbers and /ALL flag.") + return nil + } + err := folders.DeleteAllMessages() + if err != nil { + fmt.Printf("ERROR: %s.\n", err) + } + return nil + } + + msgids := []int64{this.MsgID} + if len(cmd.Args) == 1 { + msgids, err = ParseNumberList(cmd.Args[0]) + if err != nil { + fmt.Printf("ERROR: %s.\n", err) + return nil + } + } + err = folders.DeleteMessages(msgids) + if err != nil { + fmt.Printf("ERROR: %s.\n", err) + } + return nil +} diff --git a/repl/set.go b/repl/set.go index bd399cb..b72e0b3 100644 --- a/repl/set.go +++ b/repl/set.go @@ -63,12 +63,12 @@ func ActionSetFolder(cmd *dclish.Command) error { return errors.New("Folder name cannot contain a %") } folder := folders.FindFolder(cmd.Args[0]) - if folder == "" { + if folder.Name == "" { return errors.New("Unable to select the folder") } - if folders.IsFolderAccess(folder, this.User.Login) { + if folders.IsFolderAccess(folder.Name, this.User.Login) { this.Folder = folder - fmt.Printf("Folder has been set to '%s'.\n", folder) + fmt.Printf("Folder has been set to '%s'.\n", folder.Name) return nil } // TODO: Should be: diff --git a/storage/folders.sql.go b/storage/folders.sql.go index 7b5bc42..4666356 100644 --- a/storage/folders.sql.go +++ b/storage/folders.sql.go @@ -62,25 +62,53 @@ func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) erro } const findFolderExact = `-- name: FindFolderExact :one -SELECT name FROM folders where name = ? +SELECT name, "always", brief, description, notify, readnew, shownew, system, expire, visibility, create_at, update_at FROM folders where name = ? ` -func (q *Queries) FindFolderExact(ctx context.Context, name string) (string, error) { +func (q *Queries) FindFolderExact(ctx context.Context, name string) (Folder, error) { row := q.db.QueryRowContext(ctx, findFolderExact, name) - err := row.Scan(&name) - return name, err + var i Folder + err := row.Scan( + &i.Name, + &i.Always, + &i.Brief, + &i.Description, + &i.Notify, + &i.Readnew, + &i.Shownew, + &i.System, + &i.Expire, + &i.Visibility, + &i.CreateAt, + &i.UpdateAt, + ) + return i, err } const findFolderPrefix = `-- name: FindFolderPrefix :one -SELECT name FROM folders where name LIKE ? +SELECT name, "always", brief, description, notify, readnew, shownew, system, expire, visibility, create_at, update_at FROM folders where name LIKE ? ORDER BY name LIMIT 1 ` -func (q *Queries) FindFolderPrefix(ctx context.Context, name string) (string, error) { +func (q *Queries) FindFolderPrefix(ctx context.Context, name string) (Folder, error) { row := q.db.QueryRowContext(ctx, findFolderPrefix, name) - err := row.Scan(&name) - return name, err + var i Folder + err := row.Scan( + &i.Name, + &i.Always, + &i.Brief, + &i.Description, + &i.Notify, + &i.Readnew, + &i.Shownew, + &i.System, + &i.Expire, + &i.Visibility, + &i.CreateAt, + &i.UpdateAt, + ) + return i, err } const getFolderExpire = `-- name: GetFolderExpire :one diff --git a/storage/messages.sql.go b/storage/messages.sql.go index ac9ea85..9f51c30 100644 --- a/storage/messages.sql.go +++ b/storage/messages.sql.go @@ -41,6 +41,15 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) er return err } +const deleteAllMessages = `-- name: DeleteAllMessages :exec +DELETE FROM messages WHERE folder = ? +` + +func (q *Queries) DeleteAllMessages(ctx context.Context, folder string) error { + _, err := q.db.ExecContext(ctx, deleteAllMessages, folder) + return err +} + const nextMsgid = `-- name: NextMsgid :one SELECT CAST(COALESCE(MIN(id), 0) AS INT) FROM messages AS m WHERE m.folder = ?1 AND m.id > ?2 diff --git a/storage/queries/folders.sql b/storage/queries/folders.sql index 1618332..da30c3f 100644 --- a/storage/queries/folders.sql +++ b/storage/queries/folders.sql @@ -22,10 +22,10 @@ GROUP By f.name ORDER BY f.name; -- name: FindFolderExact :one -SELECT name FROM folders where name = ?; +SELECT * FROM folders where name = ?; -- name: FindFolderPrefix :one -SELECT name FROM folders where name LIKE ? +SELECT * FROM folders where name LIKE ? ORDER BY name LIMIT 1; diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql index 52eed05..7750b8f 100644 --- a/storage/queries/messages.sql +++ b/storage/queries/messages.sql @@ -31,3 +31,6 @@ SELECT CAST(COALESCE(MAX(id), 0) AS INT) FROM messages AS m -- name: PrevMsgidIgnoringSeen :one SELECT CAST(MAX(id) AS INT) FROM messages AS m WHERE m.folder = ?1 AND m.id < ?2; + +-- name: DeleteAllMessages :exec +DELETE FROM messages WHERE folder = ?; diff --git a/this/this.go b/this/this.go index c644852..22a6426 100644 --- a/this/this.go +++ b/this/this.go @@ -29,7 +29,7 @@ var Store *sqlx.DB var Q *storage.Queries // Folder is the current folder. -var Folder string +var Folder storage.Folder // MsgID is the current message id. var MsgID int64 @@ -73,19 +73,22 @@ func StartThis(login, name string) error { if User.Disabled == 1 { return errors.New("User is disabled") } - Folder = "GENERAL" + Folder, err = Q.GetFolder(ctx, "GENERAL") + if err != nil { + return err + } MsgID, err = Q.NextMsgid(ctx, storage.NextMsgidParams{ - Folder: Folder, + Folder: Folder.Name, ID: 0, Login: User.Login, }) if MsgID == 0 { MsgID, err = Q.NextMsgidIgnoringSeen(ctx, storage.NextMsgidIgnoringSeenParams{ - Folder: Folder, + Folder: Folder.Name, ID: 0, }) } - fmt.Printf("User: %s\nFolder: %s\nMsgID: %d\n", User, Folder, MsgID) + fmt.Printf("User: %s\nFolder: %s\nMsgID: %d\n", User, Folder.Name, MsgID) return nil } -- GitLab