diplomail (Stage A): add in-game personal mail subsystem
Tests · Go / test (push) Successful in 1m44s
Tests · Integration / integration (pull_request) Successful in 1m44s
Tests · Go / test (pull_request) Successful in 2m45s

Phase 28 of ui/PLAN.md needs a persistent player-to-player mail
channel; the existing `mail` package is a transactional email
outbox and the `notification` catalog is one-way platform events.
Stage A lands the schema (diplomail_messages / _recipients /
_translations), a single-recipient personal send/read/delete
service path, a `diplomail.message.received` push kind plumbed
through the notification pipeline, and an unread-counts endpoint
that drives the lobby badge. Admin / system mail, lifecycle hooks,
paid-tier broadcast, multi-game broadcast, bulk purge and language
detection / translation cache come in stages B–D.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-15 18:28:55 +02:00
parent 77cb7c78b6
commit 535e27008f
28 changed files with 3069 additions and 12 deletions
@@ -0,0 +1,29 @@
//
// 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 DiplomailMessages struct {
MessageID uuid.UUID `sql:"primary_key"`
GameID uuid.UUID
GameName string
Kind string
SenderKind string
SenderUserID *uuid.UUID
SenderUsername *string
SenderIP string
Subject string
Body string
BodyLang string
BroadcastScope string
CreatedAt time.Time
}
@@ -0,0 +1,26 @@
//
// 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 DiplomailRecipients struct {
RecipientID uuid.UUID `sql:"primary_key"`
MessageID uuid.UUID
GameID uuid.UUID
UserID uuid.UUID
RecipientUserName string
RecipientRaceName *string
DeliveredAt *time.Time
ReadAt *time.Time
DeletedAt *time.Time
NotifiedAt *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 DiplomailTranslations struct {
TranslationID uuid.UUID `sql:"primary_key"`
MessageID uuid.UUID
TargetLang string
TranslatedSubject string
TranslatedBody string
Translator string
TranslatedAt time.Time
}
@@ -0,0 +1,114 @@
//
// 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 DiplomailMessages = newDiplomailMessagesTable("backend", "diplomail_messages", "")
type diplomailMessagesTable struct {
postgres.Table
// Columns
MessageID postgres.ColumnString
GameID postgres.ColumnString
GameName postgres.ColumnString
Kind postgres.ColumnString
SenderKind postgres.ColumnString
SenderUserID postgres.ColumnString
SenderUsername postgres.ColumnString
SenderIP postgres.ColumnString
Subject postgres.ColumnString
Body postgres.ColumnString
BodyLang postgres.ColumnString
BroadcastScope postgres.ColumnString
CreatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type DiplomailMessagesTable struct {
diplomailMessagesTable
EXCLUDED diplomailMessagesTable
}
// AS creates new DiplomailMessagesTable with assigned alias
func (a DiplomailMessagesTable) AS(alias string) *DiplomailMessagesTable {
return newDiplomailMessagesTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new DiplomailMessagesTable with assigned schema name
func (a DiplomailMessagesTable) FromSchema(schemaName string) *DiplomailMessagesTable {
return newDiplomailMessagesTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new DiplomailMessagesTable with assigned table prefix
func (a DiplomailMessagesTable) WithPrefix(prefix string) *DiplomailMessagesTable {
return newDiplomailMessagesTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new DiplomailMessagesTable with assigned table suffix
func (a DiplomailMessagesTable) WithSuffix(suffix string) *DiplomailMessagesTable {
return newDiplomailMessagesTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newDiplomailMessagesTable(schemaName, tableName, alias string) *DiplomailMessagesTable {
return &DiplomailMessagesTable{
diplomailMessagesTable: newDiplomailMessagesTableImpl(schemaName, tableName, alias),
EXCLUDED: newDiplomailMessagesTableImpl("", "excluded", ""),
}
}
func newDiplomailMessagesTableImpl(schemaName, tableName, alias string) diplomailMessagesTable {
var (
MessageIDColumn = postgres.StringColumn("message_id")
GameIDColumn = postgres.StringColumn("game_id")
GameNameColumn = postgres.StringColumn("game_name")
KindColumn = postgres.StringColumn("kind")
SenderKindColumn = postgres.StringColumn("sender_kind")
SenderUserIDColumn = postgres.StringColumn("sender_user_id")
SenderUsernameColumn = postgres.StringColumn("sender_username")
SenderIPColumn = postgres.StringColumn("sender_ip")
SubjectColumn = postgres.StringColumn("subject")
BodyColumn = postgres.StringColumn("body")
BodyLangColumn = postgres.StringColumn("body_lang")
BroadcastScopeColumn = postgres.StringColumn("broadcast_scope")
CreatedAtColumn = postgres.TimestampzColumn("created_at")
allColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
mutableColumns = postgres.ColumnList{GameIDColumn, GameNameColumn, KindColumn, SenderKindColumn, SenderUserIDColumn, SenderUsernameColumn, SenderIPColumn, SubjectColumn, BodyColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
defaultColumns = postgres.ColumnList{SenderIPColumn, SubjectColumn, BodyLangColumn, BroadcastScopeColumn, CreatedAtColumn}
)
return diplomailMessagesTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
MessageID: MessageIDColumn,
GameID: GameIDColumn,
GameName: GameNameColumn,
Kind: KindColumn,
SenderKind: SenderKindColumn,
SenderUserID: SenderUserIDColumn,
SenderUsername: SenderUsernameColumn,
SenderIP: SenderIPColumn,
Subject: SubjectColumn,
Body: BodyColumn,
BodyLang: BodyLangColumn,
BroadcastScope: BroadcastScopeColumn,
CreatedAt: CreatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -0,0 +1,105 @@
//
// 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 DiplomailRecipients = newDiplomailRecipientsTable("backend", "diplomail_recipients", "")
type diplomailRecipientsTable struct {
postgres.Table
// Columns
RecipientID postgres.ColumnString
MessageID postgres.ColumnString
GameID postgres.ColumnString
UserID postgres.ColumnString
RecipientUserName postgres.ColumnString
RecipientRaceName postgres.ColumnString
DeliveredAt postgres.ColumnTimestampz
ReadAt postgres.ColumnTimestampz
DeletedAt postgres.ColumnTimestampz
NotifiedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type DiplomailRecipientsTable struct {
diplomailRecipientsTable
EXCLUDED diplomailRecipientsTable
}
// AS creates new DiplomailRecipientsTable with assigned alias
func (a DiplomailRecipientsTable) AS(alias string) *DiplomailRecipientsTable {
return newDiplomailRecipientsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new DiplomailRecipientsTable with assigned schema name
func (a DiplomailRecipientsTable) FromSchema(schemaName string) *DiplomailRecipientsTable {
return newDiplomailRecipientsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new DiplomailRecipientsTable with assigned table prefix
func (a DiplomailRecipientsTable) WithPrefix(prefix string) *DiplomailRecipientsTable {
return newDiplomailRecipientsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new DiplomailRecipientsTable with assigned table suffix
func (a DiplomailRecipientsTable) WithSuffix(suffix string) *DiplomailRecipientsTable {
return newDiplomailRecipientsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newDiplomailRecipientsTable(schemaName, tableName, alias string) *DiplomailRecipientsTable {
return &DiplomailRecipientsTable{
diplomailRecipientsTable: newDiplomailRecipientsTableImpl(schemaName, tableName, alias),
EXCLUDED: newDiplomailRecipientsTableImpl("", "excluded", ""),
}
}
func newDiplomailRecipientsTableImpl(schemaName, tableName, alias string) diplomailRecipientsTable {
var (
RecipientIDColumn = postgres.StringColumn("recipient_id")
MessageIDColumn = postgres.StringColumn("message_id")
GameIDColumn = postgres.StringColumn("game_id")
UserIDColumn = postgres.StringColumn("user_id")
RecipientUserNameColumn = postgres.StringColumn("recipient_user_name")
RecipientRaceNameColumn = postgres.StringColumn("recipient_race_name")
DeliveredAtColumn = postgres.TimestampzColumn("delivered_at")
ReadAtColumn = postgres.TimestampzColumn("read_at")
DeletedAtColumn = postgres.TimestampzColumn("deleted_at")
NotifiedAtColumn = postgres.TimestampzColumn("notified_at")
allColumns = postgres.ColumnList{RecipientIDColumn, MessageIDColumn, GameIDColumn, UserIDColumn, RecipientUserNameColumn, RecipientRaceNameColumn, DeliveredAtColumn, ReadAtColumn, DeletedAtColumn, NotifiedAtColumn}
mutableColumns = postgres.ColumnList{MessageIDColumn, GameIDColumn, UserIDColumn, RecipientUserNameColumn, RecipientRaceNameColumn, DeliveredAtColumn, ReadAtColumn, DeletedAtColumn, NotifiedAtColumn}
defaultColumns = postgres.ColumnList{}
)
return diplomailRecipientsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
RecipientID: RecipientIDColumn,
MessageID: MessageIDColumn,
GameID: GameIDColumn,
UserID: UserIDColumn,
RecipientUserName: RecipientUserNameColumn,
RecipientRaceName: RecipientRaceNameColumn,
DeliveredAt: DeliveredAtColumn,
ReadAt: ReadAtColumn,
DeletedAt: DeletedAtColumn,
NotifiedAt: NotifiedAtColumn,
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 DiplomailTranslations = newDiplomailTranslationsTable("backend", "diplomail_translations", "")
type diplomailTranslationsTable struct {
postgres.Table
// Columns
TranslationID postgres.ColumnString
MessageID postgres.ColumnString
TargetLang postgres.ColumnString
TranslatedSubject postgres.ColumnString
TranslatedBody postgres.ColumnString
Translator postgres.ColumnString
TranslatedAt postgres.ColumnTimestampz
AllColumns postgres.ColumnList
MutableColumns postgres.ColumnList
DefaultColumns postgres.ColumnList
}
type DiplomailTranslationsTable struct {
diplomailTranslationsTable
EXCLUDED diplomailTranslationsTable
}
// AS creates new DiplomailTranslationsTable with assigned alias
func (a DiplomailTranslationsTable) AS(alias string) *DiplomailTranslationsTable {
return newDiplomailTranslationsTable(a.SchemaName(), a.TableName(), alias)
}
// Schema creates new DiplomailTranslationsTable with assigned schema name
func (a DiplomailTranslationsTable) FromSchema(schemaName string) *DiplomailTranslationsTable {
return newDiplomailTranslationsTable(schemaName, a.TableName(), a.Alias())
}
// WithPrefix creates new DiplomailTranslationsTable with assigned table prefix
func (a DiplomailTranslationsTable) WithPrefix(prefix string) *DiplomailTranslationsTable {
return newDiplomailTranslationsTable(a.SchemaName(), prefix+a.TableName(), a.TableName())
}
// WithSuffix creates new DiplomailTranslationsTable with assigned table suffix
func (a DiplomailTranslationsTable) WithSuffix(suffix string) *DiplomailTranslationsTable {
return newDiplomailTranslationsTable(a.SchemaName(), a.TableName()+suffix, a.TableName())
}
func newDiplomailTranslationsTable(schemaName, tableName, alias string) *DiplomailTranslationsTable {
return &DiplomailTranslationsTable{
diplomailTranslationsTable: newDiplomailTranslationsTableImpl(schemaName, tableName, alias),
EXCLUDED: newDiplomailTranslationsTableImpl("", "excluded", ""),
}
}
func newDiplomailTranslationsTableImpl(schemaName, tableName, alias string) diplomailTranslationsTable {
var (
TranslationIDColumn = postgres.StringColumn("translation_id")
MessageIDColumn = postgres.StringColumn("message_id")
TargetLangColumn = postgres.StringColumn("target_lang")
TranslatedSubjectColumn = postgres.StringColumn("translated_subject")
TranslatedBodyColumn = postgres.StringColumn("translated_body")
TranslatorColumn = postgres.StringColumn("translator")
TranslatedAtColumn = postgres.TimestampzColumn("translated_at")
allColumns = postgres.ColumnList{TranslationIDColumn, MessageIDColumn, TargetLangColumn, TranslatedSubjectColumn, TranslatedBodyColumn, TranslatorColumn, TranslatedAtColumn}
mutableColumns = postgres.ColumnList{MessageIDColumn, TargetLangColumn, TranslatedSubjectColumn, TranslatedBodyColumn, TranslatorColumn, TranslatedAtColumn}
defaultColumns = postgres.ColumnList{TranslatedSubjectColumn, TranslatedAtColumn}
)
return diplomailTranslationsTable{
Table: postgres.NewTable(schemaName, tableName, alias, allColumns...),
//Columns
TranslationID: TranslationIDColumn,
MessageID: MessageIDColumn,
TargetLang: TargetLangColumn,
TranslatedSubject: TranslatedSubjectColumn,
TranslatedBody: TranslatedBodyColumn,
Translator: TranslatorColumn,
TranslatedAt: TranslatedAtColumn,
AllColumns: allColumns,
MutableColumns: mutableColumns,
DefaultColumns: defaultColumns,
}
}
@@ -16,6 +16,9 @@ func UseSchema(schema string) {
AuthChallenges = AuthChallenges.FromSchema(schema)
BlockedEmails = BlockedEmails.FromSchema(schema)
DeviceSessions = DeviceSessions.FromSchema(schema)
DiplomailMessages = DiplomailMessages.FromSchema(schema)
DiplomailRecipients = DiplomailRecipients.FromSchema(schema)
DiplomailTranslations = DiplomailTranslations.FromSchema(schema)
EngineVersions = EngineVersions.FromSchema(schema)
EntitlementRecords = EntitlementRecords.FromSchema(schema)
EntitlementSnapshots = EntitlementSnapshots.FromSchema(schema)