From 5997178d3c38283d5dea99265f13e59420ecc498 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Sun, 11 May 2025 21:01:38 +0100
Subject: [PATCH] Move to storage module.

---
 folders/connection.go                    |  64 --------
 folders/folders.go                       | 191 +++++++++--------------
 folders/messages.go                      | 121 +++++---------
 folders/sql/1_create_table.down.sql      |  21 ---
 folders/sql/1_create_table.up.sql        | 168 --------------------
 folders/users.go                         |  57 +++----
 main.go                                  |   6 +-
 repl/folders.go                          |  36 +++--
 repl/messages.go                         |  44 +++---
 repl/misc.go                             |   3 -
 repl/repl.go                             |   4 +-
 repl/set.go                              |   9 +-
 storage/display.go                       |   8 +-
 storage/folders.sql.go                   |  33 ++--
 storage/messages.sql.go                  |  27 ++++
 storage/migrations/1_create_table.up.sql |   7 +-
 storage/models.go                        |   2 +-
 storage/queries/folders.sql              |  12 +-
 storage/queries/messages.sql             |   3 +
 this/this.go                             |   4 +-
 20 files changed, 256 insertions(+), 564 deletions(-)
 delete mode 100644 folders/connection.go
 delete mode 100644 folders/sql/1_create_table.down.sql
 delete mode 100644 folders/sql/1_create_table.up.sql

diff --git a/folders/connection.go b/folders/connection.go
deleted file mode 100644
index 039ff10..0000000
--- 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 811f10b..bf10e97 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 1e49fb7..87c9f02 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 8a1a47e..0000000
--- 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 14b47eb..0000000
--- 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 e1b20ab..1530913 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 2a66475..23781ce 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 d9b23a3..da4b0a9 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 f9e05e8..f8068b1 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 8a7d5e7..fd168a4 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 5dda56e..3bb6eb2 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 d947b07..bd399cb 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 11d62f7..cf41bf6 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 00286ee..cad1cd7 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 0b8d4d9..5e37783 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 1329304..4899e33 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 cfcd898..ac7da0b 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 70d6749..7882910 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 07dcf67..9f6ea22 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 a5ea9d4..9154f15 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 {
-- 
GitLab