Skip to content
Snippets Groups Projects
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
No related branches found
No related tags found
No related merge requests found
...@@ -23,19 +23,25 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain ...@@ -23,19 +23,25 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain
## Things to do ## Things to do
* Implement a better dclish parser.
* Implement each command. * Implement each command.
* Next: ???? * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, INDEX, SELECT
* Messages: ADD, CURRENT, DIRECTORY
* Editor - need an embedded editor * Editor - need an embedded editor
* An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/) * An EDT inspired [editor](https://sourceforge.net/projects/edt-text-editor/)
* [gkilo](https://github.com/vcnovaes/gkilo) * [gkilo](https://github.com/vcnovaes/gkilo)
* This [kilo editor](https://viewsourcecode.org/snaptoken/kilo/) tutorial * 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. * Cleanup help output.
* Remove the node related flags. * Remove the node/cluster/newsgroup/mailing-list related flags.
* Database * 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 ## Module links
......
...@@ -7,13 +7,14 @@ import ( ...@@ -7,13 +7,14 @@ import (
"strings" "strings"
"git.lyda.ie/kevin/bulletin/folders" "git.lyda.ie/kevin/bulletin/folders"
"github.com/chzyer/readline"
_ "modernc.org/sqlite" // Loads sqlite driver. _ "modernc.org/sqlite" // Loads sqlite driver.
) )
// UserData is the type for holding user data. Things like preferences, // UserData is the type for holding user data. Things like preferences,
// unread message counts, signatures, etc. // unread message counts, signatures, etc.
type UserData struct { type UserData struct {
Account string Login string
FullName string FullName string
Folders *folders.Store Folders *folders.Store
CurrentFolder string CurrentFolder string
...@@ -24,30 +25,57 @@ type UserData struct { ...@@ -24,30 +25,57 @@ type UserData struct {
var User *UserData var User *UserData
// ValidName makes sure that an account name is a valid name. // ValidName makes sure that an account name is a valid name.
func ValidName(acc string) error { func ValidName(login string) error {
if acc == "" { if login == "" {
return errors.New("empty account is invalid") return errors.New("empty account is invalid")
} }
if strings.ContainsAny(acc, "./") { if strings.ContainsAny(login, "./") {
return fmt.Errorf("account name '%s' is invalid", acc) return fmt.Errorf("account name '%s' is invalid", login)
} }
return nil return nil
} }
// Open verifies that an account exists. // Open verifies that an account exists.
func Open(acc string) error { func Open(login string) error {
err := ValidName(acc) err := ValidName(login)
if err != nil { if err != nil {
return err return err
} }
User = &UserData{ User = &UserData{
Account: acc, Login: login,
} }
User.Folders, err = folders.Open(acc) User.Folders, err = folders.Open(login)
if err != nil { if err != nil {
return err 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 return nil
} }
...@@ -56,12 +84,3 @@ func Open(acc string) error { ...@@ -56,12 +84,3 @@ func Open(acc string) error {
func (u *UserData) Close() { func (u *UserData) Close() {
u.Folders.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
}
// 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()
}
...@@ -2,63 +2,133 @@ ...@@ -2,63 +2,133 @@
package folders package folders
import ( import (
"embed"
"errors" "errors"
"os" "fmt"
"path"
"github.com/adrg/xdg"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
)
// FolderVisibility is the folder visibility level.
type FolderVisibility string
// Included to connect to sqlite. // Values for FolderVisibility.
_ "github.com/golang-migrate/migrate/v4/database/sqlite" const (
_ "modernc.org/sqlite" FolderPublic FolderVisibility = "public"
FolderSemiPrivate = "semi-private"
FolderPrivate = "private"
) )
//go:embed sql/*.sql // FolderCreateOptions are a list of folder options.
var fs embed.FS 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. // FolderListOptions are a list of folder options.
type Store struct { type FolderListOptions struct {
user string
db *sqlx.DB
} }
// Open opens the folders database. // FolderListRow are a list of folder options.
func Open(user string) (*Store, error) { type FolderListRow struct {
fdir := path.Join(xdg.DataHome, "BULLETIN") Name string `db:"name"`
err := os.MkdirAll(fdir, 0700) Count int `db:"count"`
if err != nil { Description string `db:"description"`
return nil, errors.New("bulletin directory problem")
} }
fdb := path.Join(fdir, "bboard.db")
// Run db migrations if needed. // ListFolder creates a new folder.
sqldir, err := iofs.New(fs, "sql") 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 { 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 { 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() return flr, nil
if err != nil && err != migrate.ErrNoChange { }
return nil, err
// 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} // DeleteFolder creates a new folder.
store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)") 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 { if err != nil {
return nil, errors.New("bulletin database problem") return err
} }
return store, nil rows, err := results.RowsAffected()
if err != nil {
return err
} }
if rows == 0 {
// Close closes the db backing the store. return errors.New("No such folder found")
func (fstore *Store) Close() { }
fstore.db.Close() if rows != 1 {
return fmt.Errorf("Unexpected number (%d) of folders removed", rows)
}
return nil
} }
// 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
}
...@@ -2,6 +2,7 @@ CREATE TABLE users ( ...@@ -2,6 +2,7 @@ CREATE TABLE users (
login VARCHAR(25) NOT NULL PRIMARY KEY, login VARCHAR(25) NOT NULL PRIMARY KEY,
name VARCHAR(53) NOT NULL, name VARCHAR(53) NOT NULL,
admin INT DEFAULT 0, admin INT DEFAULT 0,
disabled INT DEFAULT 0,
create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, create_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL update_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
) WITHOUT ROWID; ) WITHOUT ROWID;
...@@ -99,3 +100,20 @@ CREATE TRIGGER co_owners_after_update_update_at ...@@ -99,3 +100,20 @@ CREATE TRIGGER co_owners_after_update_update_at
BEGIN BEGIN
UPDATE co_owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner; UPDATE co_owners SET update_at=CURRENT_TIMESTAMP WHERE folder=NEW.folder AND owner=NEW.owner;
END; END;
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,
shutdown INT DEFAULT 0,
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);
// Package folders are all the routines and sql for managing folders.
package folders
import "errors"
// User is the user structure.
type User struct {
Login string `db:"login"`
Name string `db:"name"`
Admin int `db:"admin"`
Disabled int `db:"disabled"`
}
// 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
}
// 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
}
// 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)
return found == 1
}
...@@ -23,17 +23,40 @@ func main() { ...@@ -23,17 +23,40 @@ func main() {
Name: "user", Name: "user",
Aliases: []string{"u"}, Aliases: []string{"u"},
Usage: "user to run bulletin as", Usage: "user to run bulletin as",
Value: "",
Required: true, Required: true,
}, },
&cli.StringFlag{
Name: "batch",
Aliases: []string{"b"},
Usage: "batch command",
},
}, },
Action: func(_ context.Context, cmd *cli.Command) error { Action: func(_ context.Context, cmd *cli.Command) error {
user := cmd.String("user") user := cmd.String("user")
batch := cmd.String("batch")
if batch != "" {
if user != "SYSTEM" {
fmt.Println("ERROR: can only run batch commands as SYSTEM.")
os.Exit(1)
}
exitcode := 0
switch batch {
case "after-boot":
fmt.Println("TODO: Delete messages with shutdown != 0.")
case "expire":
fmt.Println("TODO: expire messages.")
default:
fmt.Println("ERROR: can only run batch commands as SYSTEM.")
exitcode = 1
}
os.Exit(exitcode)
}
err := accounts.Open(user) err := accounts.Open(user)
if err != nil { if err != nil {
return err return err
} }
err = repl.Loop(user) err = repl.Loop()
if err != nil { if err != nil {
return err return err
} }
...@@ -43,6 +66,6 @@ func main() { ...@@ -43,6 +66,6 @@ func main() {
err := cmd.Run(context.Background(), os.Args) err := cmd.Run(context.Background(), os.Args)
if err != nil { if err != nil {
fmt.Printf("ERROR: %s.", err) fmt.Printf("ERROR: %s.\n", err)
} }
} }
...@@ -59,24 +59,11 @@ suppressed with /NOINDENT.`, ...@@ -59,24 +59,11 @@ suppressed with /NOINDENT.`,
Description: `/FOLDER=(foldername,[...]) Description: `/FOLDER=(foldername,[...])
Specifies the foldername into which the message is to be added. Does Specifies the foldername into which the message is to be added. Does
not change the current selected folder. Folders can be either local or not change the current selected folder.
remote folders. Thus, a nodename can precede the foldername (this
assumes that the remote node is capable of supporting this feature, i.e.
the BULLCP process is running on that node. If it is not, you will
receive an error message). If the the foldername is specified with only
a nodename, i.e. FOO::, the foldername is assumed to be GENERAL. NOTE:
Specifying remote nodes is only possible if that remote node is running
a special BULLCP process. If it isn't, the only way to add messages to
that remote node is via the /NODE command. However, /FOLDER is a much
quicker method, and much more versatile.
You can specify logical names which translate to one or more folder You can specify logical names which translate to one or more folder
names. I.e. $ DEFINE ALL_FOLDERS "VAX1,VAX2,VAX3", and then specify names. I.e. $ DEFINE ALL_FOLDERS "VAX1,VAX2,VAX3", and then specify
ALL_FOLDERS after /FOLDER=. Note that the quotation marks are required. ALL_FOLDERS after /FOLDER=. Note that the quotation marks are required.`,
When using /FOLDER for remote nodes, proxy logins are used to determine
if privileged options are allowed. If they are not allowed, the message
will still be added, but without the privileged settings.`,
OptArg: true, OptArg: true,
}, },
"/INDENT": { "/INDENT": {
...@@ -307,25 +294,7 @@ more information.)`, ...@@ -307,25 +294,7 @@ more information.)`,
Description: `/DESCRIPTION=description Description: `/DESCRIPTION=description
Specifies the description of the folder, which is displayed using the Specifies the description of the folder, which is displayed using the
SHOW FOLDER command. If omitted, you are prompted for a description. SHOW FOLDER command. If omitted, you are prompted for a description.`,
If this folder is to receive messages from a network mailing list
via the BBOARD feature, and you wish to use the POST and RESPOND/LIST
commands, the address of the mailing list should be included in the
description. This is done by enclosing the address using <> and
placing it at the end of the description, i.e.
INFOVAX MAILING LIST <INFO-VAX@KL.SRI.COM>
If a mailer protocol is needs to be added to the network address in
order for it to be sent by VMS MAIL, i.e. protocol%"address", the
appropriate protocol can be specified by either hardcoding it into the
file BULLNEWS.INC before compiling BULLETIN, or by defining the system
logical name BULL_NEWS_MAILER (it is the same protocol used by the NEWS
feature in order to respond to NEWS messages). The default protocol is
IN%. If desired, you can specify the protocol with the address, i.e.
INFOVAX MAILING LIST <IN%"INFO-VAX@KL.SRI.COM">`,
OptArg: true, OptArg: true,
}, },
"/EXPIRE": { "/EXPIRE": {
...@@ -1246,16 +1215,13 @@ folders that have been created. ...@@ -1246,16 +1215,13 @@ folders that have been created.
Format: Format:
SELECT [node-name::][folder-name] SELECT [folder-name]
The complete folder name need not be specified. BULLETIN will try to The complete folder name need not be specified. BULLETIN will try to
find the closest matching name. I.e. INFOV can be used for INFOVAX. find the closest matching name. I.e. INFOV can be used for INFOVAX.
Omitting the folder name will select the default general messages. Omitting the folder name will select the default general messages.
The node name can be specified only if the remote node has the special
BULLCP process running (invoked by BULLETIN/STARTUP command.)
After selecting a folder, the user will notified of the number of unread 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 messages, and the message pointer will be placed at the first unread
message.`, message.`,
...@@ -1380,9 +1346,6 @@ users must be at least 2. ...@@ -1380,9 +1346,6 @@ users must be at least 2.
SET BBOARD [username] SET BBOARD [username]
BBOARD cannot be set for remote folders. See also the command SET
DIGEST for options on formatting BBOARD messages.
If BULLCP is running, BBOARD is updated every 15 minutes. If you want If BULLCP is running, BBOARD is updated every 15 minutes. If you want
to length this period, define BULL_BBOARD_UPDATE to be the number of to length this period, define BULL_BBOARD_UPDATE to be the number of
minutes, between updates. I.e. DEFINE/SYSTEM BULL_BBOARD_UPDATE "30" minutes, between updates. I.e. DEFINE/SYSTEM BULL_BBOARD_UPDATE "30"
...@@ -1601,7 +1564,7 @@ on that command for more information. ...@@ -1601,7 +1564,7 @@ on that command for more information.
Format: Format:
SET FOLDER [node-name::][folder-name] SET FOLDER [folder-name]
3 /MARKED 3 /MARKED
Selects messages that have been marked (indicated by an asterisk). Selects messages that have been marked (indicated by an asterisk).
After using /MARKED, in order to see all messages, the folder will have After using /MARKED, in order to see all messages, the folder will have
......
...@@ -5,35 +5,30 @@ import ( ...@@ -5,35 +5,30 @@ import (
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"git.lyda.ie/kevin/bulletin/accounts" "git.lyda.ie/kevin/bulletin/accounts"
"git.lyda.ie/kevin/bulletin/dclish" "git.lyda.ie/kevin/bulletin/dclish"
"git.lyda.ie/kevin/bulletin/folders" "git.lyda.ie/kevin/bulletin/folders"
) )
// ActionDirectory handles the `DIRECTORY` command. This lists all the
// messages in the current folder.
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.
accounts.User.CurrentFolder = folder
}
fmt.Println("TODO: List messages in folder")
return nil
}
// ActionIndex handles the `INDEX` command. This lists all the folders. // ActionIndex handles the `INDEX` command. This lists all the folders.
func ActionIndex(cmd *dclish.Command) error { func ActionIndex(_ *dclish.Command) error {
fmt.Printf("TODO: implement INDEX:\n%s\n\n", cmd.Description) options := folders.FolderListOptions{}
rows, err := accounts.User.Folders.ListFolder(accounts.User.Login, options)
if err != nil {
return err
}
fmt.Println("The following folders are present")
fmt.Println("Name Count Description")
for _, row := range rows {
fmt.Printf("%-25s %5d %s\n", row.Name, row.Count, row.Description)
}
return nil return nil
} }
// ActionCreate handles the `CREATE` command. This creates a folder. // ActionCreate handles the `CREATE` command. This creates a folder.
func ActionCreate(cmd *dclish.Command) error { func ActionCreate(cmd *dclish.Command) error {
options := folders.FolderOptions{} options := folders.FolderCreateOptions{}
if cmd.Flags["/ALWAYS"].Value == "true" { if cmd.Flags["/ALWAYS"].Value == "true" {
options.Always = 1 options.Always = 1
} }
...@@ -41,6 +36,7 @@ func ActionCreate(cmd *dclish.Command) error { ...@@ -41,6 +36,7 @@ func ActionCreate(cmd *dclish.Command) error {
options.Brief = 1 options.Brief = 1
} }
if cmd.Flags["/DESCRIPTION"].Value == "" { if cmd.Flags["/DESCRIPTION"].Value == "" {
// TODO: prompt the user for a description.
return errors.New("Description is required - use /DESCRIPTION") return errors.New("Description is required - use /DESCRIPTION")
} }
options.Description = cmd.Flags["/DESCRIPTION"].Value options.Description = cmd.Flags["/DESCRIPTION"].Value
...@@ -50,7 +46,7 @@ func ActionCreate(cmd *dclish.Command) error { ...@@ -50,7 +46,7 @@ func ActionCreate(cmd *dclish.Command) error {
if cmd.Flags["/OWNER"].Value != "" { if cmd.Flags["/OWNER"].Value != "" {
options.Owner = cmd.Flags["/OWNER"].Value options.Owner = cmd.Flags["/OWNER"].Value
} else { } else {
options.Owner = accounts.User.Account options.Owner = accounts.User.Login
} }
if cmd.Flags["/READNEW"].Value == "true" { if cmd.Flags["/READNEW"].Value == "true" {
options.Readnew = 1 options.Readnew = 1
......
...@@ -84,7 +84,7 @@ func init() { ...@@ -84,7 +84,7 @@ func init() {
} }
sort.Strings(flgs) sort.Strings(flgs)
for i := range flgs { for i := range flgs {
fmt.Fprintf(buf, "\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description) fmt.Fprintf(buf, "\n\n%s %s", flgs[i], commands[c].Flags[flgs[i]].Description)
} }
} }
helpmap[c] = buf.String() helpmap[c] = buf.String()
......
// Package repl implements the main event loop.
package repl
import (
"fmt"
"strings"
"git.lyda.ie/kevin/bulletin/accounts"
"git.lyda.ie/kevin/bulletin/dclish"
)
// ActionDirectory handles the `DIRECTORY` command. This lists all the
// messages in the current folder.
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.
accounts.User.CurrentFolder = folder
}
fmt.Println("TODO: List messages in folder")
return nil
}
...@@ -5,17 +5,19 @@ import ( ...@@ -5,17 +5,19 @@ import (
"fmt" "fmt"
"path" "path"
"git.lyda.ie/kevin/bulletin/accounts"
"github.com/adrg/xdg" "github.com/adrg/xdg"
"github.com/chzyer/readline" "github.com/chzyer/readline"
) )
// Loop is the main event loop. // Loop is the main event loop.
func Loop(user string) error { func Loop() error {
fmt.Printf("TODO: get config for user %s using xdg.\n", user) // fmt.Printf("TODO: get config for user %s using xdg.\n", user)
rl, err := readline.NewEx( rl, err := readline.NewEx(
&readline.Config{ &readline.Config{
Prompt: "BULLETIN> ", Prompt: "BULLETIN> ",
HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN", fmt.Sprintf("%s.history", user)), HistoryFile: path.Join(xdg.ConfigHome, "BULLETIN",
fmt.Sprintf("%s.history", accounts.User.Login)),
// TODO: AutoComplete: completer, // TODO: AutoComplete: completer,
InterruptPrompt: "^C", InterruptPrompt: "^C",
EOFPrompt: "EXIT", EOFPrompt: "EXIT",
...@@ -26,6 +28,19 @@ func Loop(user string) error { ...@@ -26,6 +28,19 @@ func Loop(user string) error {
} }
defer rl.Close() defer rl.Close()
// TODO: Remove once commands are implemented.
unimplemented := 0
total := 0
for c := range commands {
if commands[c].Action == nil {
unimplemented++
}
total++
}
fmt.Printf("TODO: %d out of %d commands still to be implemented.\n",
unimplemented, total)
// TODO: END
for { for {
line, err := rl.Readline() line, err := rl.Readline()
if err != nil { if err != nil {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment