Events
Every event builder routes updates of a specific shape to your callback. Pick a builder, pass filters into its constructor, and register the pair with client.addEventHandler(cb, builder). There's no middleware chain, no next() — just plain callbacks that fire when Telegram pushes a matching update over the socket.
NewMessage#
Fires on every incoming and/or outgoing message. Builder params: chats, blacklistChats, func, incoming, outgoing, fromUsers, forwards, pattern (a RegExp). The event exposes .message (a custom Message with .reply({ message }), .message text, .senderId, .out, .id), .isPrivate, and .chatId.
import { NewMessage, NewMessageEvent } from "teleproto/events";
client.addEventHandler(async (event: NewMessageEvent) => {
const text = (event.message.message ?? "").trim();
await event.message.reply({ message: `pong: ${text}` });
}, new NewMessage({
pattern: /^\/ping(?:\s+(.+))?$/i,
incoming: true,
forwards: false,
}));EditedMessage#
Same builder shape as NewMessage; same event fields. Useful for moderation — flag when someone silently rewrites a message after the fact.
import { EditedMessage, EditedMessageEvent } from "teleproto/events";
client.addEventHandler((event: EditedMessageEvent) => {
console.log(
`[edit] chat=${event.chatId} msg=${event.message.id} now=\"${event.message.message}\"`,
);
}, new EditedMessage({ chats: ["@my_moderated_group"] }));DeletedMessage#
Fired when messages are deleted in a chat your client can see. The event carries the deleted ids and (where Telegram tells us) the chat.
import { DeletedMessage, DeletedMessageEvent } from "teleproto/events";
client.addEventHandler((event: DeletedMessageEvent) => {
// event.deletedIds is a number[]; event.chatId may be undefined for PMs.
console.log(`deleted in ${event.chatId}:`, event.deletedIds);
}, new DeletedMessage({}));Telegram does not tell clients which user deleted the message — only that it's gone. If you need an audit trail, snapshot the message body in your own store before the delete event ever arrives.
CallbackQuery#
Fires when a user taps an inline keyboard button. Builder accepts Partial<{ chats, func, fromUsers, blacklistUsers, pattern }>. The event has .query (an Api.UpdateBotCallbackQuery or Api.UpdateInlineBotCallbackQuery), .patternMatch, .chatId, and .answer({ message, cacheTime, url, alert }) to close the loading spinner client-side.
import { Button } from "teleproto/tl/custom/button";
import { CallbackQuery, CallbackQueryEvent, NewMessage } from "teleproto/events";
// Send a message with one inline button.
client.addEventHandler(async (event) => {
if (event.message.message !== "/menu") return;
await event.message.reply({
message: "pick one",
buttons: [Button.inline("Click me", Buffer.from("click:1"))],
});
}, new NewMessage({ pattern: /^\/menu$/ }));
// Handle the tap.
client.addEventHandler(async (event: CallbackQueryEvent) => {
await event.answer({
message: "you tapped it",
alert: false,
cacheTime: 0,
});
}, new CallbackQuery({ pattern: /^click:/ }));InlineQuery#
Fires when a user types @yourbot foo in any chat. Reach for the InlineBuilder helper to build typed result objects (articles, photos, GIFs) without hand-rolling raw Api.InputBotInlineResult* constructors.
import { InlineQuery, InlineQueryEvent } from "teleproto/events";
import { InlineBuilder } from "teleproto/tl/custom/inlineBuilder";
client.addEventHandler(async (event: InlineQueryEvent) => {
const builder = new InlineBuilder(client);
await event.answer([
await builder.article({
title: `echo: ${event.query.query}`,
description: "tap to send",
text: event.query.query,
}),
]);
}, new InlineQuery({}));Album#
When a user sends multiple photos or videos as a group, Telegram delivers them as separate updates that share a grouped_id. The Album builder buffers briefly and fires once with all items together — saving you from stitching them yourself.
import { Album, AlbumEvent } from "teleproto/events";
client.addEventHandler((event: AlbumEvent) => {
// Telegram batches the photos sent together; teleproto groups them by grouped_id.
console.log(`album of ${event.messages.length} items in chat ${event.chatId}`);
}, new Album({}));ChatAction#
Joins, leaves, title changes, photo changes, pins, unpins — structural events on a chat. One handler covers them all; inspect the boolean flags on the event to dispatch on what actually happened.
import { ChatAction, ChatActionEvent } from "teleproto/events";
client.addEventHandler((event: ChatActionEvent) => {
if (event.userJoined) console.log(`+ ${event.userId} joined ${event.chatId}`);
if (event.userLeft) console.log(`- ${event.userId} left ${event.chatId}`);
if (event.newTitle) console.log(`renamed to ${event.newTitle}`);
}, new ChatAction({}));MessageRead#
Fires when someone else reads your messages — useful for read receipts in support bots and for backing off auto-reply chains when the human has clearly caught up.
import { MessageRead, MessageReadEvent } from "teleproto/events";
client.addEventHandler((event: MessageReadEvent) => {
console.log(`chat ${event.chatId} read up to id ${event.maxId}`);
}, new MessageRead({}));UserUpdate#
Online/offline status flips and typing indicators. Cheap and very chatty — apply chats or func filters before you log anything in production.
import { UserUpdate, UserUpdateEvent } from "teleproto/events";
client.addEventHandler((event: UserUpdateEvent) => {
if (event.typing) console.log(`${event.userId} is typing in ${event.chatId}`);
if (event.online !== undefined) {
console.log(`${event.userId} ${event.online ? "online" : "offline"}`);
}
}, new UserUpdate({}));Raw#
new Raw({}) receives every Api.TypeUpdate unfiltered. Useful while learning what Telegram actually pushes for a given action — log update.classNameand watch the firehose. Not for production handlers; reach for a typed builder once you know what you're after.
import { Raw } from "teleproto/events";
import type { Api } from "teleproto";
// Receives every update Telegram pushes — useful while exploring.
client.addEventHandler((update: Api.TypeUpdate) => {
console.log(update.className);
}, new Raw({}));Combining filters#
For complex predicates, use func: (e) => boolean on any builder. It runs after the cheap structural filters (chats, incoming, etc.), so you only pay for the expensive check on events that already passed the first gate. Use pattern when a regex is enough; reach for func when you need to inspect multiple fields.
import { NewMessage, NewMessageEvent } from "teleproto/events";
// func runs after the cheap filters. Use it for cross-field logic.
client.addEventHandler((event: NewMessageEvent) => {
console.log("long message from a friend:", event.message.message);
}, new NewMessage({
incoming: true,
func: (e) =>
(e.message.message ?? "").length > 200 &&
e.message.senderId?.toString() === "123456789",
}));