This commit is contained in:
2026-06-26 20:42:38 -04:00
commit a332e85658
134 changed files with 17288 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.env
node_modules
dist
+9
View File
@@ -0,0 +1,9 @@
export const apps = [
{
name: "poem-bot",
script: "dist/index.cjs",
interpreter: "node",
instances: 1,
watch: false,
},
];
+30
View File
@@ -0,0 +1,30 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig(globalIgnores(["dist/*"]), [
{
files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
plugins: { js },
extends: ["js/recommended"],
ignores: ["/dist/*"],
rules: {
"require-await": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
},
languageOptions: { globals: globals.browser },
},
tseslint.configs.recommended,
]);
+6126
View File
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
{
"name": "jolb-poem-bot",
"version": "1.0.0",
"type": "module",
"description": "",
"main": "index.js",
"scripts": {
"dev": "tsup src/server/index.ts --watch --onSuccess \"node dist/index.cjs\"",
"lint": "eslint",
"build": "tsup src/server/index.ts --minify --clean",
"start": "pm2 start ecosystem.config.js",
"check:circles": "npx madge --circular src/"
},
"author": "",
"license": "ISC",
"dependencies": {
"@grammyjs/conversations": "^2.1.1",
"@grammyjs/menu": "^1.3.1",
"@prisma/adapter-better-sqlite3": "^7.4.1",
"@prisma/client": "^7.4.1",
"crypto-hash": "^4.0.1",
"dotenv": "^17.3.1",
"grammy": "^1.40.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^25.3.0",
"eslint": "^10.0.0",
"globals": "^17.3.0",
"jiti": "^2.6.1",
"pm2": "^6.0.14",
"prisma": "^7.4.1",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0",
"tsup": "^8.5.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0"
}
}
+9
View File
@@ -0,0 +1,9 @@
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: './prisma/schema.prisma',
datasource: {
url: env('DATABASE_URL'),
},
});
+62
View File
@@ -0,0 +1,62 @@
datasource db {
provider = "postgresql"
}
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
enum PlayerState {
IN_ARCHIVES
LOBBY
POST_GAME
SETTING_UP_GAME
SPECTATING
START
TRYING_LEAVE
TYPING
VIEWING_ARCHIVED_POEM
WAITING_AFTER_WRITING
WAITING_TO_WRITE
WRITING
}
model Player {
playerId String @id
createdAt DateTime @default(now())
currentGame Game? @relation("playersInGame", fields: [currentGameId], references: [gameId])
currentGameId Int?
state PlayerState @default(LOBBY)
previousState PlayerState @default(LOBBY)
userName String
games Game[]
}
model Game {
gameId Int @id @default(autoincrement())
config GameConfig? @relation(fields: [gameConfigId], references: [configId])
createdAt DateTime @default(now())
joinCode String
poem Poem?
poemId Int?
gameConfigId Int?
hostPlayer Player @relation(fields: [hostPlayerId], references: [playerId])
hostPlayerId String
inProgress Boolean @default(false)
players Player[] @relation("playersInGame")
}
model GameConfig {
configId Int @id @default(autoincrement())
games Game[]
}
model Poem {
poemId Int @id @default(autoincrement())
createdAt DateTime @default(now())
game Game @relation(fields: [gameId], references: [gameId])
gameId Int @unique
finished Boolean @default(false)
lines Json
}
+351
View File
@@ -0,0 +1,351 @@
import { HAS_JOINED, HAS_LEFT, HOST_LEFT, HOST_RESTARTED } from "const";
import { getPlayerOrThrow, getPlayers, leaveGame } from "data";
import { setPlayerState } from "player-state";
import {
Context,
GameOptions,
GameOverrides,
GameProps,
PlayerState,
} from "types";
import { generateGameId } from "./generate-game-id";
import { Player } from "../player";
import { Poem } from "../poem";
import { forPlayers, randomStarDecorator } from "utils";
import { messagePlayer } from "messaging";
import { AllStateFn, OthersStateFn, AllFn, OthersFn } from "./types";
export const forAllOtherPlayers = async ({
callback,
fromPlayerId,
players,
}: {
fromPlayerId: number;
callback: (player: Player) => Promise<void>;
players: Player[];
}) => {
const otherPlayers = players.filter((player) => player.id !== fromPlayerId);
await forPlayers(otherPlayers, callback);
};
export const gameDefaults: Partial<GameProps> = {
inProgress: false,
};
// TODO decompose this into smaller bits
export class Game {
currentWriterId?: number;
destroy: () => void;
options: GameOptions;
hostId?: number;
id: string;
inProgress: boolean;
poem: Poem;
playerIds: number[];
constructor(overrides: GameOverrides) {
const {
currentWriterId,
destroy,
hostId,
id,
inProgress,
options,
poemProps,
playerIds,
} = {
...gameDefaults,
...overrides,
};
this.currentWriterId = currentWriterId;
this.destroy = destroy ? () => destroy(this.id) : () => null;
this.hostId = hostId;
this.id = id ?? generateGameId();
this.inProgress = !!inProgress;
this.options = { ...(options || {}) };
this.playerIds = playerIds ?? [];
this.poem = new Poem({ gameId: this.id, ...(poemProps ?? {}) });
/* Object.values(helperFns).forEach(
(fn) => (this[fn.name as keyof Helpers] = fn),
); */
}
advanceTurnOrder = async (ctx: Context, previousPlayerId?: number) => {
const nextPlayer = this.getActivePlayer();
const gameDone = !nextPlayer;
if (gameDone) {
await this.finish(ctx);
return;
}
if (previousPlayerId && this.hasPlayer(previousPlayerId)) {
await setPlayerState(
previousPlayerId,
ctx,
PlayerState.WAITING_AFTER_WRITING,
);
}
const { id: nextPlayerId } = nextPlayer;
// messages to the previous and next player are already handled by state changes
await forPlayers(this.getPlayers(), async (player) => {
if (player.id === nextPlayerId) {
await setPlayerState(nextPlayerId, ctx, PlayerState.WRITING);
} else if (![previousPlayerId].includes(player.id)) {
player.refreshMessage(ctx);
}
});
};
removePlayer = async (player: Player, ctx: Context) => {
if (player.id === this.hostId) {
await this.removeHost(ctx);
return;
}
if (!this.getHost()) {
return;
}
this.removePlayerId(player.id);
await this.messageAllOtherPlayers({
ctx,
customMessage: HAS_LEFT(player.userName),
fromPlayerId: player.id,
players: this.getPlayers(),
state: null,
});
if (this.poem.completed) return;
const playerWasActive =
this.inProgress &&
!this.getPlayers().find((p) => p.state === PlayerState.WRITING);
if (playerWasActive) {
await this.advanceTurnOrder(ctx, player.id);
} else {
await this.refreshPlayerMessages({ ctx, players: this.getPlayers() });
}
};
removeHost = async (ctx: Context) => {
await this.messageAllOtherPlayers({
ctx,
customMessage: HOST_LEFT,
fromPlayerId: this.hostId!,
players: this.getPlayers(),
state: PlayerState.START,
});
this.removePlayerId(this.hostId);
const nonHostPlayers = this.getPlayers().filter(
(player) => player.id !== this.hostId,
);
await forPlayers(
nonHostPlayers,
async (player) => await leaveGame(ctx, this, player),
);
this.destroy();
};
startGame = async (ctx: Context) => {
await this.messageAllOtherPlayers({
ctx,
customMessage: "The host has started the game.",
fromPlayerId: this.hostId!,
players: this.getPlayers(),
state: null,
});
this.inProgress = true;
this.shufflePlayers();
const firstPlayer = this.getPlayers()[0];
await this.transitionAllOtherPlayers({
ctx,
fromPlayerId: firstPlayer.id,
players: this.getPlayers(),
state: PlayerState.WAITING_TO_WRITE,
});
await setPlayerState(firstPlayer.id, ctx, PlayerState.WRITING);
};
addPlayer = async (playerId: number, ctx: Context) => {
const player = getPlayerOrThrow(playerId);
this.playerIds.push(playerId);
if (this.playerIds.length === 1) {
this.hostId = playerId;
} else {
await this.messageAllOtherPlayers({
ctx,
customMessage: HAS_JOINED(player.userName),
fromPlayerId: playerId,
players: this.getPlayers(),
state: null,
});
await this.refreshOtherPlayerMessages({
ctx,
players: this.getPlayers(),
fromPlayerId: playerId,
});
}
};
finish = async (ctx: Context) => {
this.poem.complete();
await this.transitionPlayers({
ctx,
players: this.getPlayers(),
state: PlayerState.POST_GAME,
});
await forPlayers(this.getPlayers(), (player) =>
this.poem.sendToPlayer(player.id, ctx),
);
};
previousLine = () => {
return this.poem.previousLine();
};
prunePlayerIds = () => {
const players = this.getPlayers();
for (const player of players) {
if (player.gameId !== this.id) {
this.removePlayerId(player.id);
}
}
};
getActivePlayer = (): Player | undefined => {
const eligiblePlayers = this.getEligiblePlayers();
return eligiblePlayers[0];
};
getEligiblePlayers = () => {
return this.getPlayers().filter((player) => player.canWrite());
};
getHost = (): Player | undefined => {
return this.getPlayers().find((player) => player.isHost());
};
getPlayers = () => {
return getPlayers(this.playerIds);
};
hasPlayer = (playerId?: number) => {
return playerId && this.playerIds.includes(playerId);
};
removePlayerId = (idToRemove?: number) => {
if (!idToRemove) return;
const idIndex = this.playerIds.findIndex((id) => id === idToRemove);
this.playerIds.splice(idIndex, 1);
};
setOptions = (newOptions: GameOptions) => {
this.options = { ...this.options, ...newOptions };
};
private shufflePlayers = () => {
this.playerIds.sort(() => Math.random() - 0.5);
};
addLine = async (ctx: Context, line: string, author: Player) => {
this.poem.addLine(line, author.userName);
await messagePlayer(
author.id,
author.state,
ctx,
`Your line has been added! ${randomStarDecorator()}`,
);
await this.advanceTurnOrder(ctx, author.id);
};
restart = async (ctx: Context) => {
this.poem = new Poem({ gameId: this.id });
this.inProgress = false;
await this.messageAllOtherPlayers({
ctx,
customMessage: HOST_RESTARTED,
fromPlayerId: this.hostId!,
players: this.getPlayers(),
state: null,
});
await this.transitionPlayers({
players: this.getPlayers(),
state: PlayerState.LOBBY,
ctx,
});
};
forAllOtherPlayers = async ({
callback,
fromPlayerId,
players,
}: {
fromPlayerId: number;
callback: (player: Player) => Promise<void>;
players: Player[];
}) => {
const otherPlayers = players.filter((player) => player.id !== fromPlayerId);
await forPlayers(otherPlayers, callback);
};
messagePlayers: AllStateFn = async ({
ctx,
customMessage = "",
players,
state,
}) => {
await forPlayers(players, (player) =>
messagePlayer(player.id, state || player.state, ctx, customMessage),
);
};
messageAllOtherPlayers: OthersStateFn = async ({
ctx,
customMessage = "",
fromPlayerId,
players,
state = null,
}) => {
await forAllOtherPlayers({
players,
fromPlayerId,
callback: (player) =>
messagePlayer(player.id, state || player.state, ctx, customMessage),
});
};
transitionPlayers: AllStateFn = async ({ ctx, players, state }) => {
await forPlayers(players, (player) =>
setPlayerState(player.id, ctx, state ?? player.state),
);
};
transitionAllOtherPlayers: OthersStateFn = async ({
ctx,
fromPlayerId,
players,
state,
}) => {
await forAllOtherPlayers({
fromPlayerId,
players,
callback: (player) =>
setPlayerState(player.id, ctx, state ?? player.state),
});
};
refreshPlayerMessages: AllFn = async ({ ctx, players }) => {
await forPlayers(players, (player) => player.refreshMessage(ctx));
};
refreshOtherPlayerMessages: OthersFn = async ({
ctx,
players,
fromPlayerId,
}) => {
await forAllOtherPlayers({
callback: (player) => player.refreshMessage(ctx),
fromPlayerId,
players,
});
};
}
+14
View File
@@ -0,0 +1,14 @@
import { getGames } from "data";
import { randomString } from "utils";
export const generateGameId = () => {
const games = getGames();
const maxAttempts = 10;
let gameId = "";
for (let i = 0; i < maxAttempts; i++) {
gameId = randomString();
if (!games[gameId]) break;
}
return gameId;
};
+83
View File
@@ -0,0 +1,83 @@
import { messagePlayer } from "messaging";
import { setPlayerState } from "player-state";
import { forPlayers } from "utils";
import { Player } from "../player";
import { AllFn, AllStateFn, OthersFn, OthersStateFn } from "./types";
export const forAllOtherPlayers = async ({
callback,
fromPlayerId,
players,
}: {
fromPlayerId: number;
callback: (player: Player) => Promise<void>;
players: Player[];
}) => {
const otherPlayers = players.filter((player) => player.id !== fromPlayerId);
await forPlayers(otherPlayers, callback);
};
export const messagePlayers: AllStateFn = async ({
ctx,
customMessage = "",
players,
state,
}) => {
await forPlayers(players, (player) =>
messagePlayer(player.id, state || player.state, ctx, customMessage),
);
};
export const messageAllOtherPlayers: OthersStateFn = async ({
ctx,
customMessage = "",
fromPlayerId,
players,
state = null,
}) => {
await forAllOtherPlayers({
players,
fromPlayerId,
callback: (player) =>
messagePlayer(player.id, state || player.state, ctx, customMessage),
});
};
export const transitionPlayers: AllStateFn = async ({
ctx,
players,
state,
}) => {
await forPlayers(players, (player) =>
setPlayerState(player.id, ctx, state ?? player.state),
);
};
export const transitionAllOtherPlayers: OthersStateFn = async ({
ctx,
fromPlayerId,
players,
state,
}) => {
await forAllOtherPlayers({
fromPlayerId,
players,
callback: (player) => setPlayerState(player.id, ctx, state ?? player.state),
});
};
export const refreshPlayerMessages: AllFn = async ({ ctx, players }) => {
await forPlayers(players, (player) => player.refreshMessage(ctx));
};
export const refreshOtherPlayerMessages: OthersFn = async ({
ctx,
players,
fromPlayerId,
}) => {
await forAllOtherPlayers({
callback: (player) => player.refreshMessage(ctx),
fromPlayerId,
players,
});
};
+1
View File
@@ -0,0 +1 @@
export { Game } from "./game";
+23
View File
@@ -0,0 +1,23 @@
import { Context, PlayerState } from "types";
import { Player } from "../player";
type BasicArgs = { ctx: Context; players: Player[] };
type BasicFromArgs = { ctx: Context; fromPlayerId: number; players: Player[] };
type StateArgs = BasicArgs & {
customMessage?: string;
noMessage?: boolean;
state: PlayerState | null;
};
type StateFromArgs = BasicArgs & {
customMessage?: string;
fromPlayerId: number;
noMessage?: boolean;
state: PlayerState | null;
};
export type AllFn = (args: BasicArgs) => Promise<void>;
export type OthersFn = (args: BasicFromArgs) => Promise<void>;
export type AllStateFn = (args: StateArgs) => Promise<void>;
export type OthersStateFn = (args: StateFromArgs) => Promise<void>;
+97
View File
@@ -0,0 +1,97 @@
import {
ArchivedPoems,
Context,
GameOverrides,
Games,
GlobalProps,
PlayerOverrides,
Players,
} from "types";
import { Game } from "../game";
import { Player } from "../player";
import { Poem } from "classes/poem";
export class Global {
activePlayers: Players;
games: Games;
archivedPoems: ArchivedPoems;
constructor(overrides?: Partial<GlobalProps>) {
const { activePlayers, games, archivedPoems } = overrides ?? {};
this.activePlayers = activePlayers ?? {};
this.games = games ?? {};
this.archivedPoems = archivedPoems ?? {};
}
addGame = (overrides: GameOverrides = {}) => {
const newGame = new Game({ ...overrides, destroy: this.deleteGame });
this.games[newGame.id] = newGame;
return newGame;
};
addPlayer = (overrides: PlayerOverrides) => {
const newPlayer = new Player({ ...overrides, destroy: this.deletePlayer });
this.activePlayers[newPlayer.id] = newPlayer;
return newPlayer;
};
deleteGame = (id: string = "") => {
delete this.games[id];
};
deletePlayer = (id: number) => {
delete this.activePlayers[id];
};
getGame = (id: string = ""): Game | undefined => {
return this.games[id];
};
getGames = () => {
return this.games;
};
getPlayer = (id: number): Player | undefined => {
return this.activePlayers[id];
};
getPlayers = (ids: number[]) => {
return ids.map((id) => this.activePlayers[id]).filter((player) => !!player);
};
joinGame = async (
ctx: Context,
game: Game,
player: Player,
newGame?: boolean,
) => {
if (player.gameId === game.id) {
throw new Error("You are already in this game.");
}
await game.addPlayer(player.id, ctx);
await player.joinGame(ctx, game.id, newGame);
};
leaveGame = async (ctx: Context, game: Game, player: Player) => {
await player.leaveGame(ctx);
await game.removePlayer(player, ctx);
};
addArchivedPoem = (poem: Poem) => {
this.archivedPoems[poem.id] = poem;
};
getArchivedPoem = (id: string = ""): Poem | undefined => {
return this.archivedPoems[id];
};
getArchivedPoems = () => {
return this.archivedPoems;
};
clearMemory = () => {
this.activePlayers = [];
this.games = {};
this.archivedPoems = {};
};
}
+4
View File
@@ -0,0 +1,4 @@
export { Game } from "./game";
export { Global } from "./global";
export { Player } from "./player";
export { Poem } from "./poem";
+1
View File
@@ -0,0 +1 @@
export { Player } from "./player";
+80
View File
@@ -0,0 +1,80 @@
import { getGame, getGameOrThrow } from "data";
import { messagePlayer } from "messaging";
import { setPlayerState } from "player-state";
import { Context, PlayerOverrides, PlayerProps, PlayerState } from "types";
export const playerDefaults: Partial<PlayerProps> = {
gameId: "",
state: PlayerState.START,
};
export class Player {
destroy: (id: number) => void;
gameId?: string;
id: number;
previousState: PlayerState = PlayerState.START;
state: PlayerState;
userName: string;
constructor(overrides: PlayerOverrides) {
const { destroy, gameId, id, state, userName } = {
...playerDefaults,
...overrides,
};
this.destroy = destroy ?? (() => null);
this.id = id;
this.userName = userName;
this.state = state ?? PlayerState.START;
this.gameId = gameId;
}
canWrite = () => {
return [PlayerState.WAITING_TO_WRITE].includes(this.state);
};
/* destroy=()=>{
TODO: run this on player disconnect, advance turn order if needed etc
} */
getGame = () => {
return getGame(this.gameId);
};
isHost = () => {
const game = this.getGame();
return this.id === game?.hostId;
};
isInGame = () => {
return !!this.gameId && this.state !== PlayerState.SETTING_UP_GAME;
};
joinGame = async (ctx: Context, gameId: string, newGame?: boolean) => {
const game = getGameOrThrow(gameId);
this.gameId = gameId;
let newState: PlayerState;
if (newGame) {
newState = PlayerState.SETTING_UP_GAME;
} else {
newState = game.inProgress ? PlayerState.SPECTATING : PlayerState.LOBBY;
}
await setPlayerState(this.id, ctx, newState);
};
leaveGame = async (ctx: Context) => {
this.gameId = undefined;
await setPlayerState(this.id, ctx, PlayerState.START);
};
refreshMessage = async (ctx: Context) => {
await messagePlayer(this.id, this.state, ctx);
};
setGameId = (id: string) => {
this.gameId = id;
};
setPreviousState = (state: PlayerState) => {
this.previousState = state;
};
}
+14
View File
@@ -0,0 +1,14 @@
import { getArchivedPoems } from "data";
export const generatePoemId = () => {
const archivedPoems = getArchivedPoems();
const maxAttempts = 10;
let id = 0;
for (let i = 0; i < maxAttempts; i++) {
// random 4-digit number
id = Math.floor(Math.random() * 9000) + 1000;
if (!archivedPoems[id]) break;
}
return id;
};
+1
View File
@@ -0,0 +1 @@
export { Poem } from "./poem";
+79
View File
@@ -0,0 +1,79 @@
import { InlineKeyboard } from "grammy";
import { RETRIEVE_FROM_ARCHIVE } from "const";
import { addArchivedPoem, getPlayerOrThrow } from "data";
import { sanitizeHtml } from "messaging";
import { CallbackWithData, Context, Line, PoemOverrides } from "types";
import { encodeCallbackData } from "utils";
import { generatePoemId } from "./generate-poem-id";
export { Player } from "../player";
export class Poem {
completed?: boolean;
gameId: string;
id: number;
lines: Line[];
messageId?: number;
constructor(overrides: PoemOverrides) {
const { gameId, id, lines } = {
...overrides,
};
this.gameId = gameId;
this.id = id ?? generatePoemId();
this.lines = lines ?? [];
}
addLine = (text: string, author: string) => {
this.lines.push({
author,
date: new Date().toLocaleString(),
text: text.trim(),
});
};
compile = (includeMeta?: boolean) => {
let compiledPoem = this.lines
.map((line) => {
if (includeMeta) {
const { author, date } = line;
const metaPrefix = `${author} (${date}):`;
return `<u>${metaPrefix}</u>\n${line.text}`;
}
return line.text;
})
.join("\n");
compiledPoem += `\n\n${RETRIEVE_FROM_ARCHIVE(this.id)}`;
return sanitizeHtml(compiledPoem);
};
complete = () => {
this.completed = true;
addArchivedPoem(this);
};
previousLine = () => {
const lineCount = this.lines.length;
if (!lineCount) return "";
return this.lines[lineCount - 1].text;
};
// TODO convert to markdown, but Telegram's markdown is weird
sendToPlayer = async (playerId: number, ctx: Context) => {
const player = getPlayerOrThrow(playerId);
const chatId = ctx.chatId;
if (!player || !chatId) return;
const compiledPoem = this.compile();
const keyboard = new InlineKeyboard().text(
"Show Metadata",
encodeCallbackData(CallbackWithData.SHOW_POEM_META, { id: this.id }),
);
await ctx.api.sendMessage(player.id, compiledPoem, {
parse_mode: "HTML",
reply_markup: keyboard,
});
};
setMessageId = (id: number) => {
this.messageId = id;
};
}
+3
View File
@@ -0,0 +1,3 @@
export { inspoRepo } from "./inspo-repo";
export * from "./maps";
export * from "./strings";
+192
View File
@@ -0,0 +1,192 @@
const makeQuote = (quote: string, author: string, work?: string) => {
return `${quote}\n\n- ${author}, "${work}"`;
};
// todo, move this to db
/* const quantumPoemLines = [
"In the ancient glade\n",
"Across old bark\n",
"The quiet shade\n",
`It's always dark\n'`,
];
const getQuantumPoem = () =>
makeQuote(
quantumPoemLines[Math.floor(Math.random() * quantumPoemLines.length)],
"Gabbo"
); */
const reMonarching = makeQuote(
`Butterflies overwinter
in the milkweed
along storm drains.`,
"Christian Gullette",
"Re-Monarching",
);
const winterRainDeepens = makeQuote(
`Winter rain deepens
lichened letters on the grave
and my old sadness`,
"Roka",
);
const bermanSnow = makeQuote(
`When it's snowing, the outdoors seem like a room.\n
Today I traded hellos with my neighbor.
Our voices hung close in the new acoustics.
A room with the walls blasted to shreds and falling.`,
"David Berman",
"Snow",
);
const stillLife = makeQuote(
`He still found breath, and yet
It was an obscure knack.`,
"Thom Gunn",
"Still Life",
);
const landlady = makeQuote(
`and when I dream images
of daring escapes through the snow
I find myself walking
always over a vast face
which is the land-
lady's, and wake up shouting.`,
"Margaret Atwood",
"The Landlady",
);
const suicideNote = makeQuote(
`The calm,
Cool face of the river
Asked me for a kiss.`,
"Langston Hughes",
"Suicide Note",
);
const capybaraHotsprings1 = makeQuote(
`There exists nothing human that can scratch even the lowest sky`,
"Yaxkin Melchy Ramos",
"The Capybara Hot Springs",
);
const howl1 = makeQuote(
`angelheaded hipsters burning for the ancient heavenly connection to the starry dynamo in the machinery of night`,
"Allen Ginsberg",
"Howl",
);
const eggTooth = makeQuote(
`Ears are the eyes on the sides of your head.
Memory lives here, between these apostrophes.
As if to predict music, the ear contains a drum.`,
"Benjamin Garcia",
"Egg Tooth",
);
const keepingThingsWhole = makeQuote(
`In a field
I am the absence
of field.`,
"Mark Strand",
"Keeping Things Whole",
);
const somberBull = makeQuote(
`I welcome terror, that somber bull,
I fight for your name held in his jaws.`,
"Andrea Cote",
"Somber Bull",
);
const theRoom = makeQuote(
`The house turns slowly round its one closed room.`,
"Kevin Hart",
"The Room",
);
const illWin = makeQuote(
`I'll win the way
I always do
by being gone
when they come.`,
"Robert Creely",
`I'll Win`,
);
const kingfishers1 = makeQuote(
`As kingfishers catch fire, dragonflies draw flame`,
"Gerard Manley Hopkins",
"As Kingfishers Catch Fire",
);
const frenchNovel = makeQuote(
`With boots we trekked through slush for a bottle of red wine\
we weren't allowed to buy, our shirts unbuttoned
under our winter coats.`,
"Richie Hofmann",
"French Novel",
);
const iHaveSlept = makeQuote(
`[...]on an amber throne of cockroach casings, on a carpet of needles from a cemetery pine[...]`,
"Diane Seuss",
"I have slept in many places, for years on mattresses that entered",
);
const toMakeAPrarie = makeQuote(
`To make a prairie it takes a clover and one bee,
One clover, and a bee.
And revery.
The revery alone will do,
If bees are few.`,
"Emily Dickinson",
"To make a prairie",
);
const blackbird1 = makeQuote(
`I was of three minds,
Like a tree
In which there are three blackbirds.`,
"Wallace Stevens",
"Thirteen Ways of Looking at a Blackbird",
);
const emergency1 = makeQuote(
`Hasn't the goal all along been
to make an unforgettable sound?`,
"Dobby Gibson",
"This Is a Test of the Federal Emergency Management Agency Wireless Warning System",
);
const stationOfMetro = makeQuote(
`The apparition of these faces in the crowd;
Petals on a wet, black bough.`,
"Ezra Pound",
"In a Station of the Metro",
);
export const inspoRepo = [
reMonarching,
winterRainDeepens,
bermanSnow,
stillLife,
landlady,
suicideNote,
capybaraHotsprings1,
howl1,
eggTooth,
keepingThingsWhole,
somberBull,
theRoom,
illWin,
kingfishers1,
frenchNovel,
iHaveSlept,
toMakeAPrarie,
blackbird1,
emergency1,
stationOfMetro,
];
+48
View File
@@ -0,0 +1,48 @@
import {
beginGameHandler,
createGameHandler,
deleteGameDescriptionHandler,
enterGameOptionConversation,
getArchivedPoemHandler,
leaveGameHandler,
restartAppHandler,
restartGameHandler,
returnToPreviousStateHandler,
setUpGameHandler,
tryJoinGameHandler,
tryLeaveGameHandler,
} from "handlers";
import {
BasicCallback,
Callback,
Context,
GameConfigCallback,
GameOption,
HandlerMap,
} from "types";
const gameOptionConversationFn =
(gameOption: GameOption) => async (ctx: Context) => {
await enterGameOptionConversation(ctx, gameOption);
};
export const callbackHandlerMap: HandlerMap<Callback> = {
[BasicCallback.BEGIN_GAME]: beginGameHandler,
[BasicCallback.CREATE_GAME]: createGameHandler,
[BasicCallback.EXIT_ARCHIVE]: restartAppHandler,
[BasicCallback.LEAVE_GAME]: leaveGameHandler,
[BasicCallback.RESTART_APP]: restartAppHandler,
[BasicCallback.RESTART_GAME]: restartGameHandler,
[BasicCallback.RETURN_TO_PREVIOUS_STATE]: returnToPreviousStateHandler,
[BasicCallback.SET_UP_GAME]: setUpGameHandler,
[BasicCallback.TRY_JOIN]: tryJoinGameHandler,
[BasicCallback.TRY_LEAVE]: tryLeaveGameHandler,
[BasicCallback.VIEW_ARCHIVED_POEMS]: getArchivedPoemHandler,
[GameConfigCallback.ADD_DESCRIPTION]: gameOptionConversationFn(
GameOption.DESCRIPTION,
),
[GameConfigCallback.EDIT_DESCRIPTION]: gameOptionConversationFn(
GameOption.DESCRIPTION,
),
[GameConfigCallback.REMOVE_DESCRIPTION]: deleteGameDescriptionHandler,
};
@@ -0,0 +1,7 @@
import { hidePoemMetaHandler, showPoemMetaHandler } from "handlers";
import { CallbackWithData, HandlerMap } from "types";
export const callbackWithDataHandlerMap: HandlerMap<CallbackWithData> = {
[CallbackWithData.HIDE_POEM_META]: hidePoemMetaHandler,
[CallbackWithData.SHOW_POEM_META]: showPoemMetaHandler,
};
+8
View File
@@ -0,0 +1,8 @@
import { inspoHandler, startHandler } from "handlers";
import { Command, HandlerMap } from "types";
export const commandHandlerMap: HandlerMap<Command> = {
[Command.START]: startHandler,
[Command.RESTART]: startHandler,
[Command.INSPO]: inspoHandler,
};
@@ -0,0 +1,14 @@
import {
archivedPoemConversation,
gameDescriptionConversation,
gameIdConversation,
} from "handlers";
import { ConversationState, ConversationFn } from "types";
export const conversationHandlerMap: {
[key in ConversationState]: ConversationFn;
} = {
[ConversationState.GET_ARCHIVED_POEM]: archivedPoemConversation,
[ConversationState.GET_GAME_DESCRIPTION]: gameDescriptionConversation,
[ConversationState.GET_GAME_ID]: gameIdConversation,
};
@@ -0,0 +1,5 @@
import { ConversationState, GameOption } from "types";
export const gameOptionConversationMap = {
[GameOption.DESCRIPTION]: ConversationState.GET_GAME_DESCRIPTION,
};
+8
View File
@@ -0,0 +1,8 @@
export { callbackHandlerMap } from "./callback-handler-map";
export { callbackWithDataHandlerMap } from "./callback-with-data-handler-map";
export { commandHandlerMap } from "./command-handler-map";
export { conversationHandlerMap } from "./conversation-handler-map";
export { gameOptionConversationMap } from "./game-option-conversation-map";
export { replyHandlerMap } from "./reply-handler-map";
export { stateKeyboardButtonMap } from "./state-keyboard-button-map";
export { stateMessageMap } from "./state-message-map";
+8
View File
@@ -0,0 +1,8 @@
import { writingReplyHandler } from "handlers";
import { archiveSearchHandler } from "handlers/replies/archive-search";
import { PlayerState, ReplyHandlerFn } from "types";
export const replyHandlerMap: { [key in PlayerState]?: ReplyHandlerFn } = {
[PlayerState.IN_ARCHIVES]: archiveSearchHandler,
[PlayerState.WRITING]: writingReplyHandler,
};
@@ -0,0 +1,23 @@
import { PlayerState } from "generated/prisma/enums";
import { PlayerModel } from "generated/prisma/models";
import { Callback, ConversationState, MessagingState } from "types";
export const stateCommandsMap: {
[key in MessagingState]: ((player: PlayerModel) => Callback[]) | null;
} = {
[PlayerState.IN_ARCHIVES]: (_player) => [],
[PlayerState.LOBBY]: (_player) => [],
[PlayerState.POST_GAME]: (_player) => [],
[PlayerState.WRITING]: (_player) => [],
[PlayerState.SETTING_UP_GAME]: (_player) => [],
[PlayerState.SPECTATING]: (_player) => [],
[PlayerState.START]: (_player) => [],
[PlayerState.TRYING_LEAVE]: (_player) => [],
[PlayerState.TYPING]: (_player) => [],
[PlayerState.VIEWING_ARCHIVED_POEM]: (_player) => [],
[PlayerState.WAITING_AFTER_WRITING]: (_player) => [],
[PlayerState.WAITING_TO_WRITE]: (_player) => [],
[ConversationState.GET_ARCHIVED_POEM]: (_player) => [],
[ConversationState.GET_GAME_DESCRIPTION]: (_player) => [],
[ConversationState.GET_GAME_ID]: (_player) => [],
};
+120
View File
@@ -0,0 +1,120 @@
import { Player } from "classes";
import { BOT_NAME, LOBBY_MESSAGE } from "const/strings";
import { getGameOrThrow } from "data";
import {
ConversationState,
GameOption,
MessagingState,
PlayerState,
} from "types";
import { randomStarDecorator } from "utils";
export const stateMessageMap: {
[key in MessagingState]: ((player: Player) => string) | null;
} = {
// player state messages
[PlayerState.IN_ARCHIVES]: (player) => {
// on first search
if (player.previousState === PlayerState.START) {
// TODO use force reply or refactor conversation to make this easier
return `Welcome to the archives! Poems completed in games are stored here with 4-digit ids. Reply to this message (tap and press "Reply") with a poem id to re-read it.`;
}
// on subsequent actions
return `Enter another poem id to search, or click the "Exit Archive" button to return to the start page.`;
},
[PlayerState.LOBBY]: (player) => {
const game = getGameOrThrow(player.gameId);
const players = game.getPlayers();
const isHost = player.isHost();
return LOBBY_MESSAGE(players, game, isHost);
},
[PlayerState.POST_GAME]: (_player) => {
return `Poem finished! Here's what you all came up with:`;
},
[PlayerState.WRITING]: (player) => {
const game = getGameOrThrow(player.gameId);
const firstPlayer = !game.poem.lines.length;
const lastPlayer = !game.getEligiblePlayers().length;
const previousLine = game.previousLine();
// TODO use force reply or refactor conversation to make this easier
let message = `It's your turn to add to the poem! ${randomStarDecorator()}\n\nReply to this message (tap and press "Reply") to add the next line.\n\n`;
if (previousLine) {
message += `Previous line: ${previousLine}\n\n`;
}
if (firstPlayer && lastPlayer) {
message += "You're writing the only line, I guess!!\n\n";
} else {
if (firstPlayer) message += `You're writing the first line!\n\n`;
if (lastPlayer) message += `You're writing the last line!\n\n`;
}
return message;
},
[PlayerState.SETTING_UP_GAME]: (player) => {
const game = getGameOrThrow(player.gameId);
const description = game?.options[GameOption.DESCRIPTION];
return `Use the buttons below to configure your game. (More configuration options TBD). Press "Create Game" when ready.${
description ? `\n\nGame Description: ${description}` : ""
}`;
},
[PlayerState.SPECTATING]: (player) => {
const game = getGameOrThrow(player.gameId);
let message =
"This game is in progress, so you are spectating until the current round is over. ";
if (game.poem.completed) {
message +=
"Players are viewing a completed poem and waiting for the host to return to the lobby.";
} else {
const eligiblePlayersRemaining = game.getEligiblePlayers().length + 1;
const oneRemaining = eligiblePlayersRemaining === 1;
message += `There ${
oneRemaining ? "is" : "are"
} ${eligiblePlayersRemaining} more player${oneRemaining ? "" : "s"} to go.`;
}
return message;
},
[PlayerState.START]: (_player) =>
`Welcome to ${BOT_NAME}!\n\nThe game is simple: work with your friends to write a poem one line at a time. However, each person only gets to see the line written by the person before them, leading to chaos and streams-of-consciousness.\n\nNOTE: This bot is a work-in-progress, with further game modes and quality-of-life features planned for future development. For comments, questions, or to report a bug, please message ${process.env.MY_USERNAME}. (Last updated: 1-18-2026)\n\nHit one of the buttons below to get started.`,
[PlayerState.TRYING_LEAVE]: (player) => {
return `Are you sure you want to leave the game?${player.isHost() ? " Since you are the host, this will end the game for all players." : ""}`;
},
[PlayerState.TYPING]: null,
[PlayerState.VIEWING_ARCHIVED_POEM]: (_player) => {
return "viewing archived poem state";
},
[PlayerState.WAITING_AFTER_WRITING]: (player) => {
const game = getGameOrThrow(player.gameId);
const totalEligible = game.getEligiblePlayers().length;
const eligiblePlayersRemaining = totalEligible || 1;
const oneRemaining = eligiblePlayersRemaining === 1;
return `There ${
oneRemaining ? "is" : "are"
} ${eligiblePlayersRemaining} more player${oneRemaining ? "" : "s"} to go.`;
},
[PlayerState.WAITING_TO_WRITE]: (player) => {
const game = getGameOrThrow(player.gameId);
const eligiblePlayers = game.getEligiblePlayers();
const oneRemaining = eligiblePlayers.length === 1;
const playerPosition =
eligiblePlayers.findIndex(
(eligiblePlayer) => eligiblePlayer.id === player.id,
) + 1;
const playerIsNext = playerPosition === 1;
const nextUpAlert = playerIsNext ? `Your turn is next!` : "";
const queueStatus = `There ${
oneRemaining ? "is" : "are"
} now ${playerPosition} player${oneRemaining ? "" : "s"} ahead of you.`;
return `Waiting to write. ${nextUpAlert || queueStatus}`;
},
// conversation state messages
[ConversationState.GET_GAME_DESCRIPTION]: (player) => {
const game = getGameOrThrow(player.gameId);
const description = game?.options[GameOption.DESCRIPTION];
return `Add a description for your game (a writing prompt, a greeting, or anything else).${
description ? `\n\nCurrent Description: ${description}` : ""
}`;
},
[ConversationState.GET_ARCHIVED_POEM]: (_player) =>
"Enter the id of the poem to check out.",
[ConversationState.GET_GAME_ID]: () => "Enter a 4-character game ID.",
};
+65
View File
@@ -0,0 +1,65 @@
import { Game, Player } from "classes";
import { GameOption } from "types";
export const MY_USERNAME = `${process.env.MY_USERNAME}`;
export const PLS_REPORT = `Please let ${MY_USERNAME} know.`;
export const BOT_NAME = "JOLB";
export const BOT_RUNNING = `${BOT_NAME} is now running.`;
export const FALLBACK_USERNAME = "User";
export const GAME_NOT_FOUND = "Game not found.";
export const GAME_NOT_FOUND_WITH_ID = (id: string) =>
`Game not found with id ${id}.`;
export const HAS_JOINED = (name: string) => `${name} has joined the game.`;
export const HAS_LEFT = (name: string) => `${name} has left the game.`;
export const HIDE_SHOW_METADATA = (show?: boolean) =>
`${show ? "Hide" : "Show"} Metadata`;
export const HOST_LEFT =
"The host has left the game. Returning to start screen...";
export const HOST_RESTARTED = `The host has restarted the game.`;
export const MISSING_BUTTON_LABEL = "Missing button label.";
export const NO_BOTS_PLS = "No bots pls.";
export const ONLY_HOST_CAN_START = "Only the host can start the game.";
export const PLAYER_NOT_FOUND = "Player not found.";
export const POEM_NOT_FOUND = "Poem not found.";
export const POEM_NOT_FOUND_WITH_ID = (id: number) =>
`Poem not found with id ${id}.`;
export const USER_NOT_FOUND = "User not found.";
export const RETRIEVE_FROM_ARCHIVE = (id: number) => `(Poem ID: ${id})`;
export const UNKNOWN_ERROR = "An unknown error occurred.";
export const VALIDATION_POEM_ID_IS_NUMBER = "Poem id must be a number.";
export const WELCOME_TO_ARCHIVES = "Welcome to the archives!";
// player state messages
export const LOBBY_MESSAGE = (
players: Player[],
game: Game,
isHost: boolean
) => {
const description = game.options[GameOption.DESCRIPTION];
let message = `Welcome to the game!\n\n`;
if (description) {
message += `Description: ${description ? `${description}\n\n` : ""}`;
}
message += `Players in lobby:\n\n${players
.map((player) => {
return `- ${player.userName}${
player.id === game.hostId ? " (host)" : ""
}`;
})
.join("\n")}\n\n`;
if (!isHost) {
message += "Waiting for host to start game.\n\n";
}
message += `Invite others with game code ${game?.id}.`;
return message;
};
export const POST_GAME_MESSAGE = ``;
export const READY_TO_WRITE_MESSAGE = ``;
export const SETTING_UP_GAME_MESSAGE = ``;
export const SPECTATING_MESSAGE = ``;
export const START_MESSAGE = ``;
export const TRYING_LEAVE_MESSAGE = ``;
export const TYPING_MESSAGE = ``;
export const VIEWING_ARCHIVED_POEM_MESSAGE = ``;
export const WAITING_AFTER_WRITING_MESSAGE = ``;
export const WAITING_TO_WRITE_MESSAGE = ``;
+57
View File
@@ -0,0 +1,57 @@
import { GameModel, PlayerModel, PoemModel } from "generated/prisma/models";
import { prisma } from "../../lib/prisma";
import { ObjName } from "./types";
export async function create<InputType, OutputType>(
objName: ObjName,
data: InputType,
) {
try {
let newObj;
switch (objName) {
case "game":
if (Array.isArray(data)) {
newObj = await prisma.game.createMany({
data: data as GameModel[],
});
} else {
newObj = await prisma.game.create({
data: data as GameModel,
include: {
players: true,
},
});
}
break;
case "player":
if (Array.isArray(data)) {
newObj = await prisma.player.createMany({
data: data as PlayerModel[],
});
} else {
newObj = await prisma.player.create({
data: data as PlayerModel,
});
}
break;
case "poem":
if (Array.isArray(data)) {
newObj = await prisma.poem.createMany({
data: (data as PoemModel[]).map((d) => ({
...d,
lines: d.lines!,
})),
});
} else {
newObj = await prisma.poem.create({
data: { ...(data as PoemModel), lines: (data as PoemModel).lines! },
});
}
break;
}
return newObj as OutputType;
} catch (e) {
console.error(e);
process.exit(1);
}
}
+53
View File
@@ -0,0 +1,53 @@
import { prisma } from "../../lib/prisma";
import { IdSearchKey, ObjName } from "./types";
export async function del(objName: ObjName, id: IdSearchKey) {
try {
switch (objName) {
case "game":
if (Array.isArray(id)) {
await prisma.game.deleteMany({
where: { gameId: { in: id as number[] } },
});
} else if (id === "all") {
await prisma.game.deleteMany({});
} else {
await prisma.game.delete({
where: { gameId: id as number },
});
}
break;
case "player":
if (Array.isArray(id)) {
await prisma.player.deleteMany({
where: { playerId: { in: id as string[] } },
});
} else if (id === "all") {
await prisma.player.deleteMany({});
} else {
await prisma.player.delete({
where: { playerId: id as string },
});
}
break;
case "poem":
if (Array.isArray(id)) {
await prisma.poem.deleteMany({
where: { poemId: { in: id as number[] } },
});
} else if (id === "all") {
await prisma.poem.deleteMany({});
} else {
await prisma.poem.delete({
where: { poemId: id as number },
});
}
break;
}
await prisma.$disconnect();
} catch (e) {
console.error(e);
await prisma.$disconnect();
process.exit(1);
}
}
+54
View File
@@ -0,0 +1,54 @@
import { prisma } from "lib/prisma";
import { IdSearchKey, Model } from "./types";
export async function get<T = Model | Model[]>(
objName: string,
id: IdSearchKey,
) {
try {
let obj;
switch (objName) {
case "game":
if (Array.isArray(id)) {
obj = await prisma.game.findMany({
where: { gameId: { in: id as number[] } },
});
} else if (id === "all") {
obj = await prisma.game.findMany();
} else {
obj = await prisma.game.findUnique({
where: { gameId: id as number },
});
}
break;
case "player":
if (Array.isArray(id)) {
obj = await prisma.player.findMany({
where: { playerId: { in: id as string[] } },
});
} else if (id === "all") {
obj = await prisma.player.findMany();
} else {
obj = await prisma.player.findUnique({
where: { playerId: id as string },
});
}
break;
case "poem":
if (Array.isArray(id)) {
obj = await prisma.poem.findMany({
where: { poemId: { in: id as number[] } },
});
} else {
obj = await prisma.poem.findUnique({
where: { poemId: id as number },
});
}
break;
}
return obj as T;
} catch (e) {
console.error(e);
process.exit(1);
}
}
+4
View File
@@ -0,0 +1,4 @@
export { create } from "./create";
export { del } from "./delete";
export { get } from "./get";
export { update } from "./update";
+11
View File
@@ -0,0 +1,11 @@
import { GameModel, PlayerModel, PoemModel } from "generated/prisma/models";
export type ObjName = "game" | "player" | "poem";
export type Model = GameModel | PlayerModel | PoemModel;
export type PartialGame = Partial<GameModel>;
export type PartialPlayer = Partial<PlayerModel>;
export type PartialPoem = Partial<PoemModel>;
export type PartialModel = PartialGame | PartialPlayer | PartialPoem;
export type IdSearchKey = number | string | string[] | number[] | "all";
+73
View File
@@ -0,0 +1,73 @@
import { prisma } from "../../lib/prisma";
import {
PartialGame,
PartialPlayer,
PartialPoem,
ObjName,
IdSearchKey,
} from "./types";
export async function update<InputType, OutputType>(
objName: ObjName,
data: InputType,
id: IdSearchKey,
) {
try {
let updatedObj;
switch (objName) {
case "game":
if (Array.isArray(id)) {
updatedObj = await prisma.game.updateManyAndReturn({
where: { gameId: { in: id as number[] } },
data: data as PartialGame,
});
} else {
updatedObj = await prisma.game.update({
where: { gameId: id as number },
data: data as PartialGame,
include: {
players: true,
poem: true,
},
});
}
break;
case "player":
if (Array.isArray(id)) {
updatedObj = await prisma.player.updateManyAndReturn({
where: { playerId: { in: id as string[] } },
data: data as PartialPlayer,
});
} else {
updatedObj = await prisma.player.update({
where: { playerId: id as string },
data: data as PartialPlayer,
});
}
break;
case "poem":
if (Array.isArray(id)) {
updatedObj = await prisma.poem.updateManyAndReturn({
where: { poemId: { in: id as number[] } },
data: {
...(data as PartialPoem),
lines: (data as PartialPoem).lines!,
},
});
} else {
updatedObj = await prisma.poem.update({
where: { poemId: id as number },
data: {
...(data as PartialPoem),
lines: (data as PartialPoem).lines!,
},
});
break;
}
}
return updatedObj as OutputType;
} catch (e) {
console.error(e);
process.exit(1);
}
}
+31
View File
@@ -0,0 +1,31 @@
import { GameModel } from "generated/prisma/models";
import { PartialGame } from "data/crud/types";
import { create, del, get, update } from "../crud";
export const createGame = async (data: PartialGame) => {
return await create<PartialGame, GameModel>("game", data);
};
export const deleteGame = async (id: number) => {
return await del("game", id);
};
export const getGame = async (id: number) => {
return await get<GameModel>("game", id);
};
export const updateGame = async (id: number, data: PartialGame) => {
return await update<PartialGame, GameModel>("game", data, id);
};
export const deleteGames = async (ids: number[]) => {
return await del("game", ids);
};
export const getGames = async (ids: number[] | "all") => {
return await get<GameModel[]>("game", ids);
};
export const updateGames = async (ids: number[], data: PartialGame) => {
return await update<PartialGame, GameModel[]>("game", data, ids);
};
+3
View File
@@ -0,0 +1,3 @@
export * from "./game";
export * from "./player";
export * from "./poem";
+103
View File
@@ -0,0 +1,103 @@
import { Prisma } from "generated/prisma/client";
import { PlayerState } from "generated/prisma/enums";
import { PlayerModel } from "generated/prisma/models";
import { PartialPlayer } from "data/crud/types";
import { prisma } from "lib/prisma";
import { Context } from "types";
import { getUserInfo } from "utils";
import { create, del, get, update } from "../crud";
export const createPlayer = async (ctx: Context) => {
const { playerId, userName } = await getUserInfo(ctx);
return await create<PartialPlayer, PlayerModel>("player", {
userName,
playerId,
});
};
export const createPlayers = async (data: PartialPlayer[]) => {
return await create<PartialPlayer[], PlayerModel[]>("player", data);
};
export const deletePlayer = async (id: number) => {
return await del("player", id);
};
export const deletePlayers = async (ids: number[]) => {
return await del("player", ids);
};
export const getPlayer = async (
findBy: string | Context,
ifNotFound?: "throw" | "create",
) => {
const playerId =
typeof findBy === "string" ? findBy : getUserInfo(findBy).playerId;
const player = await get<PlayerModel>("player", playerId);
if (ifNotFound && !player) {
if (ifNotFound === "throw") {
throw new Error(`Player with id ${playerId} not found`);
}
return await createPlayer(ifNotFound);
}
return player;
};
export const getPlayers = async (ids: string[]) => {
return await get<PlayerModel[]>("player", ids);
};
export const updatePlayer = async (id: number, data: Partial<PlayerModel>) => {
return await update<Partial<PlayerModel>, PlayerModel>("player", data, id);
};
export const updatePlayers = async (
ids: number[],
data: Partial<PlayerModel>,
) => {
return await update<Partial<PlayerModel>, PlayerModel[]>("player", data, ids);
};
export const updatePlayerState = async (id: number, newState: PlayerState) => {
await prisma.$executeRaw`
UPDATE "Player"
SET "previousState" = "state",
"state" = ${newState}
WHERE "id" IS ${id});
`;
};
export const updatePlayerStates = async (
ids: number[],
newState: PlayerState,
) => {
await prisma.$executeRaw`
UPDATE "Player"
SET "previousState" = "state",
"state" = ${newState}
WHERE "id" IN (${Prisma.join(ids)});
`;
};
export const updatePlayerGame = async (id: number, currentGameId: number) => {
return await update<PartialPlayer, PlayerModel>(
"player",
{ currentGameId },
id,
);
};
export const updatePlayerGames = async (
ids: number[],
currentGameId: number,
) => {
return await update<PartialPlayer, PlayerModel[]>(
"player",
{ currentGameId },
ids,
);
};
export const updatePlayerUsername = async (id: number, userName: string) => {
return await update<PartialPlayer, PlayerModel>("player", { userName }, id);
};
+35
View File
@@ -0,0 +1,35 @@
import { PoemModel } from "generated/prisma/models";
import { create, del, get, update } from "../crud";
import { PartialPoem } from "data/crud/types";
export const createPoem = async (data: PartialPoem) => {
return await create<PartialPoem, PoemModel>("poem", data);
};
export const deletePoem = async (id: number) => {
return await del("poem", id);
};
export const getPoem = async (id: number) => {
return await get<PoemModel>("poem", id);
};
export const updatePoem = async (id: number, data: PartialPoem) => {
return await update<PartialPoem, PoemModel>("poem", data, id);
};
export const createPoems = async (data: PartialPoem[]) => {
return await create<PartialPoem[], PoemModel[]>("poem", data);
};
export const deletePoems = async (ids: number[]) => {
return await del("poem", ids);
};
export const getPoems = async (ids: number[]) => {
return await get<PoemModel[]>("poem", ids);
};
export const updatePoems = async (ids: number[], data: PartialPoem) => {
return await update<PartialPoem, PoemModel[]>("poem", data, ids);
};
+3
View File
@@ -0,0 +1,3 @@
export * from "./crud";
export * from "./helpers";
export * from "./validation";
+3
View File
@@ -0,0 +1,3 @@
export const validateGame = () => {
return true;
};
+3
View File
@@ -0,0 +1,3 @@
export { validateGame } from "./game";
export { validatePlayer } from "./player";
export { validatePoem } from "./poem";
+3
View File
@@ -0,0 +1,3 @@
export const validatePlayer = () => {
return true;
};
+3
View File
@@ -0,0 +1,3 @@
export const validatePoem = () => {
return true;
};
+39
View File
@@ -0,0 +1,39 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser'
export { Prisma }
export * as $Enums from './enums'
export * from './enums';
/**
* Model Player
*
*/
export type Player = Prisma.PlayerModel
/**
* Model Game
*
*/
export type Game = Prisma.GameModel
/**
* Model GameConfig
*
*/
export type GameConfig = Prisma.GameConfigModel
/**
* Model Poem
*
*/
export type Poem = Prisma.PoemModel
+59
View File
@@ -0,0 +1,59 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import * as $Class from "./internal/class"
import * as Prisma from "./internal/prismaNamespace"
export * as $Enums from './enums'
export * from "./enums"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient()
* // Fetch zero or more Players
* const players = await prisma.player.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model Player
*
*/
export type Player = Prisma.PlayerModel
/**
* Model Game
*
*/
export type Game = Prisma.GameModel
/**
* Model GameConfig
*
*/
export type GameConfig = Prisma.GameConfigModel
/**
* Model Poem
*
*/
export type Poem = Prisma.PoemModel
+401
View File
@@ -0,0 +1,401 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import type * as Prisma from "./internal/prismaNamespace"
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type IntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type EnumPlayerStateFilter<$PrismaModel = never> = {
equals?: $Enums.PlayerState | Prisma.EnumPlayerStateFieldRefInput<$PrismaModel>
in?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
notIn?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumPlayerStateFilter<$PrismaModel> | $Enums.PlayerState
}
export type SortOrderInput = {
sort: Prisma.SortOrder
nulls?: Prisma.NullsOrder
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
mode?: Prisma.QueryMode
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type EnumPlayerStateWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.PlayerState | Prisma.EnumPlayerStateFieldRefInput<$PrismaModel>
in?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
notIn?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumPlayerStateWithAggregatesFilter<$PrismaModel> | $Enums.PlayerState
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumPlayerStateFilter<$PrismaModel>
_max?: Prisma.NestedEnumPlayerStateFilter<$PrismaModel>
}
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type JsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonFilterBase<$PrismaModel>>, 'path'>>
export type JsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
export type JsonWithAggregatesFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
Required<JsonWithAggregatesFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<JsonWithAggregatesFilterBase<$PrismaModel>>, 'path'>>
export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedJsonFilter<$PrismaModel>
_max?: Prisma.NestedJsonFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedIntNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
}
export type NestedEnumPlayerStateFilter<$PrismaModel = never> = {
equals?: $Enums.PlayerState | Prisma.EnumPlayerStateFieldRefInput<$PrismaModel>
in?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
notIn?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumPlayerStateFilter<$PrismaModel> | $Enums.PlayerState
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel>
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedIntNullableWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntNullableWithAggregatesFilter<$PrismaModel> | number | null
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
_avg?: Prisma.NestedFloatNullableFilter<$PrismaModel>
_sum?: Prisma.NestedIntNullableFilter<$PrismaModel>
_min?: Prisma.NestedIntNullableFilter<$PrismaModel>
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type NestedFloatNullableFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> | null
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> | null
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type NestedEnumPlayerStateWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.PlayerState | Prisma.EnumPlayerStateFieldRefInput<$PrismaModel>
in?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
notIn?: $Enums.PlayerState[] | Prisma.ListEnumPlayerStateFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumPlayerStateWithAggregatesFilter<$PrismaModel> | $Enums.PlayerState
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumPlayerStateFilter<$PrismaModel>
_max?: Prisma.NestedEnumPlayerStateFilter<$PrismaModel>
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel>
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedJsonFilter<$PrismaModel = never> =
| Prisma.PatchUndefined<
Prisma.Either<Required<NestedJsonFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>,
Required<NestedJsonFilterBase<$PrismaModel>>
>
| Prisma.OptionalFlat<Omit<Required<NestedJsonFilterBase<$PrismaModel>>, 'path'>>
export type NestedJsonFilterBase<$PrismaModel = never> = {
equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
path?: string[]
mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel>
string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel>
array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null
lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel>
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
}
+27
View File
@@ -0,0 +1,27 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
export const PlayerState = {
IN_ARCHIVES: 'IN_ARCHIVES',
LOBBY: 'LOBBY',
POST_GAME: 'POST_GAME',
SETTING_UP_GAME: 'SETTING_UP_GAME',
SPECTATING: 'SPECTATING',
START: 'START',
TRYING_LEAVE: 'TRYING_LEAVE',
TYPING: 'TYPING',
VIEWING_ARCHIVED_POEM: 'VIEWING_ARCHIVED_POEM',
WAITING_AFTER_WRITING: 'WAITING_AFTER_WRITING',
WAITING_TO_WRITE: 'WAITING_TO_WRITE',
WRITING: 'WRITING'
} as const
export type PlayerState = (typeof PlayerState)[keyof typeof PlayerState]
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models'
export type * from './prismaNamespace'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
Player: 'Player',
Game: 'Game',
GameConfig: 'GameConfig',
Poem: 'Poem'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const PlayerScalarFieldEnum = {
playerId: 'playerId',
createdAt: 'createdAt',
currentGameId: 'currentGameId',
state: 'state',
previousState: 'previousState',
userName: 'userName'
} as const
export type PlayerScalarFieldEnum = (typeof PlayerScalarFieldEnum)[keyof typeof PlayerScalarFieldEnum]
export const GameScalarFieldEnum = {
gameId: 'gameId',
createdAt: 'createdAt',
joinCode: 'joinCode',
poemId: 'poemId',
gameConfigId: 'gameConfigId',
hostPlayerId: 'hostPlayerId',
inProgress: 'inProgress'
} as const
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
export const GameConfigScalarFieldEnum = {
configId: 'configId'
} as const
export type GameConfigScalarFieldEnum = (typeof GameConfigScalarFieldEnum)[keyof typeof GameConfigScalarFieldEnum]
export const PoemScalarFieldEnum = {
poemId: 'poemId',
createdAt: 'createdAt',
gameId: 'gameId',
finished: 'finished',
lines: 'lines'
} as const
export type PoemScalarFieldEnum = (typeof PoemScalarFieldEnum)[keyof typeof PoemScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
export const JsonNullValueInput = {
JsonNull: JsonNull
} as const
export type JsonNullValueInput = (typeof JsonNullValueInput)[keyof typeof JsonNullValueInput]
export const QueryMode = {
default: 'default',
insensitive: 'insensitive'
} as const
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode]
export const NullsOrder = {
first: 'first',
last: 'last'
} as const
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
export const JsonNullValueFilter = {
DbNull: DbNull,
JsonNull: JsonNull,
AnyNull: AnyNull
} as const
export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter]
+15
View File
@@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/Player'
export type * from './models/Game'
export type * from './models/GameConfig'
export type * from './models/Poem'
export type * from './commonInputTypes'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
import { ONLY_HOST_CAN_START } from "const";
import { getPlayer } from "data";
import { messagePlayer } from "messaging";
import { HandlerFn } from "types";
export const beginGameHandler: HandlerFn = async (ctx) => {
const player = getPlayer(ctx);
const game = getGameOrThrow(player.gameId);
const isHost = player.isHost();
if (!isHost) {
await messagePlayer(player.id, player.state, ctx, ONLY_HOST_CAN_START);
return;
}
await game.startGame(ctx);
};
+8
View File
@@ -0,0 +1,8 @@
import { getContextPlayerOrThrow } from "data";
import { setPlayerState } from "player-state";
import { HandlerFn, PlayerState } from "types";
export const createGameHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
await setPlayerState(player.id, ctx, PlayerState.LOBBY);
};
@@ -0,0 +1,12 @@
import { getContextPlayerOrThrow, getGameOrThrow } from "data";
import { setPlayerState } from "player-state";
import { HandlerFn, PlayerState } from "types";
const { SETTING_UP_GAME } = PlayerState;
export const deleteGameDescriptionHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
const game = getGameOrThrow(player.gameId);
game.setOptions({ description: undefined });
await setPlayerState(player.id, ctx, SETTING_UP_GAME);
};
+3
View File
@@ -0,0 +1,3 @@
import { HandlerFn } from "types";
export const exitArchiveHandler: HandlerFn = async (_ctx) => {};
@@ -0,0 +1,8 @@
import { getContextPlayerOrCreate } from "data";
import { setPlayerState } from "player-state";
import { HandlerFn, PlayerState } from "types";
export const getArchivedPoemHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrCreate(ctx);
await setPlayerState(player.id, ctx, PlayerState.IN_ARCHIVES);
};
+13
View File
@@ -0,0 +1,13 @@
export { beginGameHandler } from "./begin-game";
export { createGameHandler } from "./create-game";
export { deleteGameDescriptionHandler } from "./delete-description";
export { exitArchiveHandler } from "./exit-archive";
export { getArchivedPoemHandler } from "./get-archived-poem";
export { leaveGameHandler } from "./leave-game";
export { restartAppHandler } from "./restart-app";
export { restartGameHandler } from "./restart-game";
export { returnToPreviousStateHandler } from "./return-to-previous-state";
export { setUpGameHandler } from "./set-up-game";
export { tryJoinGameHandler } from "./try-join-game";
export { tryLeaveGameHandler } from "./try-leave";
export * from "./utils";
+8
View File
@@ -0,0 +1,8 @@
import { getContextPlayerOrThrow, getGameOrThrow, leaveGame } from "data";
import { HandlerFn } from "types";
export const leaveGameHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
const game = getGameOrThrow(player.gameId);
await leaveGame(ctx, game, player);
};
+8
View File
@@ -0,0 +1,8 @@
import { getContextPlayerOrCreate } from "data";
import { setPlayerState } from "player-state";
import { HandlerFn, PlayerState } from "types";
export const restartAppHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrCreate(ctx);
await setPlayerState(player.id, ctx, PlayerState.START);
};
+10
View File
@@ -0,0 +1,10 @@
import { getContextPlayerOrThrow, getGameOrThrow } from "data";
import { HandlerFn } from "types";
export const restartGameHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
const isHost = player.isHost();
if (!isHost) return;
const game = getGameOrThrow(player.gameId);
await game.restart(ctx);
};
@@ -0,0 +1,17 @@
import { getContextPlayerOrThrow, getGame, leaveGame } from "data";
import { setPlayerState } from "player-state";
import { HandlerFn, PlayerState } from "types";
export const returnToPreviousStateHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
const game = getGame(player.gameId);
if (game && player.state === PlayerState.SETTING_UP_GAME) {
await leaveGame(ctx, game, player);
return;
}
await setPlayerState(
player.id,
ctx,
player.previousState || PlayerState.START,
);
};
+8
View File
@@ -0,0 +1,8 @@
import { addGame, getContextPlayerOrThrow, joinGame } from "data";
import { HandlerFn } from "types";
export const setUpGameHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
const game = addGame();
await joinGame(ctx, game, player, true);
};
+6
View File
@@ -0,0 +1,6 @@
import { ConversationState, HandlerFn } from "types";
import { enterConversation } from "utils";
export const tryJoinGameHandler: HandlerFn = async (ctx) => {
await enterConversation(ConversationState.GET_GAME_ID, ctx);
};
+8
View File
@@ -0,0 +1,8 @@
import { getContextPlayerOrThrow } from "data";
import { setPlayerState } from "player-state/set-player-state";
import { HandlerFn, PlayerState } from "types";
export const tryLeaveGameHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrThrow(ctx);
await setPlayerState(player.id, ctx, PlayerState.TRYING_LEAVE);
};
@@ -0,0 +1,11 @@
import { gameOptionConversationMap } from "const";
import { Context, GameOption } from "types";
import { enterConversation } from "utils";
export const enterGameOptionConversation = async (
ctx: Context,
option: GameOption
) => {
const conversation = gameOptionConversationMap[option];
await enterConversation(conversation, ctx);
};
+1
View File
@@ -0,0 +1 @@
export { enterGameOptionConversation } from "./configure-option-handler";
+2
View File
@@ -0,0 +1,2 @@
export { inspoHandler } from "./inspo";
export { startHandler } from "./start";
+9
View File
@@ -0,0 +1,9 @@
import { inspoRepo } from "const";
import { getContextPlayerOrCreate } from "data";
import { HandlerFn } from "types";
export const inspoHandler: HandlerFn = async (ctx) => {
const player = getContextPlayerOrCreate(ctx);
const inspo = inspoRepo[Math.floor(Math.random() * inspoRepo.length)];
await ctx.api.sendMessage(player.id, inspo, { parse_mode: "MarkdownV2" });
};
+46
View File
@@ -0,0 +1,46 @@
import { Poem } from "classes";
import {
addArchivedPoem,
addGame,
addPlayer,
getContextPlayerOrCreate,
getGame,
joinGame,
leaveGame,
} from "data";
import { setPlayerState } from "player-state";
import { HandlerFn, PlayerState } from "types";
const testingGame = false;
const testingArchive = false;
export const startHandler: HandlerFn = async (ctx) => {
if (testingArchive) {
const testPoem = new Poem({ id: 1234, gameId: "TEST" });
testPoem.addLine("Line", "Test author");
addArchivedPoem(testPoem);
}
if (testingGame) {
const testGame =
getGame("test") ||
addGame({ hostId: Number(process.env.MY_USERID), id: "TEST" });
for (const testuser of [
{
id: Number(process.env.MY_USERID),
name: process.env.MY_USERNAME ?? "You",
},
{ id: Number(process.env.TEST_USER_1_ID), name: "First Test User" },
{ id: Number(process.env.TEST_USER_2_ID), name: "Second Test User" },
]) {
const newPlayer = addPlayer({ id: testuser.id, userName: testuser.name });
await joinGame(ctx, testGame, newPlayer);
}
return;
}
const player = getContextPlayerOrCreate(ctx);
const game = getGame(player.gameId);
if (game) {
await leaveGame(ctx, game, player);
}
await setPlayerState(player.id, ctx, PlayerState.START);
};
@@ -0,0 +1,45 @@
import { POEM_NOT_FOUND_WITH_ID, VALIDATION_POEM_ID_IS_NUMBER } from "const";
import { getArchivedPoem, getContextPlayerOrCreate } from "data";
import { messagePlayer } from "messaging";
import { setPlayerState } from "player-state";
import { BasicCallback, ConversationFn, PlayerState } from "types";
import { getConversationValue } from "utils";
export const archivedPoemConversation: ConversationFn = async (
conversation,
ctx,
) => {
const player = getContextPlayerOrCreate(ctx);
await getConversationValue(conversation, {
exitCallback: BasicCallback.EXIT_ARCHIVE,
doEach: async () => {},
handleCancel: async () => {
await setPlayerState(player.id, ctx, PlayerState.START);
return;
},
handleSuccess: async (poemId: string) => {
const poem = getArchivedPoem(poemId);
await poem?.sendToPlayer(player.id, ctx);
await setPlayerState(player.id, ctx, PlayerState.IN_ARCHIVES);
return;
},
stay: true,
validate: async (poemId: string) => {
const poemIdNumber = Number(poemId);
let errorMessage = "";
if (isNaN(poemIdNumber)) {
errorMessage = VALIDATION_POEM_ID_IS_NUMBER;
} else {
const poem = getArchivedPoem(poemId);
if (!poem) {
errorMessage = POEM_NOT_FOUND_WITH_ID(poemIdNumber);
}
}
if (errorMessage) {
await messagePlayer(player.id, player.state, ctx, errorMessage);
return false;
}
return true;
},
});
};
+48
View File
@@ -0,0 +1,48 @@
import { GAME_NOT_FOUND_WITH_ID } from "const";
import {
getContextPlayerOrThrow,
getGame,
getGameOrThrow,
joinGame,
} from "data";
import { messagePlayer } from "messaging";
import { setPlayerState } from "player-state";
import {
BasicCallback,
ConversationFn,
ConversationState,
PlayerState,
} from "types";
import { getConversationValue } from "utils";
export const gameIdConversation: ConversationFn = async (conversation, ctx) => {
const player = getContextPlayerOrThrow(ctx);
await getConversationValue(conversation, {
exitCallback: BasicCallback.RETURN_TO_PREVIOUS_STATE,
doEach: async () => {
await messagePlayer(player.id, ConversationState.GET_GAME_ID, ctx);
},
handleCancel: async () => {
await setPlayerState(player.id, ctx, PlayerState.START);
return;
},
handleSuccess: async (gameId: string) => {
const game = getGameOrThrow(gameId);
await joinGame(ctx, game, player);
return;
},
validate: async (gameId: string) => {
const game = getGame(gameId);
if (game) {
return true;
}
await messagePlayer(
player.id,
player.state,
ctx,
GAME_NOT_FOUND_WITH_ID(gameId),
);
return false;
},
});
};
@@ -0,0 +1,40 @@
import { getContextPlayerOrThrow, getGameOrThrow } from "data";
import { messagePlayer } from "messaging";
import { setPlayerState } from "player-state";
import {
BasicCallback,
ConversationFn,
ConversationState,
PlayerState,
} from "types";
import { getConversationValue } from "utils";
export const gameDescriptionConversation: ConversationFn = async (
conversation,
ctx,
) => {
const player = getContextPlayerOrThrow(ctx);
const game = getGameOrThrow(player.gameId);
await getConversationValue(conversation, {
exitCallback: BasicCallback.RETURN_TO_PREVIOUS_STATE,
doEach: async () =>
await messagePlayer(
player.id,
ConversationState.GET_GAME_DESCRIPTION,
ctx,
),
handleCancel: async () => {
await setPlayerState(player.id, ctx, PlayerState.SETTING_UP_GAME);
},
handleSuccess: async (description: string) => {
game.setOptions({ description });
await setPlayerState(player.id, ctx, PlayerState.SETTING_UP_GAME);
return;
},
// eslint-disable-next-line require-await
validate: async (_description: string) => {
return true;
},
});
};
@@ -0,0 +1 @@
export { gameDescriptionConversation } from "./game-description";
+3
View File
@@ -0,0 +1,3 @@
export { archivedPoemConversation } from "./archived-poem-id";
export { gameIdConversation } from "./game-id";
export * from "./game-options";
+1
View File
@@ -0,0 +1 @@
export { hidePoemMetaHandler, showPoemMetaHandler } from "./toggle-poem-meta";
@@ -0,0 +1,47 @@
import { HIDE_SHOW_METADATA, POEM_NOT_FOUND } from "const";
import { getArchivedPoem } from "data";
import { InlineKeyboard } from "grammy";
import { CallbackData, CallbackWithData, Context, DataHandlerFn } from "types";
import { encodeCallbackData } from "utils";
const togglePoemMeta = async (
ctx: Context,
data: CallbackData,
includeMeta?: boolean,
) => {
const poem = getArchivedPoem(data.id);
const keyboard = new InlineKeyboard().text(
HIDE_SHOW_METADATA(includeMeta),
encodeCallbackData(
includeMeta
? CallbackWithData.HIDE_POEM_META
: CallbackWithData.SHOW_POEM_META,
data,
),
);
try {
const compiledPoem = poem?.compile(includeMeta);
if (compiledPoem) {
await ctx.editMessageText(compiledPoem ?? POEM_NOT_FOUND, {
parse_mode: "HTML",
reply_markup: keyboard,
});
}
} catch (error) {
console.log(error);
}
};
export const showPoemMetaHandler: DataHandlerFn = async (
ctx,
data: CallbackData,
) => {
await togglePoemMeta(ctx, data, true);
};
export const hidePoemMetaHandler: DataHandlerFn = async (
ctx,
data: CallbackData,
) => {
await togglePoemMeta(ctx, data);
};
+5
View File
@@ -0,0 +1,5 @@
export * from "./callbacks";
export * from "./commands";
export * from "./conversations";
export * from "./data-callbacks";
export * from "./replies";
+25
View File
@@ -0,0 +1,25 @@
import { VALIDATION_POEM_ID_IS_NUMBER, POEM_NOT_FOUND_WITH_ID } from "const";
import { getArchivedPoem, getContextPlayerOrCreate } from "data";
import { messagePlayer } from "messaging";
import { setPlayerState } from "player-state";
import { PlayerState, ReplyHandlerFn } from "types";
export const archiveSearchHandler: ReplyHandlerFn = async (ctx, poemId) => {
const player = getContextPlayerOrCreate(ctx);
const poemIdNumber = Number(poemId);
let errorMessage = "";
if (isNaN(poemIdNumber)) {
errorMessage = VALIDATION_POEM_ID_IS_NUMBER;
} else {
const poem = getArchivedPoem(poemId);
if (!poem) {
errorMessage = POEM_NOT_FOUND_WITH_ID(poemIdNumber);
} else {
await poem?.sendToPlayer(player.id, ctx);
}
}
if (errorMessage) {
await messagePlayer(player.id, player.state, ctx, errorMessage);
}
await setPlayerState(player.id, ctx, PlayerState.IN_ARCHIVES);
};
+1
View File
@@ -0,0 +1 @@
export * from "./writing";
+9
View File
@@ -0,0 +1,9 @@
import { getContextPlayerOrThrow, getGameOrThrow } from "data";
import { ReplyHandlerFn } from "types";
export const writingReplyHandler: ReplyHandlerFn = async (ctx, reply) => {
// TODO add validation
const player = getContextPlayerOrThrow(ctx);
const game = getGameOrThrow(player.gameId);
await game.addLine(ctx, reply, player);
};
+9
View File
@@ -0,0 +1,9 @@
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "../generated/prisma/client";
const connectionString = `${process.env.DATABASE_URL}`;
const adapter = new PrismaBetterSqlite3({ url: connectionString });
const prisma = new PrismaClient({ adapter });
export { prisma };
+40
View File
@@ -0,0 +1,40 @@
// https://core.telegram.org/bots/api#formatting-options
import { ParseMode } from "grammy/types";
// TODO figure out how to use telegram's markdown because it doesn't seem to work
export const sanitizeMarkdownV2 = (markdown: string) => {
const regex = new RegExp(/[_*[\]()~`>#+-=|{}.!]/g);
const sanitizedMarkdown = markdown;
return sanitizedMarkdown.replace(regex, "\\$&");
};
export const sanitizeHtml = (html: string) => {
const sanitizedHtml = html;
sanitizedHtml.replaceAll("<", "&lt");
sanitizedHtml.replaceAll(">", "&gt");
sanitizedHtml.replaceAll("&", "&amp");
return sanitizedHtml;
};
export const sanitizeText = (
str: string,
parseMode: ParseMode = "MarkdownV2",
): string => {
switch (parseMode) {
case "Markdown":
return str;
case "MarkdownV2":
return sanitizeMarkdownV2(str);
case "HTML":
return sanitizeHtml(str);
}
};
export const getSanitizedMessage = (
msg: string,
parseMode: ParseMode = "MarkdownV2",
) => {
const sanitizedMessage = sanitizeText(msg, parseMode);
const newMessage = `${sanitizedMessage.trim()}`;
return newMessage;
};
+7
View File
@@ -0,0 +1,7 @@
import { Player } from "classes";
import { MessagingState } from "types";
import { stateCommandsMap } from "const/maps/state-keyboard-button-map";
export const getCommandsForState = (player: Player, state: MessagingState) => {
return stateCommandsMap[player.state];
};
+11
View File
@@ -0,0 +1,11 @@
import { Player } from "classes";
import { stateMessageMap } from "const";
import { MessagingState } from "types";
export const getMessageForState = (player: Player, state: MessagingState) => {
const messageFn = stateMessageMap[state];
if (!messageFn) {
return "";
}
return messageFn(player);
};
+2
View File
@@ -0,0 +1,2 @@
export * from "./format-message";
export { messagePlayer } from "./message-player";
+32
View File
@@ -0,0 +1,32 @@
import { ParseMode } from "grammy/types";
import { getPlayerOrThrow } from "data";
import { Context, MessagingState } from "types";
import { getSanitizedMessage } from "./format-message";
import { getMessageForState } from "./get-message";
import { getKeyboardForState } from "./get-commands";
import { getReplyMarkup } from "utils";
export const messagePlayer = async (
playerId: number,
state: MessagingState,
ctx: Context,
customString = "",
) => {
const player = getPlayerOrThrow(playerId);
const stateMessage = customString || getMessageForState(player, state);
if (!stateMessage) return;
const keyboard = getKeyboardForState(player, state);
const formattedMessage = getSanitizedMessage(stateMessage);
const replyMarkup = getReplyMarkup({ keyboard, player, state });
try {
await ctx.api.sendMessage(player.id, formattedMessage, {
parse_mode: "MarkdownV2" as ParseMode,
reply_markup: replyMarkup,
});
} catch (error) {
console.log(error);
}
};
+1
View File
@@ -0,0 +1 @@
export { setPlayerState } from "./set-player-state";
+60
View File
@@ -0,0 +1,60 @@
import { PlayerState } from "generated/prisma/enums";
import {
getPlayer,
getPlayers,
updatePlayerState,
updatePlayerStates,
} from "data";
import { messagePlayer } from "messaging";
import { Context, StateConfig } from "types";
import { stateRegistry } from "./states/registry";
import { PlayerModel } from "generated/prisma/models";
const executeStateChanges = async (
player: PlayerModel,
oldState: PlayerState,
newState: PlayerState,
ctx: Context,
noMessage?: boolean,
) => {
console.log(
`transitioning player ${player.userName} from ${oldState} to ${newState}`,
);
const oldConfig: StateConfig | undefined = stateRegistry[oldState!];
const newConfig = stateRegistry[newState];
await oldConfig?.exit(player);
if (!noMessage) await messagePlayer(player.playerId, newState, ctx);
await newConfig.enter(player);
};
export const setPlayerState = async (
playerId: number,
ctx: Context,
newState: PlayerState,
noMessage?: boolean,
) => {
const player = await getPlayer(playerId);
const { state: oldState, userName } = player;
console.log(
`transitioning player ${userName} from ${oldState} to ${newState}`,
);
await updatePlayerState(playerId, newState);
executeStateChanges(player, oldState, newState, ctx, noMessage);
};
export const setPlayerStates = async (
playerIds: number[],
ctx: Context,
newState: PlayerState,
noMessage?: boolean,
) => {
const players = await getPlayers(playerIds);
const oldStates = players.map((p) => p.state);
await updatePlayerStates(playerIds, newState);
for (const [i, player] of players.entries()) {
const oldState = oldStates[i];
await executeStateChanges(player, oldState, newState, ctx, noMessage);
}
};
+7
View File
@@ -0,0 +1,7 @@
import { StateConfig, StateManagementFn } from "types";
const enter: StateManagementFn = async (_player) => {};
const exit: StateManagementFn = async (_player) => {};
export const defaultState: StateConfig = { enter, exit };
+7
View File
@@ -0,0 +1,7 @@
import { StateConfig, StateManagementFn } from "types";
const enter: StateManagementFn = async (_player) => {};
const exit: StateManagementFn = async (_player) => {};
export const inArchivesState: StateConfig = { enter, exit };
+7
View File
@@ -0,0 +1,7 @@
import { StateConfig, StateManagementFn } from "types";
const enter: StateManagementFn = async (_player) => {};
const exit: StateManagementFn = async (_player) => {};
export const lobbyState: StateConfig = { enter, exit };
+7
View File
@@ -0,0 +1,7 @@
import { StateConfig, StateManagementFn } from "types";
const enter: StateManagementFn = async (_player) => {};
const exit: StateManagementFn = async (_player) => {};
export const postGameState: StateConfig = { enter, exit };
+29
View File
@@ -0,0 +1,29 @@
import { PlayerState } from "generated/prisma/enums";
import { StateConfig } from "types";
import { defaultState } from "./default";
import { lobbyState } from "./lobby";
import { settingUpGameState } from "./setting-up-game";
import { spectatingState } from "./spectating";
import { waitingAfterWritingState } from "./waiting-after-writing";
import { waitingToWriteState } from "./waiting-to-write";
import { typingState } from "./typing";
import { tryingLeaveState } from "./trying-leave";
import { postGameState } from "./post-game";
import { viewingArchivedPoemState } from "./viewing-archived-poem";
import { writingState } from "./writing";
import { inArchivesState } from "./in-archives";
export const stateRegistry: { [key in PlayerState]: StateConfig } = {
[PlayerState.IN_ARCHIVES]: inArchivesState,
[PlayerState.LOBBY]: lobbyState,
[PlayerState.SETTING_UP_GAME]: settingUpGameState,
[PlayerState.SPECTATING]: spectatingState,
[PlayerState.POST_GAME]: postGameState,
[PlayerState.WRITING]: writingState,
[PlayerState.START]: defaultState,
[PlayerState.TRYING_LEAVE]: tryingLeaveState,
[PlayerState.TYPING]: typingState,
[PlayerState.VIEWING_ARCHIVED_POEM]: viewingArchivedPoemState,
[PlayerState.WAITING_AFTER_WRITING]: waitingAfterWritingState,
[PlayerState.WAITING_TO_WRITE]: waitingToWriteState,
};

Some files were not shown because too many files have changed in this diff Show More