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"
)
2022-12-23 06:03:37 +00:00
//go:embed schema.sql
2022-07-16 05:56:12 +00:00
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-12-14 07:52:29 +00:00
ClearSessions ( ) error
2022-08-01 15:15:18 +00:00
2022-12-23 06:03:37 +00:00
// General
GetObject ( owner , name string ) ( * Object , error )
// Defaults
Ensure ( ) error
2022-08-01 15:15:18 +00:00
// Presence
2022-12-14 07:52:29 +00:00
ActiveSessions ( ) ( [ ] Session , error )
2022-08-01 15:15:18 +00:00
AvatarBySessionID ( string ) ( * Object , error )
BedroomBySessionID ( string ) ( * Object , error )
MoveInto ( toMove Object , container Object ) error
2022-12-14 07:52:29 +00:00
Earshot ( Object ) ( [ ] Object , error )
2022-08-01 15:15:18 +00:00
}
type Account struct {
ID int
Name string
Pwhash string
2022-12-23 06:03:37 +00:00
God bool
2022-08-01 15:15:18 +00:00
}
2022-12-14 07:52:29 +00:00
type Session struct {
ID string
AccountID int
}
2022-08-01 15:15:18 +00:00
type Object struct {
ID int
Avatar bool
Bedroom bool
Data map [ string ] string
2022-12-14 07:52:29 +00:00
OwnerID int
2022-12-20 08:38:47 +00:00
Script 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-12-23 06:03:37 +00:00
// Ensure checks for and then creates default resources if they do not exist (like the Foyer)
func ( db * pgDB ) Ensure ( ) error {
// TODO this is sloppy but shrug
_ , err := db . pool . Exec ( context . Background ( ) , schema )
//log.Println(err)
sysAcc , err := db . GetAccount ( "system" )
2022-07-16 05:56:12 +00:00
if err != nil {
2022-12-23 06:03:37 +00:00
// TODO actually check error. for now assuming it means does not exist
sysAcc , err = db . CreateGod ( "system" , "" )
if err != nil {
return fmt . Errorf ( "failed to create system account: %w" , err )
}
2022-07-16 05:56:12 +00:00
}
2022-12-23 06:03:37 +00:00
log . Printf ( "%#v" , sysAcc )
if _ , err := db . GetObject ( "system" , "foyer" ) ; err != nil {
data := map [ string ] string { }
data [ "name" ] = "foyer"
data [ "description" ] = "a big room. the ceiling is painted with constellations."
foyer := & Object {
Data : data ,
Script : "" ,
// TODO default room script
}
if err = db . CreateObject ( sysAcc , foyer ) ; err != nil {
return err
}
}
return nil
}
2022-08-01 15:15:18 +00:00
2022-12-23 06:03:37 +00:00
func ( db * pgDB ) CreateGod ( name , password string ) ( account * Account , err error ) {
2022-08-01 15:15:18 +00:00
account = & Account {
Name : name ,
Pwhash : password ,
2022-12-23 06:03:37 +00:00
God : true ,
2022-08-01 15:15:18 +00:00
}
2022-12-23 06:03:37 +00:00
return account , db . createAccount ( account )
}
func ( db * pgDB ) CreateAccount ( name , password string ) ( account * Account , err error ) {
account = & Account {
Name : name ,
Pwhash : password ,
}
return account , db . createAccount ( account )
}
func ( db * pgDB ) createAccount ( account * Account ) ( err error ) {
ctx := context . Background ( )
tx , err := db . pool . Begin ( ctx )
if err != nil {
return
}
defer tx . Rollback ( ctx )
stmt := "INSERT INTO accounts (name, pwhash, god) VALUES ( $1, $2, $3 ) RETURNING id"
err = tx . QueryRow ( ctx , stmt , account . Name , account . Pwhash , account . God ) . Scan ( & account . ID )
2022-08-01 15:15:18 +00:00
// 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
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 ,
2022-12-23 06:03:37 +00:00
Script : "" ,
2022-08-01 15:15:18 +00:00
}
2022-12-23 06:03:37 +00:00
stmt = "INSERT INTO objects ( avatar, data, owner, script ) VALUES ( $1, $2, $3, $4 ) RETURNING id"
err = tx . QueryRow ( ctx , stmt , av . Avatar , av . Data , account . ID , av . Script ) . Scan ( & av . ID )
2022-08-01 15:15:18 +00:00
if err != nil {
return
}
2022-08-01 16:00:43 +00:00
stmt = "INSERT INTO permissions (object) VALUES ( $1 )"
_ , err = tx . Exec ( ctx , stmt , av . ID )
2022-08-01 15:15:18 +00:00
if err != nil {
return
}
2022-08-01 16:00:43 +00:00
data = map [ string ] string { }
data [ "name" ] = "your private bedroom"
2022-08-01 15:15:18 +00:00
bedroom := & Object {
Bedroom : true ,
2022-08-01 16:00:43 +00:00
Data : data ,
2022-12-23 06:03:37 +00:00
Script : "" ,
2022-08-01 16:00:43 +00:00
}
2022-12-23 06:03:37 +00:00
stmt = "INSERT INTO objects ( bedroom, data, owner, script ) VALUES ( $1, $2, $3, $4 ) RETURNING id"
err = tx . QueryRow ( ctx , stmt , bedroom . Bedroom , bedroom . Data , account . ID , bedroom . Script ) . Scan ( & bedroom . ID )
2022-08-01 16:00:43 +00:00
if err != nil {
return
2022-08-01 15:15:18 +00:00
}
2022-08-01 16:00:43 +00:00
stmt = "INSERT INTO permissions (object) VALUES ( $1 )"
_ , err = tx . Exec ( ctx , stmt , bedroom . ID )
2022-08-01 15:15:18 +00:00
if err != nil {
return
}
2022-12-23 06:03:37 +00:00
return tx . Commit ( ctx )
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
}
2022-12-23 06:03:37 +00:00
if a . Pwhash == "" {
return nil , errors . New ( "this account cannot be logged into" )
}
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 := `
2022-12-20 08:38:47 +00:00
SELECT id , avatar , data , owner , script
2022-08-01 15:15:18 +00:00
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 (
2022-12-20 08:38:47 +00:00
& avatar . ID , & avatar . Avatar , & avatar . Data , & avatar . OwnerID , & avatar . Script )
2022-08-01 15:15:18 +00:00
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 := `
2022-08-01 16:00:43 +00:00
SELECT id , bedroom , data
2022-08-01 15:15:18 +00:00
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 (
2022-08-01 16:00:43 +00:00
& bedroom . ID , & bedroom . Bedroom , & bedroom . Data )
2022-08-01 15:15:18 +00:00
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 16:00:43 +00:00
stmt = "INSERT INTO contains (contained, container) VALUES ($1, $2)"
2022-08-01 15:15:18 +00:00
_ , 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
}
2022-12-14 07:52:29 +00:00
func ( db * pgDB ) Earshot ( obj Object ) ( [ ] Object , error ) {
stmt := `
2022-12-20 08:38:47 +00:00
SELECT id , avatar , bedroom , data , owner , script FROM objects
2022-12-14 07:52:29 +00:00
WHERE id IN (
SELECT contained FROM contains
WHERE container = (
SELECT container FROM contains WHERE contained = $ 1 LIMIT 1 ) ) `
rows , err := db . pool . Query ( context . Background ( ) , stmt , obj . ID )
if err != nil {
return nil , err
}
out := [ ] Object { }
for rows . Next ( ) {
heard := Object { }
2022-12-20 08:38:47 +00:00
if err = rows . Scan (
& heard . ID , & heard . Avatar ,
& heard . Bedroom , & heard . Data ,
& heard . OwnerID , & heard . Script ) ; err != nil {
2022-12-14 07:52:29 +00:00
return nil , err
}
out = append ( out , heard )
}
return out , nil
}
2022-12-23 06:03:37 +00:00
func ( db * pgDB ) GetObject ( owner , name string ) ( obj * Object , err error ) {
ctx := context . Background ( )
obj = & Object { }
stmt := `
SELECT id , avatar , data , owner , script
FROM objects
WHERE owner = $ 1 AND data [ ' name ' ] = $ 2 `
err = db . pool . QueryRow ( ctx , stmt , owner , fmt . Sprintf ( ` "%s" ` , name ) ) . Scan (
& obj . ID , & obj . Avatar , & obj . Data , & obj . OwnerID , & obj . Script )
// TODO i think the escaping here is going to create a sadness ^
return
}
2022-12-14 07:52:29 +00:00
func ( db * pgDB ) ActiveSessions ( ) ( out [ ] Session , err error ) {
stmt := ` SELECT id, account FROM sessions `
rows , err := db . pool . Query ( context . Background ( ) , stmt )
if err != nil {
return
}
for rows . Next ( ) {
s := Session { }
if err = rows . Scan ( & s . ID , & s . AccountID ) ; err != nil {
return
}
out = append ( out , s )
}
return
}
func ( db * pgDB ) ClearSessions ( ) ( err error ) {
_ , err = db . pool . Exec ( context . Background ( ) , "DELETE FROM sessions" )
return
}
2022-12-23 06:03:37 +00:00
func ( db * pgDB ) CreateObject ( owner * Account , obj * Object ) error {
ctx := context . Background ( )
stmt := `
INSERT INTO objects ( avatar , bedroom , data , script , owner )
VALUES ( $ 1 , $ 2 , $ 3 , $ 4 , $ 5 )
RETURNING id
`
err := db . pool . QueryRow ( ctx , stmt ,
obj . Avatar , obj . Bedroom , obj . Data , obj . Script , owner . ID ) . Scan (
& obj . ID )
if err != nil {
return err
}
return nil
}
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 ]
}