Writing a Telegram Bot to support my Alcoholism
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:
- Include a few standard commands, such as using /start or /help
- Allow users to enroll (i.e. become active) or leave using /enroll or /leave
- Allow users to check the list of active users using /active
- Allow users to start a new beer event using /beer
- Allow users to join (using /ok), not join (using /nok) or unsure (using /pending)
- Allow users to see the current status of the event (i.e. who’s gonna join) using /status
- /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
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:
node-telegram-bot-api
: A wrapper around the Telegram Bot API. This makes it easy to communicate with Telegram using TypeScript.dayjs
: We need to use dates for some functionality, and this is best handled withdayjs
instead of the defaultDateTime
in JS.prisma
: We need to store some stuff in a database, andprisma
is a nice package for handling databases.
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:
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
:
// 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
- An
Event
holds anid
(autogenerated), adate
String (e.g. 2025-02-02), acreator
in form of a UserId, and a list of votes for an event. - A
User
holds anid
(this is the Telegram id), afirstName
and alastName
, a booleanisEnrolled
(true
for active andfalse
for inactive), as well as a list ofVote
and a list ofEvent
(the events which this user started with /beer). - A
Vote
holds anid
(autogenerated), avote
(ok
,nok
orpending
), anevent
in form of an Event Id, and aUser
in form of a User Id.
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.



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:
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.
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
- /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
/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";
};
/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.
- Alexander Schoch
- Maxime Muster
/beer
The beer command should
- check if there’s an event for today already. If so, return.
- create a new event
- vote /ok for the event creator. Of course they’re gonna join!
- find all active users
- generate the text to send to everybody
- send a message to all active users
- return the text to send to the creator
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:
And this is what everybody else sees:
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;
}
I'm in!:
- Alexander Schoch
Not today, sorry:
- Maxime Muster
/ok, /nok and /pending
The function for these commands, vote
, should execute this logic:
- find an event for today. If no event exists, notify the user that there’s no such event
- find all votes with today’s Event Id and the user’s user Id
- if there’s a vote, update it with the new vote
- otherwise, create a new vote
- return the voteOptions, some text and the current status
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 })}`;
};
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
Done. You can always update your vote using one of the following commands: /ok; /nok; /pending
Not today, sorry:
- Maxime Muster
- Alexander Schoch
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
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
Subscribe via RSS.
MIT 2025 © Alexander Schoch.