Stage 4: lobby & social (matchmaking, friends, blocks, chat+nudge, invitations, profile, email, multi-player drop-out)
Engine: multi-player drop-out-and-continue with a per-game tile disposition (remove default / return), resigned seats skipped and excluded from the win, leaver rack never revealed; 2-player behaviour unchanged. New domains (service/store, no HTTP yet): internal/social (friend request/accept graph, per-user blocks, per-game chat with nudge as a message kind, content filter via mvdan.cc/xurls/v2 + leet/separator normaliser + phone heuristic) and internal/lobby (in-memory variant-keyed matchmaking pool, friend-game invitations invite->accept with lazy 7-day expiry). account gains profile editing and the email confirm-code flow (Mailer seam: SMTP or log mailer). Migration 00003_social.sql + regenerated jet. main wires the new services into the server (accessors for the Stage 6 handlers); robot substitution stays in Stage 5, REST/stream/push in Stage 6/8. Docs (PLAN, ARCHITECTURE, FUNCTIONAL+ru, TESTING, README) updated.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Blocks struct {
|
||||
BlockerID uuid.UUID `sql:"primary_key"`
|
||||
BlockedID uuid.UUID `sql:"primary_key"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatMessages struct {
|
||||
MessageID uuid.UUID `sql:"primary_key"`
|
||||
GameID uuid.UUID
|
||||
SenderID uuid.UUID
|
||||
Kind string
|
||||
Body string
|
||||
SenderIP *string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type EmailConfirmations struct {
|
||||
ConfirmationID uuid.UUID `sql:"primary_key"`
|
||||
AccountID uuid.UUID
|
||||
Email string
|
||||
CodeHash string
|
||||
ExpiresAt time.Time
|
||||
Attempts int16
|
||||
ConsumedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Friendships struct {
|
||||
RequesterID uuid.UUID `sql:"primary_key"`
|
||||
AddresseeID uuid.UUID `sql:"primary_key"`
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
RespondedAt *time.Time
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GameInvitationInvitees struct {
|
||||
InvitationID uuid.UUID `sql:"primary_key"`
|
||||
AccountID uuid.UUID `sql:"primary_key"`
|
||||
Seat int16
|
||||
Response string
|
||||
RespondedAt *time.Time
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GameInvitations struct {
|
||||
InvitationID uuid.UUID `sql:"primary_key"`
|
||||
InviterID uuid.UUID
|
||||
Variant string
|
||||
TurnTimeoutSecs int32
|
||||
HintsAllowed bool
|
||||
HintsPerPlayer int16
|
||||
DropoutTiles string
|
||||
Status string
|
||||
GameID *uuid.UUID
|
||||
ExpiresAt time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -29,4 +29,5 @@ type Games struct {
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
FinishedAt *time.Time
|
||||
DropoutTiles string
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Blocks = newBlocksTable("backend", "blocks", "")
|
||||
|
||||
type blocksTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
BlockerID postgres.ColumnString
|
||||
BlockedID postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type BlocksTable struct {
|
||||
blocksTable
|
||||
|
||||
EXCLUDED blocksTable
|
||||
}
|
||||
|
||||
// AS creates new BlocksTable with assigned alias
|
||||
func (a BlocksTable) AS(alias string) *BlocksTable {
|
||||
return newBlocksTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new BlocksTable with assigned schema name
|
||||
func (a BlocksTable) FromSchema(schemaName string) *BlocksTable {
|
||||
return newBlocksTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new BlocksTable with assigned table prefix
|
||||
func (a BlocksTable) WithPrefix(prefix string) *BlocksTable {
|
||||
return newBlocksTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new BlocksTable with assigned table suffix
|
||||
func (a BlocksTable) WithSuffix(suffix string) *BlocksTable {
|
||||
return newBlocksTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newBlocksTable(schemaName, tableName, alias string) *BlocksTable {
|
||||
return &BlocksTable{
|
||||
blocksTable: newBlocksTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newBlocksTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newBlocksTableImpl(schemaName, tableName, alias string) blocksTable {
|
||||
var (
|
||||
BlockerIDColumn = postgres.StringColumn("blocker_id")
|
||||
BlockedIDColumn = postgres.StringColumn("blocked_id")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{BlockerIDColumn, BlockedIDColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{CreatedAtColumn}
|
||||
)
|
||||
|
||||
return blocksTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
BlockerID: BlockerIDColumn,
|
||||
BlockedID: BlockedIDColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var ChatMessages = newChatMessagesTable("backend", "chat_messages", "")
|
||||
|
||||
type chatMessagesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
MessageID postgres.ColumnString
|
||||
GameID postgres.ColumnString
|
||||
SenderID postgres.ColumnString
|
||||
Kind postgres.ColumnString
|
||||
Body postgres.ColumnString
|
||||
SenderIP postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type ChatMessagesTable struct {
|
||||
chatMessagesTable
|
||||
|
||||
EXCLUDED chatMessagesTable
|
||||
}
|
||||
|
||||
// AS creates new ChatMessagesTable with assigned alias
|
||||
func (a ChatMessagesTable) AS(alias string) *ChatMessagesTable {
|
||||
return newChatMessagesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new ChatMessagesTable with assigned schema name
|
||||
func (a ChatMessagesTable) FromSchema(schemaName string) *ChatMessagesTable {
|
||||
return newChatMessagesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new ChatMessagesTable with assigned table prefix
|
||||
func (a ChatMessagesTable) WithPrefix(prefix string) *ChatMessagesTable {
|
||||
return newChatMessagesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new ChatMessagesTable with assigned table suffix
|
||||
func (a ChatMessagesTable) WithSuffix(suffix string) *ChatMessagesTable {
|
||||
return newChatMessagesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newChatMessagesTable(schemaName, tableName, alias string) *ChatMessagesTable {
|
||||
return &ChatMessagesTable{
|
||||
chatMessagesTable: newChatMessagesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newChatMessagesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newChatMessagesTableImpl(schemaName, tableName, alias string) chatMessagesTable {
|
||||
var (
|
||||
MessageIDColumn = postgres.StringColumn("message_id")
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
SenderIDColumn = postgres.StringColumn("sender_id")
|
||||
KindColumn = postgres.StringColumn("kind")
|
||||
BodyColumn = postgres.StringColumn("body")
|
||||
SenderIPColumn = postgres.StringColumn("sender_ip")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, SenderIDColumn, KindColumn, BodyColumn, SenderIPColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{GameIDColumn, SenderIDColumn, KindColumn, BodyColumn, SenderIPColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{KindColumn, BodyColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return chatMessagesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
MessageID: MessageIDColumn,
|
||||
GameID: GameIDColumn,
|
||||
SenderID: SenderIDColumn,
|
||||
Kind: KindColumn,
|
||||
Body: BodyColumn,
|
||||
SenderIP: SenderIPColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var EmailConfirmations = newEmailConfirmationsTable("backend", "email_confirmations", "")
|
||||
|
||||
type emailConfirmationsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
ConfirmationID postgres.ColumnString
|
||||
AccountID postgres.ColumnString
|
||||
Email postgres.ColumnString
|
||||
CodeHash postgres.ColumnString
|
||||
ExpiresAt postgres.ColumnTimestampz
|
||||
Attempts postgres.ColumnInteger
|
||||
ConsumedAt postgres.ColumnTimestampz
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type EmailConfirmationsTable struct {
|
||||
emailConfirmationsTable
|
||||
|
||||
EXCLUDED emailConfirmationsTable
|
||||
}
|
||||
|
||||
// AS creates new EmailConfirmationsTable with assigned alias
|
||||
func (a EmailConfirmationsTable) AS(alias string) *EmailConfirmationsTable {
|
||||
return newEmailConfirmationsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new EmailConfirmationsTable with assigned schema name
|
||||
func (a EmailConfirmationsTable) FromSchema(schemaName string) *EmailConfirmationsTable {
|
||||
return newEmailConfirmationsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new EmailConfirmationsTable with assigned table prefix
|
||||
func (a EmailConfirmationsTable) WithPrefix(prefix string) *EmailConfirmationsTable {
|
||||
return newEmailConfirmationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new EmailConfirmationsTable with assigned table suffix
|
||||
func (a EmailConfirmationsTable) WithSuffix(suffix string) *EmailConfirmationsTable {
|
||||
return newEmailConfirmationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newEmailConfirmationsTable(schemaName, tableName, alias string) *EmailConfirmationsTable {
|
||||
return &EmailConfirmationsTable{
|
||||
emailConfirmationsTable: newEmailConfirmationsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newEmailConfirmationsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newEmailConfirmationsTableImpl(schemaName, tableName, alias string) emailConfirmationsTable {
|
||||
var (
|
||||
ConfirmationIDColumn = postgres.StringColumn("confirmation_id")
|
||||
AccountIDColumn = postgres.StringColumn("account_id")
|
||||
EmailColumn = postgres.StringColumn("email")
|
||||
CodeHashColumn = postgres.StringColumn("code_hash")
|
||||
ExpiresAtColumn = postgres.TimestampzColumn("expires_at")
|
||||
AttemptsColumn = postgres.IntegerColumn("attempts")
|
||||
ConsumedAtColumn = postgres.TimestampzColumn("consumed_at")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
allColumns = postgres.ColumnList{ConfirmationIDColumn, AccountIDColumn, EmailColumn, CodeHashColumn, ExpiresAtColumn, AttemptsColumn, ConsumedAtColumn, CreatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{AccountIDColumn, EmailColumn, CodeHashColumn, ExpiresAtColumn, AttemptsColumn, ConsumedAtColumn, CreatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{AttemptsColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return emailConfirmationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
ConfirmationID: ConfirmationIDColumn,
|
||||
AccountID: AccountIDColumn,
|
||||
Email: EmailColumn,
|
||||
CodeHash: CodeHashColumn,
|
||||
ExpiresAt: ExpiresAtColumn,
|
||||
Attempts: AttemptsColumn,
|
||||
ConsumedAt: ConsumedAtColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var Friendships = newFriendshipsTable("backend", "friendships", "")
|
||||
|
||||
type friendshipsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
RequesterID postgres.ColumnString
|
||||
AddresseeID postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
RespondedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type FriendshipsTable struct {
|
||||
friendshipsTable
|
||||
|
||||
EXCLUDED friendshipsTable
|
||||
}
|
||||
|
||||
// AS creates new FriendshipsTable with assigned alias
|
||||
func (a FriendshipsTable) AS(alias string) *FriendshipsTable {
|
||||
return newFriendshipsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new FriendshipsTable with assigned schema name
|
||||
func (a FriendshipsTable) FromSchema(schemaName string) *FriendshipsTable {
|
||||
return newFriendshipsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new FriendshipsTable with assigned table prefix
|
||||
func (a FriendshipsTable) WithPrefix(prefix string) *FriendshipsTable {
|
||||
return newFriendshipsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new FriendshipsTable with assigned table suffix
|
||||
func (a FriendshipsTable) WithSuffix(suffix string) *FriendshipsTable {
|
||||
return newFriendshipsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newFriendshipsTable(schemaName, tableName, alias string) *FriendshipsTable {
|
||||
return &FriendshipsTable{
|
||||
friendshipsTable: newFriendshipsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newFriendshipsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newFriendshipsTableImpl(schemaName, tableName, alias string) friendshipsTable {
|
||||
var (
|
||||
RequesterIDColumn = postgres.StringColumn("requester_id")
|
||||
AddresseeIDColumn = postgres.StringColumn("addressee_id")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
RespondedAtColumn = postgres.TimestampzColumn("responded_at")
|
||||
allColumns = postgres.ColumnList{RequesterIDColumn, AddresseeIDColumn, StatusColumn, CreatedAtColumn, RespondedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{StatusColumn, CreatedAtColumn, RespondedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, CreatedAtColumn}
|
||||
)
|
||||
|
||||
return friendshipsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
RequesterID: RequesterIDColumn,
|
||||
AddresseeID: AddresseeIDColumn,
|
||||
Status: StatusColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
RespondedAt: RespondedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var GameInvitationInvitees = newGameInvitationInviteesTable("backend", "game_invitation_invitees", "")
|
||||
|
||||
type gameInvitationInviteesTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
InvitationID postgres.ColumnString
|
||||
AccountID postgres.ColumnString
|
||||
Seat postgres.ColumnInteger
|
||||
Response postgres.ColumnString
|
||||
RespondedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type GameInvitationInviteesTable struct {
|
||||
gameInvitationInviteesTable
|
||||
|
||||
EXCLUDED gameInvitationInviteesTable
|
||||
}
|
||||
|
||||
// AS creates new GameInvitationInviteesTable with assigned alias
|
||||
func (a GameInvitationInviteesTable) AS(alias string) *GameInvitationInviteesTable {
|
||||
return newGameInvitationInviteesTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new GameInvitationInviteesTable with assigned schema name
|
||||
func (a GameInvitationInviteesTable) FromSchema(schemaName string) *GameInvitationInviteesTable {
|
||||
return newGameInvitationInviteesTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new GameInvitationInviteesTable with assigned table prefix
|
||||
func (a GameInvitationInviteesTable) WithPrefix(prefix string) *GameInvitationInviteesTable {
|
||||
return newGameInvitationInviteesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new GameInvitationInviteesTable with assigned table suffix
|
||||
func (a GameInvitationInviteesTable) WithSuffix(suffix string) *GameInvitationInviteesTable {
|
||||
return newGameInvitationInviteesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newGameInvitationInviteesTable(schemaName, tableName, alias string) *GameInvitationInviteesTable {
|
||||
return &GameInvitationInviteesTable{
|
||||
gameInvitationInviteesTable: newGameInvitationInviteesTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newGameInvitationInviteesTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newGameInvitationInviteesTableImpl(schemaName, tableName, alias string) gameInvitationInviteesTable {
|
||||
var (
|
||||
InvitationIDColumn = postgres.StringColumn("invitation_id")
|
||||
AccountIDColumn = postgres.StringColumn("account_id")
|
||||
SeatColumn = postgres.IntegerColumn("seat")
|
||||
ResponseColumn = postgres.StringColumn("response")
|
||||
RespondedAtColumn = postgres.TimestampzColumn("responded_at")
|
||||
allColumns = postgres.ColumnList{InvitationIDColumn, AccountIDColumn, SeatColumn, ResponseColumn, RespondedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{SeatColumn, ResponseColumn, RespondedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{ResponseColumn}
|
||||
)
|
||||
|
||||
return gameInvitationInviteesTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
InvitationID: InvitationIDColumn,
|
||||
AccountID: AccountIDColumn,
|
||||
Seat: SeatColumn,
|
||||
Response: ResponseColumn,
|
||||
RespondedAt: RespondedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// Code generated by go-jet DO NOT EDIT.
|
||||
//
|
||||
// WARNING: Changes to this file may cause incorrect behavior
|
||||
// and will be lost if the code is regenerated
|
||||
//
|
||||
|
||||
package table
|
||||
|
||||
import (
|
||||
"github.com/go-jet/jet/v2/postgres"
|
||||
)
|
||||
|
||||
var GameInvitations = newGameInvitationsTable("backend", "game_invitations", "")
|
||||
|
||||
type gameInvitationsTable struct {
|
||||
postgres.Table
|
||||
|
||||
// Columns
|
||||
InvitationID postgres.ColumnString
|
||||
InviterID postgres.ColumnString
|
||||
Variant postgres.ColumnString
|
||||
TurnTimeoutSecs postgres.ColumnInteger
|
||||
HintsAllowed postgres.ColumnBool
|
||||
HintsPerPlayer postgres.ColumnInteger
|
||||
DropoutTiles postgres.ColumnString
|
||||
Status postgres.ColumnString
|
||||
GameID postgres.ColumnString
|
||||
ExpiresAt postgres.ColumnTimestampz
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
DefaultColumns postgres.ColumnList
|
||||
}
|
||||
|
||||
type GameInvitationsTable struct {
|
||||
gameInvitationsTable
|
||||
|
||||
EXCLUDED gameInvitationsTable
|
||||
}
|
||||
|
||||
// AS creates new GameInvitationsTable with assigned alias
|
||||
func (a GameInvitationsTable) AS(alias string) *GameInvitationsTable {
|
||||
return newGameInvitationsTable(a.SchemaName(), a.TableName(), alias)
|
||||
}
|
||||
|
||||
// Schema creates new GameInvitationsTable with assigned schema name
|
||||
func (a GameInvitationsTable) FromSchema(schemaName string) *GameInvitationsTable {
|
||||
return newGameInvitationsTable(schemaName, a.TableName(), a.Alias())
|
||||
}
|
||||
|
||||
// WithPrefix creates new GameInvitationsTable with assigned table prefix
|
||||
func (a GameInvitationsTable) WithPrefix(prefix string) *GameInvitationsTable {
|
||||
return newGameInvitationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
|
||||
}
|
||||
|
||||
// WithSuffix creates new GameInvitationsTable with assigned table suffix
|
||||
func (a GameInvitationsTable) WithSuffix(suffix string) *GameInvitationsTable {
|
||||
return newGameInvitationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
|
||||
}
|
||||
|
||||
func newGameInvitationsTable(schemaName, tableName, alias string) *GameInvitationsTable {
|
||||
return &GameInvitationsTable{
|
||||
gameInvitationsTable: newGameInvitationsTableImpl(schemaName, tableName, alias),
|
||||
EXCLUDED: newGameInvitationsTableImpl("", "excluded", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func newGameInvitationsTableImpl(schemaName, tableName, alias string) gameInvitationsTable {
|
||||
var (
|
||||
InvitationIDColumn = postgres.StringColumn("invitation_id")
|
||||
InviterIDColumn = postgres.StringColumn("inviter_id")
|
||||
VariantColumn = postgres.StringColumn("variant")
|
||||
TurnTimeoutSecsColumn = postgres.IntegerColumn("turn_timeout_secs")
|
||||
HintsAllowedColumn = postgres.BoolColumn("hints_allowed")
|
||||
HintsPerPlayerColumn = postgres.IntegerColumn("hints_per_player")
|
||||
DropoutTilesColumn = postgres.StringColumn("dropout_tiles")
|
||||
StatusColumn = postgres.StringColumn("status")
|
||||
GameIDColumn = postgres.StringColumn("game_id")
|
||||
ExpiresAtColumn = postgres.TimestampzColumn("expires_at")
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
allColumns = postgres.ColumnList{InvitationIDColumn, InviterIDColumn, VariantColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, StatusColumn, GameIDColumn, ExpiresAtColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{InviterIDColumn, VariantColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, StatusColumn, GameIDColumn, ExpiresAtColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{HintsAllowedColumn, HintsPerPlayerColumn, DropoutTilesColumn, StatusColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
)
|
||||
|
||||
return gameInvitationsTable{
|
||||
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
|
||||
|
||||
//Columns
|
||||
InvitationID: InvitationIDColumn,
|
||||
InviterID: InviterIDColumn,
|
||||
Variant: VariantColumn,
|
||||
TurnTimeoutSecs: TurnTimeoutSecsColumn,
|
||||
HintsAllowed: HintsAllowedColumn,
|
||||
HintsPerPlayer: HintsPerPlayerColumn,
|
||||
DropoutTiles: DropoutTilesColumn,
|
||||
Status: StatusColumn,
|
||||
GameID: GameIDColumn,
|
||||
ExpiresAt: ExpiresAtColumn,
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
DefaultColumns: defaultColumns,
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ type gamesTable struct {
|
||||
CreatedAt postgres.ColumnTimestampz
|
||||
UpdatedAt postgres.ColumnTimestampz
|
||||
FinishedAt postgres.ColumnTimestampz
|
||||
DropoutTiles postgres.ColumnString
|
||||
|
||||
AllColumns postgres.ColumnList
|
||||
MutableColumns postgres.ColumnList
|
||||
@@ -90,9 +91,10 @@ func newGamesTableImpl(schemaName, tableName, alias string) gamesTable {
|
||||
CreatedAtColumn = postgres.TimestampzColumn("created_at")
|
||||
UpdatedAtColumn = postgres.TimestampzColumn("updated_at")
|
||||
FinishedAtColumn = postgres.TimestampzColumn("finished_at")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn}
|
||||
DropoutTilesColumn = postgres.StringColumn("dropout_tiles")
|
||||
allColumns = postgres.ColumnList{GameIDColumn, VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, DropoutTilesColumn}
|
||||
mutableColumns = postgres.ColumnList{VariantColumn, DictVersionColumn, SeedColumn, StatusColumn, PlayersColumn, ToMoveColumn, TurnStartedAtColumn, TurnTimeoutSecsColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, EndReasonColumn, CreatedAtColumn, UpdatedAtColumn, FinishedAtColumn, DropoutTilesColumn}
|
||||
defaultColumns = postgres.ColumnList{StatusColumn, ToMoveColumn, TurnStartedAtColumn, HintsAllowedColumn, HintsPerPlayerColumn, MoveCountColumn, CreatedAtColumn, UpdatedAtColumn, DropoutTilesColumn}
|
||||
)
|
||||
|
||||
return gamesTable{
|
||||
@@ -115,6 +117,7 @@ func newGamesTableImpl(schemaName, tableName, alias string) gamesTable {
|
||||
CreatedAt: CreatedAtColumn,
|
||||
UpdatedAt: UpdatedAtColumn,
|
||||
FinishedAt: FinishedAtColumn,
|
||||
DropoutTiles: DropoutTilesColumn,
|
||||
|
||||
AllColumns: allColumns,
|
||||
MutableColumns: mutableColumns,
|
||||
|
||||
@@ -12,7 +12,13 @@ package table
|
||||
func UseSchema(schema string) {
|
||||
AccountStats = AccountStats.FromSchema(schema)
|
||||
Accounts = Accounts.FromSchema(schema)
|
||||
Blocks = Blocks.FromSchema(schema)
|
||||
ChatMessages = ChatMessages.FromSchema(schema)
|
||||
Complaints = Complaints.FromSchema(schema)
|
||||
EmailConfirmations = EmailConfirmations.FromSchema(schema)
|
||||
Friendships = Friendships.FromSchema(schema)
|
||||
GameInvitationInvitees = GameInvitationInvitees.FromSchema(schema)
|
||||
GameInvitations = GameInvitations.FromSchema(schema)
|
||||
GameMoves = GameMoves.FromSchema(schema)
|
||||
GamePlayers = GamePlayers.FromSchema(schema)
|
||||
Games = Games.FromSchema(schema)
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
-- +goose Up
|
||||
-- Stage 4 lobby & social: the friend graph, per-user blocks, per-game chat (with
|
||||
-- nudge folded in as a message kind), email confirm-codes, and friend-game
|
||||
-- invitations -- plus the per-game drop-out tile disposition the multi-player
|
||||
-- engine needs. Matchmaking is an in-memory pool and persists nothing.
|
||||
SET search_path = backend, pg_catalog;
|
||||
|
||||
-- The disposition of a dropped-out player's tiles in a game with three or more
|
||||
-- seats (docs/ARCHITECTURE.md §6), chosen at creation: 'remove' burns them
|
||||
-- (default), 'return' puts them back in the bag. Moot for a two-player game,
|
||||
-- which ends on the first drop-out. engine.DropoutTiles owns the stable labels.
|
||||
ALTER TABLE games
|
||||
ADD COLUMN dropout_tiles text NOT NULL DEFAULT 'remove',
|
||||
ADD CONSTRAINT games_dropout_tiles_chk CHECK (dropout_tiles IN ('remove', 'return'));
|
||||
|
||||
-- The friend graph. A row is created by the requester as 'pending' and flipped to
|
||||
-- 'accepted' by the addressee; declining, cancelling or unfriending deletes the
|
||||
-- row. Friendship is symmetric: a player's friends are the accepted rows in
|
||||
-- either direction. A pair has at most one row (guarded in Go against either
|
||||
-- direction existing).
|
||||
CREATE TABLE friendships (
|
||||
requester_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
addressee_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
status text NOT NULL DEFAULT 'pending',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
responded_at timestamptz,
|
||||
PRIMARY KEY (requester_id, addressee_id),
|
||||
CONSTRAINT friendships_status_chk CHECK (status IN ('pending', 'accepted')),
|
||||
CONSTRAINT friendships_distinct_chk CHECK (requester_id <> addressee_id)
|
||||
);
|
||||
CREATE INDEX friendships_addressee_idx ON friendships (addressee_id);
|
||||
|
||||
-- Per-user blocks. blocker_id has blocked blocked_id; the effect is applied
|
||||
-- mutually by the social checks (a block in either direction suppresses chat
|
||||
-- visibility and prevents requests/invitations between the pair).
|
||||
CREATE TABLE blocks (
|
||||
blocker_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
blocked_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (blocker_id, blocked_id),
|
||||
CONSTRAINT blocks_distinct_chk CHECK (blocker_id <> blocked_id)
|
||||
);
|
||||
CREATE INDEX blocks_blocked_idx ON blocks (blocked_id);
|
||||
|
||||
-- Per-game chat. A nudge ("it's your move") is a kind='nudge' row with an empty
|
||||
-- body, so one journal carries both chatter and nudges. body is capped at 60
|
||||
-- runes (enforced again in Go on input, where the content filter also rejects
|
||||
-- links/emails/phone numbers). sender_ip holds the gateway-forwarded client IP as
|
||||
-- a validated string (text, not inet, to avoid go-jet literal friction; the
|
||||
-- gateway populates it in Stage 6). Chat is part of the game archive and is never
|
||||
-- purged; it cascades away only with its game.
|
||||
CREATE TABLE chat_messages (
|
||||
message_id uuid PRIMARY KEY,
|
||||
game_id uuid NOT NULL REFERENCES games (game_id) ON DELETE CASCADE,
|
||||
sender_id uuid NOT NULL REFERENCES accounts (account_id),
|
||||
kind text NOT NULL DEFAULT 'message',
|
||||
body text NOT NULL DEFAULT '',
|
||||
sender_ip text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT chat_messages_kind_chk CHECK (kind IN ('message', 'nudge')),
|
||||
CONSTRAINT chat_messages_body_len_chk CHECK (char_length(body) <= 60),
|
||||
CONSTRAINT chat_messages_nudge_empty_chk CHECK (kind <> 'nudge' OR body = '')
|
||||
);
|
||||
CREATE INDEX chat_messages_game_idx ON chat_messages (game_id, created_at);
|
||||
-- Backs the once-per-hour nudge rate-limit lookup (latest nudge by a sender).
|
||||
CREATE INDEX chat_messages_nudge_idx ON chat_messages (game_id, sender_id, created_at)
|
||||
WHERE kind = 'nudge';
|
||||
|
||||
-- Pending email confirm-codes. code_hash is the hex-encoded SHA-256 of the
|
||||
-- 6-digit code (the plaintext is never stored, matching the session model);
|
||||
-- expires_at bounds the TTL and attempts caps brute force. A row is consumed
|
||||
-- (consumed_at stamped) on success. A re-request deletes the prior pending row
|
||||
-- for the same (account, lowercased email) and inserts a fresh one.
|
||||
CREATE TABLE email_confirmations (
|
||||
confirmation_id uuid PRIMARY KEY,
|
||||
account_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
email text NOT NULL,
|
||||
code_hash text NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
attempts smallint NOT NULL DEFAULT 0,
|
||||
consumed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT email_confirmations_attempts_chk CHECK (attempts >= 0)
|
||||
);
|
||||
CREATE INDEX email_confirmations_account_idx ON email_confirmations (account_id);
|
||||
|
||||
-- A friend-game invitation. The inviter (seat 0) proposes the game settings to
|
||||
-- 1..3 invitees; the game starts only when every invitee has accepted, and any
|
||||
-- decline cancels the whole invitation. Lazily expired after expires_at (no
|
||||
-- background sweep). game_id is set when the game is started.
|
||||
CREATE TABLE game_invitations (
|
||||
invitation_id uuid PRIMARY KEY,
|
||||
inviter_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
variant text NOT NULL,
|
||||
turn_timeout_secs integer NOT NULL,
|
||||
hints_allowed boolean NOT NULL DEFAULT true,
|
||||
hints_per_player smallint NOT NULL DEFAULT 1,
|
||||
dropout_tiles text NOT NULL DEFAULT 'remove',
|
||||
status text NOT NULL DEFAULT 'pending',
|
||||
game_id uuid REFERENCES games (game_id) ON DELETE SET NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT game_invitations_variant_chk CHECK (variant IN ('english', 'russian_scrabble', 'erudit')),
|
||||
CONSTRAINT game_invitations_dropout_tiles_chk CHECK (dropout_tiles IN ('remove', 'return')),
|
||||
CONSTRAINT game_invitations_status_chk CHECK (status IN ('pending', 'declined', 'cancelled', 'expired', 'started')),
|
||||
CONSTRAINT game_invitations_turn_timeout_chk CHECK (turn_timeout_secs > 0),
|
||||
CONSTRAINT game_invitations_hints_per_player_chk CHECK (hints_per_player >= 0)
|
||||
);
|
||||
CREATE INDEX game_invitations_inviter_idx ON game_invitations (inviter_id);
|
||||
|
||||
-- One row per invitee (the inviter is implicit seat 0). seat is the invitee's
|
||||
-- seat in the started game (1..3, in invitation order). response tracks each
|
||||
-- invitee's pending/accepted/declined decision.
|
||||
CREATE TABLE game_invitation_invitees (
|
||||
invitation_id uuid NOT NULL REFERENCES game_invitations (invitation_id) ON DELETE CASCADE,
|
||||
account_id uuid NOT NULL REFERENCES accounts (account_id) ON DELETE CASCADE,
|
||||
seat smallint NOT NULL,
|
||||
response text NOT NULL DEFAULT 'pending',
|
||||
responded_at timestamptz,
|
||||
PRIMARY KEY (invitation_id, account_id),
|
||||
CONSTRAINT game_invitation_invitees_response_chk CHECK (response IN ('pending', 'accepted', 'declined')),
|
||||
CONSTRAINT game_invitation_invitees_seat_chk CHECK (seat BETWEEN 1 AND 3)
|
||||
);
|
||||
CREATE INDEX game_invitation_invitees_account_idx ON game_invitation_invitees (account_id);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE game_invitation_invitees;
|
||||
DROP TABLE game_invitations;
|
||||
DROP TABLE email_confirmations;
|
||||
DROP TABLE chat_messages;
|
||||
DROP TABLE blocks;
|
||||
DROP TABLE friendships;
|
||||
ALTER TABLE games
|
||||
DROP CONSTRAINT games_dropout_tiles_chk,
|
||||
DROP COLUMN dropout_tiles;
|
||||
Reference in New Issue
Block a user