hermeticum/server/db/db.go

250 lines
5.8 KiB
Go
Raw Normal View History

package db
import (
"context"
_ "embed"
"errors"
2022-07-28 03:30:23 +00:00
"fmt"
2022-07-28 01:45:21 +00:00
"log"
2022-07-28 03:30:23 +00:00
"math/rand"
"github.com/google/uuid"
"github.com/jackc/pgx/v4/pgxpool"
)
// go:embed schema.sql
var schema string
2022-08-01 15:15:18 +00:00
// TODO I have a suspicion that I'm going to want to move to an ORM like model where the object struct has a DB member and various methods on it. For now I'm just going to keep adding shit to the DB interface because if it doesn't get too long in the end then it's fine.
2022-07-28 01:45:21 +00:00
type DB interface {
2022-08-01 15:15:18 +00:00
// Accounts
2022-07-28 01:45:21 +00:00
CreateAccount(string, string) (*Account, error)
ValidateCredentials(string, string) (*Account, error)
GetAccount(string) (*Account, error)
StartSession(Account) (string, error)
EndSession(string) error
2022-08-01 15:15:18 +00:00
// Presence
AvatarBySessionID(string) (*Object, error)
BedroomBySessionID(string) (*Object, error)
MoveInto(toMove Object, container Object) error
}
type Account struct {
ID int
Name string
Pwhash string
}
type Object struct {
ID int
Avatar bool
Bedroom bool
Data map[string]string
2022-07-28 01:45:21 +00:00
}
type pgDB struct {
pool *pgxpool.Pool
}
2022-07-28 01:45:21 +00:00
func NewDB(connURL string) (DB, error) {
pool, err := pgxpool.Connect(context.Background(), connURL)
if err != nil {
return nil, err
}
2022-07-28 01:45:21 +00:00
pgdb := &pgDB{
pool: pool,
}
2022-07-28 01:45:21 +00:00
return pgdb, nil
}
2022-08-01 15:15:18 +00:00
func (db *pgDB) CreateAccount(name, password string) (account *Account, err error) {
ctx := context.Background()
tx, err := db.pool.Begin(ctx)
if err != nil {
2022-08-01 15:15:18 +00:00
return
}
2022-08-01 15:15:18 +00:00
defer tx.Rollback(ctx)
account = &Account{
Name: name,
Pwhash: password,
}
stmt := "INSERT INTO accounts (name, pwhash) VALUES ( $1, $2 ) RETURNING id"
err = tx.QueryRow(ctx, stmt, name, password).Scan(&account.ID)
// TODO handle and cleanup unqiue violations
if err != nil {
2022-08-01 15:15:18 +00:00
return
}
2022-08-01 15:15:18 +00:00
var pid int
stmt = "INSERT INTO permissions DEFAULT VALUES RETURNING id"
err = tx.QueryRow(ctx, stmt).Scan(&pid)
if err != nil {
2022-08-01 15:15:18 +00:00
return
}
2022-08-01 15:15:18 +00:00
data := map[string]string{}
data["name"] = account.Name
data["description"] = fmt.Sprintf("a gaseous form. it smells faintly of %s.", randSmell())
av := &Object{
Avatar: true,
Data: data,
}
stmt = "INSERT INTO objects ( avatar, data, perms, owner ) VALUES ( $1, $2, $3, $4 ) RETURNING id"
err = tx.QueryRow(ctx, stmt, av.Avatar, av.Data, pid, account.ID).Scan(&av.ID)
if err != nil {
return
}
stmt = "INSERT INTO permissions DEFAULT VALUES RETURNING id"
err = tx.QueryRow(ctx, stmt).Scan(&pid)
if err != nil {
return
}
bedroom := &Object{
Bedroom: true,
}
stmt = "INSERT INTO objects ( bedroom, data, perms, owner ) VALUES ( $1, $2, $3, $4 )"
_, err = tx.Exec(ctx, stmt, bedroom.Bedroom, bedroom.Data, pid, account.ID)
if err != nil {
return
}
err = tx.Commit(ctx)
2022-08-01 15:15:18 +00:00
return
}
2022-07-28 01:45:21 +00:00
func (db *pgDB) ValidateCredentials(name, password string) (*Account, error) {
a, err := db.GetAccount(name)
if err != nil {
return nil, err
}
// TODO hashing lol
if a.Pwhash != password {
return nil, errors.New("invalid credentials")
}
return a, nil
}
2022-08-01 15:15:18 +00:00
func (db *pgDB) GetAccount(name string) (a *Account, err error) {
a = &Account{}
stmt := "SELECT id, name, pwhash FROM accounts WHERE name = $1"
err = db.pool.QueryRow(context.Background(), stmt, name).Scan(&a.ID, &a.Name, &a.Pwhash)
return
}
2022-08-01 15:15:18 +00:00
func (db *pgDB) StartSession(a Account) (sid string, err error) {
sid = uuid.New().String()
_, err = db.pool.Exec(context.Background(),
"INSERT INTO sessions (id, account) VALUES ( $1, $2 )", sid, a.ID)
if err != nil {
2022-08-01 15:15:18 +00:00
return
}
2022-08-01 15:15:18 +00:00
// Clean up any ghosts to prevent avatar duplication
// TODO subquery
stmt := "DELETE FROM contains WHERE contained = (SELECT id FROM objects WHERE objects.avatar = true and objects.owner = $1)"
_, err = db.pool.Exec(context.Background(), stmt, a.ID)
if err != nil {
2022-08-01 15:15:18 +00:00
log.Printf("failed to clean up ghosts for %d: %s", a.ID, err.Error())
err = nil
}
return
}
2022-07-28 01:45:27 +00:00
2022-08-01 15:15:18 +00:00
func (db *pgDB) EndSession(sid string) (err error) {
2022-07-28 01:45:27 +00:00
if sid == "" {
log.Println("db.EndSession called with empty session id")
2022-08-01 15:15:18 +00:00
return
2022-07-28 01:45:27 +00:00
}
2022-08-01 15:15:18 +00:00
var o *Object
if o, err = db.AvatarBySessionID(sid); err == nil {
if _, err = db.pool.Exec(context.Background(),
"DELETE FROM contains WHERE contained = $1", o.ID); err != nil {
log.Printf("failed to remove avatar from room: %s", err.Error())
}
} else {
log.Printf("failed to find avatar for session %s: %s", sid, err.Error())
2022-07-28 01:45:27 +00:00
}
2022-08-01 15:15:18 +00:00
_, err = db.pool.Exec(context.Background(), "DELETE FROM sessions WHERE id = $1", sid)
2022-07-28 01:45:27 +00:00
2022-08-01 15:15:18 +00:00
return
2022-07-28 01:45:27 +00:00
}
2022-07-28 03:30:23 +00:00
2022-08-01 15:15:18 +00:00
func (db *pgDB) AvatarBySessionID(sid string) (avatar *Object, err error) {
avatar = &Object{}
// TODO subquery
stmt := `
SELECT (id,avatar,bedroom,data)
FROM objects WHERE avatar = true AND owner = (
SELECT a.id FROM sessions s JOIN accounts a ON s.account = a.id WHERE s.id = $1)`
err = db.pool.QueryRow(context.Background(), stmt, sid).Scan(
&avatar.ID, &avatar.Avatar, &avatar.Bedroom, &avatar.Data)
return
2022-07-28 03:30:23 +00:00
}
2022-08-01 15:15:18 +00:00
func (db *pgDB) BedroomBySessionID(sid string) (bedroom *Object, err error) {
bedroom = &Object{}
2022-07-28 03:30:23 +00:00
2022-08-01 15:15:18 +00:00
// TODO subquery
stmt := `
SELECT (id,avatar,bedroom,data)
FROM objects WHERE bedroom = true AND owner = (
SELECT a.id FROM sessions s JOIN accounts a ON s.account = a.id WHERE s.id = $1)`
err = db.pool.QueryRow(context.Background(), stmt, sid).Scan(
&bedroom.ID, &bedroom.Avatar, &bedroom.Bedroom, &bedroom.Data)
return
}
2022-07-28 03:30:23 +00:00
2022-08-01 15:15:18 +00:00
func (db *pgDB) MoveInto(toMove Object, container Object) error {
ctx := context.Background()
tx, err := db.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
2022-07-28 03:30:23 +00:00
2022-08-01 15:15:18 +00:00
stmt := "DELETE FROM contains WHERE contained = $1"
_, err = tx.Exec(ctx, stmt, toMove.ID)
2022-07-28 03:30:23 +00:00
if err != nil {
2022-08-01 15:15:18 +00:00
return err
2022-07-28 03:30:23 +00:00
}
2022-08-01 15:15:18 +00:00
stmt = "INSERT INTO contains (contained, container) VALUES ($1, $1)"
_, err = tx.Exec(ctx, stmt, toMove.ID, container.ID)
if err != nil {
return err
}
2022-07-28 03:30:23 +00:00
2022-08-01 15:15:18 +00:00
return tx.Commit(ctx)
2022-07-28 03:30:23 +00:00
}
func randSmell() string {
// TODO seeding
smells := []string{
"lavender",
"wet soil",
"juniper",
"pine sap",
"wood smoke",
}
ix := rand.Intn(len(smells))
return smells[ix]
}