dograma
Version:
NodeJS/Browser MTProto API Telegram client library,
515 lines (489 loc) • 16.2 kB
text/typescript
import { Api } from "../tl";
import type { Entity, EntityLike } from "../define";
import { getPeerId as peerUtils, parseID } from "../Utils";
import {
_entityType,
_EntityType,
sleep,
isArrayLike,
returnBigInt,
} from "../Helpers";
import { errors, utils } from "../";
import type { TelegramClient } from "../";
import bigInt from "big-integer";
import { LogLevel } from "../extensions/Logger";
import { MTProtoSender } from "../network";
// UserMethods {
// region Invoking Telegram request
/** @hidden */
export async function invoke<R extends Api.AnyRequest>(
client: TelegramClient,
request: R,
sender?: MTProtoSender
): Promise<R["__response"]> {
if (request.classType !== "request") {
throw new Error("You can only invoke MTProtoRequests");
}
if (!sender) {
sender = client._sender;
}
if (sender == undefined) {
throw new Error(
"Cannot send requests while disconnected. You need to call .connect()"
);
}
await request.resolve(client, utils);
client._lastRequest = new Date().getTime();
let attempt: number;
for (attempt = 0; attempt < client._requestRetries; attempt++) {
try {
const promise = sender.send(request);
const result = await promise;
client.session.processEntities(result);
client._entityCache.add(result);
return result;
} catch (e: any) {
if (
e instanceof errors.ServerError ||
e.errorMessage === "RPC_CALL_FAIL" ||
e.errorMessage === "RPC_MCGET_FAIL"
) {
client._log.warn(
`Telegram is having internal issues ${e.constructor.name}`
);
await sleep(2000);
} else if (
e instanceof errors.FloodWaitError ||
e instanceof errors.FloodTestPhoneWaitError
) {
if (e.seconds <= client.floodSleepThreshold) {
client._log.info(
`Sleeping for ${e.seconds}s on flood wait (Caused by ${request.className})`
);
await sleep(e.seconds * 1000);
} else {
throw e;
}
} else if (
e instanceof errors.PhoneMigrateError ||
e instanceof errors.NetworkMigrateError ||
e instanceof errors.UserMigrateError
) {
client._log.info(`Phone migrated to ${e.newDc}`);
const shouldRaise =
e instanceof errors.PhoneMigrateError ||
e instanceof errors.NetworkMigrateError;
if (shouldRaise && (await client.isUserAuthorized())) {
throw e;
}
await client._switchDC(e.newDc);
} else {
throw e;
}
}
}
throw new Error(`Request was unsuccessful ${attempt} time(s)`);
}
/** @hidden */
export async function getMe(
client: TelegramClient,
inputPeer = false
): Promise<Api.InputPeerUser | Api.User> {
if (inputPeer && client._selfInputPeer) {
return client._selfInputPeer;
}
const me = (
await client.invoke(
new Api.users.GetUsers({ id: [new Api.InputUserSelf()] })
)
)[0] as Api.User;
client._bot = me.bot;
if (!client._selfInputPeer) {
client._selfInputPeer = utils.getInputPeer(
me,
false
) as Api.InputPeerUser;
}
return inputPeer ? client._selfInputPeer : me;
}
/** @hidden */
export async function isBot(client: TelegramClient) {
if (client._bot === undefined) {
const me = await client.getMe();
if (me) {
return !(me instanceof Api.InputPeerUser) ? me.bot : undefined;
}
}
return client._bot;
}
/** @hidden */
export async function isUserAuthorized(client: TelegramClient) {
try {
await client.invoke(new Api.updates.GetState());
return true;
} catch (e) {
return false;
}
}
/** @hidden */
export async function getEntity(
client: TelegramClient,
entity: EntityLike | EntityLike[]
): Promise<Entity | Entity[]> {
const single = !isArrayLike(entity);
let entityArray: EntityLike[] = [];
if (isArrayLike<EntityLike>(entity)) {
entityArray = entity;
} else {
entityArray.push(entity);
}
const inputs = [];
for (const x of entityArray) {
if (typeof x === "string") {
const valid = parseID(x);
if (valid) {
inputs.push(await client.getInputEntity(valid));
} else {
inputs.push(x);
}
} else {
inputs.push(await client.getInputEntity(x));
}
}
const lists = new Map<number, any[]>([
[_EntityType.USER, []],
[_EntityType.CHAT, []],
[_EntityType.CHANNEL, []],
]);
for (const x of inputs) {
try {
lists.get(_entityType(x))!.push(x);
} catch (e) {}
}
let users = lists.get(_EntityType.USER)!;
let chats = lists.get(_EntityType.CHAT)!;
let channels = lists.get(_EntityType.CHANNEL)!;
if (users.length) {
users = await client.invoke(
new Api.users.GetUsers({
id: users,
})
);
}
if (chats.length) {
const chatIds = chats.map((x) => x.chatId);
chats = (
await client.invoke(new Api.messages.GetChats({ id: chatIds }))
).chats;
}
if (channels.length) {
channels = (
await client.invoke(new Api.channels.GetChannels({ id: channels }))
).chats;
}
const idEntity = new Map<string, any>();
for (const user of users) {
idEntity.set(peerUtils(user), user);
}
for (const channel of channels) {
idEntity.set(peerUtils(channel), channel);
}
for (const chat of chats) {
idEntity.set(peerUtils(chat), chat);
}
const result = [];
for (const x of inputs) {
if (typeof x === "string") {
result.push(await _getEntityFromString(client, x));
} else if (!(x instanceof Api.InputPeerSelf)) {
result.push(idEntity.get(peerUtils(x)));
} else {
for (const [key, u] of idEntity.entries()) {
if (u instanceof Api.User && u.self) {
result.push(u);
break;
}
}
}
}
return single ? result[0] : result;
}
/** @hidden */
export async function getInputEntity(
client: TelegramClient,
peer: EntityLike
): Promise<Api.TypeInputPeer> {
// Short-circuit if the input parameter directly maps to an InputPeer
try {
return utils.getInputPeer(peer);
// eslint-disable-next-line no-empty
} catch (e) {}
// Next in priority is having a peer (or its ID) cached in-memory
try {
if (typeof peer == "string") {
const valid = parseID(peer);
if (valid) {
const res = client._entityCache.get(peer);
if (res) {
return res;
}
}
}
if (
typeof peer === "number" ||
typeof peer === "bigint" ||
bigInt.isInstance(peer)
) {
const res = client._entityCache.get(peer.toString());
if (res) {
return res;
}
}
// 0x2d45687 == crc32(b'Peer')
if (
typeof peer == "object" &&
!bigInt.isInstance(peer) &&
peer.SUBCLASS_OF_ID === 0x2d45687
) {
const res = client._entityCache.get(utils.getPeerId(peer));
if (res) {
return res;
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
// Then come known strings that take precedence
if (typeof peer == "string") {
if (["me", "this", "self"].includes(peer)) {
return new Api.InputPeerSelf();
}
}
// No InputPeer, cached peer, or known string. Fetch from disk cache
try {
if (peer != undefined) {
return client.session.getInputEntity(peer);
}
// eslint-disable-next-line no-empty
} catch (e) {}
// Only network left to try
if (typeof peer === "string") {
return utils.getInputPeer(await _getEntityFromString(client, peer));
}
// If we're a bot and the user has messaged us privately users.getUsers
// will work with accessHash = 0. Similar for channels.getChannels.
// If we're not a bot but the user is in our contacts, it seems to work
// regardless. These are the only two special-cased requests.
if (typeof peer === "number") {
peer = returnBigInt(peer);
}
peer = utils.getPeer(peer);
if (peer instanceof Api.PeerUser) {
const users = await client.invoke(
new Api.users.GetUsers({
id: [
new Api.InputUser({
userId: peer.userId,
accessHash: bigInt.zero,
}),
],
})
);
if (users.length && !(users[0] instanceof Api.UserEmpty)) {
// If the user passed a valid ID they expect to work for
// channels but would be valid for users, we get UserEmpty.
// Avoid returning the invalid empty input peer for that.
//
// We *could* try to guess if it's a channel first, and if
// it's not, work as a chat and try to validate it through
// another request, but that becomes too much work.
return utils.getInputPeer(users[0]);
}
} else if (peer instanceof Api.PeerChat) {
return new Api.InputPeerChat({
chatId: peer.chatId,
});
} else if (peer instanceof Api.PeerChannel) {
try {
const channels = await client.invoke(
new Api.channels.GetChannels({
id: [
new Api.InputChannel({
channelId: peer.channelId,
accessHash: bigInt.zero,
}),
],
})
);
return utils.getInputPeer(channels.chats[0]);
} catch (e) {
if (client._log.canSend(LogLevel.ERROR)) {
console.error(e);
}
}
}
throw new Error(
`Could not find the input entity for ${JSON.stringify(peer)}.
Please read https://` +
"docs.telethon.dev/en/latest/concepts/entities.html to" +
" find out more details."
);
}
/** @hidden */
export async function _getEntityFromString(
client: TelegramClient,
string: string
) {
const phone = utils.parsePhone(string);
if (phone) {
try {
const result = await client.invoke(
new Api.contacts.GetContacts({
hash: bigInt.zero,
})
);
if (!(result instanceof Api.contacts.ContactsNotModified)) {
for (const user of result.users) {
if (user instanceof Api.User && user.phone === phone) {
return user;
}
}
}
} catch (e: any) {
if (e.errorMessage === "BOT_METHOD_INVALID") {
throw new Error(
"Cannot get entity by phone number as a " +
"bot (try using integer IDs, not strings)"
);
}
throw e;
}
}
const id = utils.parseID(string);
if (id != undefined) {
return getInputEntity(client, id);
} else if (["me", "this"].includes(string.toLowerCase())) {
return client.getMe();
} else {
const { username, isInvite } = utils.parseUsername(string);
if (isInvite) {
const invite = await client.invoke(
new Api.messages.CheckChatInvite({
hash: username,
})
);
if (invite instanceof Api.ChatInvite) {
throw new Error(
"Cannot get entity from a channel (or group) " +
"that you are not part of. Join the group and retry"
);
} else if (invite instanceof Api.ChatInviteAlready) {
return invite.chat;
}
} else if (username) {
try {
const result = await client.invoke(
new Api.contacts.ResolveUsername({ username: username })
);
const pid = utils.getPeerId(result.peer, false);
if (result.peer instanceof Api.PeerUser) {
for (const x of result.users) {
if (returnBigInt(x.id).equals(returnBigInt(pid))) {
return x;
}
}
} else {
for (const x of result.chats) {
if (returnBigInt(x.id).equals(returnBigInt(pid))) {
return x;
}
}
}
} catch (e: any) {
if (e.errorMessage === "USERNAME_NOT_OCCUPIED") {
throw new Error(`No user has "${username}" as username`);
}
throw e;
}
}
}
throw new Error(`Cannot find any entity corresponding to "${string}"`);
}
/** @hidden */
export async function getPeerId(
client: TelegramClient,
peer: EntityLike,
addMark = true
) {
if (typeof peer == "string") {
const valid = parseID(peer);
if (valid) {
return utils.getPeerId(peer, addMark);
} else {
peer = await client.getInputEntity(peer);
}
}
if (
typeof peer == "number" ||
typeof peer == "bigint" ||
bigInt.isInstance(peer)
) {
return utils.getPeerId(peer, addMark);
}
if (peer.SUBCLASS_OF_ID == 0x2d45687 || peer.SUBCLASS_OF_ID == 0xc91c90b6) {
peer = await client.getInputEntity(peer);
}
if (peer instanceof Api.InputPeerSelf) {
peer = await client.getMe(true);
}
return utils.getPeerId(peer, addMark);
}
/** @hidden */
export async function _getPeer(client: TelegramClient, peer: EntityLike) {
if (!peer) {
return undefined;
}
const [i, cls] = utils.resolveId(
returnBigInt(await client.getPeerId(peer))
);
return new cls({
userId: i,
channelId: i,
chatId: i,
});
}
/** @hidden */
export async function _getInputDialog(client: TelegramClient, dialog: any) {
try {
if (dialog.SUBCLASS_OF_ID == 0xa21c9795) {
// crc32(b'InputDialogPeer')
dialog.peer = await client.getInputEntity(dialog.peer);
return dialog;
} else if (dialog.SUBCLASS_OF_ID == 0xc91c90b6) {
//crc32(b'InputPeer')
return new Api.InputDialogPeer({
peer: dialog,
});
}
} catch (e) {}
return new Api.InputDialogPeer({
peer: dialog,
});
}
/** @hidden */
export async function _getInputNotify(client: TelegramClient, notify: any) {
try {
if (notify.SUBCLASS_OF_ID == 0x58981615) {
if (notify instanceof Api.InputNotifyPeer) {
notify.peer = await client.getInputEntity(notify.peer);
}
return notify;
}
} catch (e) {}
return new Api.InputNotifyPeer({
peer: await client.getInputEntity(notify),
});
}
/** @hidden */
export function _selfId(client: TelegramClient) {
return client._selfInputPeer ? client._selfInputPeer.userId : undefined;
}