diff --git a/folders/connection.go b/folders/connection.go deleted file mode 100644 index 039ff10e22cce3dfba66266d75cde67ab62e82b9..0000000000000000000000000000000000000000 --- a/folders/connection.go +++ /dev/null @@ -1,64 +0,0 @@ -// Package folders are all the routines and sql for managing folders. -package folders - -import ( - "embed" - "errors" - "os" - "path" - - "github.com/adrg/xdg" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/source/iofs" - "github.com/jmoiron/sqlx" - - // Included to connect to sqlite. - _ "github.com/golang-migrate/migrate/v4/database/sqlite" - _ "modernc.org/sqlite" -) - -//go:embed sql/*.sql -var fs embed.FS - -// Store is the store for folders. -type Store struct { - user string - db *sqlx.DB -} - -// Open opens the folders database. -func Open(user string) (*Store, error) { - fdir := path.Join(xdg.DataHome, "BULLETIN") - err := os.MkdirAll(fdir, 0700) - if err != nil { - return nil, errors.New("bulletin directory problem") - } - fdb := path.Join(fdir, "bboard.db") - - // Run db migrations if needed. - sqldir, err := iofs.New(fs, "sql") - if err != nil { - return nil, err - } - m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)") - if err != nil { - return nil, err - } - err = m.Up() - if err != nil && err != migrate.ErrNoChange { - return nil, err - } - m.Close() - - store := &Store{user: user} - store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)") - if err != nil { - return nil, errors.New("bulletin database problem") - } - return store, nil -} - -// Close closes the db backing the store. -func (fstore *Store) Close() { - fstore.db.Close() -} diff --git a/folders/folders.go b/folders/folders.go index 811f10bcedbff5e49d9c60fd2f7c95d5ea0c8114..bf10e978a18814d3e069ca10351d0a7f1297be30 100644 --- a/folders/folders.go +++ b/folders/folders.go @@ -2,165 +2,120 @@ package folders import ( + "context" "errors" - "fmt" "strings" - "github.com/jmoiron/sqlx" + "git.lyda.ie/kevin/bulletin/storage" + "git.lyda.ie/kevin/bulletin/this" ) -// FolderVisibility is the folder visibility level. -type FolderVisibility string +// ValidFolder validates the folder name for this user. +func ValidFolder(folder string) (string, error) { + if strings.Contains(folder, "%") { + return "", errors.New("Folder name cannot contain a %") + } + correct := FindFolder(folder) + if correct == "" { + return "", errors.New("Unable to select the folder") + } + if !IsFolderAccess(correct, 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 correct, nil +} // Values for FolderVisibility. const ( - FolderPublic FolderVisibility = "public" - FolderSemiPrivate = "semi-private" - FolderPrivate = "private" + FolderPublic int64 = 0 + FolderSemiPrivate = 1 + FolderPrivate = 2 ) -// FolderCreateOptions are a list of folder options. -type FolderCreateOptions struct { - Always int - Brief int - Description string - Notify int - Owner string - Readnew int - Shownew int - System int - Expire int - Visibility FolderVisibility -} - // CreateFolder creates a new folder. -func (s *Store) CreateFolder(name string, options FolderCreateOptions) error { - if !IsAlphaNum(name) { +func CreateFolder(owner string, options storage.CreateFolderParams) error { + if !IsAlphaNum(options.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, - shownew, system, expire, visibility) - VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, - name, - options.Always, - options.Brief, - options.Description, - options.Notify, - options.Owner, - options.Readnew, - options.Shownew, - options.System, - 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 -} + options.Name = strings.ToUpper(options.Name) -// FolderListOptions are a list of folder options. -type FolderListOptions struct { -} + ctx := context.TODO() + tx, err := this.Store.Begin() + if err != nil { + return err + } + defer tx.Rollback() + qtx := this.Q.WithTx(tx) + err = qtx.CreateFolder(ctx, options) + if err != nil { + return err + } + err = qtx.AddFolderOwner(ctx, storage.AddFolderOwnerParams{ + Folder: options.Name, + Login: owner, + }) + if err != nil { + return err + } -// FolderListRow are a list of folder options. -type FolderListRow struct { - Name string `db:"name"` - Count int `db:"count"` - Description string `db:"description"` + // TODO: process this error a bit more to give a better error message. + return tx.Commit() } -// ListFolder creates a new folder. -func (s *Store) ListFolder(user string, _ FolderListOptions) ([]FolderListRow, error) { - // TODO: get message counts. - var rows *sqlx.Rows - var err error - if s.IsUserAdmin(user) { - rows, err = s.db.Queryx( - `SELECT name, 0 as count, description FROM folders - ORDER BY name`, - ) - } else { - // TODO: limit access. - rows, err = s.db.Queryx( - `SELECT f.name, count(m.id) as count, f.description FROM folders AS f - LEFT JOIN messages AS m ON f.name = m.folder - GROUP By f.name - ORDER BY f.name`) - } - flr := []FolderListRow{} +// ListFolder provides a list of folders that this.User has access to. +func ListFolder() ([]storage.ListFolderRow, error) { + // TODO: need to check access. + ctx := context.TODO() + rows, err := this.Q.ListFolder(ctx) if err != nil { // TODO: process this error a bit more to give a better error message. - return flr, err + return []storage.ListFolderRow{}, err } - for rows.Next() { - row := FolderListRow{} - err := rows.StructScan(&row) - if err != nil { - // TODO: process this error a bit more to give a better error message. - return flr, err - } - flr = append(flr, row) - } - return flr, nil + return rows, 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) +func FindFolder(name string) string { + ctx := context.TODO() + folder, _ := this.Q.FindFolderExact(ctx, name) if folder != "" { return folder } - s.db.Get(&folder, - `SELECT name FROM folders where name LIKE $1 - ORDER BY name LIMIT 1`, name+"%") + folder, _ = this.Q.FindFolderPrefix(ctx, name) return folder } // IsFolderAccess checks if a user can access a folder. -func (s *Store) IsFolderAccess(name, user string) bool { - found := 0 - s.db.Get(&found, - `SELECT 1 FROM folders AS f LEFT JOIN co_owners AS c ON f.name = c.folder - WHERE f.name = $1 AND - (f.visibility = "public" - OR (f.owner = $2 OR c.OWNER = $2))`, - name, user) +func IsFolderAccess(name, login string) bool { + ctx := context.TODO() + found, _ := this.Q.IsFolderAccess(ctx, storage.IsFolderAccessParams{ + Name: name, + Login: login, + }) return found == 1 } // IsFolderOwner checks if a user is a folder owner. -func (s *Store) IsFolderOwner(name, user string) bool { - found := 0 - s.db.Get(&found, - `SELECT 1 FROM folders AS f LEFT JOIN co_owners AS c - ON f.name = c.folder - WHERE f.name = $1 AND (f.owner = '$2' OR c.OWNER = '$2')`, - name, user) +func IsFolderOwner(folder, login string) bool { + ctx := context.TODO() + found, _ := this.Q.IsFolderOwner(ctx, storage.IsFolderOwnerParams{ + Folder: folder, + Login: login, + }) return found == 1 } -// DeleteFolder creates a new folder. -func (s *Store) DeleteFolder(name string) error { +// DeleteFolder deletes a folder. +func DeleteFolder(name string) error { // TODO: make sure user can delete this table. - results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name) + ctx := context.TODO() + err := this.Q.DeleteFolder(ctx, name) // TODO: process this error a bit more to give a better error message. if err != nil { return err } - rows, err := results.RowsAffected() - if err != nil { - return err - } - if rows == 0 { - return errors.New("No such folder found") - } - if rows != 1 { - return fmt.Errorf("Unexpected number (%d) of folders removed", rows) - } return nil } diff --git a/folders/messages.go b/folders/messages.go index 1e49fb76d4000c6ebbe7f852c6b1ca2d06696c6e..87c9f025457b62c57bd1ae18da2c9bf3b4c7e035 100644 --- a/folders/messages.go +++ b/folders/messages.go @@ -1,110 +1,71 @@ package folders import ( + "context" "errors" - "fmt" - "strings" "time" + + "git.lyda.ie/kevin/bulletin/storage" + "git.lyda.ie/kevin/bulletin/this" ) // CreateMessage creates a new folder. -func (s *Store) CreateMessage(author, subject, message, folder string, permanent, shutdown int, expiration *time.Time) error { +func CreateMessage(author, subject, message, folder string, permanent, shutdown int, expiration *time.Time) error { + ctx := context.TODO() if expiration == nil { - var days int - err := s.db.Get(&days, "SELECT expire FROM folders WHERE name = $1", folder) + days, err := this.Q.GetFolderExpire(ctx, folder) if err != nil { return err } if days <= 0 { + // TODO: Get from site config. days = 14 } - exp := time.Now().AddDate(0, 0, days) + exp := time.Now().AddDate(0, 0, int(days)) expiration = &exp } // TODO: replace _ with rows and check. - _, err := s.db.Exec( - `INSERT INTO messages - (id, folder, author, subject, message, permanent, shutdown, expiration) - VALUES - ((SELECT COALESCE(MAX(id), 0) + 1 FROM messages WHERE folder = $1), $1, $2, $3, $4, $5, $6, $7)`, - folder, - author, - subject, - message, - permanent, - shutdown, - expiration, // TODO: handle this being NULL - ) + err := this.Q.CreateMessage(ctx, storage.CreateMessageParams{ + Folder: folder, + Folder_2: folder, + Author: author, + Subject: subject, + Message: message, + Permanent: int64(permanent), + Shutdown: int64(shutdown), + Expiration: *expiration, + }) // TODO: process this error a bit more to give a better error message. return err } -// Message contains a message -type Message struct { - ID int `db:"id"` - Folder string `db:"folder"` - Author string `db:"author"` - Subject string `db:"subject"` - Message string `db:"message"` - Expires time.Time `db:"expiration"` - CreateAt time.Time `db:"create_at"` - UpdateAt time.Time `db:"update_at"` -} - -// String renders a message. -func (m *Message) String() string { - buf := &strings.Builder{} - // TODO: Show if an edit has happened. - fmt.Fprintf(buf, "From: \"%s\" %s\n", m.Author, m.CreateAt.Format("02-JAN-2006 15:04:05")) - fmt.Fprintf(buf, "To: %s\n", m.Folder) - fmt.Fprintf(buf, "Subj: %s\n\n", m.Subject) - fmt.Fprintf(buf, "%s\n", m.Message) - - return buf.String() -} - // ReadMessage reads a message for a user. -func (s *Store) ReadMessage(login, folder string, msgid int) (*Message, error) { - msg := &Message{} - s.db.Get(msg, - `SELECT id, folder, author, subject, message, expiration, create_at, update_at - 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") +func ReadMessage(login, folder string, msgid int64) (*storage.Message, error) { + ctx := context.TODO() + msg, err := this.Q.ReadMessage(ctx, storage.ReadMessageParams{ + Folder: folder, + ID: msgid, + }) + if err != nil { + return nil, err } - // TODO: replace _ with rows and check. - s.db.Exec( - "INSERT INTO read (login, folder, msgid) VALUES ($1, $2, $3)", - login, folder, msgid) - return msg, nil -} + if msg.ID != int64(msgid) || msgid == 0 { + return nil, errors.New("Specified message was not found") + } + err = this.Q.SetMessageSeen(ctx, storage.SetMessageSeenParams{ + Login: login, + Folder: folder, + Msgid: int64(msgid), + }) -// ListMessagesOptions has the options for a ListMessages call. -type ListMessagesOptions struct { - // TODO: is this needed? Maybe for message order? + return &msg, nil } // 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 - +func ListMessages(folder string) ([]storage.ListMessagesRow, error) { + ctx := context.TODO() + // TODO: options aren't implemented - need to set them? + rows, err := this.Q.ListMessages(ctx, folder) + return rows, err } diff --git a/folders/sql/1_create_table.down.sql b/folders/sql/1_create_table.down.sql deleted file mode 100644 index 8a1a47efb2c60d66a1e51323a8ca98c71013d3e2..0000000000000000000000000000000000000000 --- a/folders/sql/1_create_table.down.sql +++ /dev/null @@ -1,21 +0,0 @@ ---- 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; - -DROP TRIGGER folders_before_delete_protect; -DROP TRIGGER folders_after_update_update_at; -DROP TRIGGER folders_before_update_validate; -DROP TRIGGER folders_before_insert_validate; -DROP TABLE folders; - -DROP TRIGGER users_before_delete_protect; -DROP TRIGGER users_before_update_protect; -DROP TRIGGER users_after_update_update_at; -DROP TABLE users; diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql deleted file mode 100644 index 14b47eb0183e39617b54e684dbd79286a80df5e5..0000000000000000000000000000000000000000 --- a/folders/sql/1_create_table.up.sql +++ /dev/null @@ -1,168 +0,0 @@ -CREATE TABLE users ( - login VARCHAR(12) NOT NULL PRIMARY KEY, - name VARCHAR(53) NOT NULL, - admin INT DEFAULT 0 NOT NULL, - moderator INT DEFAULT 0 NOT NULL, - alert INT NOT NULL DEFAULT 0, --- 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 -) WITHOUT ROWID; - -CREATE TRIGGER users_after_update_update_at - AFTER UPDATE ON users FOR EACH ROW - WHEN NEW.update_at = OLD.update_at --- avoid infinite loop -BEGIN - UPDATE users SET update_at=CURRENT_TIMESTAMP WHERE login=NEW.login; -END; - -INSERT INTO users (login, name, admin) - VALUES ('SYSTEM', 'System User', 1); - -CREATE TRIGGER users_before_update_protect - AFTER UPDATE ON users FOR EACH ROW - WHEN OLD.login = 'SYSTEM' AND (NEW.login != OLD.login OR NEW.admin != 1) -BEGIN - SELECT RAISE (ABORT, 'SYSTEM user is protected'); -END; - -CREATE TRIGGER users_before_delete_protect - BEFORE DELETE on users FOR EACH ROW - WHEN OLD.login = 'SYSTEM' -BEGIN - SELECT RAISE (ABORT, 'SYSTEM user is protected'); -END; - -CREATE TABLE folders ( - name VARCHAR(25) NOT NULL PRIMARY KEY, - always INT DEFAULT 0 NOT NULL, - brief INT DEFAULT 0 NOT NULL, - description VARCHAR(53) DEFAULT 0 NOT NULL, - notify INT DEFAULT 0 NOT NULL, - readnew INT DEFAULT 0 NOT NULL, - shownew INT DEFAULT 0 NOT NULL, - system INT DEFAULT 0 NOT NULL, - expire INT DEFAULT 14 NOT NULL, - visibility TEXT DEFAULT 'public' NOT NULL, - create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL -) WITHOUT ROWID; - -CREATE TRIGGER folders_before_insert_validate - BEFORE INSERT on folders -BEGIN - SELECT - CASE - WHEN NEW.name != UPPER(NEW.name) OR NEW.name GLOB '*[^A-Z0-9_-]*' THEN - RAISE (ABORT, 'Invalid folder name') - END; -END; - -CREATE TRIGGER folders_before_update_validate - BEFORE UPDATE on folders -BEGIN - SELECT - CASE - WHEN NEW.name != UPPER(NEW.name) OR NEW.name GLOB '*[^A-Z0-9_-]*' THEN - RAISE (ABORT, 'Invalid folder name') - WHEN OLD.name = 'GENERAL' AND OLD.name != NEW.name THEN - RAISE (ABORT, 'GENERAL folder is protected') - END; -END; - -CREATE TRIGGER folders_after_update_update_at - AFTER UPDATE ON folders FOR EACH ROW - WHEN NEW.update_at = OLD.update_at --- avoid infinite loop -BEGIN - UPDATE folders SET update_at=CURRENT_TIMESTAMP WHERE name=NEW.name; -END; - -CREATE TRIGGER folders_before_delete_protect - BEFORE DELETE on folders FOR EACH ROW - WHEN OLD.name = 'GENERAL' -BEGIN - SELECT RAISE (ABORT, 'GENERAL folder is protected'); -END; - -INSERT INTO folders (name, description, system, shownew) - VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1); - -CREATE TABLE owners ( - folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE, - owner VARCHAR(25) REFERENCES users(login) ON UPDATE CASCADE, - create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY (folder, owner) -) WITHOUT ROWID; - -CREATE TRIGGER owners_after_update_update_at - AFTER UPDATE ON owners FOR EACH ROW - WHEN NEW.update_at = OLD.update_at --- avoid infinite loop -BEGIN - UPDATE owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner; -END; - -INSERT INTO owners (folder, owner) VALUES ('GENERAL', 'SYSTEM'); - -CREATE TABLE messages ( - id INT NOT NULL, - folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE, - author VARCHAR(25) REFERENCES users(login) ON UPDATE CASCADE, - subject VARCHAR(53) NOT NULL, - message TEXT NOT NULL, - permanent INT DEFAULT 0 NOT NULL, - shutdown INT DEFAULT 0 NOT NULL, - expiration TIMESTAMP NOT NULL, - create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY (id, folder) -) WITHOUT ROWID; -CREATE INDEX messages_idx_shutdown ON messages(shutdown); -CREATE INDEX messages_idx_expiration ON messages(expiration); - -CREATE TABLE seen ( - 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 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 - ON UPDATE CASCADE -) WITHOUT ROWID; - -CREATE TABLE access ( - login VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE, - folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE, - PRIMARY KEY (login, folder) -) WITHOUT ROWID; - ---- User folder configs. -CREATE TABLE config ( - login VARCHAR(25) REFERENCES users(login) ON DELETE CASCADE ON UPDATE CASCADE, - folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE, - always INT NOT NULL DEFAULT 0, - alert INT NOT NULL DEFAULT 0, --- 0=no, 1=brief, 2=readnew - PRIMARY KEY (login, folder) -) WITHOUT ROWID; - ---- System configs. -CREATE TABLE system ( - name VARCHAR(12) NOT NULL PRIMARY KEY, - default_expire INT NOT NULL DEFAULT -1, - expire_limit INT NOT NULL DEFAULT -1 -); diff --git a/folders/users.go b/folders/users.go index e1b20ab01a4c96ff9245b4e2544cd470669a6788..1530913d28e023f30fae4ca90829b2c40bc690be 100644 --- a/folders/users.go +++ b/folders/users.go @@ -1,46 +1,35 @@ -// Package folders are all the routines and sql for managing folders. package folders -import "errors" +import ( + "context" + "strings" -// User is the user structure. -type User struct { - Login string `db:"login"` - Name string `db:"name"` - Admin int `db:"admin"` - Disabled int `db:"disabled"` -} + "git.lyda.ie/kevin/bulletin/storage" + "git.lyda.ie/kevin/bulletin/this" +) // GetUser gets a user. -func (s *Store) GetUser(login string) (*User, error) { - user := &User{} - err := s.db.Get(user, - "SELECT login, name, admin, disabled FROM users WHERE login = $1", - login) - return user, err +func GetUser(login string) (*storage.User, error) { + ctx := context.TODO() + user, err := this.Q.GetUser(ctx, login) + + return &user, err } -// AddUser gets a user. -func (s *Store) AddUser(user User) error { - result, err := s.db.NamedExec( - `INSERT INTO users (login, name, admin) VALUES (:login, :name, :admin)`, - user) - if err != nil { - return err - } - rows, err := result.RowsAffected() - if err != nil { - return err - } - if rows != 1 { - return errors.New("Failed to add user") - } - return nil +// AddUser adds a user. +func AddUser(user storage.User) (*storage.User, error) { + ctx := context.TODO() + newuser, err := this.Q.AddUser(ctx, storage.AddUserParams{ + Login: strings.ToUpper(user.Login), + Name: user.Name, + Admin: user.Admin, + }) + return &newuser, err } // IsUserAdmin checks if a user is an admin. -func (s *Store) IsUserAdmin(user string) bool { - found := 0 - s.db.Get(&found, "SELECT admin FROM users WHERE login = $1", user) +func IsUserAdmin(login string) bool { + ctx := context.TODO() + found, _ := this.Q.IsUserAdmin(ctx, login) return found == 1 } diff --git a/main.go b/main.go index 2a66475d8df4ae8d39432c5d4c3a466f13f70576..23781ce2083f930f1347f617b62e351106a83751 100644 --- a/main.go +++ b/main.go @@ -7,9 +7,9 @@ import ( "fmt" "os" - "git.lyda.ie/kevin/bulletin/users" "git.lyda.ie/kevin/bulletin/batch" "git.lyda.ie/kevin/bulletin/repl" + "git.lyda.ie/kevin/bulletin/this" "github.com/urfave/cli/v3" ) @@ -46,7 +46,7 @@ func main() { fmt.Println("ERROR: can only run batch commands as SYSTEM.") os.Exit(1) } - err := users.Open(user, cmd.String("name")) + err := this.StartThis(user, cmd.String("name")) if err != nil { fmt.Printf("ERROR: %s.", err) os.Exit(1) @@ -66,7 +66,7 @@ func main() { os.Exit(exitcode) } - err := users.Open(user, cmd.String("name")) + err := this.StartThis(user, cmd.String("name")) if err != nil { return err } diff --git a/repl/folders.go b/repl/folders.go index d9b23a34a5341d229f575ab37105fd4d37da09b8..da4b0a925c0e7249dd0df912b4b1aafa58166ee4 100644 --- a/repl/folders.go +++ b/repl/folders.go @@ -7,15 +7,16 @@ import ( "strconv" "strings" - "git.lyda.ie/kevin/bulletin/users" + "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/dclish" "git.lyda.ie/kevin/bulletin/folders" + "git.lyda.ie/kevin/bulletin/storage" + "git.lyda.ie/kevin/bulletin/this" ) // ActionIndex handles the `INDEX` command. This lists all the folders. func ActionIndex(_ *dclish.Command) error { - options := folders.FolderListOptions{} - rows, err := users.User.Folders.ListFolder(users.User.Login, options) + rows, err := folders.ListFolder() if err != nil { return err } @@ -30,7 +31,8 @@ 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{} + options := storage.CreateFolderParams{} + options.Name = cmd.Args[0] if cmd.Flags["/ALWAYS"].Value == "true" { options.Always = 1 } @@ -43,11 +45,6 @@ func ActionCreate(cmd *dclish.Command) error { if cmd.Flags["/NOTIFY"].Value == "true" { options.Notify = 1 } - if cmd.Flags["/OWNER"].Value != "" { - options.Owner = cmd.Flags["/OWNER"].Value - } else { - options.Owner = users.User.Login - } if cmd.Flags["/READNEW"].Value == "true" { options.Readnew = 1 } @@ -62,7 +59,7 @@ func ActionCreate(cmd *dclish.Command) error { if err != nil { return fmt.Errorf("Invalid expiry value '%s'", cmd.Flags["/EXPIRE"].Value) } - options.Expire = expire + options.Expire = int64(expire) } options.Visibility = folders.FolderPublic if cmd.Flags["/PRIVATE"].Value == "true" && cmd.Flags["/SEMIPRIVATE"].Value == "true" { @@ -75,10 +72,17 @@ func ActionCreate(cmd *dclish.Command) error { options.Visibility = folders.FolderSemiPrivate } + var owner string + if cmd.Flags["/OWNER"].Value != "" { + owner = cmd.Flags["/OWNER"].Value + } else { + owner = this.User.Login + } + // Verify options... if options.Description == "" { var err error - options.Description, err = users.GetLine("Enter one line description of folder: ") + options.Description, err = ask.GetLine("Enter one line description of folder: ") if err != nil { return nil } @@ -86,7 +90,7 @@ func ActionCreate(cmd *dclish.Command) error { if options.Description == "" || len(options.Description) > 53 { return errors.New("Description must exist and be under 53 characters") } - err := users.User.Folders.CreateFolder(cmd.Args[0], options) + err := folders.CreateFolder(owner, options) // TODO: handle the /ID flag. return err } @@ -96,12 +100,12 @@ func ActionSelect(cmd *dclish.Command) error { if strings.Contains(cmd.Args[0], "%") { return errors.New("Folder name cannot contain a %") } - folder := users.User.Folders.FindFolder(cmd.Args[0]) + folder := folders.FindFolder(cmd.Args[0]) if folder == "" { return errors.New("Unable to select the folder") } - if users.User.Folders.IsFolderAccess(folder, users.User.Login) { - users.User.CurrentFolder = folder + if folders.IsFolderAccess(folder, this.User.Login) { + this.Folder = folder fmt.Printf("Folder has been set to '%s'.\n", folder) return nil } @@ -119,7 +123,7 @@ func ActionModify(cmd *dclish.Command) error { // ActionRemove handles the `REMOVE` command. This modifies a folder. func ActionRemove(cmd *dclish.Command) error { - err := users.User.Folders.DeleteFolder(cmd.Args[0]) + err := folders.DeleteFolder(cmd.Args[0]) if err == nil { fmt.Println("Folder removed.") } diff --git a/repl/messages.go b/repl/messages.go index f9e05e8c09438f244f7ce1e2d1cd1c900bd0117f..f8068b165677e68a585d8c913281e5bc3c5a241c 100644 --- a/repl/messages.go +++ b/repl/messages.go @@ -8,9 +8,11 @@ import ( "strings" "time" - "git.lyda.ie/kevin/bulletin/users" + "git.lyda.ie/kevin/bulletin/ask" "git.lyda.ie/kevin/bulletin/dclish" "git.lyda.ie/kevin/bulletin/editor" + "git.lyda.ie/kevin/bulletin/folders" + "git.lyda.ie/kevin/bulletin/this" ) // ActionDirectory handles the `DIRECTORY` command. This lists all the @@ -18,23 +20,13 @@ import ( func ActionDirectory(cmd *dclish.Command) error { // TODO: flag parsing. if len(cmd.Args) == 1 { - if strings.Contains(cmd.Args[0], "%") { - return errors.New("Folder name cannot contain a %") - } - folder := users.User.Folders.FindFolder(cmd.Args[0]) - if folder == "" { - return errors.New("Unable to select the folder") - } - if !users.User.Folders.IsFolderAccess(folder, users.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") + folder, err := folders.ValidFolder(cmd.Args[0]) + if err != nil { + return err } - users.User.CurrentFolder = folder + this.Folder = folder } - msgs, err := users.User.Folders.ListMessages( - users.User.CurrentFolder, nil) + msgs, err := folders.ListMessages(this.Folder) if err != nil { return err } @@ -129,11 +121,11 @@ 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{users.User.CurrentFolder} + optFolder = []string{this.Folder} } // TODO: check if folders exist. if optSubject == "" { - optSubject, _ = users.GetLine("Enter subject of message: ") + optSubject, _ = ask.GetLine("Enter subject of message: ") if optSubject == "" { return errors.New("Must enter a subject") } @@ -145,7 +137,7 @@ func ActionAdd(cmd *dclish.Command) error { return err } for i := range optFolder { - err = users.User.Folders.CreateMessage(users.User.Login, optSubject, message, + err = folders.CreateMessage(this.User.Login, optSubject, message, optFolder[i], optPermanent, optShutdown, optExpiration) } return nil @@ -183,21 +175,21 @@ func ActionNext(cmd *dclish.Command) error { // ActionRead handles the `READ` command. func ActionRead(cmd *dclish.Command) error { - // TODO: We need to set users.User.CurrentMessage when we change folder. - msgid := users.User.CurrentMessage + // TODO: We need to set this.MsgID when we change folder. + msgid := this.MsgID if len(cmd.Args) == 1 { - var err error - msgid, err = strconv.Atoi(cmd.Args[0]) + id, err := strconv.Atoi(cmd.Args[0]) if err != nil { return err } + msgid = int64(id) } - msg, err := users.User.Folders.ReadMessage( - users.User.Login, users.User.CurrentFolder, msgid) + msg, err := folders.ReadMessage( + this.User.Login, this.Folder, msgid) if err != nil { return err } - // TODO: update users.User.CurrentMessage + // TODO: update this.MsgID fmt.Printf("%s\n", msg) return nil } diff --git a/repl/misc.go b/repl/misc.go index 8a7d5e7c67f96efcb13f3926bc530667e1f4ee4c..fd168a48dc1ebfe36a7ac1a9822c9d05d586c113 100644 --- a/repl/misc.go +++ b/repl/misc.go @@ -5,13 +5,11 @@ import ( "fmt" "os" - "git.lyda.ie/kevin/bulletin/users" "git.lyda.ie/kevin/bulletin/dclish" ) // ActionQuit handles the `QUIT` command. func ActionQuit(_ *dclish.Command) error { - users.User.Close() // TODO: IIRC, quit should not update unread data. Check old code to confirm. fmt.Println("QUIT") os.Exit(0) @@ -20,7 +18,6 @@ func ActionQuit(_ *dclish.Command) error { // ActionExit handles the `EXIT` command. func ActionExit(_ *dclish.Command) error { - users.User.Close() // TODO: update unread data. fmt.Println("EXIT") os.Exit(0) diff --git a/repl/repl.go b/repl/repl.go index 5dda56ef593915e7850593d7cfb7fd66b9cfa28c..3bb6eb235e4b9c617fee398008c2231555fbf320 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -6,7 +6,7 @@ import ( "path" "unicode" - "git.lyda.ie/kevin/bulletin/users" + "git.lyda.ie/kevin/bulletin/this" "github.com/adrg/xdg" "github.com/chzyer/readline" ) @@ -18,7 +18,7 @@ func Loop() error { &readline.Config{ Prompt: "BULLETIN> ", HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN", - fmt.Sprintf("%s.history", users.User.Login)), + fmt.Sprintf("%s.history", this.User.Login)), // TODO: AutoComplete: completer, InterruptPrompt: "^C", EOFPrompt: "EXIT", diff --git a/repl/set.go b/repl/set.go index d947b071bbb7f9835007e7578fa9e36665fd612b..bd399cb3d919c4ec6ae9a2f79b0339b12a9f6b98 100644 --- a/repl/set.go +++ b/repl/set.go @@ -6,8 +6,9 @@ import ( "fmt" "strings" - "git.lyda.ie/kevin/bulletin/users" "git.lyda.ie/kevin/bulletin/dclish" + "git.lyda.ie/kevin/bulletin/folders" + "git.lyda.ie/kevin/bulletin/this" ) // ActionSetAccess handles the `SET ACCESS` command. @@ -61,12 +62,12 @@ func ActionSetFolder(cmd *dclish.Command) error { if strings.Contains(cmd.Args[0], "%") { return errors.New("Folder name cannot contain a %") } - folder := users.User.Folders.FindFolder(cmd.Args[0]) + folder := folders.FindFolder(cmd.Args[0]) if folder == "" { return errors.New("Unable to select the folder") } - if users.User.Folders.IsFolderAccess(folder, users.User.Login) { - users.User.CurrentFolder = folder + if folders.IsFolderAccess(folder, this.User.Login) { + this.Folder = folder fmt.Printf("Folder has been set to '%s'.\n", folder) return nil } diff --git a/storage/display.go b/storage/display.go index 11d62f7788c77f4c1065dc07dd1b56642ac3ad7d..cf41bf664907ca1dd352b8be1f42454f23d29fc0 100644 --- a/storage/display.go +++ b/storage/display.go @@ -6,8 +6,8 @@ import ( "time" ) -// Full renders a message. -func (m *Message) Full() string { +// String renders a message. +func (m *Message) String() string { buf := &strings.Builder{} changed := "*" if m.CreateAt.Compare(m.UpdateAt) == 0 { @@ -22,8 +22,8 @@ func (m *Message) Full() string { return buf.String() } -// Short renders a message. -func (m *Message) Short(expire bool) string { +// String renders a message row. +func (m *ListMessagesRow) String(expire bool) string { var t time.Time if expire { t = m.Expiration diff --git a/storage/folders.sql.go b/storage/folders.sql.go index 00286ee3c6cf40c34ebbe73c89966ca562978dad..cad1cd795e650d8e6d5e383c6d0d6d7a324dbc8a 100644 --- a/storage/folders.sql.go +++ b/storage/folders.sql.go @@ -9,8 +9,22 @@ import ( "context" ) +const addFolderOwner = `-- name: AddFolderOwner :exec +INSERT INTO owners (folder, login) VALUES (?, ?) +` + +type AddFolderOwnerParams struct { + Folder string + Login string +} + +func (q *Queries) AddFolderOwner(ctx context.Context, arg AddFolderOwnerParams) error { + _, err := q.db.ExecContext(ctx, addFolderOwner, arg.Folder, arg.Login) + return err +} + const createFolder = `-- name: CreateFolder :exec - INSERT INTO folders ( +INSERT INTO folders ( name, always, brief, description, notify, readnew, shownew, system, expire, visibility ) VALUES ( @@ -90,34 +104,33 @@ func (q *Queries) GetFolderExpire(ctx context.Context, name string) (int64, erro } const isFolderAccess = `-- name: IsFolderAccess :one -SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder - WHERE f.name = ? AND (f.visibility = 0 OR c.OWNER = ?) +SELECT 1 FROM folders AS f LEFT JOIN owners AS o ON f.name = c.folder + WHERE f.name = ? AND (f.visibility = 0 OR o.login = ?) ` type IsFolderAccessParams struct { Name string - Owner string + Login string } func (q *Queries) IsFolderAccess(ctx context.Context, arg IsFolderAccessParams) (int64, error) { - row := q.db.QueryRowContext(ctx, isFolderAccess, arg.Name, arg.Owner) + row := q.db.QueryRowContext(ctx, isFolderAccess, arg.Name, arg.Login) var column_1 int64 err := row.Scan(&column_1) return column_1, err } const isFolderOwner = `-- name: IsFolderOwner :one -SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder - WHERE f.name = ? AND c.OWNER = ? +SELECT 1 FROM owners WHERE folder = ? AND login = ? ` type IsFolderOwnerParams struct { - Name string - Owner string + Folder string + Login string } func (q *Queries) IsFolderOwner(ctx context.Context, arg IsFolderOwnerParams) (int64, error) { - row := q.db.QueryRowContext(ctx, isFolderOwner, arg.Name, arg.Owner) + row := q.db.QueryRowContext(ctx, isFolderOwner, arg.Folder, arg.Login) var column_1 int64 err := row.Scan(&column_1) return column_1, err diff --git a/storage/messages.sql.go b/storage/messages.sql.go index 0b8d4d90a771e3e3770771cfd7d08b19d277daa9..5e37783894dc9ae6c1b85b050b7eded2ae459aa1 100644 --- a/storage/messages.sql.go +++ b/storage/messages.sql.go @@ -118,6 +118,33 @@ func (q *Queries) MarkMessage(ctx context.Context, arg MarkMessageParams) error return err } +const readMessage = `-- name: ReadMessage :one +SELECT id, folder, author, subject, message, permanent, shutdown, expiration, create_at, update_at FROM messages WHERE folder = ? AND id = ? +` + +type ReadMessageParams struct { + Folder string + ID int64 +} + +func (q *Queries) ReadMessage(ctx context.Context, arg ReadMessageParams) (Message, error) { + row := q.db.QueryRowContext(ctx, readMessage, arg.Folder, arg.ID) + var i Message + err := row.Scan( + &i.ID, + &i.Folder, + &i.Author, + &i.Subject, + &i.Message, + &i.Permanent, + &i.Shutdown, + &i.Expiration, + &i.CreateAt, + &i.UpdateAt, + ) + return i, err +} + const setMessageSeen = `-- name: SetMessageSeen :exec INSERT INTO seen (login, folder, msgid) VALUES (?, ?, ?) ` diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql index 1329304d9a8c0f7b6103554cc0df3a3daec856bc..4899e330808792e09cc999ecce73de52e56ea2bf 100644 --- a/storage/migrations/1_create_table.up.sql +++ b/storage/migrations/1_create_table.up.sql @@ -86,18 +86,19 @@ END; CREATE TABLE owners ( folder VARCHAR(25) REFERENCES folders(name) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, - owner VARCHAR(25) REFERENCES users(login) + login VARCHAR(25) REFERENCES users(login) ON UPDATE CASCADE NOT NULL, create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY (folder, owner) + PRIMARY KEY (folder, login) ) WITHOUT ROWID; CREATE TRIGGER owners_after_update_update_at AFTER UPDATE ON owners FOR EACH ROW WHEN NEW.update_at = OLD.update_at --- avoid infinite loop BEGIN - UPDATE owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner; + UPDATE owners SET update_at=CURRENT_TIMESTAMP + WHERE folder=NEW.folder AND login=NEW.login; END; CREATE TABLE messages ( diff --git a/storage/models.go b/storage/models.go index cfcd89887e7c93496a365b355250c3d3d9b29c4f..ac7da0b7f55da831fd3336c58ae3b620a733227b 100644 --- a/storage/models.go +++ b/storage/models.go @@ -56,7 +56,7 @@ type Message struct { type Owner struct { Folder string - Owner string + Login string CreateAt time.Time UpdateAt time.Time } diff --git a/storage/queries/folders.sql b/storage/queries/folders.sql index 70d6749a36b5f527a87c11e97e6a3d0afa3975c7..7882910b0c6a19c2570d78ea4bb9b254faf3776b 100644 --- a/storage/queries/folders.sql +++ b/storage/queries/folders.sql @@ -1,11 +1,14 @@ -- name: CreateFolder :exec - INSERT INTO folders ( +INSERT INTO folders ( name, always, brief, description, notify, readnew, shownew, system, expire, visibility ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ); +-- name: AddFolderOwner :exec +INSERT INTO owners (folder, login) VALUES (?, ?); + -- name: ListFolderForAdmin :many SELECT f.name, count(m.id) as count, f.description FROM folders AS f LEFT JOIN messages AS m ON f.name = m.folder @@ -27,12 +30,11 @@ ORDER BY name LIMIT 1; -- name: IsFolderAccess :one -SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder - WHERE f.name = ? AND (f.visibility = 0 OR c.OWNER = ?); +SELECT 1 FROM folders AS f LEFT JOIN owners AS o ON f.name = c.folder + WHERE f.name = ? AND (f.visibility = 0 OR o.login = ?); -- name: IsFolderOwner :one -SELECT 1 FROM folders AS f LEFT JOIN owners AS c ON f.name = c.folder - WHERE f.name = ? AND c.OWNER = ?; +SELECT 1 FROM owners WHERE folder = ? AND login = ?; -- name: DeleteFolder :exec DELETE FROM folders WHERE name=?; diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql index 07dcf67c16db4b2629cb01c74f8c2e57ec86098a..9f6ea221d4611cb9e046a419ea5c103f64ba8ad7 100644 --- a/storage/queries/messages.sql +++ b/storage/queries/messages.sql @@ -18,3 +18,6 @@ WHERE folder = ?; -- name: GetFirstMessageID :one SELECT id FROM messages WHERE folder = ? and id = MIN(id) GROUP BY folder; + +-- name: ReadMessage :one +SELECT * FROM messages WHERE folder = ? AND id = ?; diff --git a/this/this.go b/this/this.go index a5ea9d4bbb372c0df21f5299656aef88f384180b..9154f1593a4d66fab99e30407eaab8e2adec8efc 100644 --- a/this/this.go +++ b/this/this.go @@ -41,8 +41,8 @@ var Folder string // MsgID is the current message id. var MsgID int64 -// StartSession starts a session. -func StartSession(login, name string) error { +// StartThis starts a session. +func StartThis(login, name string) error { // Validate the login name. err := users.ValidLogin(login) if err != nil {