diff --git a/NOTES.md b/NOTES.md
index 15e3277de66fe844ef30af6fd84b4ed5719f53dc..16aa923e155942dfdd07ac96fdfcde6441b590f0 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -29,7 +29,7 @@ repl.commands?
* Implement each command.
* Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~
- * Messages: ~~ADD~~, CURRENT, DIRECTORY, BACK, CHANGE, FIRST, REMOVE, NEXT, READ
+ * Messages: ~~ADD~~, CURRENT, ~~DIRECTORY~~, BACK, CHANGE, FIRST, REMOVE, NEXT, ~~READ~~
* Messages edit: CHANGE, REPLY, FORWARD
* Moving messages: COPY, MOVE
* Compound commands: SET and SHOW
@@ -60,7 +60,8 @@ repl.commands?
* Commands for a local mail system?
* Commands to connect to Mattermost or mastodon?
* Commands to manage users.
- * `SHOW VERSION` - use [this](https://github.com/earthboundkid/versioninfo)
+ * `SHOW VERSION` - versioninfo doesn't work; what else?
+ * Check db version; notify user if it changes; refuse to write to db if it has.
## Module links
diff --git a/dclish/dclish.go b/dclish/dclish.go
index 0302781f228afc4fcd836574770c2a08d8d540eb..237f1978d6f0328c5241cf7d91aa803ea685b382 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -140,6 +140,10 @@ func (c Commands) ParseAndRun(line string) error {
}
cmd.Args = []string{}
if len(words) == 1 {
+ if len(cmd.Args) < cmd.MinArgs {
+ fmt.Println("ERROR: Not enough args.")
+ return nil
+ }
return cmd.Action(cmd)
}
// TODO: need to clean this up.
diff --git a/editor/tview.go b/editor/tview.go
index 541678df070270cc4140060d9df0b6bdee07641c..50f3ab9b747f5130dc8b41fb654580053a345935 100644
--- a/editor/tview.go
+++ b/editor/tview.go
@@ -10,6 +10,20 @@ import (
// Editor is the editor for text files.
func Editor(placeholder, title string) (string, error) {
+ theme := tview.Theme{
+ PrimitiveBackgroundColor: tcell.ColorDefault,
+ ContrastBackgroundColor: tcell.ColorDefault,
+ MoreContrastBackgroundColor: tcell.ColorDefault,
+ BorderColor: tcell.ColorDefault,
+ TitleColor: tcell.ColorDefault,
+ GraphicsColor: tcell.ColorDefault,
+ PrimaryTextColor: tcell.ColorDefault,
+ SecondaryTextColor: tcell.ColorDefault,
+ TertiaryTextColor: tcell.ColorDefault,
+ InverseTextColor: tcell.ColorDefault,
+ ContrastSecondaryTextColor: tcell.ColorDefault,
+ }
+ tview.Styles = theme
app := tview.NewApplication()
textArea := tview.NewTextArea().
diff --git a/folders/folders.go b/folders/folders.go
index 04b69a13ca5d24aefecbe0dfa7284dc2689dbc09..811f10bcedbff5e49d9c60fd2f7c95d5ea0c8114 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -4,6 +4,7 @@ package folders
import (
"errors"
"fmt"
+ "strings"
"github.com/jmoiron/sqlx"
)
@@ -34,6 +35,10 @@ type FolderCreateOptions struct {
// CreateFolder creates a new folder.
func (s *Store) CreateFolder(name string, options FolderCreateOptions) error {
+ if !IsAlphaNum(name) {
+ return errors.New("Folder can only have letters and numbers")
+ }
+ name = strings.ToUpper(name)
_, err := s.db.Exec(
`INSERT INTO folders
(name, always, brief, description, notify, owner, readnew,
@@ -52,6 +57,7 @@ func (s *Store) CreateFolder(name string, options FolderCreateOptions) error {
options.Expire,
options.Visibility,
)
+ // TODO: turn _ into rows and make sure it was added.
// TODO: process this error a bit more to give a better error message.
return err
}
@@ -102,6 +108,19 @@ func (s *Store) ListFolder(user string, _ FolderListOptions) ([]FolderListRow, e
return flr, nil
}
+// FindFolder finds a folder based on the prefix.
+func (s *Store) FindFolder(name string) string {
+ var folder string
+ s.db.Get(&folder, "SELECT name FROM folders where name = $1", name)
+ if folder != "" {
+ return folder
+ }
+ s.db.Get(&folder,
+ `SELECT name FROM folders where name LIKE $1
+ ORDER BY name LIMIT 1`, name+"%")
+ return folder
+}
+
// IsFolderAccess checks if a user can access a folder.
func (s *Store) IsFolderAccess(name, user string) bool {
found := 0
diff --git a/folders/messages.go b/folders/messages.go
index abb310626039466f52182ccf7d2b33720566934a..bd08ec9e7b1ee7a089f1f2f9cb70b2e61bccbdaa 100644
--- a/folders/messages.go
+++ b/folders/messages.go
@@ -2,6 +2,7 @@
package folders
import (
+ "errors"
"fmt"
"strings"
"time"
@@ -66,16 +67,45 @@ func (m *Message) String() string {
// ReadMessage reads a message for a user.
func (s *Store) ReadMessage(login, folder string, msgid int) (*Message, error) {
msg := &Message{}
- err := s.db.Get(msg,
+ s.db.Get(msg,
`SELECT id, folder, author, subject, message, expiration, create_at, update_at
- FROM messages WHERE folder = $1, id = $2`, folder, msgid)
- if err != nil {
- return nil, err
+ FROM messages WHERE folder = $1 AND id = $2`, folder, msgid)
+ if msg.ID != msgid || msgid == 0 {
+ return nil, errors.New("Specified message was not found")
}
// TODO: replace _ with rows and check.
- _, err = s.db.Exec(
+ s.db.Exec(
"INSERT INTO read (login, folder, msgid) VALUES ($1, $2, $3)",
login, folder, msgid)
- return msg, err
+ return msg, nil
+}
+
+// ListMessagesOptions has the options for a ListMessages call.
+type ListMessagesOptions struct {
+ // TODO: is this needed? Maybe for message order?
+}
+
+// ListMessages lists messages.
+func (s *Store) ListMessages(folder string, options *ListMessagesOptions) ([]Message, error) {
+ messages := []Message{}
+ if options != nil {
+ return messages, errors.New("TODO: options aren't implemented")
+ }
+ rows, err := s.db.Queryx(
+ `SELECT id, folder, author, subject, message, expiration, create_at, update_at
+ FROM messages WHERE folder = $1`, folder)
+ if err != nil {
+ return messages, nil
+ }
+ for rows.Next() {
+ msg := Message{}
+ err = rows.StructScan(&msg)
+ if err != nil {
+ return []Message{}, nil
+ }
+ messages = append(messages, msg)
+ }
+ return messages, nil
+
}
diff --git a/folders/sql/1_create_table.down.sql b/folders/sql/1_create_table.down.sql
index 4070921fe631b62d42878b713da994824eacb233..8a1a47efb2c60d66a1e51323a8ca98c71013d3e2 100644
--- a/folders/sql/1_create_table.down.sql
+++ b/folders/sql/1_create_table.down.sql
@@ -1,4 +1,11 @@
--- Dropped in reverse order to deal with foreign keys.
+DROP TABLE mark;
+DROP TABLE read;
+
+DROP INDEX messages_idx_expiration;
+DROP INDEX messages_idx_shutdown;
+DROP TABLE messages;
+
DROP TRIGGER co_owners_after_update_update_at;
DROP TABLE co_owners;
diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql
index 86631a20bf01f5baff868703dd5bf6badd3c1d9c..c8f55c2f0fe97672adcf357c6e56bc2f817c8b41 100644
--- a/folders/sql/1_create_table.up.sql
+++ b/folders/sql/1_create_table.up.sql
@@ -1,5 +1,5 @@
CREATE TABLE users (
- login VARCHAR(25) NOT NULL PRIMARY KEY,
+ login VARCHAR(12) NOT NULL PRIMARY KEY,
name VARCHAR(53) NOT NULL,
admin INT DEFAULT 0,
disabled INT DEFAULT 0,
@@ -122,7 +122,19 @@ CREATE TABLE read (
folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
msgid INT,
PRIMARY KEY (folder, login, msgid),
- CONSTRAINT FK_id_folder
+ CONSTRAINT read_fk_id_folder
+ FOREIGN KEY (msgid, folder)
+ REFERENCES messages(id, folder)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+) WITHOUT ROWID;
+
+CREATE TABLE mark (
+ login VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE,
+ folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE,
+ msgid INT,
+ PRIMARY KEY (folder, login, msgid),
+ CONSTRAINT mark_fk_id_folder
FOREIGN KEY (msgid, folder)
REFERENCES messages(id, folder)
ON DELETE CASCADE
diff --git a/folders/verify.go b/folders/verify.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab28a6ace05b2bbca1e9a3aec7be168ddd32f362
--- /dev/null
+++ b/folders/verify.go
@@ -0,0 +1,14 @@
+// Package folders are all the routines and sql for managing folders.
+package folders
+
+import "unicode"
+
+// IsAlphaNum checks that a string just contains a-z0-9.
+func IsAlphaNum(s string) bool {
+ for _, r := range s {
+ if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/repl/command.go b/repl/command.go
index f597551573cb8caa247c6f9c24ab3999bd835bf0..11913d98e5e115c859d880e661a41e87cffa280e 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -1214,6 +1214,7 @@ After selecting a folder, the user will notified of the number of unread
messages, and the message pointer will be placed at the first unread
message.`,
Action: ActionSelect,
+ MinArgs: 1,
MaxArgs: 1,
Flags: dclish.Flags{
"/MARKED": {
diff --git a/repl/folders.go b/repl/folders.go
index 2d9351f2bb65560b167fb1a9574becb64c1a1ee2..900f8b74dbd806b335238370b975aed3187305ed 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strconv"
+ "strings"
"git.lyda.ie/kevin/bulletin/accounts"
"git.lyda.ie/kevin/bulletin/dclish"
@@ -28,6 +29,7 @@ func ActionIndex(_ *dclish.Command) error {
// ActionCreate handles the `CREATE` command. This creates a folder.
func ActionCreate(cmd *dclish.Command) error {
+ // Populate options...
options := folders.FolderCreateOptions{}
if cmd.Flags["/ALWAYS"].Value == "true" {
options.Always = 1
@@ -35,11 +37,9 @@ func ActionCreate(cmd *dclish.Command) error {
if cmd.Flags["/BRIEF"].Value == "true" {
options.Brief = 1
}
- if cmd.Flags["/DESCRIPTION"].Value == "" {
- // TODO: prompt the user for a description.
- return errors.New("Description is required - use /DESCRIPTION")
+ if cmd.Flags["/DESCRIPTION"].Value != "" {
+ options.Description = cmd.Flags["/DESCRIPTION"].Value
}
- options.Description = cmd.Flags["/DESCRIPTION"].Value
if cmd.Flags["/NOTIFY"].Value == "true" {
options.Notify = 1
}
@@ -75,6 +75,17 @@ func ActionCreate(cmd *dclish.Command) error {
options.Visibility = folders.FolderSemiPrivate
}
+ // Verify options...
+ if options.Description == "" {
+ var err error
+ options.Description, err = accounts.GetLine("Enter one line description of folder: ")
+ if err != nil {
+ return nil
+ }
+ }
+ if options.Description == "" || len(options.Description) > 53 {
+ return errors.New("Description must exist and be under 53 characters")
+ }
err := accounts.User.Folders.CreateFolder(cmd.Args[0], options)
// TODO: handle the /ID flag.
return err
@@ -82,10 +93,22 @@ func ActionCreate(cmd *dclish.Command) error {
// ActionSelect handles the `SELECT` command. This selects a folder.
func ActionSelect(cmd *dclish.Command) error {
- if accounts.User.Folders.IsFolderAccess(cmd.Args[0], accounts.User.Login) {
- accounts.User.CurrentFolder = cmd.Args[0]
- }
- return nil
+ if strings.Contains(cmd.Args[0], "%") {
+ return errors.New("Folder name cannot contain a %")
+ }
+ folder := accounts.User.Folders.FindFolder(cmd.Args[0])
+ if folder == "" {
+ return errors.New("Unable to select the folder")
+ }
+ if accounts.User.Folders.IsFolderAccess(folder, accounts.User.Login) {
+ accounts.User.CurrentFolder = folder
+ fmt.Printf("Folder has been set to '%s'.\n", folder)
+ return nil
+ }
+ // 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")
}
// ActionModify handles the `MODIFY` command. This modifies a folder.
diff --git a/repl/messages.go b/repl/messages.go
index 507b091028bae05269690a4502b627ba74d0e751..ae292944a4f57175362566136746454e4d3c3128 100644
--- a/repl/messages.go
+++ b/repl/messages.go
@@ -18,11 +18,36 @@ import (
func ActionDirectory(cmd *dclish.Command) error {
// TODO: flag parsing.
if len(cmd.Args) == 1 {
- folder := strings.ToUpper(cmd.Args[0])
- // TODO: Check folder is valid.
+ if strings.Contains(cmd.Args[0], "%") {
+ return errors.New("Folder name cannot contain a %")
+ }
+ folder := accounts.User.Folders.FindFolder(cmd.Args[0])
+ if folder == "" {
+ return errors.New("Unable to select the folder")
+ }
+ if !accounts.User.Folders.IsFolderAccess(folder, accounts.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")
+ }
accounts.User.CurrentFolder = folder
}
- fmt.Println("TODO: List messages in folder")
+ msgs, err := accounts.User.Folders.ListMessages(
+ accounts.User.CurrentFolder, nil)
+ if err != nil {
+ return err
+ }
+ if len(msgs) == 0 {
+ fmt.Println("There are no messages present.")
+ return nil
+ }
+ fmt.Printf("%4s %-43s %-12s %-10s\n", "#", "Subject", "From", "Date")
+ for _, msg := range msgs {
+ fmt.Printf("%4d %-43s %-12s %-10s\n", msg.ID, msg.Subject, msg.Author,
+ msg.CreateAt.Format("2006-05-04 15:02:01"))
+ }
+
return nil
}
@@ -158,7 +183,22 @@ func ActionNext(cmd *dclish.Command) error {
// ActionRead handles the `READ` command.
func ActionRead(cmd *dclish.Command) error {
- fmt.Printf("TODO: unimplemented...\n%s\n", cmd.Description)
+ // TODO: We need to set accounts.User.CurrentMessage when we change folder.
+ msgid := accounts.User.CurrentMessage
+ if len(cmd.Args) == 1 {
+ var err error
+ msgid, err = strconv.Atoi(cmd.Args[0])
+ if err != nil {
+ return err
+ }
+ }
+ msg, err := accounts.User.Folders.ReadMessage(
+ accounts.User.Login, accounts.User.CurrentFolder, msgid)
+ if err != nil {
+ return err
+ }
+ // TODO: update accounts.User.CurrentMessage
+ fmt.Printf("%s\n", msg)
return nil
}
diff --git a/repl/repl.go b/repl/repl.go
index 88e97a4e41e30ab6321da51f00f94da5ababae01..07afdf823180cb9de6a1f48f59ccc8b17a996ddb 100644
--- a/repl/repl.go
+++ b/repl/repl.go
@@ -4,6 +4,7 @@ package repl
import (
"fmt"
"path"
+ "unicode"
"git.lyda.ie/kevin/bulletin/accounts"
"github.com/adrg/xdg"
@@ -54,6 +55,19 @@ func Loop() error {
if len(line) == 0 {
continue
}
+ prependRead := false
+ for _, r := range line {
+ if unicode.IsDigit(r) {
+ prependRead = true
+ break
+ }
+ if !unicode.IsSpace(r) {
+ break
+ }
+ }
+ if prependRead {
+ line = "READ " + line
+ }
err = commands.ParseAndRun(line)
if err != nil {
fmt.Printf("ERROR: %s.\n", err)