diff --git a/NOTES.md b/NOTES.md index a9d2d3e558706776c3e48a594edb1740be23fa5c..25b966959058313cddea018ca9dfac2ee59dee44 100644 --- a/NOTES.md +++ b/NOTES.md @@ -34,6 +34,8 @@ sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain files? How would it work? * Cleanup help output. * Remove the node related flags. + * Database + * trigger to limit values for 'visibility'; ## Module links diff --git a/dclish/dclish.go b/dclish/dclish.go index 19c93fe4f09966690007736b3a358f9a8872206e..0302781f228afc4fcd836574770c2a08d8d540eb 100644 --- a/dclish/dclish.go +++ b/dclish/dclish.go @@ -4,6 +4,7 @@ package dclish import ( "fmt" "strings" + "unicode" ) // ActionFunc is the function that a command runs. @@ -11,10 +12,10 @@ type ActionFunc func(*Command) error // Flag is a flag for a command. type Flag struct { + OptArg bool Value string Default string Description string - // TODO: Toggle bool } // Flags is the list of flags. @@ -34,10 +35,81 @@ type Command struct { // Commands is the full list of commands. type Commands map[string]*Command +func split(line string) []string { + words := []string{} + buf := strings.Builder{} + state := "start" + for _, c := range line { + switch state { + case "start": + if unicode.IsSpace(c) { + continue + } + if c == '"' { + state = "dquote" + } else if c == '\'' { + state = "squote" + } else { + state = "raw" + buf.WriteRune(c) + } + case "dquote": + if c == '"' { + words = append(words, buf.String()) + buf.Reset() + state = "start" + } else { + buf.WriteRune(c) + } + case "squote": + if c == '\'' { + words = append(words, buf.String()) + buf.Reset() + state = "start" + } else { + buf.WriteRune(c) + } + case "dquote-raw": + if c == '"' { + state = "raw" + } + buf.WriteRune(c) + case "squote-raw": + if c == '\'' { + state = "raw" + } + buf.WriteRune(c) + case "raw": + if unicode.IsSpace(c) { + words = append(words, buf.String()) + buf.Reset() + state = "start" + } else if c == '/' { + words = append(words, buf.String()) + buf.Reset() + state = "raw" + buf.WriteRune(c) + } else if c == '"' { + state = "dquote-raw" + buf.WriteRune(c) + } else if c == '\'' { + state = "squote-raw" + buf.WriteRune(c) + } else { + buf.WriteRune(c) + } + } + } + if len(buf.String()) > 0 { + words = append(words, buf.String()) + } + return words +} + // ParseAndRun parses a command line and runs the command. func (c Commands) ParseAndRun(line string) error { // TODO: this doesn't handle a DCL command line completely. - words := strings.Fields(line) + words := split(line) cmd, ok := c[strings.ToUpper(words[0])] if !ok { wordup := strings.ToUpper(words[0]) @@ -75,27 +147,33 @@ func (c Commands) ParseAndRun(line string) error { for i := range args { if strings.HasPrefix(args[i], "/") { flag, val, assigned := strings.Cut(args[i], "=") + var wordup string if assigned { - wordup := strings.ToUpper(flag) - flg, ok := cmd.Flags[wordup] - if !ok { - fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i]) - return nil - } - flg.Value = val + wordup = strings.ToUpper(flag) } else { - wordup := strings.ToUpper(args[i]) - value := "true" - if strings.HasPrefix(wordup, "/NO") { - wordup = strings.Replace(wordup, "/NO", "/", 1) - value = "false" - } - flg, ok := cmd.Flags[wordup] + wordup = args[i] + } + toggleValue := "true" + flg, ok := cmd.Flags[wordup] + if !ok { + wordup = strings.Replace(wordup, "/NO", "/", 1) + flg, ok = cmd.Flags[wordup] if !ok { fmt.Printf("ERROR: Flag '%s' not recognised.\n", args[i]) return nil } - flg.Value = value + toggleValue = "false" + } + if !flg.OptArg && assigned { + fmt.Printf("ERROR: Flag '%s' is a toggle.\n", args[i]) + return nil + } + if flg.OptArg { + if assigned { + flg.Value = strings.Trim(val, "\"'") + } + } else { + flg.Value = toggleValue } } else { if len(cmd.Args) == cmd.MaxArgs { diff --git a/folders/folders.go b/folders/folders.go index 2fd334bf4aad947db4f3fdce6adaa8cba8a31e51..039ff10e22cce3dfba66266d75cde67ab62e82b9 100644 --- a/folders/folders.go +++ b/folders/folders.go @@ -2,7 +2,6 @@ package folders import ( - "database/sql" "embed" "errors" "os" @@ -11,6 +10,7 @@ import ( "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" @@ -23,7 +23,7 @@ var fs embed.FS // Store is the store for folders. type Store struct { user string - db *sql.DB + db *sqlx.DB } // Open opens the folders database. @@ -35,6 +35,7 @@ func Open(user string) (*Store, error) { } fdb := path.Join(fdir, "bboard.db") + // Run db migrations if needed. sqldir, err := iofs.New(fs, "sql") if err != nil { return nil, err @@ -50,7 +51,7 @@ func Open(user string) (*Store, error) { m.Close() store := &Store{user: user} - store.db, err = sql.Open("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)") + store.db, err = sqlx.Connect("sqlite", "file://"+fdb+"?_pragma=foreign_keys(1)") if err != nil { return nil, errors.New("bulletin database problem") } diff --git a/folders/manage-folders.go b/folders/manage-folders.go new file mode 100644 index 0000000000000000000000000000000000000000..8beaf58b7eca969165b011972630b007591dee37 --- /dev/null +++ b/folders/manage-folders.go @@ -0,0 +1,75 @@ +// 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 +} diff --git a/folders/sql/1_create_table.up.sql b/folders/sql/1_create_table.up.sql index 6094a1cdb7c4f74ebb93615ae3e20e45ec1e146e..02b78c886f35f3f59b0c5e6159d98bbd65e10695 100644 --- a/folders/sql/1_create_table.up.sql +++ b/folders/sql/1_create_table.up.sql @@ -86,7 +86,7 @@ INSERT INTO folders (name, description, system, shownew, owner) VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1, 'SYSTEM'); CREATE TABLE co_owners ( - folder VARCHAR(25) REFERENCES folders(name) ON UPDATE CASCADE, + 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, diff --git a/go.mod b/go.mod index 4f17e6ab7216c567010304d9a3b4881b16543ed8..bb985d3bddcf100c541110f0c5b6753cc3efd8bd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.24.2 require ( github.com/adrg/xdg v0.5.3 github.com/chzyer/readline v1.5.1 + github.com/davecgh/go-spew v1.1.1 github.com/golang-migrate/migrate/v4 v4.18.3 + github.com/jmoiron/sqlx v1.4.0 github.com/urfave/cli/v3 v3.3.2 modernc.org/sqlite v1.37.0 ) @@ -23,7 +25,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect golang.org/x/sys v0.33.0 // indirect - modernc.org/libc v1.65.0 // indirect + modernc.org/libc v1.65.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.10.0 // indirect ) diff --git a/go.sum b/go.sum index 95baed5b57cce81c2990d3cd0c5a588e636f8cf9..7ea4c6a9a3de68b90931c7d940137c6e08b13f19 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,15 @@ +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA= github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk= github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4B/swMiAmDLs= @@ -18,8 +21,12 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= @@ -40,6 +47,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= modernc.org/libc v1.65.0 h1:e183gLDnAp9VJh6gWKdTy0CThL9Pt7MfcR/0bgb7Y1Y= modernc.org/libc v1.65.0/go.mod h1:7m9VzGq7APssBTydds2zBcxGREwvIGpuUBaKTXdm2Qs= +modernc.org/libc v1.65.1 h1:EwykJ3C7c5pCiZTU3dLkgRm3VdFGNFc8UXXzhhEZvbQ= +modernc.org/libc v1.65.1/go.mod h1:+LU/iIPTqxVVdAl3E++KC9npafs4zI4pkLiolMVDatc= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= diff --git a/main.go b/main.go index 2ea43e3f0713cecaaf725b47d31290fff1d286dd..caeb3111336ca16b504e799e53b45e62683adf39 100644 --- a/main.go +++ b/main.go @@ -43,6 +43,6 @@ func main() { err := cmd.Run(context.Background(), os.Args) if err != nil { - fmt.Println(err) + fmt.Printf("ERROR: %s.", err) } } diff --git a/repl/command.go b/repl/command.go index 77f5d1bf9a2d8ea90ad86092bd3a1de5b084fe1d..f913639f04d16faef1041d61ae7480c463542d95 100644 --- a/repl/command.go +++ b/repl/command.go @@ -33,12 +33,6 @@ A node which does not have BULLCP running cannot have a message broadcasted to it, (even though it is able to create a remote folder). See also /ALL and /BELL.`, - }, - "/CLUSTER": { - Description: `/[NO]CLUSTER - -This option specifies that broadcasted messages should be sent to all -nodes in the cluster. /CLUSTER is the default.`, }, "/EDIT": { Description: `/[NO]EDIT @@ -52,6 +46,7 @@ BULLETIN command line.`, Specifies the time at which the message is to expire. Either absolute time: [dd-mmm-yyyy] hh:mm:ss, or delta time: dddd [hh:mm:ss] can be used.`, + OptArg: true, }, "/EXTRACT": { Description: `Specifies that the text of the previously read message should be included @@ -82,36 +77,21 @@ 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, }, - "/LOCAL": { - Description: `Specifies that when /BROADCAST is specified for a remote folder, the -message is broadcasted ONLY on the local node.`, - }, - "/NODES": { - Description: `/NODES=(nodes[,...]) - -Specifies to send the message to the listed DECNET nodes. The BULLETIN -utility must be installed properly on the other nodes. (See -installation notes). You can specify a different username to use at the -other nodes by either using the USERNAME qualifier, or by specifying the -nodename with 2 semi-colons followed by the username, i.e. -nodename::username. If you specify a username, you will be prompted for -the password of the account on the other nodes. + "/INDENT": { + Description: `See /EXTRACT for information on this qualifier. -Additionally, you can specify logical names which translate to one or -more node names. I.e. $ DEFINE ALL_NODES "VAX1,VAX2,VAX3", and then -specify /NODES=ALL_NODES. Note that the quotation marks are required. - -NOTE: It is preferable to use /FOLDER instead of /NODE if possible, -since adding messages via /FOLDER is much quicker.`, - }, - "/NOINDENT": { - Description: `See /EXTRACT for information on this qualifier.`, +Defaults to set - use /NOINDENT to suppress.`, + Default: "true", }, - "/NOSIGNATURE": { - Description: `Specifies to suppress the automatically appended signature, if one exists. + "/SIGNATURE": { + Description: `Specifies to automatically appended signature, if one exists. Signatures are appended for postings to mailing lists and to responds. -See the help topic POST Signature_file for signature information.`, +See the help topic POST Signature_file for signature information. + +Defaults to set - use /NOSIGNATURE to suppress.`, + Default: "true", }, "/PERMANENT": { Description: `If specified, message will be a permanent message and will never expire. @@ -122,9 +102,10 @@ user has privileges.`, Description: `/SUBJECT=description Specifies the subject of the message to be added.`, + OptArg: true, }, "/SHUTDOWN": { - Description: `/SHUTDOWN[=nodename] + Description: `/SHUTDOWN This option is restricted to privileged users. If specified, message will be automatically deleted after a computer shutdown has occurred. This option is restricted to SYSTEM folders. @@ -137,6 +118,7 @@ node reboots, you have the option of specifying that node name. NOTE: If the folder is a remote folder, the message will be deleted after the remote node reboots, not the node from which the message was added. The nodename cannot be specified with a remote folder.`, + OptArg: true, }, "/SYSTEM": { Description: `This option is restricted to privileged users. If specified, message @@ -145,10 +127,6 @@ when a user logs in. System messages should be as brief as possible to avoid the possibility that system messages could scroll off the screen. This option is restricted to SYSTEM folders.`, }, - "/USERNAME": { - Description: `Specifies username to be used at remote DECNET nodes when adding messages -to DECNET nodes via the /NODE qualifier.`, - }, }, }, "BACK": { @@ -167,14 +145,6 @@ further reads in the selected folder. The default is /HEADER.`, }, }, }, - "BULLETIN": { - Description: `The BULLETIN utility permits a user to create a message for reading by -all users. Users are notified upon logging in that new messages have -been added, and what the topic of the messages are. Actual reading of -the messages is optional. (See the command SET READNEW for info on -automatic reading.) Messages are automatically deleted when their -expiration date has passed.`, - }, "CHANGE": { Description: `Replaces or modifies existing stored message. This is for changing part or all of a message without causing users who have already seen the @@ -207,6 +177,7 @@ added /EDIT to your BULLETIN command line.`, Specifies the time at which the message is to expire. Either absolute time: [dd-mmm-yyyy] hh:mm:ss, or delta time: dddd [hh:mm:ss] can be used. If no time is specified, you will be prompted for the time.`, + OptArg: true, }, "/GENERAL": { Description: `Specifies that the message is to be converted from a SYSTEM message to @@ -231,11 +202,12 @@ date and message headers can be changed if a range is specified. The key words CURRENT and LAST can also be specified in the range, in place of an actual number, i.e. CURRENT-LAST, 1-CURRENT, etc.`, + OptArg: true, }, "/PERMANENT": { Description: `Specifies that the message is to be made permanent.`, }, - "/SHUTDOWN[=nodename]": { + "/SHUTDOWN": { Description: `Specifies that the message is to expire after the next computer shutdown. This option is restricted to SYSTEM folders.`, }, @@ -243,6 +215,7 @@ shutdown. This option is restricted to SYSTEM folders.`, Description: `/SUBJECT=description Specifies the subject of the message to be added.`, + OptArg: true, }, "/SYSTEM": { Description: `Specifies that the message is to be made a SYSTEM message. This is a @@ -353,42 +326,24 @@ 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, + }, + "/EXPIRE": { + Description: `/EXPIRE=days + +Sets the default number of days for messages to expire. + +Default value is 14.`, + OptArg: true, + Default: "14", }, "/ID": { Description: `Designates that the name specified as the owner name is a rights identifier. The creator's process must have the identifier presently assigned to it. Any process which has that identifier assigned to it will be able to control the folder as if it were the folder's owner. -This is used to allow more than one use to control a folder. - -Note: This feature will not work during remote access to the folder.`, - }, - "/NODE": { - Description: `/NODE=node - -Specifies that the folder is a remote folder at the specified node. -A remote folder is a folder in which the messages are actually stored -on a folder at a remote DECNET node. The specified node is checked to -see if a folder of the same name is located on that node. If so, the -folder will then be modified to point to that folder. For example if -there was a folder on node A with name INFO, and you issued the command: - CREATE INFO/NODE=A -from node B, then if INFO is selected on node B, you will actually -obtain the folder INFO on node A. In this manner, a folder can be shared -between more than one node. This capability is only present if the BULLCP -process is running on the remote node via the BULL/STARTUP command. -If the remote folder name is different from the local folder name, the -remote folder name is specified using the /REMOTENAME qualifier. - -NOTE: If a message is added to a remote node, the message is stored -immediately. However, a user logging into another node might not be -immediately alerted that the message is present. That information is -only updated every 15 minutes (same algorithm for updating BBOARD -messages), or if a user accesses that folder. Thus, if the folder is -located on node A, and the message is added from node B, and a user logs -in to node C, the BULLETIN login notification might not notify the user -of the message. However, if the message is added with /BROADCAST, the -message will be broadcasted immediately to all nodes.`, +This is used to allow more than one use to control a folder.`, + OptArg: true, }, "/NOTIFY": { Description: `Specifies that all users automatically have NOTIFY set for this folder. @@ -399,6 +354,7 @@ more information.)`, Description: `/OWNER=username Specifies the owner of the folder. This is a privileged command. See also /ID.`, + OptArg: true, }, "/PRIVATE": { Description: `Specifies that the folder can only be accessed by users who have been @@ -413,12 +369,6 @@ compilation of this program). NOTE: See HELP SET ACCESS for more info.`, Description: `Specifies that all users automatically have READNEW set for this folder. Only a privileged user can use this qualifier. (See HELP SET READNEW for more information.)`, - }, - "/REMOTENAME": { - Description: `/REMOTENAME=foldername -Valid only if /NODE is present, i.e. that the folder is a remote folder. -Specifies the name of the remote folder name. If not specified, it is -assumed that the remote name is the same as the local name.`, }, "/SHOWNEW": { Description: `Specifies that all users automatically have SHOWNEW set for this folder. @@ -489,22 +439,6 @@ remote folder at a time.`, "/IMMEDIATE": { Description: `Specifies that the message is to be deleted immediately.`, }, - "/NODES": { - Description: `/NODES=(nodes[,...]) - -Specifies to delete the message at the listed DECNET nodes. The BULLETIN -utility must be installed properly on the other nodes. You can specify -a different username to use at the other nodes by either using the -USERNAME qualifier, or by specifying the nodename with 2 semi-colons -followed by the username, i.e. nodename::username. If you specify a -username, you will be prompted for the password of the account on the -other nodes. The /SUBJECT must be specified to identify the specific -message that is to be deleted. - -Additionally, you can specify logical names which translate to one or -more node names. I.e. $ DEFINE ALL_NODES "VAX1,VAX2,VAX3", and then -specify /NODES=ALL_NODES. Note that the quotation marks are required.`, - }, "/SUBJECT": { Description: `/SUBJECT=subject @@ -514,10 +448,7 @@ The specified subject need not be the exact subject of the message. It can be a substring of the subject. This is in case you have forgotten the exact subject that was specified. Case is not critical either. You will be notified if the deletion was successful.`, - }, - "/USERNAME": { - Description: `Specifies username to be used at remote DECNET nodes when deleting messages -on other DECNET nodes via the /NODE qualifier.`, + OptArg: true, }, }, }, @@ -552,6 +483,7 @@ folder.`, Description: `/END=message_number Indicates the last message number you want to display.`, + OptArg: true, }, "/FOLDERS": { Description: `Lists the available message folders. Shows last message date and number @@ -604,12 +536,14 @@ are to be displayed. This cannot be used in conjunction with /MARKED.`, Specifies that only messages which contain the specified string are to be displayed. This cannot be used in conjunction with /MARKED. If no string is specified, the previously specified string is used.`, + OptArg: true, }, "/SINCE": { Description: `/SINCE=date Displays a listing of all the messages created on or after the specified date. If no date is specified, the default is TODAY.`, + OptArg: true, }, "/START": { Description: `/START=message_number @@ -617,6 +551,7 @@ specified date. If no date is specified, the default is TODAY.`, Indicates the first message number you want to display. For example, to display all the messages beginning with number three, enter the command line DIRECTORY/START=3. Not valid with /FOLDER.`, + OptArg: true, }, "/SUBJECT": { Description: `/SUBJECT=[string] @@ -625,6 +560,7 @@ Specifies that only messages which contain the specified string in it's subject header are to be displayed. This cannot be used in conjunction with /MARKED. If no string is specified, the previously specified string is used.`, + OptArg: true, }, }, }, @@ -808,6 +744,7 @@ than one word, enclose the text in quotation marks ("). If you omit this qualifier, the description of the message will be used as the subject.`, + OptArg: true, }, }, }, @@ -879,6 +816,7 @@ Note: This feature will not work during remote access to the folder.`, Description: `/NAME=foldername Specifies a new name for the folder.`, + OptArg: true, }, "/OWNER": { Description: `/OWNER=username @@ -886,6 +824,7 @@ Specifies a new name for the folder.`, Specifies a new owner for the folder. If the owner does not have privileges, BULLETIN will prompt for the password of the new owner account in order to okay the modification. See also /ID.`, + OptArg: true, }, }, }, @@ -995,6 +934,7 @@ message is printed at the beginning. The default is to write the header.`, Indicates that you will be notified by a broadcast message when the file or files have been printed. If /NONOTIFY is specified, there is no notification. The default is /NOTIFY.`, + Default: "true", }, "/NOW": { Description: `Sends all messages that have been queued for printing with the PRINT @@ -1005,6 +945,7 @@ command during this session to the printer.`, The name of the queue to which a message is to be sent. If the /QUEUE qualifier is not specified, the message is queued to SYS$PRINT.`, + OptArg: true, }, }, }, @@ -1091,6 +1032,7 @@ the contents of the terminal's memory.`, Specifies to read the first message created on or after the specified date. If no date is specified, the default is TODAY.`, + OptArg: true, }, }, }, @@ -1102,6 +1044,7 @@ remove the folder. REMOVE folder-name`, MinArgs: 1, MaxArgs: 1, + Action: ActionRemove, }, "REPLY": { Description: `Adds message with subject of message being the subject of the currently @@ -1112,14 +1055,21 @@ the same as the ADD command except for /NOINDENT and /EXTRACT. REPLY [file-name]`, MaxArgs: 1, Flags: dclish.Flags{ + "/EDIT": { + Description: `Specifies that the editor is to be used for creating the reply +message.`, + }, "/EXTRACT": { Description: `Specifies that the text of the message should be included in the reply -mail message. This qualifier is valid only when used with /EDIT. The +message. This qualifier is valid only when used with /EDIT. The text of the message is indented with > at the beginning of each line. This can be suppressed with /NOINDENT.`, }, - "/NOINDENT": { - Description: `See /EXTRACT for information on this qualifier.`, + "/INDENT": { + Description: `See /EXTRACT for information on this qualifier. + +Defaults to set - use /NOINDENT to suppress.`, + Default: "true", }, }, }, @@ -1139,6 +1089,7 @@ of the message.`, "/CC": { Description: `/CC=user[s] Specifies additional users that should receive the reply.`, + OptArg: true, }, "/EDIT": { Description: `Specifies that the editor is to be used for creating the reply mail @@ -1156,13 +1107,19 @@ associated with the folder. The mailing list address should be stored in the folder description. See CREATE/DESCRIPTION or MODIFY/DESCRIPTION for more informaton.`, }, - "/NOINDENT": { - Description: `See /EXTRACT for information on this qualifier.`, + "/INDENT": { + Description: `See /EXTRACT for information on this qualifier. + +Defaults to set - use /NOINDENT to suppress.`, + Default: "true", }, - "/NOSIGNATURE": { - Description: `Specifies to suppress the automatically appended signature, if one exists. + "/SIGNATURE": { + Description: `Specifies to automatically appended signature, if one exists. Signatures are appended for postings to mailing lists and to responds. -See the help topic POST Signature_file for signature information.`, +See the help topic POST Signature_file for signature information. + +Defaults to set - use /NOSIGNATURE to suppress.`, + Default: "true", }, "/SUBJECT": { Description: `/SUBJECT=text @@ -1172,6 +1129,7 @@ than one word, enclose the text in quotation marks ("). If you omit this qualifier, the description of the message will be used as the subject preceeded by "RE: ".`, + OptArg: true, }, }, }, @@ -1208,6 +1166,7 @@ a match. If, during a search, no more matches or messages are found, the next folder in the list is automatically selected. The presently selected folder can be included in the search by specifying "" as the first folder in the list.`, + OptArg: true, }, "/REPLY": { Description: `Specifies that messages are to be searched for that are replies to the @@ -1223,6 +1182,7 @@ message.`, Description: `/START=message_number Specifies the message number to start the search at.`, + OptArg: true, }, "/SUBJECT": { Description: `Specifies that only the subject of the messages are to be searched.`, diff --git a/repl/folders.go b/repl/folders.go index 7611b81c7305ffd4cd0d33ffa2bdad60b10fe2fd..4dbbdade011eed3434455b00a69ba1bcd97ff296 100644 --- a/repl/folders.go +++ b/repl/folders.go @@ -2,11 +2,14 @@ package repl import ( + "errors" "fmt" + "strconv" "strings" "git.lyda.ie/kevin/bulletin/accounts" "git.lyda.ie/kevin/bulletin/dclish" + "git.lyda.ie/kevin/bulletin/folders" ) // ActionDirectory handles the `DIRECTORY` command. This lists all the @@ -30,8 +33,55 @@ func ActionIndex(cmd *dclish.Command) error { // ActionCreate handles the `CREATE` command. This creates a folder. func ActionCreate(cmd *dclish.Command) error { - fmt.Printf("TODO: implement CREATE:\n%s\n\n", cmd.Description) - return nil + options := folders.FolderOptions{} + if cmd.Flags["/ALWAYS"].Value == "true" { + options.Always = 1 + } + if cmd.Flags["/BRIEF"].Value == "true" { + options.Brief = 1 + } + if cmd.Flags["/DESCRIPTION"].Value == "" { + return errors.New("Description is required - use /DESCRIPTION") + } + options.Description = cmd.Flags["/DESCRIPTION"].Value + if cmd.Flags["/NOTIFY"].Value == "true" { + options.Notify = 1 + } + if cmd.Flags["/OWNER"].Value != "" { + options.Owner = cmd.Flags["/OWNER"].Value + } else { + options.Owner = accounts.User.Account + } + if cmd.Flags["/READNEW"].Value == "true" { + options.Readnew = 1 + } + if cmd.Flags["/SHOWNEW"].Value == "true" { + options.Shownew = 1 + } + if cmd.Flags["/SYSTEM"].Value == "true" { + options.System = 1 + } + if cmd.Flags["/EXPIRE"].Value != "" { + expire, err := strconv.Atoi(cmd.Flags["/EXPIRE"].Value) + if err != nil { + return fmt.Errorf("Invalid expiry value '%s'", cmd.Flags["/EXPIRE"].Value) + } + options.Expire = expire + } + options.Visibility = folders.FolderPublic + if cmd.Flags["/PRIVATE"].Value == "true" && cmd.Flags["/SEMIPRIVATE"].Value == "true" { + return errors.New("Private or semi-private - pick one") + } + if cmd.Flags["/PRIVATE"].Value == "true" { + options.Visibility = folders.FolderPrivate + } + if cmd.Flags["/SEMIPRIVATE"].Value == "true" { + options.Visibility = folders.FolderSemiPrivate + } + + err := accounts.User.Folders.CreateFolder(cmd.Args[0], options) + // TODO: handle the /ID flag. + return err } // ActionSelect handles the `SELECT` command. This selects a folder. @@ -45,3 +95,12 @@ func ActionModify(cmd *dclish.Command) error { fmt.Printf("TODO: implement MODIFY:\n%s\n\n", cmd.Description) return nil } + +// ActionRemove handles the `REMOVE` command. This modifies a folder. +func ActionRemove(cmd *dclish.Command) error { + err := accounts.User.Folders.DeleteFolder(cmd.Args[0]) + if err == nil { + fmt.Println("Folder removed.") + } + return err +} diff --git a/repl/help.go b/repl/help.go index d3dc916d1b3e5757455edaa0b3de6a653e7a9f51..e7b6ae555d0cfbfc16f51a76b3558631a1d822ac 100644 --- a/repl/help.go +++ b/repl/help.go @@ -10,6 +10,12 @@ import ( ) var helpmap = map[string]string{ + "BULLETIN": `The BULLETIN utility permits a user to create a message for reading by +all users. Users are notified upon logging in that new messages have +been added, and what the topic of the messages are. Actual reading of +the messages is optional. (See the command SET READNEW for info on +automatic reading.) Messages are automatically deleted when their +expiration date has passed.`, "FOLDERS": `All messages are divided into separate folders. The default folder is GENERAL. New folders can be created by any user. As an example, the following creates a folder for GAMES related messages: diff --git a/repl/misc.go b/repl/misc.go index a5b62f330ab8cca7e7b9d70b0e8d970d73fd51a0..0bb3a730a1739c9e7427221f2b61eda1cc483b24 100644 --- a/repl/misc.go +++ b/repl/misc.go @@ -2,7 +2,8 @@ package repl import ( - "errors" + "fmt" + "os" "git.lyda.ie/kevin/bulletin/accounts" "git.lyda.ie/kevin/bulletin/dclish" @@ -12,12 +13,16 @@ import ( func ActionQuit(_ *dclish.Command) error { accounts.User.Close() // TODO: IIRC, quit should not update unread data. Check old code to confirm. - return errors.New("QUIT") + fmt.Println("QUIT") + os.Exit(0) + return nil } // ActionExit handles the `EXIT` command. func ActionExit(_ *dclish.Command) error { accounts.User.Close() // TODO: update unread data. - return errors.New("EXIT") + fmt.Println("EXIT") + os.Exit(0) + return nil } diff --git a/repl/repl.go b/repl/repl.go index 0f3536642795c308ecc81df622652c00864cbf0d..fea8113eb9b94f495449022a4acbec5b5d1e9029 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -41,7 +41,7 @@ func Loop(user string) error { } err = commands.ParseAndRun(line) if err != nil { - return err + fmt.Printf("ERROR: %s.\n", err) } } }