Writing a Telegram Bot to support my Alcoholism

Alexander Schoch,programmingjavascripttypescriptenglishprisma
Back

Jump to: TypeScript Crash Course, Code Start, Full Code, Comments

In this blog post, we’re gonna look at how to write a simple Telegram bot using TypeScript. The example bot in this post should allow a group to organize drinking events, where one user can start an event and members can then vote if they’ll join or not.

Specifically, this bot should be able to do the following:

AS
/help
BB

- /help: Print this message

- /active: Get all active users

- /enroll: Join the active users

- /leave: Leave the active users

- /beer: Start a beer event

- /ok; /nok; /pending: Vote on an existing beer event

- /status: Check who's gonna join today

AS
/beer
BB

I told all active members. Use /status to check who'll join.

Task clear? Let’s get started!

Project Setup

First, we’ll need a JavaScript runtime. For this project, we’re gonna use Bun. Why? Because TypeScript setup with plain Node.js is a PITA. You can install Bun by following the instructions on their Website.

Next, we create a folder and initialize a new project.

mkdir telegram_bot cd telegram_bot bun init

The bun init command will ask you a few questions, and you can just press Enter for all of them; the defaults are fine.

Next, install the necessary packages for this project with bun install. --save-dev are dependences that we’re only gonna need for development purposes:

bun install node-telegram-bot-api dayjs @prisma/client bun install --save-dev @types/node-telegram-bot-api prisma

The packages we just installed are:

Next, we need to prepare a few things to start off: A database (MariaDB) and a Telegram bot API Token.

Database setup

The easiest way to start a local database is to use docker. You can install Docker here.

With docker installed, spin up a MariaDB container using

docker run --name mariadb --env MARIADB_ROOT_PASSWORD=sup3rs3cr3t -p 3306:3306 -d mariadb

Now, write the database connection into a new file named .env, like so:

.env
DATABASE_URL = "mysql://root:sup3rs3cr3t@localhost:3306/beerBot"

Next, we need to create a database schema. This schema tells prisma what tables the database contains and what columns these tables have. for that, create a new folder prisma and write the following content into prisma/schema.prisma:

prisma/schema.prisma
// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model Event { id Int @id @default(autoincrement()) date String @unique creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade) creatorId Int votes Vote[] } model User { id Int @id firstName String lastName String isEnrolled Boolean votes Vote[] events Event[] } model Vote { id Int @id @default(autoincrement()) vote String event Event @relation(fields: [eventId], references: [id], onDelete: Cascade) eventId Int user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int }

This schema configures the database, such that

onDelete: Cascade basically means that whenever we delete e.g. a User, this user’s events and the Votes thereof are also deleted. If we don’t do that, we’ll have orphan objects in the database.

@unique means that there can only ever be one event with a certain date, and the database will throw an error otherwise. This allows us to use findUnique instead of findMany for fetching an event by date.

Now, migrate this to the database by running

bunx prisma migrate dev

and entering some name for the migration.

API Token

To get a new Telegram Bot, Generate one using @BotFather, the Telegram bot to manage bots.

AS
/newbot
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
AS
Beer Bot
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
AS
my_fancy_beer_bot

Done! Congratulations on your new bot. You will find it at t.me/my_fancy_beer_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:

XXXXXYYYYYZZZZZ

Keep your token secure and store it safely, it can be used by anyone to control your bot.

For a description of the Bot API, see this page: https://core.telegram.org/bots/api

Copy the API Token and write it into the previously created .env, like so:

.env
DATABASE_URL = "mysql://root:sup3rs3cr3t@localhost:3306/beerBot" API_KEY = "XXXXXYYYYYZZZZZ"

Great! Now, let’s write some code.

TypeScript crash course

If you’re already familiar with TypeScript, you can continue with Listener.

Here, a few features that are unique to TypeScript are introduced. It is expected that you have some knowledge of other programming languages, such as Python.

Destructuring

A very common syntactical candy of JavaScript is object destructuring. We can use this to only pick some elements from an object, like so:

const obj = { a: 'some text', b: 'some other text', c: 'some more text' }; const {a, b} = obj;

We can also use this to pass an object into a function and then only select some fields from this object:

const some_function = ({ a, b }) => a + b; some_function(obj);

Also, notice that an arrow function with only one statement does not need {}.

Arrow Functions

The modern way to define functions in JavaScript is the arrow syntax:

const function_name = (arguments) => { // do something }

TS

TypeScript is a utility for type checking in JavaScript. This is really handy for finding bugs before they occur in production. For this project, we’re mostly gonna attach types to functions:

const function_name = (arguments: ArgumentsType): ReturnType => { // do something }

In order to check if your typing is correct, use the tsc command:

bunx tsc

async / await

JavaScript is a single-threaded, asynchronous language. This means that all code is run on one thread (no parallelism) and that function calls that take longer can be sent to the call stack. These functions are only run when the thread is idle, thus preventing slow function calls to slow down the whole program.

In the context of this bot, the one functionality that can potentially be slow is the call to the database. This call might have to go over the internet (depending on the location of your database) and is thus slower than normal JavaScript statements. For this reason, prisma provides its functionality via async functions.

This means that prisma functions (or any async function, for that matter) will always return a Promise: An object that resolves as soon as the function finishes. In order to wait until a function finishes, we can use the await keyword. Note that we can only use await inside an async function. The return type of an async function is Promise<return_type>.

Listener

First, let’s load all the libraries that we’re gonna use for this project. This also includes Type imports that don’t affect the program, but are great for debugging.

import Bot from "node-telegram-bot-api"; import dayjs from "dayjs"; import { PrismaClient } from "@prisma/client"; import type BotType from "node-telegram-bot-api"; import type { Message } from "node-telegram-bot-api";

Next, we need to instanciate bot and prisma and get the token out of .env.

const token = process.env.API_KEY || ""; const bot = new Bot(token, { polling: true }); const prisma = new PrismaClient();

Notice the || "" at the end of the const token line. The reason for this is type safety: It is possible that process.env.API_KEY is undefined (e.g. if there’s a typo in your .env). In this case, we don’t want token to be undefined but just an empty string, so we use the || operator (remember as “if falsy, use the value to the right”) to return "". In this case, the Telegram API will return an error.

Next, we define a few types that we’re gonna use later, as well as the voteOptions object. These options are the ones that users can pick to vote on an event. The VoteOptionsType defines that this voteOptions object is indexed by a string and contains string values.

interface VoteOptionsType { [index: string]: string; } interface CommandArgsType { command: string; args: string[]; prisma: PrismaClient; msg: Message; bot: BotType; } const voteOptions = { ok: "I'm in!", nok: "Not today, sorry", pending: "Not sure yet", } as VoteOptionsType;

Now, we write the event listener for the bot. For this, node-telegram-bot-api provides a handy function:

console.log("Running Telegram Bot. Listening..."); bot.onText(/^\/.+/, async (msg, match) => { console.log( `[debug] ${formatName({ firstName: msg.from?.first_name, lastName: msg.from?.last_name })} ran ${msg.text}`, ); });

This will now start up a server and run the callback function whenever a text sent to the bot matches the RegEx /^\/.+/. This RegEx means “starts with a slash (^\/), followed by one or more characters (.+)”. If a message matches this RegEx, the function async (msg, match) => {...} is executed. msg is the object that holds all the information about the sent message.

Inside this callback function, we first split the message by command and args (not used here, but very handy for further functionality) by splitting at a space and taking the first value as command and the rest as args. We then put all useful variables that our bot could use later into an object args_to_commands. command will be the command the user ran, args all arguments after that command, prisma the object for database operations, msg the message object and bot the bot object for sending messages.

msg.text?.split(" ")[0] tells JS that msg.text might be undefined, and the ? skips further execution if it is. If msg.text.split(" ") becomes undefined, the || "" will set it to an empty string.

bot.onText(/^\/.+/, async (msg, match) => { console.log( `[debug] ${formatName({ firstName: msg.from?.first_name, lastName: msg.from?.last_name })} ran ${msg.text}`, ); const command = msg.text?.split(" ")[0] || ""; const args = msg.text?.split(" ").slice(1) || []; const args_to_commands = { command, args, prisma, msg, bot, }; });

Next, we write a swich statement that will execute a function depending on what command the user ran. If the command matches no case, execute the default block.

The await createUser(args_to_commands) function (defined later) is invoked here, as the user object is used in almost all functions. It thus makes sense to make sure that a user already exists before running any other function.

We will later implement an async function for each command the user can run. These functions will take the args_to_commands object, do something with it and then return a string. This return value is then sent back to the user.

bot.onText(/^\/.+/, async (msg, match) => { console.log( `[debug] ${formatName({ firstName: msg.from?.first_name, lastName: msg.from?.last_name })} ran ${msg.text}`, ); const command = msg.text?.split(" ")[0] || ""; const args = msg.text?.split(" ").slice(1) || []; const args_to_commands = { command, args, prisma, msg, bot, }; await createUser(args_to_commands); let response = ""; switch (true) { case /^\/start/.test(command): response = await start(args_to_commands); break; case /^\/help/.test(command): response = await help(args_to_commands); break; case /^\/enroll/.test(command): response = await enroll(args_to_commands); break; case /^\/leave/.test(command): response = await leave(args_to_commands); break; case /^\/active/.test(command): response = await active(args_to_commands); break; case /^\/beer/.test(command): response = await beer(args_to_commands); break; case /^\/status/.test(command): response = await status(args_to_commands); break; case Object.keys(voteOptions) .map((v) => "/" + v) .includes(command): response = await vote(args_to_commands); break; default: response = await nomatch(args_to_commands); break; } if (response) bot.sendMessage(msg.chat.id, response, { parse_mode: "Markdown" }); });

A few utility functions

In order to make our lives easier later, we define a few functions that do common tasks for us.

formatName takes a first name and a last name and returns a string that concatenates these two. The reason for this function is that both first_name and last_name can be undefined or null, and this would result in ugly formatting, because an empty last name can result in two spaces back-to-back. (x) => x is a function that returns itself, and filter will then cast the result to a boolean. undefined, null, "" and 0 will be cast to false.

createUser will fetch a user from the database, and, if it did not exist, create it. This makes sure that the user object for the current user always exists.

getToday will return the current date as a YYYY-MM-DD string.

const formatName = ({ firstName, lastName, }: { firstName: string | undefined; lastName: string | undefined; }): string => { return [firstName, lastName].filter((x) => x).join(" "); }; const createUser = async ({ msg, prisma, }: CommandArgsType): Promise<undefined> => { const user = await prisma.user.findUnique({ where: { id: msg.from?.id || 0, }, }); if (user === null) await prisma.user.create({ data: { id: msg.from?.id || 0, firstName: msg.from?.first_name || "", lastName: msg.from?.last_name || "", isEnrolled: true, }, }); }; const getToday = (): string => dayjs().format("YYYY-MM-DD");

/start, /help and nomatch

The first message to a bot is always /start. In our case, this command should inform the user what this bot does and how to use it. It will also create a user object (as we call await createUser in the listener).

const start = async (args: CommandArgsType): Promise<string> => { return `Welcome to the beer bot! I've automatically enrolled you, and you can leave anytime by using the /leave command. Here's a list of all commands: ${await help(args)} ` };

“Why is the formatting this weird”, you might ask. We use backticks to create a multi-line string, and any indent would be visible in the string, hence the missing indents.

The start function calls the help function, which should return a list of commands:

const help = async (args: CommandArgsType): Promise<string> => { return `- /help: Print this message - /active: Get all active users - /enroll: Join the active users - /leave: Leave the active users - /beer: Start a beer event - /ok; /nok; /pending: Vote on an existing beer event - /status: Check who's gonna join today `; };

The nomatch function should just tell the user that the function wasn’t found:

const nomatch = async ({ command }: CommandArgsType): Promise<string> => { return `Sorry, I did not find the command ${command}`; };

With these commands in place, we can now actually try these functions. For that, type

bun run index.ts

and chat with the bot by searching for @my_fancy_beer_bot (or whatever you called yours) in Telegram.

AS
/start
BB

Welcome to the beer bot! I've automatically enrolled you, and you can leave anytime by using the /leave command. Here's a list of all commands:

- /help: Print this message

- /active: Get all active users

- /enroll: Join the active users

- /leave: Leave the active users

- /beer: Start a beer event

- /ok; /nok; /pending: Vote on an existing beer event

- /status: Check who's gonna join today

AS
/help
BB

- /help: Print this message

- /active: Get all active users

- /enroll: Join the active users

- /leave: Leave the active users

- /beer: Start a beer event

- /ok; /nok; /pending: Vote on an existing beer event

- /status: Check who's gonna join today

AS
/wrong_command
BB
Sorry, I did not find the command /wrong_command

/enroll and /leave

These two functions just update a User object and set isEnrolled to either true or false.

const enroll = async ({ msg, prisma }: CommandArgsType): Promise<string> => { await prisma.user.update({ where: { id: msg.from?.id || 0, }, data: { isEnrolled: true, }, }); return "done"; }; const leave = async ({ msg, prisma }: CommandArgsType): Promise<string> => { await prisma.user.update({ where: { id: msg.from?.id || 0, }, data: { isEnrolled: false, }, }); return "done"; };
AS
/enroll
BB
done
AS
/leave
BB
done

/active

This command should get all users in the database that have isEnrolled = true and return a list of all these users.

const active = async ({ msg, prisma }: CommandArgsType): Promise<string> => { const activeUsers = await prisma.user.findMany({ where: { isEnrolled: true, }, orderBy: [ { firstName: 'asc' } ] }); return activeUsers.map((user) => `- ${formatName(user)}`).join("\n"); };

The last line of this function applies the - ${formatName(user)} “function” to all elements and then joins them with a newline (\n). Also, notice the user argument. The user object has, among others, the fields firstName and lastName. This code passes all fields of the user object to the formatName function, which in turn only picks the firstName and lastName fields and ignores the rest.

orderBy is an option in prisma to sort the database query result by, in this case, firstName in ascending order.

AS
/active
BB

- Alexander Schoch

- Maxime Muster

/beer

The beer command should

Most of the code in here should feel familiar by now. One thing that needs some context is the last part:

First, we filter the activeUsers by “not being the event creator”. We don’t wanna send a notification to the creator, as they already know about the event (duh!). Next, we loop over all remaining users and run bot.sendMessage on them. Telegram has a nice feature called “keyboards”. This allows us to, instead of the typical keyboard, display three buttons: /ok, /nok and /pending. Pressing one of these buttons will just send the content of the button as a text message.

const beer = async ({ msg, bot, prisma }: CommandArgsType): Promise<string> => { const today = getToday(); // if an event exists already, stop here const existingEvent = await prisma.event.findUnique({ where: { date: today, }, include: { creator: true, }, }); if (existingEvent !== null) return `${formatName(existingEvent.creator)} has already created an event.`; // create a new event const event = await prisma.event.create({ data: { date: today, creatorId: msg.from?.id || 0, }, include: { creator: true, }, }); // the creator of the event obviously votes /ok await prisma.vote.create({ data: { vote: "ok", eventId: event.id, userId: event.creator.id, }, }); // find all active users to notify const activeUsers = await prisma.user.findMany({ where: { isEnrolled: true, orderBy: [ { firstName: 'asc' } ] }, }); // list all the voteOptions as a string const voteOptionsText = Object.keys(voteOptions) .map((v) => `/${v} - ${voteOptions[v]}`) .join("\n") // full text to send to active users const text = `${formatName(event.creator)} wants to drink beer tonight. You in?\n\n${voteOptionsText}`; // send active users a message activeUsers .filter((user) => user.id !== msg.from?.id || 0) .forEach((user) => { bot.sendMessage(user.id, text, { reply_markup: { keyboard: [Object.keys(voteOptions).map((v) => ({ text: "/" + v }))], }, }); }); return "I told all active members. Use /status to check who'll join."; };

This is what the event creator sees:

AS
/beer
BB
I told all active members. Use /status to check who'll join.

And this is what everybody else sees:

BB

Alexander Schoch wants to drink beer tonight. You in?

/ok - I'm in!

/nok - Not today, sorry

/pending - Not sure yet

/status

Now, let’s check who’ll join for a beer later!

For this, we need to get the event and all votes with that Event Id, and the user of each vote. Prisma already does this for us: By using the include keyword, we can fetch relations as well.

If no event was found (prisma returns null), we should return a text notifying the user that there’s no event today.

Otherwise, we should display the votes grouped by voteOption. For this, we first filter the voteOptions keys (i.e. ok, nok and pending) by the number of votes with that vote string (lines 20-23 below). We then map over the voteOptions that actually have votes and render the text of that option (lines 26+27). Afterwards, we filter the votes by the current voteOption, map over them to add a - beforehand and join them with a newline. If this is confusing for you, try to print the loop elements before .filter, .map and .join to get more insight.

const status = async ({ prisma }: CommandArgsType): Promise<string> => { const today = getToday(); const event = await prisma.event.findUnique({ where: { date: today, }, include: { votes: { include: { user: true, }, }, }, }); if (event === null) return "There's no event today. Use /beer to start one!"; const text = Object.keys(voteOptions) .filter( (voteOption) => event.votes.filter((vote) => vote.vote === voteOption).length > 0, ) .map( (voteOption) => voteOptions[voteOption] + ":\n" + event.votes .filter((vote) => vote.vote === voteOption) .map((vote) => `- ${formatName(vote.user)}`) .join("\n"), ) .join("\n"); return text; }
AS
/status
BB

I'm in!:

- Alexander Schoch

Not today, sorry:

- Maxime Muster

/ok, /nok and /pending

The function for these commands, vote, should execute this logic:

const vote = async ({ msg, prisma, command, args, bot, }: CommandArgsType): Promise<string> => { const removeSlash = command.replace("/", ""); const today = getToday(); const event = await prisma.event.findUnique({ where: { date: today, }, }); if (event === null) return "There's no event today. Use /beer to create one!"; const votes = await prisma.vote.findMany({ where: { userId: msg.from?.id || 0, eventId: event.id, }, }); if (votes.length === 0) { // create a new vote await prisma.vote.create({ data: { eventId: event.id, userId: msg.from?.id || 0, vote: removeSlash, }, }); } else { // update an existing vote await prisma.vote.update({ where: { id: votes[0].id, }, data: { vote: removeSlash, }, }); } const voteOptionsText = Object.keys(voteOptions) .map((voteOption) => "/" + voteOption) .join("; "); return `Done. You can always update your vote using one of the following commands: ${voteOptionsText}\n\n${await status({ msg, prisma, command, args, bot })}`; };
AS
/ok
BB

Done. You can always update your vote using one of the following commands: /ok; /nok; /pending

I'm in!:

- Alexander Schoch

Not today, sorry:

- Maxime Muster

AS
/nok
BB

Done. You can always update your vote using one of the following commands: /ok; /nok; /pending

Not today, sorry:

- Maxime Muster

- Alexander Schoch

AS
/pending
BB

Done. You can always update your vote using one of the following commands: /ok; /nok; /pending

Not today, sorry:

- Maxime Muster

Not sure yet:

- Alexander Schoch

That’s it!

And that’s the code. This post was a short demonstration on what you could do with the Telegram Bot API, but there’s so much more that you could try!

As usual, feedback is very much welcome (e.g. using the comments at the bottom of this page), and feel free to share your results with me!

Full Code

index.ts
import Bot from "node-telegram-bot-api"; import dayjs from "dayjs"; import { PrismaClient } from "@prisma/client"; import type BotType from "node-telegram-bot-api"; import type { Message } from "node-telegram-bot-api"; const token = process.env.API_KEY || ""; const bot = new Bot(token, { polling: true }); const prisma = new PrismaClient(); interface VoteOptionsType { [index: string]: string; } interface CommandArgsType { command: string; args: string[]; prisma: PrismaClient; msg: Message; bot: BotType; } const voteOptions = { ok: "I'm in!", nok: "Not today, sorry", pending: "Not sure yet", } as VoteOptionsType; console.log("Running Telegram Bot. Listening..."); bot.onText(/^\/.+/, async (msg, match) => { console.log( `[debug] ${formatName({ firstName: msg.from?.first_name, lastName: msg.from?.last_name })} ran ${msg.text}`, ); const command = msg.text?.split(" ")[0] || ""; const args = msg.text?.split(" ").slice(1) || []; const args_to_commands = { command, args, prisma, msg, bot, }; await createUser(args_to_commands); let response = ""; switch (true) { case /^\/start/.test(command): response = await start(args_to_commands); break; case /^\/help/.test(command): response = await help(args_to_commands); break; case /^\/enroll/.test(command): response = await enroll(args_to_commands); break; case /^\/leave/.test(command): response = await leave(args_to_commands); break; case /^\/active/.test(command): response = await active(args_to_commands); break; case /^\/beer/.test(command): response = await beer(args_to_commands); break; case /^\/status/.test(command): response = await status(args_to_commands); break; case Object.keys(voteOptions) .map((v) => "/" + v) .includes(command): response = await vote(args_to_commands); break; default: response = await nomatch(args_to_commands); break; } if (response) bot.sendMessage(msg.chat.id, response, { parse_mode: "Markdown" }); }); /* * UTILITY FUNCTIONS */ const formatName = ({ firstName, lastName, }: { firstName: string | undefined; lastName: string | undefined; }): string => { return [firstName, lastName].filter((x) => x).join(" "); }; const createUser = async ({ msg, prisma, }: CommandArgsType): Promise<undefined> => { const user = await prisma.user.findUnique({ where: { id: msg.from?.id || 0, }, }); if (user === null) await prisma.user.create({ data: { id: msg.from?.id || 0, firstName: msg.from?.first_name || "", lastName: msg.from?.last_name || "", isEnrolled: true, }, }); }; const getToday = (): string => dayjs().format("YYYY-MM-DD"); /* * COMMANDS */ const start = async (args: CommandArgsType): Promise<string> => { return `Welcome to the beer bot! I've automatically enrolled you, and you can leave anytime by using the /leave command. Here's a list of all commands: ${await help(args)} `; }; const help = async (args: CommandArgsType): Promise<string> => { return `- /help: Print this message - /active: Get all active users - /enroll: Join the active users - /leave: Leave the active users - /beer: Start a beer event - /ok; /nok; /pending: Vote on an existing beer event - /status: Check who's gonna join today `; }; const enroll = async ({ msg, prisma }: CommandArgsType): Promise<string> => { await prisma.user.update({ where: { id: msg.from?.id || 0, }, data: { isEnrolled: true, }, }); return "done"; }; const leave = async ({ msg, prisma }: CommandArgsType): Promise<string> => { await prisma.user.update({ where: { id: msg.from?.id || 0, }, data: { isEnrolled: false, }, }); return "done"; }; const active = async ({ msg, prisma }: CommandArgsType): Promise<string> => { const activeUsers = await prisma.user.findMany({ where: { isEnrolled: true, }, orderBy: [ { firstName: 'asc' } ] }); return activeUsers.map((user) => `- ${formatName(user)}`).join("\n"); }; const beer = async ({ msg, bot, prisma }: CommandArgsType): Promise<string> => { const today = getToday(); // if an event exists already, stop here const existingEvent = await prisma.event.findUnique({ where: { date: today, }, include: { creator: true, }, }); if (existingEvent !== null) return `${formatName(existingEvent.creator)} has already created an event.`; // create a new event const event = await prisma.event.create({ data: { date: today, creatorId: msg.from?.id || 0, }, include: { creator: true, }, }); // the creator of the event obviously votes /ok await prisma.vote.create({ data: { vote: "ok", eventId: event.id, userId: event.creator.id, }, }); // find all active users to notify const activeUsers = await prisma.user.findMany({ where: { isEnrolled: true, }, orderBy: [ { firstName: 'asc' } ] }); // list all the voteOptions as a string const voteOptionsText = Object.keys(voteOptions) .map((v) => `/${v} - ${voteOptions[v]}`) .join("\n") // full text to send to active users const text = `${formatName(event.creator)} wants to drink beer tonight. You in?\n\n${voteOptionsText}`; // send active users a message activeUsers // .filter((user) => user.id !== msg.from?.id || 0) .forEach((user) => { bot.sendMessage(user.id, text, { reply_markup: { keyboard: [Object.keys(voteOptions).map((v) => ({ text: "/" + v }))], }, }); }); return "I told all active members. Use /status to check who'll join."; }; const status = async ({ prisma }: CommandArgsType): Promise<string> => { const today = getToday(); const event = await prisma.event.findUnique({ where: { date: today, }, include: { votes: { include: { user: true, }, }, }, }); if (event === null) return "There's no event today. Use /beer to start one!"; const text = Object.keys(voteOptions) .filter( (voteOption) => event.votes.filter((vote) => vote.vote === voteOption).length > 0, ) .map( (voteOption) => voteOptions[voteOption] + ":\n" + event.votes .filter((vote) => vote.vote === voteOption) .map((vote) => `- ${formatName(vote.user)}`) .join("\n"), ) .join("\n"); return text; }; const vote = async ({ msg, prisma, command, args, bot, }: CommandArgsType): Promise<string> => { const removeSlash = command.replace("/", ""); const today = getToday(); const event = await prisma.event.findUnique({ where: { date: today, }, }); if (event === null) return "There's no event today. Use /beer to create one!"; const votes = await prisma.vote.findMany({ where: { userId: msg.from?.id || 0, eventId: event.id, }, }); if (votes.length === 0) { // create a new vote await prisma.vote.create({ data: { eventId: event.id, userId: msg.from?.id || 0, vote: removeSlash, }, }); } else { // update an existing vote await prisma.vote.update({ where: { id: votes[0].id, }, data: { vote: removeSlash, }, }); } const voteOptionsText = Object.keys(voteOptions) .map((voteOption) => "/" + voteOption) .join("; "); return `Done. You can always update your vote using one of the following commands: ${voteOptionsText}\n\n${await status({ msg, prisma, command, args, bot })}`; }; const nomatch = async ({ command }: CommandArgsType): Promise<string> => { return `Sorry, I did not find the command ${command}`; };

Comments

Source Code

Subscribe via RSS.

MIT 2025 © Alexander Schoch.