TelegramPWA/src/lib/gramjs/client/TelegramClient.js
2022-05-30 15:40:25 +04:00

1180 lines
41 KiB
JavaScript

const os = require('os');
const Logger = require('../extensions/Logger');
const { sleep } = require('../Helpers');
const errors = require('../errors');
const MemorySession = require('../sessions/Memory');
const Helpers = require('../Helpers');
const utils = require('../Utils');
const Session = require('../sessions/Abstract');
const { LAYER } = require('../tl/AllTLObjects');
const {
constructors,
requests,
} = require('../tl');
const {
ConnectionTCPObfuscated,
MTProtoSender,
UpdateConnectionState,
} = require('../network');
const {
authFlow,
checkAuthorization,
} = require('./auth');
const { downloadFile } = require('./downloadFile');
const { uploadFile } = require('./uploadFile');
const { updateTwoFaSettings } = require('./2fa');
const DEFAULT_DC_ID = 2;
const WEBDOCUMENT_DC_ID = 4;
const DEFAULT_IPV4_IP = 'zws2.web.telegram.org';
const DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]';
const EXPORTED_SENDER_RECONNECT_TIMEOUT = 1000; // 1 sec
const EXPORTED_SENDER_RELEASE_TIMEOUT = 30000; // 30 sec
const WEBDOCUMENT_REQUEST_PART_SIZE = 131072; // 128kb
const PING_INTERVAL = 3000; // 3 sec
const PING_TIMEOUT = 5000; // 5 sec
const PING_FAIL_ATTEMPTS = 3;
const PING_FAIL_INTERVAL = 100; // ms
// An unusually long interval is a sign of returning from background mode...
const PING_INTERVAL_TO_WAKE_UP = 5000; // 5 sec
// ... so we send a quick "wake-up" ping to confirm than connection was dropped ASAP
const PING_WAKE_UP_TIMEOUT = 3000; // 3 sec
// We also send a warning to the user even a bit more quickly
const PING_WAKE_UP_WARNING_TIMEOUT = 1000; // 1 sec
const PING_DISCONNECT_DELAY = 60000; // 1 min
// All types
const sizeTypes = ['w', 'y', 'd', 'x', 'c', 'm', 'b', 'a', 's'];
class TelegramClient {
static DEFAULT_OPTIONS = {
connection: ConnectionTCPObfuscated,
useIPV6: false,
proxy: undefined,
timeout: 10,
requestRetries: 5,
connectionRetries: Infinity,
retryDelay: 1000,
autoReconnect: true,
sequentialUpdates: false,
floodSleepLimit: 60,
deviceModel: undefined,
systemVersion: undefined,
appVersion: undefined,
langCode: 'en',
systemLangCode: 'en',
baseLogger: 'gramjs',
useWSS: false,
additionalDcsDisabled: false,
testServers: false,
};
/**
*
* @param session {StringSession|LocalStorageSession}
* @param apiId
* @param apiHash
* @param opts
*/
constructor(session, apiId, apiHash, opts = TelegramClient.DEFAULT_OPTIONS) {
if (!apiId || !apiHash) {
throw Error('Your API ID or Hash are invalid. Please read "Requirements" on README.md');
}
const args = { ...TelegramClient.DEFAULT_OPTIONS, ...opts };
this.apiId = apiId;
this.apiHash = apiHash;
this._useIPV6 = args.useIPV6;
// this._entityCache = new Set()
if (typeof args.baseLogger === 'string') {
this._log = new Logger();
} else {
this._log = args.baseLogger;
}
// Determine what session we will use
if (typeof session === 'string' || !session) {
try {
throw new Error('not implemented');
} catch (e) {
session = new MemorySession();
}
} else if (!(session instanceof Session)) {
throw new Error('The given session must be str or a session instance');
}
this.floodSleepLimit = args.floodSleepLimit;
this._eventBuilders = [];
this._phoneCodeHash = {};
this.session = session;
// this._entityCache = EntityCache();
this.apiId = parseInt(apiId, 10);
this.apiHash = apiHash;
this._requestRetries = args.requestRetries;
this._connectionRetries = args.connectionRetries;
this._retryDelay = args.retryDelay || 0;
if (args.proxy) {
this._log.warn('proxies are not supported');
}
this._proxy = args.proxy;
this._timeout = args.timeout;
this._autoReconnect = args.autoReconnect;
this._connection = args.connection;
// TODO add proxy support
this._floodWaitedRequests = {};
this._initWith = (x) => {
return new requests.InvokeWithLayer({
layer: LAYER,
query: new requests.InitConnection({
apiId: this.apiId,
deviceModel: args.deviceModel || os.type()
.toString() || 'Unknown',
systemVersion: args.systemVersion || os.release()
.toString() || '1.0',
appVersion: args.appVersion || '1.0',
langCode: args.langCode,
langPack: '', // this should be left empty.
systemLangCode: args.systemLangCode,
query: x,
proxy: undefined, // no proxies yet.
}),
});
};
this._args = args;
// These will be set later
this._config = undefined;
this.phoneCodeHashes = [];
this._exportedSenderPromises = {};
this._exportedSenderReleaseTimeouts = {};
this._additionalDcsDisabled = args.additionalDcsDisabled;
this._loopStarted = false;
this._isSwitchingDc = false;
this._destroyed = false;
}
// region Connecting
/**
* Connects to the Telegram servers, executing authentication if required.
* Note that authenticating to the Telegram servers is not the same as authenticating
* the app, which requires to send a code first.
* @returns {Promise<void>}
*/
async connect() {
await this._initSession();
if (this._sender === undefined) {
// only init sender once to avoid multiple loops.
this._sender = new MTProtoSender(this.session.getAuthKey(), {
logger: this._log,
dcId: this.session.dcId,
retries: this._connectionRetries,
delay: this._retryDelay,
autoReconnect: this._autoReconnect,
connectTimeout: this._timeout,
authKeyCallback: this._authKeyCallback.bind(this),
updateCallback: this._handleUpdate.bind(this),
isMainSender: true,
});
}
// set defaults vars
this._sender.userDisconnected = false;
this._sender._user_connected = false;
this._sender.isReconnecting = false;
this._sender._disconnected = true;
const connection = new this._connection(
this.session.serverAddress, this.session.port, this.session.dcId, this._log, this._args.testServers,
);
const newConnection = await this._sender.connect(connection);
if (!newConnection) {
// we're already connected so no need to reset auth key.
if (!this._loopStarted) {
this._updateLoop();
this._loopStarted = true;
}
return;
}
this.session.setAuthKey(this._sender.authKey);
await this._sender.send(this._initWith(
new requests.help.GetConfig({}),
));
if (!this._loopStarted) {
this._updateLoop();
this._loopStarted = true;
}
this._isSwitchingDc = false;
}
async _initSession() {
await this.session.load();
if (!this.session.serverAddress || (this.session.serverAddress.includes(':') !== this._useIPV6)) {
this.session.setDC(DEFAULT_DC_ID, this._useIPV6
? DEFAULT_IPV6_IP : DEFAULT_IPV4_IP, this._args.useWSS ? 443 : 80);
}
}
async _updateLoop() {
let lastPongAt;
while (!this._destroyed) {
await Helpers.sleep(PING_INTERVAL);
if (this._sender.isReconnecting || this._isSwitchingDc) {
lastPongAt = undefined;
continue;
}
try {
const ping = () => {
return this._sender.send(new requests.PingDelayDisconnect({
pingId: Helpers.getRandomInt(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER),
disconnectDelay: PING_DISCONNECT_DELAY,
}));
};
const pingAt = Date.now();
const lastInterval = lastPongAt ? pingAt - lastPongAt : undefined;
if (!lastInterval || lastInterval < PING_INTERVAL_TO_WAKE_UP) {
await attempts(() => timeout(ping, PING_TIMEOUT), PING_FAIL_ATTEMPTS, PING_FAIL_INTERVAL);
} else {
let wakeUpWarningTimeout = setTimeout(() => {
this._handleUpdate(new UpdateConnectionState(UpdateConnectionState.disconnected));
wakeUpWarningTimeout = undefined;
}, PING_WAKE_UP_WARNING_TIMEOUT);
await timeout(ping, PING_WAKE_UP_TIMEOUT);
if (wakeUpWarningTimeout) {
clearTimeout(wakeUpWarningTimeout);
wakeUpWarningTimeout = undefined;
}
this._handleUpdate(new UpdateConnectionState(UpdateConnectionState.connected));
}
lastPongAt = Date.now();
} catch (err) {
// eslint-disable-next-line no-console
console.warn(err);
lastPongAt = undefined;
if (this._sender.isReconnecting || this._isSwitchingDc) {
continue;
}
await this.disconnect();
await this.connect();
}
// We need to send some content-related request at least hourly
// for Telegram to keep delivering updates, otherwise they will
// just stop even if we're connected. Do so every 30 minutes.
// TODO Call getDifference instead since it's more relevant
if (new Date().getTime() - this._lastRequest > 30 * 60 * 1000) {
try {
await this.invoke(new requests.updates.GetState());
} catch (e) {
// we don't care about errors here
}
lastPongAt = undefined;
}
}
await this.disconnect();
}
/**
* Disconnects from the Telegram server
* @returns {Promise<void>}
*/
async disconnect() {
if (this._sender) {
await this._sender.disconnect();
}
await Promise.all(
Object.values(this._exportedSenderPromises)
.map((promise) => {
return promise && promise.then((sender) => {
if (sender) {
return sender.disconnect();
}
return undefined;
});
}),
);
this._exportedSenderPromises = {};
}
/**
* Disconnects all senders and removes all handlers
* @returns {Promise<void>}
*/
async destroy() {
this._destroyed = true;
try {
await this.disconnect();
} catch (err) {
// Do nothing
}
this.session.delete();
this._eventBuilders = [];
}
async _switchDC(newDc) {
this._log.info(`Reconnecting to new data center ${newDc}`);
const DC = utils.getDC(newDc);
this.session.setDC(newDc, DC.ipAddress, DC.port);
// authKey's are associated with a server, which has now changed
// so it's not valid anymore. Set to None to force recreating it.
await this._sender.authKey.setKey(undefined);
this.session.setAuthKey(undefined);
this._isSwitchingDc = true;
await this.disconnect();
return this.connect();
}
_authKeyCallback(authKey, dcId) {
this.session.setAuthKey(authKey, dcId);
}
// endregion
// export region
async _cleanupExportedSender(dcId) {
if (this.session.dcId !== dcId) {
this.session.setAuthKey(undefined, dcId);
}
const sender = await this._exportedSenderPromises[dcId];
this._exportedSenderPromises[dcId] = undefined;
await sender.disconnect();
}
async _connectSender(sender, dcId) {
// if we don't already have an auth key we want to use normal DCs not -1
const dc = utils.getDC(dcId, Boolean(sender.authKey.getKey()));
// eslint-disable-next-line no-constant-condition
while (true) {
try {
await sender.connect(new this._connection(
dc.ipAddress,
dc.port,
dcId,
this._log,
this._args.testServers,
));
if (this.session.dcId !== dcId && !sender._authenticated) {
this._log.info(`Exporting authorization for data center ${dc.ipAddress}`);
const auth = await this.invoke(new requests.auth.ExportAuthorization({ dcId }));
const req = this._initWith(new requests.auth.ImportAuthorization({
id: auth.id,
bytes: auth.bytes,
}));
await sender.send(req);
sender._authenticated = true;
}
sender.dcId = dcId;
sender.userDisconnected = false;
return sender;
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
await Helpers.sleep(1000);
await sender.disconnect();
}
}
}
async _borrowExportedSender(dcId, shouldReconnect, existingSender) {
if (this._additionalDcsDisabled) {
return undefined;
}
if (!this._exportedSenderPromises[dcId] || shouldReconnect) {
this._exportedSenderPromises[dcId] = this._connectSender(
existingSender || this._createExportedSender(dcId),
dcId,
);
}
let sender;
try {
sender = await this._exportedSenderPromises[dcId];
if (!sender.isConnected()) {
if (sender.isConnecting) {
await Helpers.sleep(EXPORTED_SENDER_RECONNECT_TIMEOUT);
return this._borrowExportedSender(dcId, false, sender);
} else {
return this._borrowExportedSender(dcId, true, sender);
}
}
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
return this._borrowExportedSender(dcId, true);
}
if (this._exportedSenderReleaseTimeouts[dcId]) {
clearTimeout(this._exportedSenderReleaseTimeouts[dcId]);
this._exportedSenderReleaseTimeouts[dcId] = undefined;
}
this._exportedSenderReleaseTimeouts[dcId] = setTimeout(() => {
this._exportedSenderReleaseTimeouts[dcId] = undefined;
sender.disconnect();
}, EXPORTED_SENDER_RELEASE_TIMEOUT);
return sender;
}
_createExportedSender(dcId) {
return new MTProtoSender(this.session.getAuthKey(dcId), {
logger: this._log,
dcId,
retries: this._connectionRetries,
delay: this._retryDelay,
autoReconnect: this._autoReconnect,
connectTimeout: this._timeout,
authKeyCallback: this._authKeyCallback.bind(this),
isMainSender: dcId === this.session.dcId,
onConnectionBreak: this._cleanupExportedSender.bind(this),
});
}
getSender(dcId) {
return dcId ? this._borrowExportedSender(dcId) : Promise.resolve(this._sender);
}
// end region
// download region
/**
* Complete flow to download a file.
* @param inputLocation {constructors.InputFileLocation}
* @param [args[partSizeKb] {number}]
* @param [args[fileSize] {number}]
* @param [args[progressCallback] {Function}]
* @param [args[start] {number}]
* @param [args[end] {number}]
* @param [args[dcId] {number}]
* @param [args[workers] {number}]
* @returns {Promise<Buffer>}
*/
downloadFile(inputLocation, args = {}) {
return downloadFile(this, inputLocation, args);
}
downloadMedia(messageOrMedia, args) {
let media;
if (messageOrMedia instanceof constructors.Message) {
media = messageOrMedia.media;
} else {
media = messageOrMedia;
}
if (typeof media === 'string') {
throw new Error('not implemented');
}
if (media instanceof constructors.MessageMediaWebPage) {
if (media.webpage instanceof constructors.WebPage) {
media = media.webpage.document || media.webpage.photo;
}
}
if (media instanceof constructors.MessageMediaPhoto || media instanceof constructors.Photo) {
return this._downloadPhoto(media, args);
} else if (media instanceof constructors.MessageMediaDocument || media instanceof constructors.Document) {
return this._downloadDocument(media, args);
} else if (media instanceof constructors.MessageMediaContact) {
return this._downloadContact(media, args);
} else if (media instanceof constructors.WebDocument || media instanceof constructors.WebDocumentNoProxy) {
return this._downloadWebDocument(media, args);
}
return undefined;
}
downloadProfilePhoto(entity, isBig = false) {
// ('User', 'Chat', 'UserFull', 'ChatFull')
const ENTITIES = [0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697];
// ('InputPeer', 'InputUser', 'InputChannel')
// const INPUTS = [0xc91c90b6, 0xe669bf46, 0x40f202fd]
// Todo account for input methods
const sizeType = isBig ? 'x' : 'm';
let photo;
if (!(ENTITIES.includes(entity.SUBCLASS_OF_ID))) {
photo = entity;
} else {
if (!entity.photo) {
// Special case: may be a ChatFull with photo:Photo
if (!entity.chatPhoto) {
return undefined;
}
return this._downloadPhoto(
entity.chatPhoto, { sizeType },
);
}
photo = entity.photo;
}
let dcId;
let loc;
if (photo instanceof constructors.UserProfilePhoto || photo instanceof constructors.ChatPhoto) {
dcId = photo.dcId;
loc = new constructors.InputPeerPhotoFileLocation({
peer: utils.getInputPeer(entity),
photoId: photo.photoId,
big: isBig,
});
} else {
// It doesn't make any sense to check if `photo` can be used
// as input location, because then this method would be able
// to "download the profile photo of a message", i.e. its
// media which should be done with `download_media` instead.
return undefined;
}
return this.downloadFile(loc, {
dcId,
});
}
downloadStickerSetThumb(stickerSet) {
if (!stickerSet.thumbs || !stickerSet.thumbs.length) {
return undefined;
}
const { thumbVersion } = stickerSet;
return this.downloadFile(
new constructors.InputStickerSetThumb({
stickerset: new constructors.InputStickerSetID({
id: stickerSet.id,
accessHash: stickerSet.accessHash,
}),
thumbVersion,
}),
{ dcId: stickerSet.thumbDcId },
);
}
_pickFileSize(sizes, sizeType) {
if (!sizeType || !sizes || !sizes.length) {
return undefined;
}
const indexOfSize = sizeTypes.indexOf(sizeType);
let size;
for (let i = indexOfSize; i < sizeTypes.length; i++) {
size = sizes.find((s) => s.type === sizeTypes[i]);
if (size) {
return size;
}
}
return undefined;
}
_downloadCachedPhotoSize(size) {
// No need to download anything, simply write the bytes
let data;
if (size instanceof constructors.PhotoStrippedSize) {
data = utils.strippedPhotoToJpg(size.bytes);
} else {
data = size.bytes;
}
return data;
}
_downloadPhoto(photo, args) {
if (photo instanceof constructors.MessageMediaPhoto) {
photo = photo.photo;
}
if (!(photo instanceof constructors.Photo)) {
return undefined;
}
const size = this._pickFileSize(photo.sizes, args.sizeType);
if (!size || (size instanceof constructors.PhotoSizeEmpty)) {
return undefined;
}
if (size instanceof constructors.PhotoCachedSize || size instanceof constructors.PhotoStrippedSize) {
return this._downloadCachedPhotoSize(size);
}
return this.downloadFile(
new constructors.InputPhotoFileLocation({
id: photo.id,
accessHash: photo.accessHash,
fileReference: photo.fileReference,
thumbSize: size.type,
}),
{
dcId: photo.dcId,
fileSize: size.size,
progressCallback: args.progressCallback,
},
);
}
_downloadDocument(doc, args) {
if (doc instanceof constructors.MessageMediaDocument) {
doc = doc.document;
}
if (!(doc instanceof constructors.Document)) {
return undefined;
}
let size;
if (args.sizeType) {
size = doc.thumbs ? this._pickFileSize(doc.thumbs, args.sizeType) : undefined;
if (!size && doc.mimeType.startsWith('video/')) {
return undefined;
}
if (size && (size instanceof constructors.PhotoCachedSize
|| size instanceof constructors.PhotoStrippedSize)) {
return this._downloadCachedPhotoSize(size);
}
}
return this.downloadFile(
new constructors.InputDocumentFileLocation({
id: doc.id,
accessHash: doc.accessHash,
fileReference: doc.fileReference,
thumbSize: size ? size.type : '',
}),
{
fileSize: size ? size.size : doc.size,
progressCallback: args.progressCallback,
start: args.start,
end: args.end,
dcId: doc.dcId,
workers: args.workers,
},
);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_downloadContact(media, args) {
throw new Error('not implemented');
}
async _downloadWebDocument(media) {
try {
const buff = [];
let offset = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
const downloaded = new requests.upload.GetWebFile({
location: new constructors.InputWebFileLocation({
url: media.url,
accessHash: media.accessHash,
}),
offset,
limit: WEBDOCUMENT_REQUEST_PART_SIZE,
});
const sender = await this._borrowExportedSender(WEBDOCUMENT_DC_ID);
const res = await sender.send(downloaded);
offset += 131072;
if (res.bytes.length) {
buff.push(res.bytes);
if (res.bytes.length < WEBDOCUMENT_REQUEST_PART_SIZE) {
break;
}
} else {
break;
}
}
return Buffer.concat(buff);
} catch (e) {
// the file is no longer saved in telegram's cache.
if (e.message === 'WEBFILE_NOT_AVAILABLE') {
return Buffer.alloc(0);
} else {
throw e;
}
}
}
async downloadStaticMap(accessHash, long, lat, w, h, zoom, scale, accuracyRadius) {
try {
const buff = [];
let offset = 0;
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const downloaded = new requests.upload.GetWebFile({
location: new constructors.InputWebFileGeoPointLocation({
geoPoint: new constructors.InputGeoPoint({
lat,
long,
accuracyRadius,
}),
accessHash,
w,
h,
zoom,
scale,
}),
offset,
limit: WEBDOCUMENT_REQUEST_PART_SIZE,
});
const sender = await this._borrowExportedSender(WEBDOCUMENT_DC_ID);
const res = await sender.send(downloaded);
offset += 131072;
if (res.bytes.length) {
buff.push(res.bytes);
if (res.bytes.length < WEBDOCUMENT_REQUEST_PART_SIZE) {
break;
}
} else {
break;
}
} catch (err) {
if (err instanceof errors.FloodWaitError) {
// eslint-disable-next-line no-console
console.warn(`getWebFile: sleeping for ${err.seconds}s on flood wait`);
await sleep(err.seconds * 1000);
continue;
}
}
}
return Buffer.concat(buff);
} catch (e) {
// the file is no longer saved in telegram's cache.
if (e.message === 'WEBFILE_NOT_AVAILABLE') {
return Buffer.alloc(0);
} else {
throw e;
}
}
}
// region Invoking Telegram request
/**
* Invokes a MTProtoRequest (sends and receives it) and returns its result
* @param request
* @param dcId Optional dcId to use when sending the request
* @returns {Promise}
*/
async invoke(request, dcId) {
if (request.classType !== 'request') {
throw new Error('You can only invoke MTProtoRequests');
}
const sender = dcId === undefined ? this._sender : await this.getSender(dcId);
this._lastRequest = new Date().getTime();
let attempt = 0;
for (attempt = 0; attempt < this._requestRetries; attempt++) {
const promise = sender.sendWithInvokeSupport(request);
try {
const result = await promise.promise;
return result;
} catch (e) {
if (e instanceof errors.ServerError || e.message === 'RPC_CALL_FAIL'
|| e.message === 'RPC_MCGET_FAIL') {
this._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 <= this.floodSleepLimit) {
this._log.info(`Sleeping for ${e.seconds}s on flood wait`);
await sleep(e.seconds * 1000);
} else {
throw e;
}
} else if (e instanceof errors.PhoneMigrateError || e instanceof errors.NetworkMigrateError
|| e instanceof errors.UserMigrateError) {
this._log.info(`Phone migrated to ${e.newDc}`);
const shouldRaise = e instanceof errors.PhoneMigrateError
|| e instanceof errors.NetworkMigrateError;
if (shouldRaise && await checkAuthorization(this)) {
throw e;
}
await this._switchDC(e.newDc);
} else if (e instanceof errors.MsgWaitError) {
// we need to resend this after the old one was confirmed.
await promise.isReady();
} else {
throw e;
}
}
}
throw new Error(`Request was unsuccessful ${attempt} time(s)`);
}
async getMe() {
try {
return (await this.invoke(new requests.users
.GetUsers({ id: [new constructors.InputUserSelf()] })))[0];
} catch (e) {
this._log.warn('error while getting me');
this._log.warn(e);
}
return undefined;
}
async start(authParams) {
if (!this.isConnected()) {
await this.connect();
}
if (await checkAuthorization(this, authParams.shouldThrowIfUnauthorized)) {
return;
}
const apiCredentials = {
apiId: this.apiId,
apiHash: this.apiHash,
};
await authFlow(this, apiCredentials, authParams);
}
uploadFile(fileParams) {
return uploadFile(this, fileParams);
}
updateTwoFaSettings(params) {
return updateTwoFaSettings(this, params);
}
// event region
addEventHandler(callback, event) {
this._eventBuilders.push([event, callback]);
}
_handleUpdate(update) {
// this.session.processEntities(update)
// this._entityCache.add(update)
if (update instanceof constructors.Updates || update instanceof constructors.UpdatesCombined) {
// TODO deal with entities
const entities = [];
for (const x of [...update.users, ...update.chats]) {
entities.push(x);
}
for (const u of update.updates) {
this._processUpdate(u, update.updates, entities);
}
} else if (update instanceof constructors.UpdateShort) {
this._processUpdate(update.update, undefined);
} else {
this._processUpdate(update, undefined);
}
// TODO add caching
// this._stateCache.update(update)
}
_processUpdate(update, others, entities) {
update._entities = entities || [];
const args = {
update,
others,
};
this._dispatchUpdate(args);
}
// endregion
// region private methods
/**
Gets a full entity from the given string, which may be a phone or
a username, and processes all the found entities on the session.
The string may also be a user link, or a channel/chat invite link.
This method has the side effect of adding the found users to the
session database, so it can be queried later without API calls,
if this option is enabled on the session.
Returns the found entity, or raises TypeError if not found.
* @param string {string}
* @returns {Promise<void>}
* @private
*/
/* CONTEST
async _getEntityFromString(string) {
const phone = utils.parsePhone(string)
if (phone) {
try {
for (const user of (await this.invoke(
new requests.contacts.GetContacts(0))).users) {
if (user.phone === phone) {
return user
}
}
} catch (e) {
if (e.message === 'BOT_METHOD_INVALID') {
throw new Error('Cannot get entity by phone number as a ' +
'bot (try using integer IDs, not strings)')
}
throw e
}
} else if (['me', 'this'].includes(string.toLowerCase())) {
return this.getMe()
} else {
const { username, isJoinChat } = utils.parseUsername(string)
if (isJoinChat) {
const invite = await this.invoke(new requests.messages.CheckChatInvite({
'hash': username,
}))
if (invite instanceof constructors.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 constructors.ChatInviteAlready) {
return invite.chat
}
} else if (username) {
try {
const result = await this.invoke(
new requests.contacts.ResolveUsername(username))
const pid = utils.getPeerId(result.peer, false)
if (result.peer instanceof constructors.PeerUser) {
for (const x of result.users) {
if (x.id === pid) {
return x
}
}
} else {
for (const x of result.chats) {
if (x.id === pid) {
return x
}
}
}
} catch (e) {
if (e.message === 'USERNAME_NOT_OCCUPIED') {
throw new Error(`No user has "${username}" as username`)
}
throw e
}
}
}
throw new Error(`Cannot find any entity corresponding to "${string}"`)
}
*/
// endregion
// users region
/**
Turns the given entity into its input entity version.
Most requests use this kind of :tl:`InputPeer`, so this is the most
suitable call to make for those cases. **Generally you should let the
library do its job** and don't worry about getting the input entity
first, but if you're going to use an entity often, consider making the
call:
Arguments
entity (`str` | `int` | :tl:`Peer` | :tl:`InputPeer`):
If a username or invite link is given, **the library will
use the cache**. This means that it's possible to be using
a username that *changed* or an old invite link (this only
happens if an invite link for a small group chat is used
after it was upgraded to a mega-group).
If the username or ID from the invite link is not found in
the cache, it will be fetched. The same rules apply to phone
numbers (``'+34 123456789'``) from people in your contact list.
If an exact name is given, it must be in the cache too. This
is not reliable as different people can share the same name
and which entity is returned is arbitrary, and should be used
only for quick tests.
If a positive integer ID is given, the entity will be searched
in cached users, chats or channels, without making any call.
If a negative integer ID is given, the entity will be searched
exactly as either a chat (prefixed with ``-``) or as a channel
(prefixed with ``-100``).
If a :tl:`Peer` is given, it will be searched exactly in the
cache as either a user, chat or channel.
If the given object can be turned into an input entity directly,
said operation will be done.
Unsupported types will raise ``TypeError``.
If the entity can't be found, ``ValueError`` will be raised.
Returns
:tl:`InputPeerUser`, :tl:`InputPeerChat` or :tl:`InputPeerChannel`
or :tl:`InputPeerSelf` if the parameter is ``'me'`` or ``'self'``.
If you need to get the ID of yourself, you should use
`get_me` with ``input_peer=True``) instead.
Example
.. code-block:: python
// If you're going to use "username" often in your code
// (make a lot of calls), consider getting its input entity
// once, and then using the "user" everywhere instead.
user = await client.get_input_entity('username')
// The same applies to IDs, chats or channels.
chat = await client.get_input_entity(-123456789)
* @param peer
* @returns {Promise<>}
*/
/* CONTEST
async getInputEntity(peer) {
// 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 {
// 0x2d45687 == crc32(b'Peer')
if (typeof peer === 'number' || peer.SUBCLASS_OF_ID === 0x2d45687) {
if (this._entityCache.has(peer)) {
return this._entityCache[peer]
}
}
// eslint-disable-next-line no-empty
} catch (e) {
}
// Then come known strings that take precedence
if (['me', 'this'].includes(peer)) {
return new constructors.InputPeerSelf()
}
// No InputPeer, cached peer, or known string. Fetch from disk cache
try {
return this.session.getInputEntity(peer)
// eslint-disable-next-line no-empty
} catch (e) {
}
// Only network left to try
if (typeof peer === 'string') {
return utils.getInputPeer(await this._getEntityFromString(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.
peer = utils.getPeer(peer)
if (peer instanceof constructors.PeerUser) {
const users = await this.invoke(new requests.users.GetUsers({
id: [new constructors.InputUser({
userId: peer.userId,
accessHash: 0,
})],
}))
if (users && !(users[0] instanceof constructors.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 constructors.PeerChat) {
return new constructors.InputPeerChat({
chatId: peer.chatId,
})
} else if (peer instanceof constructors.PeerChannel) {
try {
const channels = await this.invoke(new requests.channels.GetChannels({
id: [new constructors.InputChannel({
channelId: peer.channelId,
accessHash: 0,
})],
}))
return utils.getInputPeer(channels.chats[0])
// eslint-disable-next-line no-empty
} catch (e) {
console.log(e)
}
}
throw new Error(`Could not find the input entity for ${peer.id || peer.channelId || peer.chatId || peer.userId}.
Please read https://` +
'docs.telethon.dev/en/latest/concepts/entities.html to' +
' find out more details.',
)
}
*/
async _dispatchUpdate(args = {
update: undefined,
others: undefined,
channelId: undefined,
ptsDate: undefined,
}) {
for (const [builder, callback] of this._eventBuilders) {
const event = builder.build(args.update);
if (event) {
await callback(event);
}
}
}
isConnected() {
if (this._sender) {
if (this._sender.isConnected()) {
return true;
}
}
return false;
}
}
function timeout(cb, ms) {
let isResolved = false;
return Promise.race([
cb(),
Helpers.sleep(ms).then(() => isResolved ? undefined : Promise.reject(new Error('TIMEOUT'))),
]).finally(() => {
isResolved = true;
});
}
async function attempts(cb, times, pause) {
for (let i = 0; i < times; i++) {
try {
// We need to `return await` here so it can be caught locally
// eslint-disable-next-line @typescript-eslint/return-await
return await cb();
} catch (err) {
if (i === times - 1) {
throw err;
}
await Helpers.sleep(pause);
}
}
return undefined;
}
module.exports = TelegramClient;