Unverified Commit 48f6e506 authored by Kevin Lyda's avatar Kevin Lyda
Browse files

Added INDEX

Initial pass at the messages table.  More help cleanup.
Added user creation.  Now emits a message showing how many
commands have been created.
parent eb357867
Loading
Loading
Loading
Loading
+12 −6
Original line number Diff line number Diff line
@@ -23,19 +23,25 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain

## Things to do

  * Implement a better dclish parser.
  * Implement each command.
    * Next: ????
    * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, INDEX, SELECT
    * Messages: ADD, CURRENT, DIRECTORY
  * Editor - need an embedded editor
    * An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/)
    * [gkilo](https://github.com/vcnovaes/gkilo)
    * This [kilo editor](https://viewsourcecode.org/snaptoken/kilo/) tutorial
  * Should there be a way to upload text files for commands that use
    files?  How would it work?
  * Cleanup help output.
    * Remove the node related flags.
    * Remove the node/cluster/newsgroup/mailing-list related flags.
  * Database
    * trigger to limit values for 'visibility';
    * trigger to limit values for 'visibility'?
  * Add some of the early announcements from the sources - see the
    conversion branch - to the GENERAL folder.
  * Add commands:
    * A way to add / delete ssh keys.
    * A way to manage files?
    * Commands for a local mail system?
    * Commands to connect to Mattermost or mastodon?
    * Commands to manage users.

## Module links

+37 −18
Original line number Diff line number Diff line
@@ -7,13 +7,14 @@ import (
	"strings"

	"git.lyda.ie/kevin/bulletin/folders"
	"github.com/chzyer/readline"
	_ "modernc.org/sqlite" // Loads sqlite driver.
)

// UserData is the type for holding user data. Things like preferences,
// unread message counts, signatures, etc.
type UserData struct {
	Account        string
	Login          string
	FullName       string
	Folders        *folders.Store
	CurrentFolder  string
@@ -24,30 +25,57 @@ type UserData struct {
var User *UserData

// ValidName makes sure that an account name is a valid name.
func ValidName(acc string) error {
	if acc == "" {
func ValidName(login string) error {
	if login == "" {
		return errors.New("empty account is invalid")
	}
	if strings.ContainsAny(acc, "./") {
		return fmt.Errorf("account name '%s' is invalid", acc)
	if strings.ContainsAny(login, "./") {
		return fmt.Errorf("account name '%s' is invalid", login)
	}
	return nil
}

// Open verifies that an account exists.
func Open(acc string) error {
	err := ValidName(acc)
func Open(login string) error {
	err := ValidName(login)
	if err != nil {
		return err
	}
	User = &UserData{
		Account: acc,
		Login: login,
	}

	User.Folders, err = folders.Open(acc)
	User.Folders, err = folders.Open(login)
	if err != nil {
		return err
	}
	user, _ := User.Folders.GetUser(login)

	if user.Login != login {
		user.Login = login
		user.Admin = 0

		fmt.Printf("Welcome new user %s\n", login)
		rl, err := readline.New("please enter your name: ")
		if err != nil {
			return err
		}
		user.Name, err = rl.Readline()
		rl.Close()

		err = User.Folders.AddUser(*user)
		if err != nil {
			return err
		}
		fmt.Println("User successfully created. Enjoy!")
	}
	if user.Disabled == 1 {
		return errors.New("User is disabled")
	}
	User.FullName = user.Name
	User.CurrentFolder = "GENERAL"
	// TODO: get the most recent unread message.
	// User.CurrentMessage =

	return nil
}
@@ -56,12 +84,3 @@ func Open(acc string) error {
func (u *UserData) Close() {
	u.Folders.Close()
}

// IsAdmin returns true if the user is an admin
func IsAdmin(acc string) bool {
	if acc == "admin" {
		return true
	}
	// TODO: Look up account otherwise.
	return false
}

folders/connection.go

0 → 100644
+64 −0
Original line number Diff line number Diff line
// 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()
}
+111 −41
Original line number Diff line number Diff line
@@ -2,63 +2,133 @@
package folders

import (
	"embed"
	"errors"
	"os"
	"path"
	"fmt"

	"github.com/adrg/xdg"
	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/source/iofs"
	"github.com/jmoiron/sqlx"
)

// FolderVisibility is the folder visibility level.
type FolderVisibility string

	// Included to connect to sqlite.
	_ "github.com/golang-migrate/migrate/v4/database/sqlite"
	_ "modernc.org/sqlite"
// Values for FolderVisibility.
const (
	FolderPublic      FolderVisibility = "public"
	FolderSemiPrivate                  = "semi-private"
	FolderPrivate                      = "private"
)

//go:embed sql/*.sql
var fs embed.FS
// 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 {
	_, 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: process this error a bit more to give a better error message.
	return err
}

// Store is the store for folders.
type Store struct {
	user string
	db   *sqlx.DB
// FolderListOptions are a list of folder options.
type FolderListOptions struct {
}

// 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")
// FolderListRow are a list of folder options.
type FolderListRow struct {
	Name        string `db:"name"`
	Count       int    `db:"count"`
	Description string `db:"description"`
}
	fdb := path.Join(fdir, "bboard.db")

	// Run db migrations if needed.
	sqldir, err := iofs.New(fs, "sql")
// 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{}
	if err != nil {
		return nil, err
		// TODO: process this error a bit more to give a better error message.
		return flr, err
	}
	m, err := migrate.NewWithSourceInstance("iofs", sqldir, "sqlite://"+fdb+"?_pragma=foreign_keys(1)")
	for rows.Next() {
		row := FolderListRow{}
		err := rows.StructScan(&row)
		if err != nil {
		return nil, err
			// TODO: process this error a bit more to give a better error message.
			return flr, err
		}
		flr = append(flr, row)
	}
	err = m.Up()
	if err != nil && err != migrate.ErrNoChange {
		return nil, err
	return flr, nil
}

// 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 (f.owner = '$2' OR c.OWNER = '$2')`,
		name, user)
	return found == 1
}
	m.Close()

	store := &Store{user: user}
	store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)")
// DeleteFolder creates a new folder.
func (s *Store) DeleteFolder(name string) error {
	results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", name)
	// TODO: process this error a bit more to give a better error message.
	if err != nil {
		return nil, errors.New("bulletin database problem")
		return err
	}
	return store, nil
	rows, err := results.RowsAffected()
	if err != nil {
		return err
	}

// Close closes the db backing the store.
func (fstore *Store) Close() {
	fstore.db.Close()
	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
}

folders/manage-folders.go

deleted100644 → 0
+0 −75
Original line number Diff line number Diff line
// Package folders are all the routines and sql for managing folders.
package folders

import (
	"errors"
	"fmt"
)

// FolderVisibility is the folder visibility level.
type FolderVisibility string

// Values for FolderVisibility.
const (
	FolderPublic      FolderVisibility = "public"
	FolderSemiPrivate                  = "semi-private"
	FolderPrivate                      = "private"
)

// FolderOptions are a list of folder options.
type FolderOptions 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 FolderOptions) error {
	_, 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: process this error a bit more to give a better error message.
	return err
}

// DeleteFolder creates a new folder.
func (s *Store) DeleteFolder(name string) error {
	results, err := s.db.Exec("DELETE FROM folders WHERE name=$1", 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
}
Loading