Chats & Channels
Dialogs, members, admin actions, joining and leaving — the user-account ops the Bot API can't touch. teleproto exposes them through high-level helpers on TelegramClient and the raw Api.* surface where the helper stops.
Listing dialogs#
A dialog is anything that shows up in your chat list — a DM, a group, a channel, the saved-messages tab. iterDialogs paginates them for you. The full param shape: iterDialogs({ limit?, offsetDate?, offsetId?, offsetPeer?, ignorePinned?, folder? }).
for await (const dialog of client.iterDialogs({
limit: 200,
ignorePinned: false,
})) {
console.log(
`[${dialog.unreadCount.toString().padStart(4)}] ${dialog.title}`,
);
}Each dialog exposes .title, .unreadCount, .entity, .message (the last one), and the underlying TL .dialog object if you need raw flags.
Resolving usernames + invite links#
Usernames go through the high-level helper. getEntity resolves and caches against the session so repeat lookups are local.
Invite links don't have a high-level helper because Telegram treats "preview" and "join" as separate requests — drop down to the raw messages.CheckChatInvite call to inspect a link without joining.
import { Api } from "teleproto";
// Public username → full entity
const channel = await client.getEntity("@some_public_channel");
// Invite link preview (without actually joining)
const preview = await client.invoke(
new Api.messages.CheckChatInvite({ hash: "AAAAAAAAAAAAAAAAAA" }),
);
// preview is ChatInvite | ChatInviteAlready | ChatInvitePeek
console.log(preview);Joining#
Public username channels and supergroups use channels.JoinChannel. Pass the result of getInputEntity — joining wants the small InputPeer shape, not the full entity.
import { Api } from "teleproto";
const channel = await client.getInputEntity("@some_public_channel");
await client.invoke(new Api.channels.JoinChannel({ channel }));Invite links — the t.me/+abc and t.me/joinchat/abc form — go through messages.ImportChatInvite with just the hash.
import { Api } from "teleproto";
// The hash is the part after the t.me/+ or t.me/joinchat/
await client.invoke(
new Api.messages.ImportChatInvite({ hash: "AAAAAAAAAAAAAAAAAA" }),
);Leaving#
Channels and supergroups leave through channels.LeaveChannel:
import { Api } from "teleproto";
const channel = await client.getInputEntity("@some_public_channel");
await client.invoke(new Api.channels.LeaveChannel({ channel }));Legacy basic groups (the pre-supergroup variant) use a different call — messages.DeleteChatUser with your own user id. Set revokeHistory: true if you want the messages you sent in that chat deleted on the way out.
import { Api } from "teleproto";
// Legacy (non-supergroup) chats use a different request
await client.invoke(
new Api.messages.DeleteChatUser({
chatId: legacyChatId,
userId: await client.getInputEntity("me"),
revokeHistory: false,
}),
);Listing participants#
iterParticipants paginates members of a chat, group, or channel. filter narrows the iteration to a subset — admins, kicked, banned, bots, or a search query.
import { Api } from "teleproto";
for await (const user of client.iterParticipants("@my_channel", {
filter: new Api.ChannelParticipantsAdmins(),
})) {
console.log(`@${user.username ?? user.id.toString()} — ${user.firstName ?? ""}`);
}Other useful filters: Api.ChannelParticipantsKicked, Api.ChannelParticipantsBanned, Api.ChannelParticipantsBots, and Api.ChannelParticipantsSearch({ q }) for a name/username substring search.
import { Api } from "teleproto";
for await (const user of client.iterParticipants("@my_group", {
limit: 50,
filter: new Api.ChannelParticipantsSearch({ q: "alex" }),
})) {
console.log(user.id.toString(), user.username);
}Admin actions#
Promotions go through channels.EditAdmin with a fully-spelled ChatAdminRights object — every flag you don't set is denied. The rank string is the custom title that shows up next to the admin's name (max 16 characters).
import { Api } from "teleproto";
await client.invoke(
new Api.channels.EditAdmin({
channel: await client.getInputEntity("@my_channel"),
userId: await client.getInputEntity("@new_admin"),
adminRights: new Api.ChatAdminRights({
changeInfo: true,
postMessages: true,
editMessages: true,
deleteMessages: true,
banUsers: true,
inviteUsers: true,
pinMessages: true,
addAdmins: false,
anonymous: false,
manageCall: false,
other: false,
}),
rank: "editor",
}),
);Restrictions, mutes, and bans follow the same pattern but with channels.EditBanned and ChatBannedRights. The flags are negative — setting sendMessages: true actually means "take away the ability to send messages." For a one-shot kick that lets the user rejoin later, use the kickParticipant shortcut.
import { Api } from "teleproto";
// Restrict (mute / ban). Same pattern as EditAdmin but with ChatBannedRights.
await client.invoke(
new Api.channels.EditBanned({
channel: await client.getInputEntity("@my_group"),
participant: await client.getInputEntity("@offender"),
bannedRights: new Api.ChatBannedRights({
untilDate: 0, // 0 = forever
viewMessages: false,
sendMessages: true, // mute
sendMedia: true,
sendStickers: true,
sendGifs: true,
sendGames: true,
sendInline: true,
sendPolls: true,
changeInfo: true,
inviteUsers: true,
pinMessages: true,
embedLinks: true,
}),
}),
);
// Shortcut: hard-kick (ban + immediate unban so they can rejoin later)
await client.kickParticipant("@my_group", "@offender");Reading message history#
History reads go through iterMessages — same pagination story as dialogs and participants. See Messages for the full parameter shape and filter options.
// Read the last 50 messages without marking them read
for await (const message of client.iterMessages("@noisy_channel", {
limit: 50,
})) {
console.log(message.id, message.message?.slice(0, 80));
}
// Mark them read explicitly when you actually want the receipt
await client.markAsRead("@noisy_channel");Iterating messages does not send a read receipt on its own — the other side won't see the double-tick. Call client.markAsRead(entity) explicitly when you actually want to acknowledge them.