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 initThe 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 prismaThe 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 withdayjsinstead of the defaultDateTimein JS.prisma: We need to store some stuff in a database, andprismais 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 mariadbNow, 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
Eventholds anid(autogenerated), adateString (e.g. 2025-02-02), acreatorin form of a UserId, and a list of votes for an event. - A
Userholds anid(this is the Telegram id), afirstNameand alastName, a booleanisEnrolled(truefor active andfalsefor inactive), as well as a list ofVoteand a list ofEvent(the events which this user started with /beer). - A
Voteholds anid(autogenerated), avote(ok,nokorpending), aneventin form of an Event Id, and aUserin 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 devand 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:
XXXXXYYYYYZZZZZKeep 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 tscasync / 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.tsand 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.