store user's persistent id in bigquery

This commit is contained in:
evanpelle
2024-12-17 14:29:17 -08:00
parent 722165c401
commit 1417808c14
10 changed files with 126 additions and 92 deletions
+11 -16
View File
@@ -12,21 +12,20 @@ import { WinCheckExecution } from "../core/execution/WinCheckExecution";
import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport";
import { createCanvas } from "./Utils";
import { DisplayMessageEvent, MessageType } from "./graphics/layers/EventsDisplay";
import { v4 as uuidv4 } from 'uuid';
import { WorkerClient } from "../core/worker/WorkerClient";
export interface LobbyConfig {
gameType: GameType
playerName: () => string
gameID: GameID
clientID: ClientID,
playerID: PlayerID,
persistentID: string,
gameType: GameType
gameID: GameID,
map: GameMap | null
difficulty: Difficulty | null
}
export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void {
const clientID = generateID()
const playerID = generateID()
const eventBus = new EventBus()
const config = getConfig()
@@ -40,14 +39,10 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
}
const transport = new Transport(
lobbyConfig.gameType == GameType.Singleplayer,
lobbyConfig,
gameConfig,
eventBus,
lobbyConfig.gameID,
clientID,
playerID,
config,
lobbyConfig.playerName
)
const onconnect = () => {
@@ -58,7 +53,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
if (message.type == "start") {
console.log('lobby: game started')
onjoin()
createClientGame(message.config, eventBus, transport, lobbyConfig.gameID, clientID).then(r => r.start())
createClientGame(lobbyConfig, message.config, eventBus, transport).then(r => r.start())
};
}
transport.connect(onconnect, onmessage)
@@ -69,7 +64,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
}
export async function createClientGame(gameConfig: GameConfig, eventBus: EventBus, transport: Transport, gameID: GameID, clientID: ClientID): Promise<GameRunner> {
export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise<GameRunner> {
const config = getConfig()
const terrainMap = await loadTerrainMap(gameConfig.gameMap);
@@ -82,18 +77,18 @@ export async function createClientGame(gameConfig: GameConfig, eventBus: EventBu
await worker.initialize()
console.log('inited path finder')
const canvas = createCanvas()
let gameRenderer = createRenderer(canvas, game, eventBus, clientID)
let gameRenderer = createRenderer(canvas, game, eventBus, lobbyConfig.clientID)
console.log(`creating private game got difficulty: ${gameConfig.difficulty}`)
return new GameRunner(
clientID,
lobbyConfig.clientID,
eventBus,
game,
gameRenderer,
new InputHandler(canvas, eventBus),
new Executor(game, gameID, worker),
new Executor(game, lobbyConfig.gameID, worker),
transport,
)
}
+21 -6
View File
@@ -1,6 +1,8 @@
import { Config } from "../core/configuration/Config";
import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
import { CreateGameRecord, generateID } from "../core/Util";
import { LobbyConfig } from "./GameRunner";
import { getPersistentIDFromCookie } from "./Main";
export class LocalServer {
@@ -11,10 +13,14 @@ export class LocalServer {
private endTurnIntervalID
private gameID: GameID
constructor(private clientID: ClientID, private config: Config, private gameConfig: GameConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
this.gameID = generateID()
constructor(
private config: Config,
private gameConfig: GameConfig,
private lobbyConfig: LobbyConfig,
private clientConnect: () => void,
private clientMessage: (message: ServerMessage) => void
) {
}
start() {
@@ -38,7 +44,7 @@ export class LocalServer {
private endTurn() {
const pastTurn: Turn = {
turnNumber: this.turns.length,
gameID: this.gameID,
gameID: this.lobbyConfig.gameID,
intents: this.intents
}
this.turns.push(pastTurn)
@@ -54,9 +60,18 @@ export class LocalServer {
clearInterval(this.endTurnIntervalID)
const players: PlayerRecord[] = [{
ip: null,
clientID: this.clientID
persistentID: getPersistentIDFromCookie(),
username: this.lobbyConfig.playerName(),
clientID: this.lobbyConfig.clientID
}]
const record = CreateGameRecord(this.gameID, this.gameConfig, players, this.turns, this.startedAt, Date.now())
const record = CreateGameRecord(
this.lobbyConfig.gameID,
this.gameConfig,
players,
this.turns,
this.startedAt,
Date.now()
)
// Clear turns because beacon only supports up to 64kb
record.turns = []
// For unload events, sendBeacon is the only reliable method
+31 -1
View File
@@ -9,6 +9,7 @@ import { UsernameInput } from "./UsernameInput";
import { SinglePlayerModal } from "./SinglePlayerModal";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
import { generateID } from "../core/Util";
@@ -71,6 +72,9 @@ class Client {
gameType: event.detail.gameType,
playerName: (): string => this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
persistentID: getPersistentIDFromCookie(),
playerID: generateID(),
clientID: generateID(),
map: event.detail.map,
difficulty: event.detail.difficulty,
},
@@ -105,4 +109,30 @@ function setFavicon(): void {
link.rel = 'shortcut icon';
link.href = favicon;
document.head.appendChild(link);
}
}
// WARNING: DO NOT EXPOSE THIS ID
export function getPersistentIDFromCookie(): string {
const COOKIE_NAME = 'player_persistent_id'
// Try to get existing cookie
const cookies = document.cookie.split(';')
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split('=').map(c => c.trim())
if (cookieName === COOKIE_NAME) {
return cookieValue
}
}
// If no cookie exists, create new ID and set cookie
const newId = crypto.randomUUID() // Using built-in UUID generator
document.cookie = [
`${COOKIE_NAME}=${newId}`,
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
'path=/',
'SameSite=Strict',
'Secure'
].join(';')
return newId
}
+35 -35
View File
@@ -1,9 +1,9 @@
import { Config } from "../core/configuration/Config"
import { EventBus, GameEvent } from "../core/EventBus"
import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game"
import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game"
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig } from "../core/Schemas"
import { LobbyConfig } from "./GameRunner"
import { LocalServer } from "./LocalServer"
import { getPersistentIDFromCookie } from "./Utils"
export class SendAllianceRequestIntentEvent implements GameEvent {
@@ -96,18 +96,17 @@ export class Transport {
private pingInterval: number | null = null
private lastPingTime: number | null = null
private isLocal: boolean
constructor(
private isLocal: boolean,
private lobbyConfig: LobbyConfig,
// gameConfig only set on private games
private gameConfig: GameConfig | null,
private eventBus: EventBus,
private gameID: GameID,
private clientID: ClientID,
private playerID: PlayerID,
private config: Config,
private playerName: () => string,
) {
this.isLocal = lobbyConfig.gameType == GameType.Singleplayer
this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e))
this.eventBus.on(SendAllianceReplyIntentEvent, (e) => this.onAllianceRequestReplyUIEvent(e))
this.eventBus.on(SendBreakAllianceIntentEvent, (e) => this.onBreakAllianceRequestUIEvent(e))
@@ -128,8 +127,8 @@ export class Transport {
if (this.socket != null && this.socket.readyState === WebSocket.OPEN) {
this.sendMsg(JSON.stringify(ClientPingMessageSchema.parse({
type: 'ping',
clientID: this.clientID,
gameID: this.gameID,
clientID: this.lobbyConfig.clientID,
gameID: this.lobbyConfig.gameID,
})))
}
}, 5 * 1000);
@@ -152,7 +151,7 @@ export class Transport {
}
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
this.localServer = new LocalServer(this.clientID, this.config, this.gameConfig, onconnect, onmessage)
this.localServer = new LocalServer(this.config, this.gameConfig, this.lobbyConfig, onconnect, onmessage)
this.localServer.start()
}
@@ -193,10 +192,11 @@ export class Transport {
JSON.stringify(
ClientJoinMessageSchema.parse({
type: "join",
gameID: this.gameID,
clientID: this.clientID,
gameID: this.lobbyConfig.gameID,
clientID: this.lobbyConfig.clientID,
lastTurn: numTurns,
persistentID: getPersistentIDFromCookie(),
persistentID: this.lobbyConfig.persistentID,
username: this.lobbyConfig.playerName()
})
)
)
@@ -221,7 +221,7 @@ export class Transport {
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
this.sendIntent({
type: "allianceRequest",
clientID: this.clientID,
clientID: this.lobbyConfig.clientID,
requestor: event.requestor.id(),
recipient: event.recipient.id(),
})
@@ -230,7 +230,7 @@ export class Transport {
private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) {
this.sendIntent({
type: "allianceRequestReply",
clientID: this.clientID,
clientID: this.lobbyConfig.clientID,
requestor: event.allianceRequest.requestor().id(),
recipient: event.allianceRequest.recipient().id(),
accept: event.accepted,
@@ -240,7 +240,7 @@ export class Transport {
private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) {
this.sendIntent({
type: "breakAlliance",
clientID: this.clientID,
clientID: this.lobbyConfig.clientID,
requestor: event.requestor.id(),
recipient: event.recipient.id(),
})
@@ -249,9 +249,9 @@ export class Transport {
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
this.sendIntent({
type: "spawn",
clientID: this.clientID,
playerID: this.playerID,
name: this.playerName(),
clientID: this.lobbyConfig.clientID,
playerID: this.lobbyConfig.playerID,
name: this.lobbyConfig.playerName(),
playerType: PlayerType.Human,
x: event.cell.x,
y: event.cell.y
@@ -261,8 +261,8 @@ export class Transport {
private onSendAttackIntent(event: SendAttackIntentEvent) {
this.sendIntent({
type: "attack",
clientID: this.clientID,
attackerID: this.playerID,
clientID: this.lobbyConfig.clientID,
attackerID: this.lobbyConfig.playerID,
targetID: event.targetID,
troops: event.troops,
sourceX: null,
@@ -275,8 +275,8 @@ export class Transport {
private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
this.sendIntent({
type: "boat",
clientID: this.clientID,
attackerID: this.playerID,
clientID: this.lobbyConfig.clientID,
attackerID: this.lobbyConfig.playerID,
targetID: event.targetID,
troops: event.troops,
x: event.cell.x,
@@ -287,8 +287,8 @@ export class Transport {
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
this.sendIntent({
type: "targetPlayer",
clientID: this.clientID,
requestor: this.playerID,
clientID: this.lobbyConfig.clientID,
requestor: this.lobbyConfig.playerID,
target: event.targetID,
})
}
@@ -296,8 +296,8 @@ export class Transport {
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
this.sendIntent({
type: "emoji",
clientID: this.clientID,
sender: this.playerID,
clientID: this.lobbyConfig.clientID,
sender: this.lobbyConfig.playerID,
recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
emoji: event.emoji
})
@@ -306,7 +306,7 @@ export class Transport {
private onSendDonateIntent(event: SendDonateIntentEvent) {
this.sendIntent({
type: "donate",
clientID: this.clientID,
clientID: this.lobbyConfig.clientID,
sender: event.sender.id(),
recipient: event.recipient.id(),
troops: event.troops,
@@ -316,8 +316,8 @@ export class Transport {
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
this.sendIntent({
type: "troop_ratio",
clientID: this.clientID,
player: this.playerID,
clientID: this.lobbyConfig.clientID,
player: this.lobbyConfig.playerID,
ratio: event.ratio,
})
}
@@ -325,8 +325,8 @@ export class Transport {
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
this.sendIntent({
type: "build_unit",
clientID: this.clientID,
player: this.playerID,
clientID: this.lobbyConfig.clientID,
player: this.lobbyConfig.playerID,
unit: event.unit,
x: event.cell.x,
y: event.cell.y,
@@ -337,8 +337,8 @@ export class Transport {
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
const msg = ClientIntentMessageSchema.parse({
type: "intent",
clientID: this.clientID,
gameID: this.gameID,
clientID: this.lobbyConfig.clientID,
gameID: this.lobbyConfig.gameID,
intent: intent
})
this.sendMsg(JSON.stringify(msg))
-25
View File
@@ -35,28 +35,3 @@ export function createCanvas(): HTMLCanvasElement {
return canvas
}
// WARNING: DO NOT EXPOSE THIS ID
export function getPersistentIDFromCookie(): string {
const COOKIE_NAME = 'player_persistent_id';
// Try to get existing cookie
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [cookieName, cookieValue] = cookie.split('=').map(c => c.trim());
if (cookieName === COOKIE_NAME) {
return cookieValue;
}
}
// If no cookie exists, create new ID and set cookie
const newId = crypto.randomUUID(); // Using built-in UUID generator
document.cookie = [
`${COOKIE_NAME}=${newId}`,
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
'path=/',
'SameSite=Strict',
'Secure'
].join(';');
return newId;
}
+7 -5
View File
@@ -222,10 +222,12 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
intent: IntentSchema
})
// WARNING: never send this message to clients.
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
type: z.literal('join'),
persistentID: z.string(),
lastTurn: z.number() // The last turn the client saw.
persistentID: z.string(), // WARNING: persistent id is private.
lastTurn: z.number(), // The last turn the client saw.
username: z.string(),
})
export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]);
@@ -233,12 +235,12 @@ export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientInten
export const PlayerRecordSchema = z.object({
clientID: z.string(),
username: z.string(),
ip: z.string().nullable(),
ip: z.string().nullable(), // WARNING: PII
persistentID: z.string(), // WARNING: PII
})
export const GameRecordSchema = z.object({
id: z.string(), // WARNING: PII
persistentID: z.string(), // WARNING: PII
id: z.string(),
gameConfig: GameConfigSchema,
players: z.array(PlayerRecordSchema),
startTimestampMS: z.number(),
+7 -2
View File
@@ -22,6 +22,8 @@ export async function archive(gameRecord: GameRecord) {
players: gameRecord.players.map(p => ({
username: p.username,
ip: p.ip,
persistentID: p.persistentID,
clientID: p.clientID,
})),
};
@@ -32,8 +34,11 @@ export async function archive(gameRecord: GameRecord) {
console.log(`wrote game metadata to BigQuery: ${gameRecord.id}`);
if (gameRecord.turns.length > 0) {
// Players will see this so make sure to clear PII.
gameRecord.players.forEach(p => p.ip = "REDACTED")
// Players may see this so make sure to clear PII.
gameRecord.players.forEach(p => {
p.ip = "REDACTED"
p.persistentID = "REDACTED"
})
console.log(`writing game ${gameRecord.id} to gcs`)
const bucket = storage.bucket("openfront-games");
const file = bucket.file(gameRecord.id);
+1
View File
@@ -10,6 +10,7 @@ export class Client {
public readonly clientID: ClientID,
public readonly persistentID: string,
public readonly ip: string | null,
public readonly username: string,
public readonly ws: WebSocket,
) { }
}
+12 -2
View File
@@ -166,9 +166,19 @@ export class GameServer {
const playerRecords: PlayerRecord[] = Array.from(this.allClients.values()).map(client => ({
ip: client.ip,
clientID: client.clientID,
username: client.username,
persistentID: client.persistentID,
}));
const record = CreateGameRecord(this.id, this.gameConfig, playerRecords, this.turns, this._startTime, Date.now())
archive(record)
archive(
CreateGameRecord(
this.id,
this.gameConfig,
playerRecords,
this.turns,
this._startTime,
Date.now()
)
)
} else {
console.log(`${this.id}: no clients joined, not archiving game`)
}
+1
View File
@@ -115,6 +115,7 @@ wss.on('connection', (ws, req) => {
clientMsg.clientID,
clientMsg.persistentID,
ip,
clientMsg.username,
ws
),
clientMsg.gameID,