From 008acd2f243957a28ecdaa867592a8b798a89687 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Mon, 19 May 2025 19:19:41 +0100
Subject: [PATCH] A bunch of improvements

  * Better onboarding - can pull ssh keys from github-like sites.
  * Cleaned up help.
  * Reviewed command status.  42 need work.
---
 NOTES.md         |  79 ++++++++++++++++-----------------
 batch/batch.go   |  47 ++++++++++++++++----
 key/key.go       |  34 +++++++++++++++
 main.go          |   2 +
 repl/accounts.go |  89 +++++++++++++++++++++++--------------
 repl/command.go  | 111 +++++++++++++++++++++++++++++++----------------
 repl/folders.go  |   1 +
 repl/messages.go |  33 ++++++++++----
 repl/show.go     |   5 ++-
 this/this.go     |  14 ++++++
 10 files changed, 287 insertions(+), 128 deletions(-)

diff --git a/NOTES.md b/NOTES.md
index dc077cc..0272fb0 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -13,7 +13,7 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
 [here](https://marc.vos.net/books/vms/help/mail/).
 
  ## sqlite
- 
+
   * sqlite trigger tracing: `.trace stdout --row --profile --stmt --expanded --plain --close`
   * https://sqlite.org/cli.html
   * https://www.sqlitetutorial.net/
@@ -21,55 +21,50 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
 ## Things to do
 
   * Run [godoc](http://localhost:6060/) and then review where the help text is lacking.
-  * Implement each command.
-    * Next: folder commands - MODIFY
-    * Messages edit: CHANGE, REPLY
-    * Moving messages: COPY, MOVE
-    * Mark messages: MARK, UNMARK
-    * ~~Compound commands: SET and SHOW - make HELP work for them.~~
-    * Mail: MAIL, FORWARD, RESPOND
-  * Review each command and fully implement it.
+  * Missing [MAIL] [RESPOND] [SET DEFAULT_EXPIRE] [SET READNEW] [SET NOREADNEW] [SET NOSHOWNEW] [SET NOPROMPT_EXPIRE] [SET EXPIRE_LIMIT] [SET PROMPT_EXPIRE] [SET SHOWNEW] [SHOW NEW] [SEARCH]
   * Run this.Skew.Safe() before... each command?  each write?
-  * Handle broadcast messages - have bulletin watch a directory and
-    display files from it.  Then have them delete the file if it's older
-    than 5 minutes (allow for failure)
+  * Handle broadcast messages - create a broadcast table and add an expiration column.
   * Database
     * trigger to limit values for 'visibility'?
   * Add commands:
-    * ~~A way to add / delete ssh keys.~~
     * Commands for a local mail system?
     * Commands to connect to Mattermost or mastodon?
-    * ~~Commands to manage users.~~
   * Make a spreadsheet for signups.
+  * Make sure ssh key is fully unique.
+
+Polishing.  Review each command and put a + next to each as it is
+fully done.
+
+Top level:
+
+  ADD       +BACK       BULLETIN   CHANGE     COPY       CREATE
+ +Ctrl-C    +CURRENT    DELETE     DIRECTORY +EXIT      +FIRST
+ +Folders    FORWARD   +HELP       INDEX     +Keypad    +LAST
+  MAIL      +MARK       MODIFY     MOVE      +NEXT       PRINT
+ +QUIT       READ       REMOVE     REPLY      RESPOND    SEARCH
+  SEEN       SELECT    +SET       +SHOW      +SSH       +UNMARK
+  UNSEEN    +USER
+
+SET:
+
+  ACCESS          +ALWAYS           BRIEF            DEFAULT_EXPIRE
+  EXPIRE_LIMIT     FOLDER          +NOALWAYS         NOBRIEF
+  NONOTIFY         NOPROMPT_EXPIRE  NOREADNEW        NOSHOWNEW
+  NOSYSTEM         NOTIFY           PROMPT_EXPIRE    READNEW
+  SHOWNEW          SYSTEM
+
+SHOW:
+
+ +FLAGS       FOLDER     -NEW        +PRIVILEGES  USER       +VERSION
+
+SSH:
+
+ +ADD       +DELETE    +FETCH     +LIST
+
+USER:
 
-Done:
-
-  * ~~Editor - need an embedded editor~~ Implemented using tview; good enough
-    * 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
-    * Using giu, a [text-editor](https://serge-hulne.medium.com/coding-a-simple-text-editor-in-go-using-giu-quick-and-dirty-b9b97ab41e4a) (needs cgo, no)
-    * [bubbletea](https://github.com/charmbracelet/bubbletea) seems to be the tui that's winning
-    * Another option is tview - [simpler](https://github.com/rivo/tview).
-  * ~~Implement batch jobs~~
-     * ~~Have install populate the database with some test data.~~
-     * ~~reboot~~
-     * ~~expire~~
-  * ~~Add a pager~~
-  * ~~SHOW VERSION~~
-  * ~~Check db version; notify user if it changes; refuse to write to db if it has.~~
-  * ~~this.Folder should be a storage.Folder.~~
-  * ~~Add some of the early announcements from the sources - see the
-    conversion branch - to the GENERAL folder.~~
-  * ~~Move to a storage layer.~~
-  * ~~Cleanup help output.~~
-    * ~~Remove the node/cluster/newsgroup/mailing-list related flags.~~
-    * ~~Remove BBOARD references.~~
-    * ~~format with `par w72j1`~~
-  * ~~Handle MARK for SELECT and DIRECTORY.~~
-  * ~~Remove all file related things.  Which means no need for most
-    (all?) /EDIT flags~~
-  * ~~Stop the seeded messages from being deleted by the expire batch command.~~
+ +ADD       +ADMIN     +DELETE    +DISABLE   +ENABLE     LIST
+ +MOD       +NAME      +NOADMIN   +NOMOD
 
 ## Module links
 
diff --git a/batch/batch.go b/batch/batch.go
index f17eb5c..0cb34b6 100644
--- a/batch/batch.go
+++ b/batch/batch.go
@@ -14,6 +14,7 @@ import (
 	"git.lyda.ie/kevin/bulletin/ask"
 	"git.lyda.ie/kevin/bulletin/key"
 	"git.lyda.ie/kevin/bulletin/storage"
+	"git.lyda.ie/kevin/bulletin/users"
 	"github.com/adrg/xdg"
 )
 
@@ -125,23 +126,53 @@ the first user.`)
 
 	// Install crontab.
 	bulletin, err := os.Executable()
-	if err != nil {
-		panic(err) // TODO: cleanup error handling.
-	}
+	ask.CheckErr(err)
 	crontab := &strings.Builder{}
 	template.Must(template.New("crontab").Parse(crontabTemplate)).
 		Execute(crontab, map[string]string{"Bulletin": bulletin})
 	fmt.Printf("Adding this to crontab:\n\n%s\n", crontab.String())
 	err = installCrontab(crontab.String())
-	if err != nil {
-		panic(err) // TODO: cleanup error handling.
-	}
+	ask.CheckErr(err)
 
 	// Mark that install has happened.
 	err = touch(touchfile)
-	if err != nil {
-		panic(err) // TODO: cleanup error handling.
+	ask.CheckErr(err)
+
+	return 0
+}
+
+// NewUser creates a new user based on command line arguments.
+func NewUser(args []string) int {
+	// Make sure we have enough args.
+	if len(args) != 3 {
+		fmt.Println("ERROR: Must supply login, site nickname and site username.")
+		return 1
 	}
 
+	// Create a user if missing.
+	login := strings.ToUpper(args[0])
+	err := users.ValidLogin(login)
+	ask.CheckErr(err)
+	store, err := storage.Open()
+	ask.CheckErr(err)
+	q := storage.New(store.DB)
+	ctx := storage.Context()
+	u, err := q.GetUser(ctx, login)
+	if u.Login == "" {
+		u, err = q.AddUser(ctx, storage.AddUserParams{
+			Login: login,
+		})
+		ask.CheckErr(err)
+	}
+	if u.Login == "" {
+		fmt.Println("ERROR: Failed to make user.")
+		return 1
+	}
+
+	response := key.Fetch(u.Login, args[1], args[2])
+	fmt.Println(response)
+	if strings.HasPrefix(response, "ERROR") {
+		return 1
+	}
 	return 0
 }
diff --git a/key/key.go b/key/key.go
index 634b254..77f267b 100644
--- a/key/key.go
+++ b/key/key.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"net/http"
 	"os"
 	"path"
 	"strings"
@@ -190,3 +191,36 @@ func Delete(public string) error {
 
 	return nil
 }
+
+// Fetch fetches keys and adds them.
+func Fetch(login, nickname, username string) string {
+	sites := map[string]string{
+		"codeberg": "https://codeberg.org/%s.keys",
+		"gitlab":   "https://gitlab.com/%s.keys",
+		"github":   "https://github.com/%s.keys",
+	}
+	site := sites[strings.ToLower(nickname)]
+	if site == "" {
+		return fmt.Sprintln("ERROR: site nickname unknown.")
+	}
+	url := fmt.Sprintf(site, username)
+	resp, err := http.Get(url)
+	if err != nil {
+		return fmt.Sprintf("ERROR: Failed to fetch ssh keys (%s).\n", err)
+	}
+	scanner := bufio.NewScanner(resp.Body)
+	keys := 0
+	for scanner.Scan() {
+		keyline := string(bytes.TrimSpace(scanner.Bytes()))
+		Add(login, keyline)
+		keys++
+	}
+	switch keys {
+	case 0:
+		return fmt.Sprintln("No keys added.")
+	case 1:
+		return fmt.Sprintln("Key is added.")
+	default:
+		return fmt.Sprintf("%d keys added.\n", keys)
+	}
+}
diff --git a/main.go b/main.go
index 06ba61b..8d48d46 100644
--- a/main.go
+++ b/main.go
@@ -49,6 +49,8 @@ func main() {
 					exitcode = batch.Expire()
 				case "install":
 					exitcode = batch.Install()
+				case "new-user":
+					exitcode = batch.NewUser(cmd.Args().Slice())
 				default:
 					fmt.Println("ERROR: can only run batch commands as SYSTEM.")
 					exitcode = 1
diff --git a/repl/accounts.go b/repl/accounts.go
index da8a3d8..6d6abeb 100644
--- a/repl/accounts.go
+++ b/repl/accounts.go
@@ -1,10 +1,7 @@
 package repl
 
 import (
-	"bufio"
-	"bytes"
 	"fmt"
-	"net/http"
 	"strings"
 
 	"git.lyda.ie/kevin/bulletin/ask"
@@ -23,12 +20,29 @@ func ActionUser(cmd *dclish.Command) error {
 
 // ActionUserAdd handles the `USER ADD` command.
 func ActionUserAdd(cmd *dclish.Command) error {
+	ctx := storage.Context()
 	login := strings.ToUpper(cmd.Args[0])
-	err := users.ValidLogin(login)
+	u, err := users.ValidExistingLogin(this.Q, login)
 	if err != nil {
 		fmt.Printf("ERROR: %s.\n", err)
 		return nil
 	}
+	if u.Login != "" {
+		fmt.Println("ERROR: User already exists.")
+		return nil
+	}
+	u, err = this.Q.AddUser(ctx, storage.AddUserParams{
+		Login: login,
+		Name:  cmd.Args[1],
+	})
+	if err != nil {
+		fmt.Printf("ERROR: %s.\n", err)
+		return nil
+	}
+	if u.Login == "" {
+		fmt.Println("ERROR: Failed to make user; unknown reason.")
+		return nil
+	}
 	return nil
 }
 
@@ -183,6 +197,35 @@ func ActionUserNomod(cmd *dclish.Command) error {
 	return actionUserMod(cmd, 0, "not a moderator")
 }
 
+// ActionUserName handles the `USER LIST` command.
+func ActionUserName(cmd *dclish.Command) error {
+	if len(cmd.Args) == 2 && this.User.Admin == 0 {
+		fmt.Println("ERROR: You are not an admin.")
+		return nil
+	}
+	login := this.User.Login
+	name := cmd.Args[0]
+	if len(cmd.Args) == 2 {
+		login = strings.ToUpper(cmd.Args[0])
+		name = cmd.Args[1]
+		_, err := users.ValidExistingLogin(this.Q, login)
+		if err != nil {
+			fmt.Printf("ERROR: %s.\n", err)
+			return nil
+		}
+	}
+	ctx := storage.Context()
+	err := this.Q.UpdateUserName(ctx,
+		storage.UpdateUserNameParams{
+			Login: login,
+			Name:  name,
+		})
+	if err != nil {
+		fmt.Printf("ERROR: Failed to update user name (%s).\n", err)
+	}
+	return nil
+}
+
 // ActionSSH handles the `SSH` command.
 func ActionSSH(cmd *dclish.Command) error {
 	fmt.Println(cmd.Description)
@@ -282,36 +325,18 @@ func ActionSSHDelete(cmd *dclish.Command) error {
 
 // ActionSSHFetch handles the `SSH FETCH` command.
 func ActionSSHFetch(cmd *dclish.Command) error {
-	sites := map[string]string{
-		"codeberg": "https://codeberg.org/%s.keys",
-		"gitlab":   "https://gitlab.com/%s.keys",
-		"github":   "https://github.com/%s.keys",
-	}
-	siteTemplate := sites[strings.ToLower(cmd.Args[0])]
-	if siteTemplate == "" {
-		fmt.Println("ERROR: site nickname unknown.")
-		return nil
-	}
-	site := fmt.Sprintf(siteTemplate, cmd.Args[1])
-	resp, err := http.Get(site)
-	if err != nil {
-		fmt.Printf("ERROR: Failed to fetch ssh keys (%s).\n", err)
+	login := this.User.Login
+	sitename := cmd.Args[0]
+	username := cmd.Args[1]
+	if len(cmd.Args) == 3 && this.User.Admin == 0 {
+		fmt.Println("ERROR: You are not an admin.")
 		return nil
 	}
-	scanner := bufio.NewScanner(resp.Body)
-	keys := 0
-	for scanner.Scan() {
-		keyline := string(bytes.TrimSpace(scanner.Bytes()))
-		key.Add(this.User.Login, keyline)
-		keys++
-	}
-	switch keys {
-	case 0:
-		fmt.Println("No keys added.")
-	case 1:
-		fmt.Println("Key is added.")
-	default:
-		fmt.Printf("%d keys added.\n", keys)
+	if len(cmd.Args) == 3 {
+		login = cmd.Args[0]
+		sitename = cmd.Args[1]
+		username = cmd.Args[2]
 	}
+	fmt.Print(key.Fetch(login, sitename, username))
 	return nil
 }
diff --git a/repl/command.go b/repl/command.go
index 06026a5..b2b37b9 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -1013,61 +1013,97 @@ message.`,
 The following actions are available:
 
   ADD        ADMIN      DELETE     DISABLE    ENABLE     LIST
-  MOD        NOADMIN    NOMOD
+  MOD        NAME       NOADMIN    NOMOD
 `,
 		Action: ActionUser,
 		Commands: dclish.Commands{
 			"ADD": {
-				Description: `  Creates a user.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserAdd,
+				Description: `  Creates a user. The name must be in quotes unless it's a single name.
+
+  Format:
+    USER ADD login name`,
+				MinArgs: 2,
+				MaxArgs: 2,
+				Action:  ActionUserAdd,
 			},
 			"DELETE": {
-				Description: `  Removes a user.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserDelete,
+				Description: `  Removes a user.
+
+  Format:
+    USER DELETE login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserDelete,
 			},
 			"ENABLE": {
-				Description: `  Enables a user.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserEnable,
+				Description: `  Enables a user.
+
+  Format:
+    USER ENABLE login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserEnable,
 			},
 			"DISABLE": {
-				Description: `  Disables a user.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserDisable,
+				Description: `  Disables a user.
+
+  Format:
+    USER DISABLE login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserDisable,
 			},
 			"ADMIN": {
-				Description: `  Makes a user an admin.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserAdmin,
+				Description: `  Makes a user an admin.
+
+  Format:
+    USER ADMIN login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserAdmin,
 			},
 			"NOADMIN": {
-				Description: `  Removes the admin bit from a user.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserNoadmin,
+				Description: `  Removes the admin bit from a user.
+
+  Format:
+    USER NOADMIN login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserNoadmin,
 			},
 			"LIST": {
 				Description: `  Lists all the users.  Must be an admin.`,
 				Action:      ActionUserList,
 			},
+			"NAME": {
+				Description: `  
+  Updates the  name for  the user.  Update name for  another user  if an
+  admin.  The  name should  be  in  quotes.  For  example, to  set  your
+  account's name to Grace Hoper, run USER NAME "Grace Hopper"
+
+  Format:
+    USER NAME [login] name`,
+				MinArgs: 1,
+				MaxArgs: 2,
+				Action:  ActionUserName,
+			},
 			"MOD": {
-				Description: `  Makes a user an mod.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserMod,
+				Description: `  Makes a user an mod.
+
+  Format:
+    USER MOD login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserMod,
 			},
 			"NOMOD": {
-				Description: `  Removes the mod bit from a user.`,
-				MinArgs:     1,
-				MaxArgs:     1,
-				Action:      ActionUserNomod,
+				Description: `  Removes the mod bit from a user.
+
+  Format:
+    USER NOMOD login`,
+				MinArgs: 1,
+				MaxArgs: 1,
+				Action:  ActionUserNomod,
 			},
 		},
 	},
@@ -1108,7 +1144,9 @@ The following commands are available:
 			},
 			"FETCH": {
 				Description: `  
-  Fetches  ssh keys  for a user from a site like github.
+  Fetches ssh keys  for a user from  a site like github.  Only an admin
+  can specify the login this will apply to - the three argument version
+  of this command.
 
   The following sites are supported:
 
@@ -1121,10 +1159,10 @@ The following commands are available:
     +---------------+--------------------------------+
 
     Format:
-      SSH FETCH site-nickname site-username`,
+      SSH FETCH [login] site-nickname site-username`,
 				Action:  ActionSSHFetch,
 				MinArgs: 2,
-				MaxArgs: 2,
+				MaxArgs: 3,
 			},
 			"LIST": {
 				Description: `  Prints a list of ssh keys for a  user. Only an admin can list ssh keys
@@ -1146,7 +1184,6 @@ characteristics of the BULLETIN Utility.
 
 The following options are available:
 
-  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE
   ACCESS           ALWAYS           BRIEF            DEFAULT_EXPIRE
   EXPIRE_LIMIT     FOLDER           NOALWAYS         NOBRIEF
   NONOTIFY         NOPROMPT_EXPIRE  NOREADNEW        NOSHOWNEW
diff --git a/repl/folders.go b/repl/folders.go
index 8c4b29d..5eb22c9 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -16,6 +16,7 @@ import (
 
 // ActionIndex handles the `INDEX` command.  This lists all the folders.
 func ActionIndex(_ *dclish.Command) error {
+	// TODO: Handle flags!
 	rows, err := folders.ListFolder()
 	if err != nil {
 		return err
diff --git a/repl/messages.go b/repl/messages.go
index 40eaee1..6c9ff65 100644
--- a/repl/messages.go
+++ b/repl/messages.go
@@ -174,10 +174,10 @@ func ActionBack(_ *dclish.Command) error {
 	if err != nil {
 		return err
 	}
-	// TODO: pager needs to report if the whole message was read
-	// and only increment if not.
-	pager.Pager(msg.String())
-	this.MsgID = msgid
+	if pager.Pager(msg.String()) {
+		this.MsgID = msgid
+		folders.MarkSeen([]int64{msgid})
+	}
 	return nil
 }
 
@@ -195,6 +195,13 @@ func ActionFirst(_ *dclish.Command) error {
 		return nil
 	}
 	this.MsgID = msgid
+	msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid)
+	if err != nil {
+		return err
+	}
+	if pager.Pager(msg.String()) {
+		folders.MarkSeen([]int64{msgid})
+	}
 	return nil
 }
 
@@ -206,6 +213,13 @@ func ActionLast(_ *dclish.Command) error {
 		return nil
 	}
 	this.MsgID = msgid
+	msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid)
+	if err != nil {
+		return err
+	}
+	if pager.Pager(msg.String()) {
+		folders.MarkSeen([]int64{msgid})
+	}
 	return nil
 }
 
@@ -220,10 +234,11 @@ func ActionNext(_ *dclish.Command) error {
 	if err != nil {
 		return err
 	}
-	// TODO: pager needs to report if the whole message was read
-	// and only increment if not.
 	pager.Pager(msg.String())
-	this.MsgID = msgid
+	if pager.Pager(msg.String()) {
+		folders.MarkSeen([]int64{msgid})
+		this.MsgID = msgid
+	}
 	return nil
 }
 
@@ -242,7 +257,7 @@ func ActionPrint(cmd *dclish.Command) error {
 	for _, msgid := range msgids {
 		msg, err := folders.ReadMessage(this.User.Login, this.Folder.Name, msgid)
 		if err != nil {
-			fmt.Printf("Message %d not found.\n")
+			fmt.Printf("Message %d not found.\n", msgid)
 		} else {
 			fmt.Print(msg.String())
 		}
@@ -290,6 +305,7 @@ func ActionForward(cmd *dclish.Command) error {
 
 // ActionSeen handles the `SEEN` command.
 func ActionSeen(cmd *dclish.Command) error {
+	// TODO: review help.
 	var err error
 	msgids := []int64{this.MsgID}
 	if len(cmd.Args) == 1 {
@@ -307,6 +323,7 @@ func ActionSeen(cmd *dclish.Command) error {
 
 // ActionUnseen handles the `UNSEEN` command.
 func ActionUnseen(cmd *dclish.Command) error {
+	// TODO: review help.
 	var err error
 	msgids := []int64{this.MsgID}
 	if len(cmd.Args) == 1 {
diff --git a/repl/show.go b/repl/show.go
index 80481f8..e4c090e 100644
--- a/repl/show.go
+++ b/repl/show.go
@@ -65,7 +65,10 @@ func ActionShowFolder(cmd *dclish.Command) error {
 
 	full := false
 	if cmd.Flags["/FULL"].Value == "true" {
-		// TODO: Check permissions.
+		if this.User.Admin == 0 {
+			fmt.Println("ERROR: You are not an admin.")
+			return nil
+		}
 		full = true
 	}
 	fmt.Printf("Settings for %s.\n", folder.Name)
diff --git a/this/this.go b/this/this.go
index 0a14a07..922426b 100644
--- a/this/this.go
+++ b/this/this.go
@@ -12,6 +12,7 @@ package this
 import (
 	"errors"
 	"fmt"
+	"strings"
 
 	"git.lyda.ie/kevin/bulletin/ask"
 	"git.lyda.ie/kevin/bulletin/storage"
@@ -37,6 +38,7 @@ var MsgID int64
 // StartThis starts a session.
 func StartThis(login string) error {
 	// Validate the login name.
+	login = strings.ToUpper(login)
 	err := users.ValidLogin(login)
 	if err != nil {
 		return err
@@ -70,6 +72,18 @@ func StartThis(login string) error {
 	} else {
 		User.LastLogin, _ = Q.UpdateUserLastLogin(ctx, User.Login)
 	}
+	if User.Name == "" {
+		fmt.Printf("Welcome new user %s\n", User.Login)
+		name, err := ask.GetLine("please enter your name: ")
+		if err != nil {
+			return err
+		}
+		User.Name = name
+		Q.UpdateUserName(ctx, storage.UpdateUserNameParams{
+			Name:  User.Name,
+			Login: User.Login,
+		})
+	}
 	if User.Disabled == 1 {
 		return errors.New("User is disabled")
 	}
-- 
GitLab