From 2112f5d826e913d7d7481fdaf36ebafe4e9929ec Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Tue, 6 May 2025 08:40:13 +0100
Subject: [PATCH] Implement quit and exit

---
 NOTES.md             | 11 +++++++
 accounts/accounts.go | 76 +++++++++++++++++++++++++++++++++++++++++---
 dclish/dclish.go     | 12 ++++---
 go.mod               | 20 ++++++++++--
 go.sum               | 25 +++++++++++++++
 main.go              |  2 +-
 repl/command.go      |  2 ++
 repl/misc.go         | 28 ++++++++++++++++
 8 files changed, 164 insertions(+), 12 deletions(-)
 create mode 100644 repl/misc.go

diff --git a/NOTES.md b/NOTES.md
index 4e0fc81..5b09ab7 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -4,10 +4,21 @@ These are the development notes for the Go version.
 
 The idea is to use the help files to implement BULLETIN.
 
+ssh `authorized_keys` should look like this:
+
+```
+command="/home/bulletin/bin/bulletin -u alice",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty KEY-TYPE KEY
+```
+
+Have readline store a history file for the user.  Configure at the `accounts.Open` step.
+
+Look up how godoc does references to other things.
+
 ## Things to do
 
   * Implement a better dclish parser.
   * Implement each command.
+    * Next: HELP - move some commands to a Help array.
   * Decide on how the boards are managed.
 
 ## Module links
diff --git a/accounts/accounts.go b/accounts/accounts.go
index beb1c48..126fd7b 100644
--- a/accounts/accounts.go
+++ b/accounts/accounts.go
@@ -1,17 +1,85 @@
 // Package accounts manages accounts.
 package accounts
 
-import "errors"
+import (
+	"database/sql"
+	"errors"
+	"fmt"
+	"os"
+	"path"
+	"strings"
 
-// Verify verifies that an account exists.
-func Verify(acc string) error {
+	"github.com/adrg/xdg"
+	_ "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
+	FullName string
+	pref     *sql.DB
+	bull     *sql.DB
+}
+
+// User is the user for this process.  It is loaded by the `Verify` function.
+var User *UserData
+
+// ValidName makes sure that an account name is a valid name.
+func ValidName(acc string) error {
 	if acc == "" {
-		return errors.New("Empty account is invalid")
+		return errors.New("empty account is invalid")
+	}
+	if strings.ContainsAny(acc, "./") {
+		return fmt.Errorf("account name '%s' is invalid", acc)
+	}
+	return nil
+}
+
+// Open verifies that an account exists.
+func Open(acc string) error {
+	err := ValidName(acc)
+	if err != nil {
+		return err
+	}
+	User = &UserData{
+		Account: acc,
 	}
+
+	prefdir := path.Join(xdg.ConfigHome, "BULLETIN")
+	err = os.MkdirAll(prefdir, 0700)
+	if err != nil {
+		return errors.New("account preference directory problem")
+	}
+	User.pref, err = sql.Open("sqlite", path.Join(prefdir, acc, ".db"))
+	if err != nil {
+		return errors.New("account preference database problem")
+	}
+
+	bulldir := path.Join(xdg.ConfigHome, "BULLETIN")
+	err = os.MkdirAll(bulldir, 0700)
+	if err != nil {
+		return errors.New("bulletin directory problem")
+	}
+	User.bull, err = sql.Open("sqlite", path.Join(bulldir, acc, ".db"))
+	if err != nil {
+		return errors.New("bulletin database problem")
+	}
+
 	return nil
 }
 
+// Close closes the resources open for the account.
+func (u *UserData) Close() {
+	u.pref.Close()
+	u.bull.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
 }
diff --git a/dclish/dclish.go b/dclish/dclish.go
index aae2845..f7fbe85 100644
--- a/dclish/dclish.go
+++ b/dclish/dclish.go
@@ -7,7 +7,7 @@ import (
 )
 
 // ActionFunc is the function that a command runs.
-type ActionFunc func(string, *Command) error
+type ActionFunc func(*Command) error
 
 // Flag is a flag for a command.
 type Flag struct {
@@ -36,12 +36,16 @@ type Commands []*Command
 func (c Commands) ParseAndRun(line string) error {
 	// TODO: this doesn't handle a DCL command line completely.
 	words := strings.Fields(line)
-	fmt.Printf("TODO ParseAndRun sees: %s\n", words)
+	fmt.Printf("TODO ParseAndRun need to parse flags: %s\n", words)
 	cmd := strings.ToUpper(words[0])
 	for i := range c {
 		if c[i].Command == cmd {
-			fmt.Printf("Command help:\n%s\n", c[i].Description)
-			return nil
+			if c[i].Action == nil {
+				fmt.Printf("Command not implemented:\n%s\n", c[i].Description)
+				return nil
+			}
+			err := c[i].Action(c[i])
+			return err
 		}
 	}
 	fmt.Printf("ERROR: Unknown command '%s'\n", cmd)
diff --git a/go.mod b/go.mod
index a7c9849..f7faa44 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,21 @@ module git.lyda.ie/kevin/bulletin
 go 1.24.2
 
 require (
-	github.com/chzyer/readline v1.5.1 // indirect
-	github.com/urfave/cli/v3 v3.3.2 // indirect
-	golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect
+	github.com/adrg/xdg v0.5.3
+	github.com/chzyer/readline v1.5.1
+	github.com/urfave/cli/v3 v3.3.2
+	modernc.org/sqlite v1.37.0
+)
+
+require (
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.10.0 // indirect
 )
diff --git a/go.sum b/go.sum
index e3a5fcc..cd2ef07 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,33 @@
+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/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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o=
 github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+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/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=
+modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
+modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=
diff --git a/main.go b/main.go
index be02f5d..2ea43e3 100644
--- a/main.go
+++ b/main.go
@@ -29,7 +29,7 @@ func main() {
 		},
 		Action: func(_ context.Context, cmd *cli.Command) error {
 			user := cmd.String("user")
-			err := accounts.Verify(user)
+			err := accounts.Open(user)
 			if err != nil {
 				return err
 			}
diff --git a/repl/command.go b/repl/command.go
index dd0a2f9..e1297ea 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -874,6 +874,7 @@ is used.
 		Command: "EXIT",
 		Description: `Exits the BULLETIN program.
 `,
+		Action: ActionExit,
 	},
 	{
 		Command: "EXTRACT",
@@ -1740,6 +1741,7 @@ as the subject preceeded by "RE: ".
 		Command: "QUIT",
 		Description: `Exits the BULLETIN program.
 `,
+		Action: ActionQuit,
 	},
 	{
 		Command: "SEARCH",
diff --git a/repl/misc.go b/repl/misc.go
new file mode 100644
index 0000000..f8bb289
--- /dev/null
+++ b/repl/misc.go
@@ -0,0 +1,28 @@
+// Package repl implements the main event loop.
+package repl
+
+import (
+	"fmt"
+	"os"
+
+	"git.lyda.ie/kevin/bulletin/accounts"
+	"git.lyda.ie/kevin/bulletin/dclish"
+)
+
+// ActionQuit handles the `QUIT` command.
+func ActionQuit(_ *dclish.Command) error {
+	accounts.User.Close()
+	fmt.Println("QUIT")
+	// TODO: IIRC, quit should not update unread data.  Check.
+	os.Exit(0)
+	return nil
+}
+
+// ActionExit handles the `EXIT` command.
+func ActionExit(_ *dclish.Command) error {
+	accounts.User.Close()
+	fmt.Println("EXIT")
+	// TODO: update unread data.
+	os.Exit(0)
+	return nil
+}
-- 
GitLab