From f3c450a12c8faacbd3fbde58ec66cef026e7c222 Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Thu, 22 May 2025 21:48:57 +0100
Subject: [PATCH] Fix SET BRIEF/READNEW/SHOWNEW

---
 folders/folders.go                       |   4 +
 repl/command.go                          |  54 ++++------
 repl/folders.go                          |  12 ++-
 repl/set.go                              | 129 +++++++++++------------
 repl/show.go                             |  24 ++---
 storage/display.go                       |  51 ++++++++-
 storage/folders.sql.go                   |  86 +++++----------
 storage/migrations/1_create_table.up.sql |  12 ++-
 storage/models.go                        |   5 +-
 storage/queries/folders.sql              |  20 ++--
 storage/queries/seed.sql                 |  21 ++--
 storage/queries/users.sql                |   3 +
 storage/seed.sql.go                      |  21 ++--
 storage/standard.sql.go                  |  15 ++-
 storage/users.sql.go                     |  20 +++-
 15 files changed, 236 insertions(+), 241 deletions(-)

diff --git a/folders/folders.go b/folders/folders.go
index ed5cae8..d05a5e6 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -104,6 +104,10 @@ func IsFolderAccess(name, login string) bool {
 // IsFolderOwner checks if a user is a folder owner.
 func IsFolderOwner(folder, login string) bool {
 	ctx := storage.Context()
+	admin, _ := this.Q.IsUserAdmin(ctx, login)
+	if admin == 1 {
+		return true
+	}
 	found, _ := this.Q.IsFolderOwner(ctx, storage.IsFolderOwnerParams{
 		Folder: folder,
 		Login:  login,
diff --git a/repl/command.go b/repl/command.go
index e6c4488..e4b9031 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -270,14 +270,6 @@ BULLCOM.CLD.`,
 				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.`,
-				OptArg: true,
-			},
 			"/NOTIFY": {
 				Description: `  Specifies  that  all users  automatically  have  NOTIFY set  for  this
   folder. Only a  privileged user can use this qualifier.  (See HELP SET
@@ -286,8 +278,7 @@ BULLCOM.CLD.`,
 			"/OWNER": {
 				Description: `/OWNER=username
 
-  Specifies the owner of the folder.  This is a privileged command.  See
-  also /ID.`,
+  Specifies the owner of the folder.  This is a privileged command.`,
 				OptArg: true,
 			},
 			"/PRIVATE": {
@@ -602,15 +593,6 @@ of the folder or a user with privileges can use this command.
 			"/DESCRIPTION": {
 				Description: `  Specifies a new  description for the folder. You will  be prompted for
   the text of the description.`,
-			},
-			"/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.`,
 			},
 			"/NAME": {
 				Description: `/NAME=foldername
@@ -623,7 +605,7 @@ of the folder or a user with privileges can use this command.
 
   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.`,
+  account in order to okay the modification.`,
 				OptArg: true,
 			},
 		},
@@ -1196,11 +1178,11 @@ READNEW setting (and visa versa).
 						Description: `  Specifies that the  SET BRIEF option is the default  for all users for
   the specified folder. This is a privileged qualifier.`,
 					},
-					"/DEFAULT": {
-						Description: `  Specifies  that the  BRIEF option  is  the default  for the  specified
-  folder. This is a privileged qualifier.  It will only affect brand new
-  users (or  those that have  never logged in).  Use /ALL to  modify all
-  users.`,
+					"/FOLDER": {
+						Description: `/FOLDER=foldername
+
+  Specifies the folder for which the option is to modified.  If not
+  specified, the selected folder is modified.`,
 					},
 					"/PERMANENT": {
 						Description: `/[NO]PERMANENT
@@ -1221,12 +1203,6 @@ privileged qualifier.`,
 					"/ALL": {
 						Description: `  Specifies that the SET NOBRIEF option is the default for all users for
   the specified folder. This is a privileged qualifier.`,
-					},
-					"/DEFAULT": {
-						Description: `  Specifies that  the NOBRIEF  option is the  default for  the specified
-  folder. This is a privileged qualifier.  It will only affect brand new
-  users (or  those that have  never logged in).  Use /ALL to  modify all
-  users.`,
 					},
 					"/FOLDER": {
 						Description: `/FOLDER=foldername
@@ -1337,6 +1313,7 @@ PROMPT_EXPIRE is the default.
 
   Format:
     SET PROMPT_EXPIRE`,
+				Action: ActionSetPromptExpire,
 			},
 			"NOPROMPT_EXPIRE": {
 				Description: `The user will not be prompted,  and the default expiration (which is set
@@ -1344,6 +1321,7 @@ by SET DEFAULT_EXPIRE) will be used.
 
   Format:
     SET NOPROMPT_EXPIRE`,
+				Action: ActionSetNoPromptExpire,
 			},
 			"READNEW": {
 				Description: `Controls whether you will be prompted upon logging in  if  you  wish  to
@@ -1367,10 +1345,17 @@ present in those other folders.  Also, it is not possible  to  EXIT  the
 READNEW mode if there are SYSTEM folders which have new messages. Typing
 the EXIT command will cause you to skip to those folders.  (See HELP SET
 SYSTEM for a description of a SYSTEM folder).`,
+				Action: ActionSetReadNew,
 				Flags: dclish.Flags{
 					"/ALL": {
 						Description: `  Specifies that the SET READNEW option is the default for all users for
   the specified folder.  This is a privileged  qualifier.`,
+					},
+					"/FOLDER": {
+						Description: `/FOLDER=foldername
+
+  Specifies  the folder  for which  the option  is to  modified. If  not
+  specified, the selected folder is modified.`,
 					},
 					"/PERMANENT": {
 						Description: `/[NO]PERMANENT
@@ -1384,6 +1369,7 @@ SYSTEM for a description of a SYSTEM folder).`,
 				Description: `Turns off READNEW.
   Format:
     SET NOREADNEW`,
+				Action: ActionSetNoReadNew,
 				Flags: dclish.Flags{
 					"/ALL": {
 						Description: `  Specifies that the  SET NOREADNEW option is the default  for all users
@@ -1414,6 +1400,12 @@ In order to apply this to a specific folder,  first  select  the  folder
 					"/ALL": {
 						Description: `  Specifies that the SET SHOWNEW option is the default for all users for
   the specified folder.  This is a privileged  qualifier.`,
+					},
+					"/FOLDER": {
+						Description: `/FOLDER=foldername
+
+  Specifies  the folder  for which  the option  is to  modified. If  not
+  specified, the selected folder is modified.`,
 					},
 					"/PERMANENT": {
 						Description: `/[NO]PERMANENT
diff --git a/repl/folders.go b/repl/folders.go
index dc67560..c8c1e5b 100644
--- a/repl/folders.go
+++ b/repl/folders.go
@@ -38,7 +38,7 @@ func ActionCreate(cmd *dclish.Command) error {
 		options.Always = 1
 	}
 	if cmd.Flags["/BRIEF"].Value == "true" {
-		options.Brief = 1
+		options.Alert = 1
 	}
 	if cmd.Flags["/DESCRIPTION"].Value != "" {
 		options.Description = cmd.Flags["/DESCRIPTION"].Value
@@ -47,10 +47,10 @@ func ActionCreate(cmd *dclish.Command) error {
 		options.Notify = 1
 	}
 	if cmd.Flags["/READNEW"].Value == "true" {
-		options.Readnew = 1
+		options.Alert = 2
 	}
 	if cmd.Flags["/SHOWNEW"].Value == "true" {
-		options.Shownew = 1
+		options.Alert = 3
 	}
 	if cmd.Flags["/SYSTEM"].Value == "true" {
 		options.System = 1
@@ -62,6 +62,11 @@ func ActionCreate(cmd *dclish.Command) error {
 		}
 		options.Expire = int64(expire)
 	}
+	if (cmd.Flags["/BRIEF"].Set && cmd.Flags["/READNEW"].Set) ||
+		(cmd.Flags["/BRIEF"].Set && cmd.Flags["/SHOWNEW"].Set) ||
+		(cmd.Flags["/READNEW"].Set && cmd.Flags["/SHOWNEW"].Set) {
+		return errors.New("Can only set one of /BRIEF, /READNEW and /SHOWNEW")
+	}
 	options.Visibility = folders.FolderPublic
 	if cmd.Flags["/PRIVATE"].Value == "true" && cmd.Flags["/SEMIPRIVATE"].Value == "true" {
 		return errors.New("Private or semi-private - pick one")
@@ -92,7 +97,6 @@ func ActionCreate(cmd *dclish.Command) error {
 		return errors.New("Description must exist and be under 53 characters")
 	}
 	err := folders.CreateFolder(owner, options)
-	// TODO: handle the /ID flag.
 	return err
 }
 
diff --git a/repl/set.go b/repl/set.go
index b41e963..3cd9b8a 100644
--- a/repl/set.go
+++ b/repl/set.go
@@ -13,6 +13,47 @@ import (
 	"git.lyda.ie/kevin/bulletin/this"
 )
 
+func setAlert(cmd *dclish.Command, alert int64) error {
+	optAll := cmd.Flags["/ALL"].Value == "true"
+
+	optPerm := false
+	if f, ok := cmd.Flags["/PERMANENT"]; ok {
+		optPerm = f.Value == "true"
+	}
+
+	folder := this.Folder
+	if cmd.Flags["/FOLDER"].Value != "" {
+		folder = folders.FindFolder(cmd.Flags["/FOLDER"].Value)
+	}
+	if folder.Name == "" {
+		return errors.New("Folder does not exist")
+	}
+
+	if optAll || optPerm {
+		if !folders.IsFolderOwner(folder.Name, this.User.Login) {
+			return errors.New("Not an admin or folder owner")
+		}
+	}
+
+	ctx := storage.Context()
+	if optAll && optPerm {
+		return this.Q.UpdateFolderAlert(ctx, storage.UpdateFolderAlertParams{
+			Alert: alert + 3,
+			Name:  folder.Name,
+		})
+	}
+	if optAll {
+		return this.Q.UpdateFolderAlert(ctx, storage.UpdateFolderAlertParams{
+			Alert: alert,
+			Name:  folder.Name,
+		})
+	}
+	return this.Q.UpdateUserAlert(ctx, storage.UpdateUserAlertParams{
+		Alert: alert,
+		Login: this.User.Login,
+	})
+}
+
 // ActionSet handles the `SET` command.
 func ActionSet(cmd *dclish.Command) error {
 	fmt.Println(cmd.Description)
@@ -46,25 +87,13 @@ func ActionSetNoalways(_ *dclish.Command) error {
 }
 
 // ActionSetBrief handles the `SET BRIEF` command.
-func ActionSetBrief(_ *dclish.Command) error {
-	// TODO: parse flags.
-	ctx := storage.Context()
-	this.Q.UpdateFolderBrief(ctx, storage.UpdateFolderBriefParams{
-		Brief: 1,
-		Name:  this.Folder.Name,
-	})
-	return nil
+func ActionSetBrief(cmd *dclish.Command) error {
+	return setAlert(cmd, storage.AlertBrief)
 }
 
 // ActionSetNobrief handles the `SET NOBRIEF` command.
-func ActionSetNobrief(_ *dclish.Command) error {
-	// TODO: parse flags.
-	ctx := storage.Context()
-	this.Q.UpdateFolderBrief(ctx, storage.UpdateFolderBriefParams{
-		Brief: 1,
-		Name:  this.Folder.Name,
-	})
-	return nil
+func ActionSetNobrief(cmd *dclish.Command) error {
+	return setAlert(cmd, storage.AlertNone)
 }
 
 // ActionSetDefaultExpire handles the `SET DEFAULT_EXPIRE` command.
@@ -140,76 +169,44 @@ func ActionSetNonotify(_ *dclish.Command) error {
 	return nil
 }
 
-// ActionSetPrivileges handles the `SET PRIVILEGES` command.
-func ActionSetPrivileges(cmd *dclish.Command) error {
-	// TODO: OK, need a better parser.
-	switch cmd.Args[0] {
-	case "CREATE":
-		if len(cmd.Args) != 2 {
-			fmt.Println("ERROR: Must pass single login.")
-			return nil
-		}
-		fmt.Println("TODO: Create user creation routine - see repl/repl.go.")
-	case "DELETE":
-		if len(cmd.Args) != 2 {
-			fmt.Println("ERROR: Must pass single login.")
-			return nil
-		}
-		fmt.Println("TODO: Create user delete routine - see repl/repl.go.")
-	case "SSH":
-		fmt.Println("TODO: Create ssh routine.")
-	case "ADMIN":
-		fmt.Println("TODO: Create an admin bit set/unset routine.")
-	case "NOADMIN":
-		fmt.Println("TODO: Create an admin bit set/unset routine.")
-	case "MOD":
-		fmt.Println("TODO: Create a mod bit set/unset routine.")
-	case "NOMOD":
-		fmt.Println("TODO: Create a mod bit set/unset routine.")
-	case "ENABLE":
-		fmt.Println("TODO: Create a disable bit set/unset routine.")
-	case "DISABLE":
-		fmt.Println("TODO: Create a disable bit set/unset routine.")
-	default:
-		fmt.Println("ERROR: Command not understood.")
-	}
-	return nil
-}
-
 // ActionSetPromptExpire handles the `SET PROMPT_EXPIRE` command.
 func ActionSetPromptExpire(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetPromptExpire.")
+	ctx := storage.Context()
+	this.Q.UpdateUserPrompt(ctx, storage.UpdateUserPromptParams{
+		Login:  this.User.Login,
+		Prompt: 1,
+	})
 	return nil
 }
 
 // ActionSetNoPromptExpire handles the `SET NOPROMPT_EXPIRE` command.
 func ActionSetNoPromptExpire(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetNopromptExpire.")
+	ctx := storage.Context()
+	this.Q.UpdateUserPrompt(ctx, storage.UpdateUserPromptParams{
+		Login:  this.User.Login,
+		Prompt: 0,
+	})
 	return nil
 }
 
 // ActionSetReadNew handles the `SET READNEW` command.
-func ActionSetReadNew(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetReadNew.")
-	return nil
+func ActionSetReadNew(cmd *dclish.Command) error {
+	return setAlert(cmd, storage.AlertReadNew)
 }
 
 // ActionSetNoReadNew handles the `SET READNEW` command.
-func ActionSetNoReadNew(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetNoReadNew.")
-	return nil
+func ActionSetNoReadNew(cmd *dclish.Command) error {
+	return setAlert(cmd, storage.AlertNone)
 }
 
 // ActionSetShowNew handles the `SET SHOWNEW` command.
-func ActionSetShowNew(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetShowNew.")
-	return nil
+func ActionSetShowNew(cmd *dclish.Command) error {
+	return setAlert(cmd, storage.AlertShowNew)
 }
 
 // ActionSetNoShowNew handles the `SET SHOWNEW` command.
-func ActionSetNoShowNew(_ *dclish.Command) error {
-	fmt.Println("TODO: implement ActionSetNoShowNew.")
-	return nil
+func ActionSetNoShowNew(cmd *dclish.Command) error {
+	return setAlert(cmd, storage.AlertNone)
 }
 
 // ActionSetSystem handles the `SET SYSTEM` command.
diff --git a/repl/show.go b/repl/show.go
index e4c090e..fdcd1c0 100644
--- a/repl/show.go
+++ b/repl/show.go
@@ -28,16 +28,9 @@ func ActionShowFlags(_ *dclish.Command) error {
 		fmt.Println("  NOTIFY is set.")
 		flagset = true
 	}
-	if this.Folder.Readnew != 0 {
-		fmt.Println("  READNEW is set.")
-		flagset = true
-	}
-	if this.Folder.Brief != 0 {
-		fmt.Println("  BRIEF is set.")
-		flagset = true
-	}
-	if this.Folder.Shownew != 0 {
-		fmt.Println("  SHOWNEW is set.")
+	if this.Folder.Alert != 0 {
+		fmt.Printf("  %s is set.\n",
+			strings.ToUpper(storage.AlertString(this.Folder.Alert)))
 		flagset = true
 	}
 	if !flagset {
@@ -100,14 +93,9 @@ func ActionShowFolder(cmd *dclish.Command) error {
 	if this.Folder.Notify != 0 {
 		fmt.Println("  Default is NOTIFY.")
 	}
-	if this.Folder.Readnew != 0 {
-		fmt.Println("  Default is READNEW.")
-	}
-	if this.Folder.Brief != 0 {
-		fmt.Println("  Default is BRIEF.")
-	}
-	if this.Folder.Shownew != 0 {
-		fmt.Println("  Default is SHOWNEW.")
+	if this.Folder.Alert != 0 {
+		fmt.Printf("  %s is set.\n",
+			strings.ToUpper(storage.AlertString(this.Folder.Alert)))
 	}
 
 	return nil
diff --git a/storage/display.go b/storage/display.go
index b995426..607b781 100644
--- a/storage/display.go
+++ b/storage/display.go
@@ -6,6 +6,48 @@ import (
 	"time"
 )
 
+// Alert values.
+const (
+	AlertNone int64 = iota
+	AlertBrief
+	AlertReadNew
+	AlertShowNew
+	AlertBriefPerm
+	AlertReadNewPerm
+	AlertShowNewPerm
+)
+
+// AlertString translates an alert number to a string.
+func AlertString(alert int64) string {
+	var as = []string{
+		"None",
+		"Brief",
+		"ReadNew",
+		"ShowNew",
+	}
+	if alert < 0 || alert >= int64(len(as)) {
+		return "Unknown"
+	}
+	return as[alert]
+}
+
+// FolderAlertString translates an alert number to a string.
+func FolderAlertString(alert int64) string {
+	var as = []string{
+		"None",
+		"Brief",
+		"ReadNew",
+		"ShowNew",
+		"Brief (permanent)",
+		"ReadNew (permanent)",
+		"ShowNew (permanent)",
+	}
+	if alert < 0 || alert >= int64(len(as)) {
+		return "Unknown"
+	}
+	return as[alert]
+}
+
 // String renders a message.
 func (m *Message) String() string {
 	buf := &strings.Builder{}
@@ -37,26 +79,25 @@ func (m *Message) OneLine(expire bool) string {
 
 // String displays a user (mainly used for debugging).
 func (u User) String() string {
-	return fmt.Sprintf("%-12s %-25.25s [a%d, m%d, !%d, d%d] [%s]",
+	return fmt.Sprintf("%-12s %-25.25s [a%d, m%d, !%d, d%d, p%d] [%s]",
 		u.Login,
 		u.Name,
 		u.Admin,
 		u.Moderator,
 		u.Alert,
 		u.Disabled,
+		u.Prompt,
 		u.LastLogin.Format("06-01-02 15:04:05"))
 }
 
 // String displays a folder (mainly used for debugging).
 func (f Folder) String() string {
-	return fmt.Sprintf("Folder %s (%s) [a%d, b%d, n%d, rn%d, sn%d, sys%d, exp%d, v%d]",
+	return fmt.Sprintf("Folder %s (%s) [a%d, !<%s>, n%d, sys%d, exp%d, v%d]",
 		f.Name,
 		f.Description,
 		f.Always,
-		f.Brief,
+		FolderAlertString(f.Alert),
 		f.Notify,
-		f.Readnew,
-		f.Shownew,
 		f.System,
 		f.Expire,
 		f.Visibility)
diff --git a/storage/folders.sql.go b/storage/folders.sql.go
index 7476fc8..c24ab00 100644
--- a/storage/folders.sql.go
+++ b/storage/folders.sql.go
@@ -24,22 +24,18 @@ func (q *Queries) AddFolderOwner(ctx context.Context, arg AddFolderOwnerParams)
 }
 
 const createFolder = `-- name: CreateFolder :exec
-INSERT INTO folders (
-  name, always, brief, description, notify, readnew, shownew, system,
-  expire, visibility
-) VALUES (
-  ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
-)
+INSERT INTO folders
+    (name, always, alert, description, notify, system, expire, visibility)
+  VALUES
+    (?, ?, ?, ?, ?, ?, ?, ?)
 `
 
 type CreateFolderParams struct {
 	Name        string
 	Always      int64
-	Brief       int64
+	Alert       int64
 	Description string
 	Notify      int64
-	Readnew     int64
-	Shownew     int64
 	System      int64
 	Expire      int64
 	Visibility  int64
@@ -49,11 +45,9 @@ func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) erro
 	_, err := q.db.ExecContext(ctx, createFolder,
 		arg.Name,
 		arg.Always,
-		arg.Brief,
+		arg.Alert,
 		arg.Description,
 		arg.Notify,
-		arg.Readnew,
-		arg.Shownew,
 		arg.System,
 		arg.Expire,
 		arg.Visibility,
@@ -62,7 +56,7 @@ func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) erro
 }
 
 const findFolderExact = `-- name: FindFolderExact :one
-SELECT name, "always", brief, description, notify, readnew, shownew, system, expire, visibility, create_at, update_at FROM folders where name = ?
+SELECT name, "always", alert, description, notify, system, expire, visibility, create_at, update_at FROM folders where name = ?
 `
 
 func (q *Queries) FindFolderExact(ctx context.Context, name string) (Folder, error) {
@@ -71,11 +65,9 @@ func (q *Queries) FindFolderExact(ctx context.Context, name string) (Folder, err
 	err := row.Scan(
 		&i.Name,
 		&i.Always,
-		&i.Brief,
+		&i.Alert,
 		&i.Description,
 		&i.Notify,
-		&i.Readnew,
-		&i.Shownew,
 		&i.System,
 		&i.Expire,
 		&i.Visibility,
@@ -86,7 +78,7 @@ func (q *Queries) FindFolderExact(ctx context.Context, name string) (Folder, err
 }
 
 const findFolderPrefix = `-- name: FindFolderPrefix :one
-SELECT name, "always", brief, description, notify, readnew, shownew, system, expire, visibility, create_at, update_at FROM folders where name LIKE ?
+SELECT name, "always", alert, description, notify, system, expire, visibility, create_at, update_at FROM folders where name LIKE ?
 ORDER BY name
 LIMIT 1
 `
@@ -97,11 +89,9 @@ func (q *Queries) FindFolderPrefix(ctx context.Context, name string) (Folder, er
 	err := row.Scan(
 		&i.Name,
 		&i.Always,
-		&i.Brief,
+		&i.Alert,
 		&i.Description,
 		&i.Notify,
-		&i.Readnew,
-		&i.Shownew,
 		&i.System,
 		&i.Expire,
 		&i.Visibility,
@@ -227,31 +217,31 @@ func (q *Queries) ListFolderForAdmin(ctx context.Context) ([]ListFolderForAdminR
 	return items, nil
 }
 
-const updateFolderAlways = `-- name: UpdateFolderAlways :exec
-UPDATE folders SET always = ? WHERE name = ?
+const updateFolderAlert = `-- name: UpdateFolderAlert :exec
+UPDATE folders SET alert = ? WHERE name = ?
 `
 
-type UpdateFolderAlwaysParams struct {
-	Always int64
-	Name   string
+type UpdateFolderAlertParams struct {
+	Alert int64
+	Name  string
 }
 
-func (q *Queries) UpdateFolderAlways(ctx context.Context, arg UpdateFolderAlwaysParams) error {
-	_, err := q.db.ExecContext(ctx, updateFolderAlways, arg.Always, arg.Name)
+func (q *Queries) UpdateFolderAlert(ctx context.Context, arg UpdateFolderAlertParams) error {
+	_, err := q.db.ExecContext(ctx, updateFolderAlert, arg.Alert, arg.Name)
 	return err
 }
 
-const updateFolderBrief = `-- name: UpdateFolderBrief :exec
-UPDATE folders SET brief = ? WHERE name = ?
+const updateFolderAlways = `-- name: UpdateFolderAlways :exec
+UPDATE folders SET always = ? WHERE name = ?
 `
 
-type UpdateFolderBriefParams struct {
-	Brief int64
-	Name  string
+type UpdateFolderAlwaysParams struct {
+	Always int64
+	Name   string
 }
 
-func (q *Queries) UpdateFolderBrief(ctx context.Context, arg UpdateFolderBriefParams) error {
-	_, err := q.db.ExecContext(ctx, updateFolderBrief, arg.Brief, arg.Name)
+func (q *Queries) UpdateFolderAlways(ctx context.Context, arg UpdateFolderAlwaysParams) error {
+	_, err := q.db.ExecContext(ctx, updateFolderAlways, arg.Always, arg.Name)
 	return err
 }
 
@@ -283,34 +273,6 @@ func (q *Queries) UpdateFolderNotify(ctx context.Context, arg UpdateFolderNotify
 	return err
 }
 
-const updateFolderReadnew = `-- name: UpdateFolderReadnew :exec
-UPDATE folders SET readnew = ? WHERE name = ?
-`
-
-type UpdateFolderReadnewParams struct {
-	Readnew int64
-	Name    string
-}
-
-func (q *Queries) UpdateFolderReadnew(ctx context.Context, arg UpdateFolderReadnewParams) error {
-	_, err := q.db.ExecContext(ctx, updateFolderReadnew, arg.Readnew, arg.Name)
-	return err
-}
-
-const updateFolderShownew = `-- name: UpdateFolderShownew :exec
-UPDATE folders SET shownew = ? WHERE name = ?
-`
-
-type UpdateFolderShownewParams struct {
-	Shownew int64
-	Name    string
-}
-
-func (q *Queries) UpdateFolderShownew(ctx context.Context, arg UpdateFolderShownewParams) error {
-	_, err := q.db.ExecContext(ctx, updateFolderShownew, arg.Shownew, arg.Name)
-	return err
-}
-
 const updateFolderSystem = `-- name: UpdateFolderSystem :exec
 UPDATE folders SET system = ? WHERE name = ?
 `
diff --git a/storage/migrations/1_create_table.up.sql b/storage/migrations/1_create_table.up.sql
index 690387f..9ce50b9 100644
--- a/storage/migrations/1_create_table.up.sql
+++ b/storage/migrations/1_create_table.up.sql
@@ -3,8 +3,10 @@ CREATE TABLE users (
   name           VARCHAR(53)  NOT NULL,
   admin          INT          DEFAULT 0 NOT NULL,
   moderator      INT          DEFAULT 0 NOT NULL,
-  alert          INT          DEFAULT 0 NOT NULL,  --- 0=no, 1=brief, 2=readnew
+  --- 0=no, 1=brief, 2=readnew, 3=shownew
+  alert          INT          DEFAULT 0 NOT NULL,
   disabled       INT          DEFAULT 0 NOT NULL,
+  prompt         INT          DEFAULT 0 NOT NULL,
   last_broadcast TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
   last_login     TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
   create_at      TIMESTAMP    DEFAULT CURRENT_TIMESTAMP NOT NULL,
@@ -35,11 +37,10 @@ END;
 CREATE TABLE folders (
   name        VARCHAR(25)  PRIMARY KEY NOT NULL,
   always      INT          DEFAULT 0 NOT NULL,
-  brief       INT          DEFAULT 0 NOT NULL,
+  --- 0=no, 1(4)=brief(p), 2(5)=readnew(p), 3(6)=shownew(p)
+  alert       INT          DEFAULT 0 NOT NULL,
   description VARCHAR(53)  DEFAULT 0 NOT NULL,
   notify      INT          DEFAULT 0 NOT NULL,
-  readnew     INT          DEFAULT 0 NOT NULL,
-  shownew     INT          DEFAULT 0 NOT NULL,
   system      INT          DEFAULT 0 NOT NULL,
   expire      INT          DEFAULT 14 NOT NULL,
   --- public=0, semiprivate=1, private=2
@@ -174,7 +175,8 @@ CREATE TABLE folder_configs (
   folder      VARCHAR(25) REFERENCES folders(name)
               ON DELETE CASCADE ON UPDATE CASCADE NOT NULL,
   always      INT     DEFAULT 0 NOT NULL,
-  alert       INT     DEFAULT 0 NOT NULL,  --- 0=no, 1=brief, 2=readnew
+  --- 0=no, 1=brief, 2=readnew, 3=shownew
+  alert       INT     DEFAULT 0 NOT NULL,
   PRIMARY KEY (login, folder)
 ) WITHOUT ROWID;
 
diff --git a/storage/models.go b/storage/models.go
index 98bbe36..2c0d9c0 100644
--- a/storage/models.go
+++ b/storage/models.go
@@ -18,11 +18,9 @@ type Broadcast struct {
 type Folder struct {
 	Name        string
 	Always      int64
-	Brief       int64
+	Alert       int64
 	Description string
 	Notify      int64
-	Readnew     int64
-	Shownew     int64
 	System      int64
 	Expire      int64
 	Visibility  int64
@@ -89,6 +87,7 @@ type User struct {
 	Moderator     int64
 	Alert         int64
 	Disabled      int64
+	Prompt        int64
 	LastBroadcast time.Time
 	LastLogin     time.Time
 	CreateAt      time.Time
diff --git a/storage/queries/folders.sql b/storage/queries/folders.sql
index 3c5f8a2..9f7e956 100644
--- a/storage/queries/folders.sql
+++ b/storage/queries/folders.sql
@@ -1,10 +1,8 @@
 -- name: CreateFolder :exec
-INSERT INTO folders (
-  name, always, brief, description, notify, readnew, shownew, system,
-  expire, visibility
-) VALUES (
-  ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
-);
+INSERT INTO folders
+    (name, always, alert, description, notify, system, expire, visibility)
+  VALUES
+    (?, ?, ?, ?, ?, ?, ?, ?);
 
 -- name: AddFolderOwner :exec
 INSERT INTO owners (folder, login) VALUES (?, ?);
@@ -45,18 +43,12 @@ UPDATE folders SET always = ? WHERE name = ?;
 -- name: UpdateFolderSystem :exec
 UPDATE folders SET system = ? WHERE name = ?;
 
--- name: UpdateFolderBrief :exec
-UPDATE folders SET brief = ? WHERE name = ?;
+-- name: UpdateFolderAlert :exec
+UPDATE folders SET alert = ? WHERE name = ?;
 
 -- name: UpdateFolderNotify :exec
 UPDATE folders SET notify = ? WHERE name = ?;
 
--- name: UpdateFolderReadnew :exec
-UPDATE folders SET readnew = ? WHERE name = ?;
-
--- name: UpdateFolderShownew :exec
-UPDATE folders SET shownew = ? WHERE name = ?;
-
 -- name: UpdateFolderVisibility :exec
 UPDATE folders SET visibility = ? WHERE name = ?;
 
diff --git a/storage/queries/seed.sql b/storage/queries/seed.sql
index 5772eb0..8d76543 100644
--- a/storage/queries/seed.sql
+++ b/storage/queries/seed.sql
@@ -1,18 +1,17 @@
 -- name: SeedUserSystem :exec
-  INSERT INTO users (login, name, admin, moderator)
-         VALUES ('SYSTEM', 'System User', 1, 1);
+INSERT INTO users (login, name, admin, moderator)
+  VALUES ('SYSTEM', 'System User', 1, 1);
 
 -- name: SeedFolderGeneral :exec
-  INSERT INTO folders (name, description, system, shownew)
-         VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1);
+INSERT INTO folders (name, description, system, alert)
+  VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1);
 
 -- name: SeedGeneralOwner :exec
-  INSERT INTO owners (folder, login) VALUES ('GENERAL', 'SYSTEM');
+INSERT INTO owners (folder, login) VALUES ('GENERAL', 'SYSTEM');
 
 -- name: SeedCreateMessage :exec
-INSERT INTO messages (
-  id,
-  folder, author, subject, message, permanent, create_at, update_at, expiration
-) VALUES (
-  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?1),
-  ?1, ?2, ?3, ?4, 1, ?5, ?5, ?6);
+INSERT INTO messages
+    (id, folder, author, subject, message, permanent, create_at, update_at, expiration)
+  VALUES
+    ((SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?1),
+     ?1, ?2, ?3, ?4, 1, ?5, ?5, ?6);
diff --git a/storage/queries/users.sql b/storage/queries/users.sql
index d87b015..ad343c7 100644
--- a/storage/queries/users.sql
+++ b/storage/queries/users.sql
@@ -23,6 +23,9 @@ UPDATE users SET name = ? WHERE login = ? AND login != 'SYSTEM';
 -- name: UpdateUserMod :exec
 UPDATE users SET moderator = ? WHERE login = ? AND login != 'SYSTEM';
 
+-- name: UpdateUserPrompt :exec
+UPDATE users SET prompt = ? WHERE login = ? AND login != 'SYSTEM';
+
 -- name: UpdateUserLastLogin :one
 UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE login = ? AND login != 'SYSTEM'
 RETURNING last_login;
diff --git a/storage/seed.sql.go b/storage/seed.sql.go
index 0f21d68..e33741e 100644
--- a/storage/seed.sql.go
+++ b/storage/seed.sql.go
@@ -11,12 +11,11 @@ import (
 )
 
 const seedCreateMessage = `-- name: SeedCreateMessage :exec
-INSERT INTO messages (
-  id,
-  folder, author, subject, message, permanent, create_at, update_at, expiration
-) VALUES (
-  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?1),
-  ?1, ?2, ?3, ?4, 1, ?5, ?5, ?6)
+INSERT INTO messages
+    (id, folder, author, subject, message, permanent, create_at, update_at, expiration)
+  VALUES
+    ((SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?1),
+     ?1, ?2, ?3, ?4, 1, ?5, ?5, ?6)
 `
 
 type SeedCreateMessageParams struct {
@@ -41,8 +40,8 @@ func (q *Queries) SeedCreateMessage(ctx context.Context, arg SeedCreateMessagePa
 }
 
 const seedFolderGeneral = `-- name: SeedFolderGeneral :exec
-  INSERT INTO folders (name, description, system, shownew)
-         VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1)
+INSERT INTO folders (name, description, system, alert)
+  VALUES ('GENERAL', 'Default general bulletin folder.', 1, 1)
 `
 
 func (q *Queries) SeedFolderGeneral(ctx context.Context) error {
@@ -51,7 +50,7 @@ func (q *Queries) SeedFolderGeneral(ctx context.Context) error {
 }
 
 const seedGeneralOwner = `-- name: SeedGeneralOwner :exec
-  INSERT INTO owners (folder, login) VALUES ('GENERAL', 'SYSTEM')
+INSERT INTO owners (folder, login) VALUES ('GENERAL', 'SYSTEM')
 `
 
 func (q *Queries) SeedGeneralOwner(ctx context.Context) error {
@@ -60,8 +59,8 @@ func (q *Queries) SeedGeneralOwner(ctx context.Context) error {
 }
 
 const seedUserSystem = `-- name: SeedUserSystem :exec
-  INSERT INTO users (login, name, admin, moderator)
-         VALUES ('SYSTEM', 'System User', 1, 1)
+INSERT INTO users (login, name, admin, moderator)
+  VALUES ('SYSTEM', 'System User', 1, 1)
 `
 
 func (q *Queries) SeedUserSystem(ctx context.Context) error {
diff --git a/storage/standard.sql.go b/storage/standard.sql.go
index be420e6..ea36c68 100644
--- a/storage/standard.sql.go
+++ b/storage/standard.sql.go
@@ -209,7 +209,7 @@ func (q *Queries) DeleteUser(ctx context.Context, login string) error {
 }
 
 const getFolder = `-- name: GetFolder :one
-SELECT name, "always", brief, description, notify, readnew, shownew, system, expire, visibility, create_at, update_at FROM folders WHERE name = ?
+SELECT name, "always", alert, description, notify, system, expire, visibility, create_at, update_at FROM folders WHERE name = ?
 `
 
 func (q *Queries) GetFolder(ctx context.Context, name string) (Folder, error) {
@@ -218,11 +218,9 @@ func (q *Queries) GetFolder(ctx context.Context, name string) (Folder, error) {
 	err := row.Scan(
 		&i.Name,
 		&i.Always,
-		&i.Brief,
+		&i.Alert,
 		&i.Description,
 		&i.Notify,
-		&i.Readnew,
-		&i.Shownew,
 		&i.System,
 		&i.Expire,
 		&i.Visibility,
@@ -460,7 +458,7 @@ func (q *Queries) ListFolderConfig(ctx context.Context) ([]FolderConfig, error)
 }
 
 const listFolders = `-- name: ListFolders :many
-SELECT name, "always", brief, description, notify, readnew, shownew, system, expire, visibility, create_at, update_at FROM folders
+SELECT name, "always", alert, description, notify, system, expire, visibility, create_at, update_at FROM folders
 `
 
 func (q *Queries) ListFolders(ctx context.Context) ([]Folder, error) {
@@ -475,11 +473,9 @@ func (q *Queries) ListFolders(ctx context.Context) ([]Folder, error) {
 		if err := rows.Scan(
 			&i.Name,
 			&i.Always,
-			&i.Brief,
+			&i.Alert,
 			&i.Description,
 			&i.Notify,
-			&i.Readnew,
-			&i.Shownew,
 			&i.System,
 			&i.Expire,
 			&i.Visibility,
@@ -662,7 +658,7 @@ func (q *Queries) ListSeen(ctx context.Context) ([]Seen, error) {
 }
 
 const listUsers = `-- name: ListUsers :many
-SELECT login, name, admin, moderator, alert, disabled, last_broadcast, last_login, create_at, update_at FROM users
+SELECT login, name, admin, moderator, alert, disabled, prompt, last_broadcast, last_login, create_at, update_at FROM users
 `
 
 func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
@@ -681,6 +677,7 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
 			&i.Moderator,
 			&i.Alert,
 			&i.Disabled,
+			&i.Prompt,
 			&i.LastBroadcast,
 			&i.LastLogin,
 			&i.CreateAt,
diff --git a/storage/users.sql.go b/storage/users.sql.go
index 613b4c1..b80de52 100644
--- a/storage/users.sql.go
+++ b/storage/users.sql.go
@@ -12,7 +12,7 @@ import (
 
 const addUser = `-- name: AddUser :one
 INSERT INTO users (login, name, admin, disabled, last_login) VALUES (?, ?, ?, ?, ?)
-RETURNING login, name, admin, moderator, alert, disabled, last_broadcast, last_login, create_at, update_at
+RETURNING login, name, admin, moderator, alert, disabled, prompt, last_broadcast, last_login, create_at, update_at
 `
 
 type AddUserParams struct {
@@ -39,6 +39,7 @@ func (q *Queries) AddUser(ctx context.Context, arg AddUserParams) (User, error)
 		&i.Moderator,
 		&i.Alert,
 		&i.Disabled,
+		&i.Prompt,
 		&i.LastBroadcast,
 		&i.LastLogin,
 		&i.CreateAt,
@@ -128,7 +129,7 @@ func (q *Queries) GetLastLoginByLogin(ctx context.Context, login string) (GetLas
 }
 
 const getUser = `-- name: GetUser :one
-SELECT login, name, admin, moderator, alert, disabled, last_broadcast, last_login, create_at, update_at FROM users WHERE login = ?
+SELECT login, name, admin, moderator, alert, disabled, prompt, last_broadcast, last_login, create_at, update_at FROM users WHERE login = ?
 `
 
 func (q *Queries) GetUser(ctx context.Context, login string) (User, error) {
@@ -141,6 +142,7 @@ func (q *Queries) GetUser(ctx context.Context, login string) (User, error) {
 		&i.Moderator,
 		&i.Alert,
 		&i.Disabled,
+		&i.Prompt,
 		&i.LastBroadcast,
 		&i.LastLogin,
 		&i.CreateAt,
@@ -241,3 +243,17 @@ func (q *Queries) UpdateUserName(ctx context.Context, arg UpdateUserNameParams)
 	_, err := q.db.ExecContext(ctx, updateUserName, arg.Name, arg.Login)
 	return err
 }
+
+const updateUserPrompt = `-- name: UpdateUserPrompt :exec
+UPDATE users SET prompt = ? WHERE login = ? AND login != 'SYSTEM'
+`
+
+type UpdateUserPromptParams struct {
+	Prompt int64
+	Login  string
+}
+
+func (q *Queries) UpdateUserPrompt(ctx context.Context, arg UpdateUserPromptParams) error {
+	_, err := q.db.ExecContext(ctx, updateUserPrompt, arg.Prompt, arg.Login)
+	return err
+}
-- 
GitLab