From 3050e18f73c18a180b7144f5a0b25aeb49eb276f Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Thu, 15 May 2025 14:13:41 +0100
Subject: [PATCH] Implement remaining batch commands.

---
 NOTES.md                     |  4 ++--
 batch/batch.go               | 26 +++++++++++++++++++++-----
 folders/folders.go           | 13 ++++++-------
 folders/messages.go          | 17 +++++++----------
 folders/users.go             |  7 +++----
 go.mod                       | 21 +++++++++------------
 go.sum                       | 17 +++++++++++++++++
 storage/batch.sql.go         | 36 ++++++++++++++++++++++++++++++++++++
 storage/connection.go        |  6 ++++++
 storage/messages.sql.go      | 28 ++++++++++------------------
 storage/queries/batch.sql    |  7 +++++++
 storage/queries/messages.sql |  8 ++++----
 this/this.go                 |  3 +--
 13 files changed, 129 insertions(+), 64 deletions(-)
 create mode 100644 storage/batch.sql.go
 create mode 100644 storage/queries/batch.sql

diff --git a/NOTES.md b/NOTES.md
index 4278c03..89823c4 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -32,8 +32,8 @@ repl.commands?
   * this.Folder should be a storage.Folder.
   * Implement batch jobs
      * ~~Have install populate the database with some test data.~~
-     * reboot
-     * expire
+     * ~~reboot~~
+     * ~~expire~~
   * Implement each command.
     * Next: folder commands - ~~CREATE~~, ~~REMOVE~~, MODIFY, ~~INDEX~~, ~~SELECT~~
     * Messages: ~~ADD~~, CURRENT, ~~DIRECTORY~~, BACK, CHANGE,
diff --git a/batch/batch.go b/batch/batch.go
index 564dfe9..d4c0d6d 100644
--- a/batch/batch.go
+++ b/batch/batch.go
@@ -2,7 +2,6 @@
 package batch
 
 import (
-	"context"
 	_ "embed"
 	"errors"
 	"fmt"
@@ -26,18 +25,36 @@ var announce206 string
 
 // Reboot deletes all messages with `shutdown` set.
 func Reboot() int {
-	fmt.Println("TODO: Delete messages with shutdown != 0.")
+	ctx := storage.Context()
+
+	store, err := storage.Open()
+	ask.CheckErr(err)
+	q := storage.New(store.DB)
+
+	rows, err := q.DeleteAllShutdownMessages(ctx)
+	ask.CheckErr(err)
+	fmt.Printf("Removed %d shutdown messages\n", rows)
 	return 0
 }
 
 // Expire deletes all messages that have hit their expiration date.
 func Expire() int {
-	fmt.Println("TODO: expire messages.")
+	ctx := storage.Context()
+
+	store, err := storage.Open()
+	ask.CheckErr(err)
+	q := storage.New(store.DB)
+
+	rows, err := q.DeleteAllExpiredMessages(ctx)
+	ask.CheckErr(err)
+	fmt.Printf("Expired %d messages\n", rows)
 	return 0
 }
 
 // Install is an interactive command used to install the crontab.
 func Install() int {
+	ctx := storage.Context()
+
 	// Check if install has run before.
 	touchfile := path.Join(xdg.Home, ".bulletin-installed")
 	if _, err := os.Stat(touchfile); err == nil {
@@ -64,7 +81,6 @@ func Install() int {
 	ask.CheckErr(err)
 
 	// Seed data.
-	ctx := context.TODO()
 	ask.CheckErr(q.SeedUserSystem(ctx))
 	ask.CheckErr(q.SeedFolderGeneral(ctx))
 	ask.CheckErr(q.SeedGeneralOwner(ctx))
@@ -89,7 +105,7 @@ func Install() int {
 	crontab := &strings.Builder{}
 	template.Must(template.New("crontab").Parse(crontabTemplate)).
 		Execute(crontab, map[string]string{"Bulletin": bulletin})
-	fmt.Printf("Add this to crontab:\n\n%s\n", crontab.String())
+	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.
diff --git a/folders/folders.go b/folders/folders.go
index bf10e97..d34bfef 100644
--- a/folders/folders.go
+++ b/folders/folders.go
@@ -2,7 +2,6 @@
 package folders
 
 import (
-	"context"
 	"errors"
 	"strings"
 
@@ -42,7 +41,7 @@ func CreateFolder(owner string, options storage.CreateFolderParams) error {
 	}
 	options.Name = strings.ToUpper(options.Name)
 
-	ctx := context.TODO()
+	ctx := storage.Context()
 	tx, err := this.Store.Begin()
 	if err != nil {
 		return err
@@ -68,7 +67,7 @@ func CreateFolder(owner string, options storage.CreateFolderParams) error {
 // ListFolder provides a list of folders that this.User has access to.
 func ListFolder() ([]storage.ListFolderRow, error) {
 	// TODO: need to check access.
-	ctx := context.TODO()
+	ctx := storage.Context()
 	rows, err := this.Q.ListFolder(ctx)
 	if err != nil {
 		// TODO: process this error a bit more to give a better error message.
@@ -79,7 +78,7 @@ func ListFolder() ([]storage.ListFolderRow, error) {
 
 // FindFolder finds a folder based on the prefix.
 func FindFolder(name string) string {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	folder, _ := this.Q.FindFolderExact(ctx, name)
 	if folder != "" {
 		return folder
@@ -90,7 +89,7 @@ func FindFolder(name string) string {
 
 // IsFolderAccess checks if a user can access a folder.
 func IsFolderAccess(name, login string) bool {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	found, _ := this.Q.IsFolderAccess(ctx, storage.IsFolderAccessParams{
 		Name:  name,
 		Login: login,
@@ -100,7 +99,7 @@ func IsFolderAccess(name, login string) bool {
 
 // IsFolderOwner checks if a user is a folder owner.
 func IsFolderOwner(folder, login string) bool {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	found, _ := this.Q.IsFolderOwner(ctx, storage.IsFolderOwnerParams{
 		Folder: folder,
 		Login:  login,
@@ -111,7 +110,7 @@ func IsFolderOwner(folder, login string) bool {
 // DeleteFolder deletes a folder.
 func DeleteFolder(name string) error {
 	// TODO: make sure user can delete this table.
-	ctx := context.TODO()
+	ctx := storage.Context()
 	err := this.Q.DeleteFolder(ctx, name)
 	// TODO: process this error a bit more to give a better error message.
 	if err != nil {
diff --git a/folders/messages.go b/folders/messages.go
index 9649991..e59c2a9 100644
--- a/folders/messages.go
+++ b/folders/messages.go
@@ -1,7 +1,6 @@
 package folders
 
 import (
-	"context"
 	"errors"
 	"time"
 
@@ -11,7 +10,7 @@ import (
 
 // CreateMessage creates a new folder.
 func CreateMessage(author, subject, message, folder string, permanent, shutdown int, expiration *time.Time) error {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	if expiration == nil {
 		days, err := this.Q.GetFolderExpire(ctx, folder)
 		if err != nil {
@@ -27,7 +26,6 @@ func CreateMessage(author, subject, message, folder string, permanent, shutdown
 	// TODO: replace _ with rows and check.
 	err := this.Q.CreateMessage(ctx, storage.CreateMessageParams{
 		Folder:     folder,
-		Folder_2:   folder,
 		Author:     author,
 		Subject:    subject,
 		Message:    message,
@@ -41,7 +39,7 @@ func CreateMessage(author, subject, message, folder string, permanent, shutdown
 
 // ReadMessage reads a message for a user.
 func ReadMessage(login, folder string, msgid int64) (*storage.Message, error) {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	msg, err := this.Q.ReadMessage(ctx, storage.ReadMessageParams{
 		Folder: folder,
 		ID:     msgid,
@@ -64,12 +62,11 @@ func ReadMessage(login, folder string, msgid int64) (*storage.Message, error) {
 
 // NextMsgid gets the next message id.
 func NextMsgid(login, folder string, msgid int64) int64 {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	newid, err := this.Q.NextMsgid(ctx, storage.NextMsgidParams{
-		Folder:   folder,
-		Folder_2: folder,
-		Login:    login,
-		ID:       msgid,
+		Folder: folder,
+		Login:  login,
+		ID:     msgid,
 	})
 	if err != nil {
 		return 0
@@ -79,7 +76,7 @@ func NextMsgid(login, folder string, msgid int64) int64 {
 
 // ListMessages lists messages.
 func ListMessages(folder string) ([]storage.Message, error) {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	// TODO: options aren't implemented - need to set them?
 	rows, err := this.Q.ListMessages(ctx, folder)
 	return rows, err
diff --git a/folders/users.go b/folders/users.go
index 1530913..b685ccb 100644
--- a/folders/users.go
+++ b/folders/users.go
@@ -1,7 +1,6 @@
 package folders
 
 import (
-	"context"
 	"strings"
 
 	"git.lyda.ie/kevin/bulletin/storage"
@@ -10,7 +9,7 @@ import (
 
 // GetUser gets a user.
 func GetUser(login string) (*storage.User, error) {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	user, err := this.Q.GetUser(ctx, login)
 
 	return &user, err
@@ -18,7 +17,7 @@ func GetUser(login string) (*storage.User, error) {
 
 // AddUser adds a user.
 func AddUser(user storage.User) (*storage.User, error) {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	newuser, err := this.Q.AddUser(ctx, storage.AddUserParams{
 		Login: strings.ToUpper(user.Login),
 		Name:  user.Name,
@@ -29,7 +28,7 @@ func AddUser(user storage.User) (*storage.User, error) {
 
 // IsUserAdmin checks if a user is an admin.
 func IsUserAdmin(login string) bool {
-	ctx := context.TODO()
+	ctx := storage.Context()
 	found, _ := this.Q.IsUserAdmin(ctx, login)
 	return found == 1
 }
diff --git a/go.mod b/go.mod
index a8ad251..eeb07db 100644
--- a/go.mod
+++ b/go.mod
@@ -10,12 +10,12 @@ require (
 	github.com/golang-migrate/migrate/v4 v4.18.3
 	github.com/jmoiron/sqlx v1.4.0
 	github.com/rivo/tview v0.0.0-20250501113434-0c592cd31026
-	github.com/urfave/cli/v3 v3.3.2
+	github.com/urfave/cli/v3 v3.3.3
 	modernc.org/sqlite v1.37.0
 )
 
 require (
-	cel.dev/expr v0.19.1 // indirect
+	cel.dev/expr v0.23.1 // indirect
 	dario.cat/mergo v1.0.1 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/Masterminds/goutils v1.1.1 // indirect
@@ -36,7 +36,7 @@ require (
 	github.com/go-sql-driver/mysql v1.9.2 // indirect
 	github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
 	github.com/gofrs/uuid v4.4.0+incompatible // indirect
-	github.com/google/cel-go v0.24.1 // indirect
+	github.com/google/cel-go v0.25.0 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -75,7 +75,7 @@ require (
 	github.com/spf13/cast v1.7.0 // indirect
 	github.com/spf13/cobra v1.9.1 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	github.com/sqlc-dev/sqlc v1.29.0 // indirect
+	github.com/sqlc-dev/sqlc v1.29.1-0.20250428024728-34f8c1bad949 // indirect
 	github.com/stephenafamo/bob v0.34.2 // indirect
 	github.com/stephenafamo/scan v0.6.2 // indirect
 	github.com/stephenafamo/sqlparser v0.0.0-20250408111851-b937299b5b7d // indirect
@@ -101,19 +101,16 @@ require (
 	golang.org/x/term v0.32.0 // indirect
 	golang.org/x/text v0.25.0 // indirect
 	golang.org/x/tools v0.33.0 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
-	google.golang.org/grpc v1.71.1 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
+	google.golang.org/grpc v1.72.0 // indirect
 	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	modernc.org/libc v1.65.2 // indirect
+	modernc.org/libc v1.65.6 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
 	modernc.org/memory v1.10.0 // indirect
 	mvdan.cc/gofumpt v0.7.0 // indirect
 )
 
-tool (
-	github.com/sqlc-dev/sqlc/cmd/sqlc
-	github.com/stephenafamo/bob/gen/bobgen-sql
-)
+tool github.com/sqlc-dev/sqlc/cmd/sqlc
diff --git a/go.sum b/go.sum
index 4b3c073..5833472 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
 cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
+cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
 dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
 dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -64,6 +66,8 @@ github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FW
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
 github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
+github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
+github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
@@ -162,6 +166,8 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs=
 github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0=
+github.com/sqlc-dev/sqlc v1.29.1-0.20250428024728-34f8c1bad949 h1:K6Vv8kT/DO8Sc8oxi6Ozw/ZSJHtsi8vu1lwgAKzmRuw=
+github.com/sqlc-dev/sqlc v1.29.1-0.20250428024728-34f8c1bad949/go.mod h1:wkPfTV76Vj0283VNLNZ/EYqB5yYZd/RX5ZrfWYJ4vMY=
 github.com/stephenafamo/bob v0.34.2 h1:eXMmAE9YPKIyFKMXmI6wYI+dQxxuAnzulbmyHBctSGk=
 github.com/stephenafamo/bob v0.34.2/go.mod h1:EVqAHXIxKPppvrkVsy/+YiUyHDWueIh0srPENffFhNE=
 github.com/stephenafamo/scan v0.6.2 h1:mEjx1P1MuimqALCXfZEV8+KAiVcByrgngqKatgHag9I=
@@ -185,6 +191,8 @@ github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
 github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/urfave/cli/v3 v3.3.2 h1:BYFVnhhZ8RqT38DxEYVFPPmGFTEf7tJwySTXsVRrS/o=
 github.com/urfave/cli/v3 v3.3.2/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
+github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
+github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
 github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
 github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ=
@@ -304,10 +312,16 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
 google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
 google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
 google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
 google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
@@ -329,12 +343,15 @@ modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
 modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
 modernc.org/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss=
 modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE=
+modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
 modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
 modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
 modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
 modernc.org/libc v1.65.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw=
 modernc.org/libc v1.65.2/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc=
+modernc.org/libc v1.65.6 h1:OhJUhmuJ6MVZdqL5qmnd0/my46DKGFhSX4WOR7ijfyE=
+modernc.org/libc v1.65.6/go.mod h1:MOiGAM9lrMBT9L8xT1nO41qYl5eg9gCp9/kWhz5L7WA=
 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/storage/batch.sql.go b/storage/batch.sql.go
new file mode 100644
index 0000000..793f70d
--- /dev/null
+++ b/storage/batch.sql.go
@@ -0,0 +1,36 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: batch.sql
+
+package storage
+
+import (
+	"context"
+)
+
+const deleteAllExpiredMessages = `-- name: DeleteAllExpiredMessages :execrows
+DELETE FROM messages
+WHERE permanent != 0 AND expiration < CURRENT_TIMESTAMP
+`
+
+func (q *Queries) DeleteAllExpiredMessages(ctx context.Context) (int64, error) {
+	result, err := q.db.ExecContext(ctx, deleteAllExpiredMessages)
+	if err != nil {
+		return 0, err
+	}
+	return result.RowsAffected()
+}
+
+const deleteAllShutdownMessages = `-- name: DeleteAllShutdownMessages :execrows
+DELETE FROM messages
+WHERE permanent != 0 AND shutdown > 0
+`
+
+func (q *Queries) DeleteAllShutdownMessages(ctx context.Context) (int64, error) {
+	result, err := q.db.ExecContext(ctx, deleteAllShutdownMessages)
+	if err != nil {
+		return 0, err
+	}
+	return result.RowsAffected()
+}
diff --git a/storage/connection.go b/storage/connection.go
index ad10c3f..616afd2 100644
--- a/storage/connection.go
+++ b/storage/connection.go
@@ -1,6 +1,7 @@
 package storage
 
 import (
+	"context"
 	"embed"
 	"errors"
 	"os"
@@ -52,3 +53,8 @@ func Open() (*sqlx.DB, error) {
 	}
 	return db, nil
 }
+
+// Context the standard context for storage calls.
+func Context() context.Context {
+	return context.TODO()
+}
diff --git a/storage/messages.sql.go b/storage/messages.sql.go
index 5fd8d92..b922e09 100644
--- a/storage/messages.sql.go
+++ b/storage/messages.sql.go
@@ -14,14 +14,13 @@ const createMessage = `-- name: CreateMessage :exec
 INSERT INTO messages (
   id, folder, author, subject, message, permanent, shutdown, expiration
 ) VALUES (
-  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?),
-  ?, ?, ?, ?, ?, ?, ?)
+  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?1),
+  ?2, ?1, ?3, ?4, ?5, ?6, ?7)
 `
 
 type CreateMessageParams struct {
-	Folder     string
-	Folder_2   string
 	Author     string
+	Folder     string
 	Subject    string
 	Message    string
 	Permanent  int64
@@ -31,9 +30,8 @@ type CreateMessageParams struct {
 
 func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) error {
 	_, err := q.db.ExecContext(ctx, createMessage,
-		arg.Folder,
-		arg.Folder_2,
 		arg.Author,
+		arg.Folder,
 		arg.Subject,
 		arg.Message,
 		arg.Permanent,
@@ -56,24 +54,18 @@ func (q *Queries) GetFirstMessageID(ctx context.Context, folder string) (int64,
 
 const nextMsgid = `-- name: NextMsgid :one
 SELECT CAST(COALESCE(MIN(id), 0) AS INT) FROM messages AS m
-  WHERE m.folder = ? AND m.id > ?
-  AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ? AND s.login = ?)
+  WHERE m.folder = ?1 AND m.id > ?2
+  AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ?1 AND s.login = ?3)
 `
 
 type NextMsgidParams struct {
-	Folder   string
-	ID       int64
-	Folder_2 string
-	Login    string
+	Folder string
+	ID     int64
+	Login  string
 }
 
 func (q *Queries) NextMsgid(ctx context.Context, arg NextMsgidParams) (int64, error) {
-	row := q.db.QueryRowContext(ctx, nextMsgid,
-		arg.Folder,
-		arg.ID,
-		arg.Folder_2,
-		arg.Login,
-	)
+	row := q.db.QueryRowContext(ctx, nextMsgid, arg.Folder, arg.ID, arg.Login)
 	var column_1 int64
 	err := row.Scan(&column_1)
 	return column_1, err
diff --git a/storage/queries/batch.sql b/storage/queries/batch.sql
new file mode 100644
index 0000000..e78577f
--- /dev/null
+++ b/storage/queries/batch.sql
@@ -0,0 +1,7 @@
+-- name: DeleteAllExpiredMessages :execrows
+DELETE FROM messages
+WHERE permanent != 0 AND expiration < CURRENT_TIMESTAMP;
+
+-- name: DeleteAllShutdownMessages :execrows
+DELETE FROM messages
+WHERE permanent != 0 AND shutdown > 0;
diff --git a/storage/queries/messages.sql b/storage/queries/messages.sql
index 62e0489..0866f46 100644
--- a/storage/queries/messages.sql
+++ b/storage/queries/messages.sql
@@ -2,8 +2,8 @@
 INSERT INTO messages (
   id, folder, author, subject, message, permanent, shutdown, expiration
 ) VALUES (
-  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?),
-  ?, ?, ?, ?, ?, ?, ?);
+  (SELECT COALESCE(MAX(id), 0) + 1 FROM messages AS m WHERE m.folder = ?1),
+  ?2, ?1, ?3, ?4, ?5, ?6, ?7);
 
 -- name: SetMessageSeen :exec
 INSERT INTO seen (login, folder, msgid) VALUES (?, ?, ?);
@@ -16,5 +16,5 @@ SELECT * FROM messages WHERE folder = ? AND id = ?;
 
 -- name: NextMsgid :one
 SELECT CAST(COALESCE(MIN(id), 0) AS INT) FROM messages AS m
-  WHERE m.folder = ? AND m.id > ?
-  AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ? AND s.login = ?);
+  WHERE m.folder = ?1 AND m.id > ?2
+  AND id NOT IN (SELECT id FROM seen AS s WHERE s.folder = ?1 AND s.login = ?3);
diff --git a/this/this.go b/this/this.go
index 55ce0aa..9ea7e69 100644
--- a/this/this.go
+++ b/this/this.go
@@ -10,7 +10,6 @@ TODO: Eventually `this` will need to handle broadcast messages.
 package this
 
 import (
-	"context"
 	"errors"
 	"fmt"
 
@@ -50,7 +49,7 @@ func StartThis(login, name string) error {
 	}
 	Q = storage.New(Store.DB)
 
-	ctx := context.TODO()
+	ctx := storage.Context()
 	User, err = Q.GetUser(ctx, login)
 
 	if User.Login != login {
-- 
GitLab