|
@@ -3,6 +3,7 @@ const { sleep } = require('../Helpers')
|
|
|
const errors = require('../errors')
|
|
|
const { addKey } = require('../crypto/RSA')
|
|
|
const { TLRequest } = require('../tl/tlobject')
|
|
|
+const utils = require('../Utils')
|
|
|
const Session = require('../sessions/Session')
|
|
|
const os = require('os')
|
|
|
const { GetConfigRequest } = require('../tl/functions/help')
|
|
@@ -171,7 +172,6 @@ class TelegramClient {
|
|
|
async _getDC(dcId, cdn = false) {
|
|
|
if (!this._config) {
|
|
|
this._config = await this.invoke(new functions.help.GetConfigRequest())
|
|
|
-
|
|
|
}
|
|
|
if (cdn && !this._cdnConfig) {
|
|
|
this._cdnConfig = await this.invoke(new functions.help.GetCdnConfigRequest())
|
|
@@ -200,6 +200,8 @@ class TelegramClient {
|
|
|
throw new Error('You can only invoke MTProtoRequests')
|
|
|
}
|
|
|
console.log('sending request..', request)
|
|
|
+ await request.resolve(this, utils)
|
|
|
+
|
|
|
if (request.CONSTRUCTOR_ID in this._floodWaitedRequests) {
|
|
|
const due = this._floodWaitedRequests[request.CONSTRUCTOR_ID]
|
|
|
const diff = Math.round(due - new Date().getTime() / 1000)
|
|
@@ -231,12 +233,12 @@ class TelegramClient {
|
|
|
if (e instanceof errors.ServerError || e instanceof errors.RpcCallFailError ||
|
|
|
e instanceof errors.RpcMcgetFailError) {
|
|
|
this._log.warn(`Telegram is having internal issues ${e.constructor.name}`)
|
|
|
- await Helpers.sleep(2000)
|
|
|
+ await sleep(2000)
|
|
|
} else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) {
|
|
|
this._floodWaitedRequests = new Date().getTime() / 1000 + e.seconds
|
|
|
if (e.seconds <= this.floodSleepLimit) {
|
|
|
this._log.info(`Sleeping for ${e.seconds}s on flood wait`)
|
|
|
- await Helpers.sleep(e.seconds * 1000)
|
|
|
+ await sleep(e.seconds * 1000)
|
|
|
} else {
|
|
|
throw e
|
|
|
}
|
|
@@ -247,7 +249,7 @@ class TelegramClient {
|
|
|
if (shouldRaise && await this.isUserAuthorized()) {
|
|
|
throw e
|
|
|
}
|
|
|
- await Helpers.sleep(1000)
|
|
|
+ await sleep(1000)
|
|
|
await this._switchDC(e.newDc)
|
|
|
} else {
|
|
|
throw e
|
|
@@ -328,7 +330,7 @@ class TelegramClient {
|
|
|
|
|
|
async sendCodeRequest(phone, forceSMS = false) {
|
|
|
let result
|
|
|
- phone = parsePhone(phone) || this._phone
|
|
|
+ phone = utils.parsePhone(phone) || this._phone
|
|
|
let phoneHash = this._phoneCodeHash[phone]
|
|
|
|
|
|
if (!phoneHash) {
|
|
@@ -398,6 +400,234 @@ class TelegramClient {
|
|
|
|
|
|
// 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
|
|
|
+ */
|
|
|
+ async _getEntityFromString(string) {
|
|
|
+ const phone = utils.parsePhone(string)
|
|
|
+ if (phone) {
|
|
|
+ try {
|
|
|
+ for (const user of (await this.invoke(
|
|
|
+ new functions.contacts.GetContactsRequest(0))).users) {
|
|
|
+ if (user.phone === phone) {
|
|
|
+ return user
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ if (e instanceof errors.BotMethodInvalidError) {
|
|
|
+ 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 await this.getMe()
|
|
|
+ } else {
|
|
|
+ const { username, isJoinChat } = utils.parseUsername(string)
|
|
|
+ if (isJoinChat) {
|
|
|
+ const invite = await this.invoke(new functions.messages.CheckChatInviteRequest({
|
|
|
+ 'hash': username,
|
|
|
+ }))
|
|
|
+ if (invite instanceof types.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 types.ChatInviteAlready) {
|
|
|
+ return invite.chat
|
|
|
+ }
|
|
|
+ } else if (username) {
|
|
|
+ try {
|
|
|
+ const result = await this.invoke(
|
|
|
+ new functions.contacts.ResolveUsernameRequest(username))
|
|
|
+ const pid = utils.getPeerId(result.peer, false)
|
|
|
+ if (result.peer instanceof types.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 instanceof errors.UsernameNotOccupiedError) {
|
|
|
+ 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<void>}
|
|
|
+ */
|
|
|
+ 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 types.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 access_hash = 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 types.PeerUser) {
|
|
|
+ const users = await this.invoke(new functions.users.GetUsersRequest({
|
|
|
+ id: new types.InputUser({
|
|
|
+ userId: peer.userId, accessHash: 0,
|
|
|
+ }),
|
|
|
+ }))
|
|
|
+ if (users && !(users[0] instanceof types.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 types.PeerChat) {
|
|
|
+ return new types.InputPeerChat({
|
|
|
+ chatId: peer.chatId,
|
|
|
+ })
|
|
|
+ } else if (peer instanceof types.PeerChannel) {
|
|
|
+ try {
|
|
|
+ const channels = await this.invoke(new functions.channels.GetChannelsRequest({
|
|
|
+ id: [new types.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.',
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ // endregion
|
|
|
+
|
|
|
+
|
|
|
async _dispatchUpdate(args = {
|
|
|
update: null,
|
|
|
others: null,
|