2022-07-16 05:56:12 +00:00
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"
2022-07-16 05:56:12 +00:00
"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-16 05:56:12 +00:00
}
2022-07-28 01:45:21 +00:00
func NewDB ( connURL string ) ( DB , error ) {
pool , err := pgxpool . Connect ( context . Background ( ) , connURL )
2022-07-16 05:56:12 +00:00
if err != nil {
return nil , err
}
2022-07-28 01:45:21 +00:00
pgdb := & pgDB {
pool : pool ,
}
2022-07-16 05:56:12 +00:00
2022-07-28 01:45:21 +00:00
return pgdb , nil
2022-07-16 05:56:12 +00:00
}
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 )
2022-07-16 05:56:12 +00:00
if err != nil {
2022-08-01 15:15:18 +00:00
return
2022-07-16 05:56:12 +00:00
}
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
2022-07-16 06:54:18 +00:00
if err != nil {
2022-08-01 15:15:18 +00:00
return
2022-07-16 06:54:18 +00:00
}
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 )
2022-07-16 06:54:18 +00:00
if err != nil {
2022-08-01 15:15:18 +00:00
return
2022-07-16 06:54:18 +00:00
}
2022-07-16 05:56:12 +00:00
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-07-16 05:56:12 +00:00
2022-08-01 15:15:18 +00:00
return
2022-07-16 05:56:12 +00:00
}
2022-07-28 01:45:21 +00:00
func ( db * pgDB ) ValidateCredentials ( name , password string ) ( * Account , error ) {
a , err := db . GetAccount ( name )
2022-07-16 05:56:12 +00:00
if err != nil {
2022-07-16 06:54:18 +00:00
return nil , err
2022-07-16 05:56:12 +00:00
}
// TODO hashing lol
2022-07-16 06:54:18 +00:00
if a . Pwhash != password {
return nil , errors . New ( "invalid credentials" )
2022-07-16 05:56:12 +00:00
}
2022-07-16 06:54:18 +00:00
return a , nil
2022-07-16 05:56:12 +00:00
}
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-07-16 05:56:12 +00:00
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 )
2022-07-16 05:56:12 +00:00
if err != nil {
2022-08-01 15:15:18 +00:00
return
2022-07-16 05:56:12 +00:00
}
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 )
2022-07-16 05:56:12 +00:00
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
2022-07-16 05:56:12 +00:00
}
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 ]
}