Forráskód Böngészése

Add auto peer resolving

painor 5 éve
szülő
commit
cd4a984416

+ 253 - 3
gramjs/Utils.js

@@ -1,5 +1,13 @@
 const { types } = require('./tl')
 const { types } = require('./tl')
 
 
+const USERNAME_RE = new RegExp('@|(?:https?:\\/\\/)?(?:www\\.)?' +
+    '(?:telegram\\.(?:me|dog)|t\\.me)\\/(@|joinchat\\/)?')
+
+
+const TG_JOIN_RE = new RegExp('tg:\\/\\/(join)\\?invite=')
+
+const VALID_USERNAME_RE = new RegExp('^([a-z]((?!__)[\\w\\d]){3,30}[a-z\\d]|gif|vid|' +
+    'pic|bing|wiki|imdb|bold|vote|like|coub)$')
 
 
 function _raiseCastFail(entity, target) {
 function _raiseCastFail(entity, target) {
     throw new Error(`Cannot cast ${entity.constructor.name} to any kind of ${target}`)
     throw new Error(`Cannot cast ${entity.constructor.name} to any kind of ${target}`)
@@ -24,6 +32,7 @@ function _raiseCastFail(entity, target) {
  */
  */
 function getInputPeer(entity, allowSelf = true, checkHash = true) {
 function getInputPeer(entity, allowSelf = true, checkHash = true) {
     if (entity.SUBCLASS_OF_ID === undefined) {
     if (entity.SUBCLASS_OF_ID === undefined) {
+        console.log('undefined')
         // e.g. custom.Dialog (can't cyclic import).
         // e.g. custom.Dialog (can't cyclic import).
 
 
         if (allowSelf && 'inputEntity' in entity) {
         if (allowSelf && 'inputEntity' in entity) {
@@ -34,8 +43,9 @@ function getInputPeer(entity, allowSelf = true, checkHash = true) {
             _raiseCastFail(entity, 'InputPeer')
             _raiseCastFail(entity, 'InputPeer')
         }
         }
     }
     }
-
     if (entity.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
     if (entity.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
+        console.log('returningt entity')
+        console.log(entity)
         return entity
         return entity
     }
     }
 
 
@@ -56,6 +66,7 @@ function getInputPeer(entity, allowSelf = true, checkHash = true) {
         return new types.InputPeerChat({ chatId: entity.id })
         return new types.InputPeerChat({ chatId: entity.id })
     }
     }
     if (entity instanceof types.Channel) {
     if (entity instanceof types.Channel) {
+        console.log('it\'s a channel')
         if ((entity.accessHash !== undefined && !entity.min) || checkHash) {
         if ((entity.accessHash !== undefined && !entity.min) || checkHash) {
             return new types.InputPeerChannel({ channelId: entity.id, accessHash: entity.accessHash })
             return new types.InputPeerChannel({ channelId: entity.id, accessHash: entity.accessHash })
         } else {
         } else {
@@ -72,7 +83,7 @@ function getInputPeer(entity, allowSelf = true, checkHash = true) {
         return new types.InputPeerUser({ userId: entity.userId, accessHash: entity.accessHash })
         return new types.InputPeerUser({ userId: entity.userId, accessHash: entity.accessHash })
     }
     }
     if (entity instanceof types.InputChannel) {
     if (entity instanceof types.InputChannel) {
-        return new types.InputPeerChannel({ userId: entity.channelId, accessHash: entity.accessHash })
+        return new types.InputPeerChannel({ channelId: entity.channelId, accessHash: entity.accessHash })
     }
     }
     if (entity instanceof types.UserEmpty) {
     if (entity instanceof types.UserEmpty) {
         return new types.InputPeerEmpty()
         return new types.InputPeerEmpty()
@@ -82,7 +93,7 @@ function getInputPeer(entity, allowSelf = true, checkHash = true) {
     }
     }
 
 
     if (entity instanceof types.ChatFull) {
     if (entity instanceof types.ChatFull) {
-        return new types.InputPeerChat(entity.id)
+        return new types.InputPeerChat({ chatId: entity.id })
     }
     }
 
 
     if (entity instanceof types.PeerChat) {
     if (entity instanceof types.PeerChat) {
@@ -214,6 +225,137 @@ function getInputMessage(message) {
     _raiseCastFail(message, 'InputMessage')
     _raiseCastFail(message, 'InputMessage')
 }
 }
 
 
+
+function getPeer(peer) {
+    try {
+        if (typeof peer === 'number') {
+            const res = resolveId(peer)
+            if (res[1] === types.PeerChannel) {
+                return new res[1]({ channelId: res[0] })
+            } else {
+                return new res[1]({ chatId: res[0] })
+            }
+        }
+        if (peer.SUBCLASS_OF_ID === undefined) {
+            throw new Error()
+        }
+        if (peer.SUBCLASS_OF_ID === 0x2d45687) {
+            return peer
+        } else if (peer instanceof types.contacts || peer instanceof types.ResolvedPeer ||
+            peer instanceof types.InputNotifyPeer || peer instanceof types.TopPeer ||
+            peer instanceof types.Dialog || peer instanceof types.DialogPeer) {
+            return peer.peer
+        } else if (peer instanceof types.ChannelFull) {
+            return new types.PeerChannel({ channelId: peer.id })
+        }
+        if (peer.SUBCLASS_OF_ID === 0x7d7c6f86 || peer.SUBCLASS_OF_ID === 0xd9c7fc18) {
+            // ChatParticipant, ChannelParticipant
+            return new types.PeerUser({ userId: peer.userId })
+        }
+
+        peer = getInputPeer(peer, false, false)
+        if (peer instanceof types.InputPeerUser) {
+            return new types.PeerUser({ userId: peer.userId })
+        } else if (peer instanceof types.InputPeerChat) {
+            return new types.PeerChat({ chatId: peer.chatId })
+        } else if (peer instanceof types.InputPeerChannel) {
+            return new types.PeerChannel({ channelId: peer.channelId })
+        }
+        // eslint-disable-next-line no-empty
+    } catch (e) {
+    }
+    _raiseCastFail(peer, 'peer')
+}
+
+
+/**
+ Convert the given peer into its marked ID by default.
+
+ This "mark" comes from the "bot api" format, and with it the peer type
+ can be identified back. User ID is left unmodified, chat ID is negated,
+ and channel ID is prefixed with -100:
+
+ * ``user_id``
+ * ``-chat_id``
+ * ``-100channel_id``
+
+ The original ID and the peer type class can be returned with
+ a call to :meth:`resolve_id(marked_id)`.
+ * @param peer
+ * @param addMark
+ */
+function getPeerId(peer, addMark = true) {
+    // First we assert it's a Peer TLObject, or early return for integers
+    if (typeof peer == 'number') {
+        return addMark ? peer : resolveId(peer)[0]
+    }
+
+    // Tell the user to use their client to resolve InputPeerSelf if we got one
+    if (peer instanceof types.InputPeerSelf) {
+        _raiseCastFail(peer, 'int (you might want to use client.get_peer_id)')
+    }
+
+    try {
+        peer = getPeer(peer)
+    } catch (e) {
+        _raiseCastFail(peer, 'int')
+    }
+    if (peer instanceof types.PeerUser) {
+        return peer.userId
+    } else if (peer instanceof types.PeerChat) {
+        // Check in case the user mixed things up to avoid blowing up
+        if (!(0 < peer.chatId <= 0x7fffffff)) {
+            peer.chatId = resolveId(peer.chatId)[0]
+        }
+
+        return addMark ? -(peer.chatId) : peer.chatId
+    } else { // if (peer instanceof types.PeerChannel)
+        // Check in case the user mixed things up to avoid blowing up
+        if (!(0 < peer.channelId <= 0x7fffffff)) {
+            peer.channelId = resolveId(peer.channelId)[0]
+        }
+        if (!addMark) {
+            return peer.channelId
+        }
+        // Concat -100 through math tricks, .to_supergroup() on
+        // Madeline IDs will be strictly positive -> log works.
+        try {
+            return -(peer.channelId + Math.pow(10, Math.floor(Math.log10(peer.channelId) + 3)))
+        } catch (e) {
+            throw new Error('Cannot get marked ID of a channel unless its ID is strictly positive')
+        }
+    }
+}
+
+/**
+ * Given a marked ID, returns the original ID and its :tl:`Peer` type.
+ * @param markedId
+ */
+function resolveId(markedId) {
+    if (markedId >= 0) {
+        return [markedId, types.PeerUser]
+    }
+
+    // There have been report of chat IDs being 10000xyz, which means their
+    // marked version is -10000xyz, which in turn looks like a channel but
+    // it becomes 00xyz (= xyz). Hence, we must assert that there are only
+    // two zeroes.
+    const m = markedId.toString().match(/-100([^0]\d*)/)
+    if (m) {
+        return [parseInt(m[1]), types.PeerChannel]
+    }
+    return [-markedId, types.PeerChat]
+}
+
+/**
+ * returns an entity pair
+ * @param entityId
+ * @param entities
+ * @param cache
+ * @param getInputPeer
+ * @returns {{inputEntity: *, entity: *}}
+ * @private
+ */
 function _getEntityPair(entityId, entities, cache, getInputPeer = getInputPeer) {
 function _getEntityPair(entityId, entities, cache, getInputPeer = getInputPeer) {
     const entity = entities.get(entityId)
     const entity = entities.get(entityId)
     let inputEntity = cache[entityId]
     let inputEntity = cache[entityId]
@@ -239,3 +381,111 @@ function getMessageId(message) {
     }
     }
     throw new Error(`Invalid message type: ${message.constructor.name}`)
     throw new Error(`Invalid message type: ${message.constructor.name}`)
 }
 }
+
+/**
+ * Parses the given phone, or returns `None` if it's invalid.
+ * @param phone
+ */
+function parsePhone(phone) {
+    if (typeof phone === 'number') {
+        return phone.toString()
+    } else {
+        phone = phone.toString().replace(/[+()\s-]/gm, '')
+        if (!isNaN(phone)) {
+            return phone
+        }
+    }
+}
+
+/**
+ Parses the given username or channel access hash, given
+ a string, username or URL. Returns a tuple consisting of
+ both the stripped, lowercase username and whether it is
+ a joinchat/ hash (in which case is not lowercase'd).
+
+ Returns ``(None, False)`` if the ``username`` or link is not valid.
+
+ * @param username {string}
+ */
+function parseUsername(username) {
+    username = username.trim()
+    const m = username.match(USERNAME_RE) || username.match(TG_JOIN_RE)
+    if (m) {
+        username = username.replace(m[0], '')
+        if (m[1]) {
+            return { username: username, isInvite: true }
+        } else {
+            username = rtrim(username, '/')
+        }
+    }
+    if (username.match(VALID_USERNAME_RE)) {
+        return { username: username.toLowerCase(), isInvite: false }
+    } else {
+        return { username: null, isInvite: false }
+    }
+}
+
+function rtrim(s, mask) {
+    while (~mask.indexOf(s[s.length - 1])) {
+        s = s.slice(0, -1)
+    }
+    return s
+}
+
+/**
+ * Gets the display name for the given :tl:`User`,
+ :tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise
+ * @param entity
+ */
+function getDisplayName(entity) {
+    if (entity instanceof types.User) {
+        if (entity.lastName && entity.firstName) {
+            return `${entity.firstName} ${entity.lastName}`
+        } else if (entity.firstName) {
+            return entity.firstName
+        } else if (entity.lastName) {
+            return entity.lastName
+        } else {
+            return ''
+        }
+    } else if (entity instanceof types.Chat || entity instanceof types.Channel) {
+        return entity.title
+    }
+    return ''
+}
+
+/**
+ * check if a given item is an array like or not
+ * @param item
+ * @returns {boolean}
+ */
+function isListLike(item) {
+    return (
+        Array.isArray(item) ||
+        (!!item &&
+            typeof item === 'object' &&
+            typeof (item.length) === 'number' &&
+            (item.length === 0 ||
+                (item.length > 0 &&
+                    (item.length - 1) in item)
+            )
+        )
+    )
+}
+
+module.exports = {
+    getMessageId,
+    _getEntityPair,
+    getInputMessage,
+    getInputDialog,
+    getInputUser,
+    getInputChannel,
+    getInputPeer,
+    parsePhone,
+    parseUsername,
+    getPeer,
+    getPeerId,
+    getDisplayName,
+    resolveId,
+    isListLike,
+}

+ 235 - 5
gramjs/client/TelegramClient.js

@@ -3,6 +3,7 @@ const { sleep } = require('../Helpers')
 const errors = require('../errors')
 const errors = require('../errors')
 const { addKey } = require('../crypto/RSA')
 const { addKey } = require('../crypto/RSA')
 const { TLRequest } = require('../tl/tlobject')
 const { TLRequest } = require('../tl/tlobject')
+const utils = require('../Utils')
 const Session = require('../sessions/Session')
 const Session = require('../sessions/Session')
 const os = require('os')
 const os = require('os')
 const { GetConfigRequest } = require('../tl/functions/help')
 const { GetConfigRequest } = require('../tl/functions/help')
@@ -171,7 +172,6 @@ class TelegramClient {
     async _getDC(dcId, cdn = false) {
     async _getDC(dcId, cdn = false) {
         if (!this._config) {
         if (!this._config) {
             this._config = await this.invoke(new functions.help.GetConfigRequest())
             this._config = await this.invoke(new functions.help.GetConfigRequest())
-
         }
         }
         if (cdn && !this._cdnConfig) {
         if (cdn && !this._cdnConfig) {
             this._cdnConfig = await this.invoke(new functions.help.GetCdnConfigRequest())
             this._cdnConfig = await this.invoke(new functions.help.GetCdnConfigRequest())
@@ -200,6 +200,8 @@ class TelegramClient {
             throw new Error('You can only invoke MTProtoRequests')
             throw new Error('You can only invoke MTProtoRequests')
         }
         }
         console.log('sending request..', request)
         console.log('sending request..', request)
+        await request.resolve(this, utils)
+
         if (request.CONSTRUCTOR_ID in this._floodWaitedRequests) {
         if (request.CONSTRUCTOR_ID in this._floodWaitedRequests) {
             const due = this._floodWaitedRequests[request.CONSTRUCTOR_ID]
             const due = this._floodWaitedRequests[request.CONSTRUCTOR_ID]
             const diff = Math.round(due - new Date().getTime() / 1000)
             const diff = Math.round(due - new Date().getTime() / 1000)
@@ -231,12 +233,12 @@ class TelegramClient {
                 if (e instanceof errors.ServerError || e instanceof errors.RpcCallFailError ||
                 if (e instanceof errors.ServerError || e instanceof errors.RpcCallFailError ||
                     e instanceof errors.RpcMcgetFailError) {
                     e instanceof errors.RpcMcgetFailError) {
                     this._log.warn(`Telegram is having internal issues ${e.constructor.name}`)
                     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) {
                 } else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) {
                     this._floodWaitedRequests = new Date().getTime() / 1000 + e.seconds
                     this._floodWaitedRequests = new Date().getTime() / 1000 + e.seconds
                     if (e.seconds <= this.floodSleepLimit) {
                     if (e.seconds <= this.floodSleepLimit) {
                         this._log.info(`Sleeping for ${e.seconds}s on flood wait`)
                         this._log.info(`Sleeping for ${e.seconds}s on flood wait`)
-                        await Helpers.sleep(e.seconds * 1000)
+                        await sleep(e.seconds * 1000)
                     } else {
                     } else {
                         throw e
                         throw e
                     }
                     }
@@ -247,7 +249,7 @@ class TelegramClient {
                     if (shouldRaise && await this.isUserAuthorized()) {
                     if (shouldRaise && await this.isUserAuthorized()) {
                         throw e
                         throw e
                     }
                     }
-                    await Helpers.sleep(1000)
+                    await sleep(1000)
                     await this._switchDC(e.newDc)
                     await this._switchDC(e.newDc)
                 } else {
                 } else {
                     throw e
                     throw e
@@ -328,7 +330,7 @@ class TelegramClient {
 
 
     async sendCodeRequest(phone, forceSMS = false) {
     async sendCodeRequest(phone, forceSMS = false) {
         let result
         let result
-        phone = parsePhone(phone) || this._phone
+        phone = utils.parsePhone(phone) || this._phone
         let phoneHash = this._phoneCodeHash[phone]
         let phoneHash = this._phoneCodeHash[phone]
 
 
         if (!phoneHash) {
         if (!phoneHash) {
@@ -398,6 +400,234 @@ class TelegramClient {
 
 
     // endregion
     // 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 = {
     async _dispatchUpdate(args = {
         update: null,
         update: null,
         others: null,
         others: null,

+ 0 - 0
gramjs/events/NewMessage.js


+ 0 - 0
gramjs/events/raw.js → gramjs/events/Raw.js


+ 199 - 3
gramjs/sessions/Session.js

@@ -2,7 +2,9 @@ const { generateRandomLong, getRandomInt } = require('../Helpers')
 const fs = require('fs').promises
 const fs = require('fs').promises
 const { existsSync, readFileSync } = require('fs')
 const { existsSync, readFileSync } = require('fs')
 const AuthKey = require('../crypto/AuthKey')
 const AuthKey = require('../crypto/AuthKey')
-
+const { TLObject } = require('../tl/tlobject')
+const utils = require('../Utils')
+const types = require('../tl/types')
 
 
 BigInt.toJSON = function() {
 BigInt.toJSON = function() {
     return { fool: this.fool.toString('hex') }
     return { fool: this.fool.toString('hex') }
@@ -26,6 +28,9 @@ class Session {
         this.timeOffset = 0n
         this.timeOffset = 0n
         this.lastMessageId = 0n
         this.lastMessageId = 0n
         this.user = undefined
         this.user = undefined
+        this._files = {}
+        this._entities = new Set()
+        this._updateStates = {}
     }
     }
 
 
     /**
     /**
@@ -63,6 +68,88 @@ class Session {
         return this._dcId
         return this._dcId
     }
     }
 
 
+    getUpdateState(entityId) {
+        return this._updateStates[entityId]
+    }
+
+    setUpdateState(entityId, state) {
+        return this._updateStates[entityId] = state
+    }
+
+    close() {
+    }
+
+    delete() {
+    }
+
+    _entityValuesToRow(id, hash, username, phone, name) {
+        // While this is a simple implementation it might be overrode by,
+        // other classes so they don't need to implement the plural form
+        // of the method. Don't remove.
+        return [id, hash, username, phone, name]
+    }
+
+    _entityToRow(e) {
+        if (!(e instanceof TLObject)) {
+            return
+        }
+        let p
+        let markedId
+        try {
+            p = utils.getInputPeer(e, false)
+            markedId = utils.getPeerId(p)
+        } catch (e) {
+            // Note: `get_input_peer` already checks for non-zero `access_hash`.
+            // See issues #354 and #392. It also checks that the entity
+            // is not `min`, because its `access_hash` cannot be used
+            // anywhere (since layer 102, there are two access hashes).
+            return
+        }
+        let pHash
+        if (p instanceof types.InputPeerUser || p instanceof types.InputPeerChannel) {
+            pHash = p.accessHash
+        } else if (p instanceof types.InputPeerChat) {
+            pHash = 0
+        } else {
+            return
+        }
+
+        let username = e.username
+        if (username) {
+            username = username.toLowerCase()
+        }
+        const phone = e.phone
+        const name = utils.getDisplayName(e)
+        return this._entityValuesToRow(markedId, pHash, username, phone, name)
+    }
+
+    _entitiesToRows(tlo) {
+        let entities = []
+        if (tlo instanceof TLObject && utils.isListLike(tlo)) {
+            // This may be a list of users already for instance
+            entities = tlo
+        } else {
+            if ('user' in tlo) {
+                entities.push(tlo.user)
+            }
+            if ('chats' in tlo && utils.isListLike(tlo.chats)) {
+                entities.concat(tlo.chats)
+            }
+            if ('users' in tlo && utils.isListLike(tlo.users)) {
+                entities.concat(tlo.users)
+            }
+        }
+        const rows = [] // Rows to add (id, hash, username, phone, name)
+        for (const e of entities) {
+            const row = this._entityToRow(e)
+            if (row) {
+                rows.push(row)
+            }
+        }
+        return rows
+    }
+
+
     static tryLoadOrCreateNew(sessionUserId) {
     static tryLoadOrCreateNew(sessionUserId) {
         if (sessionUserId === undefined) {
         if (sessionUserId === undefined) {
             return new Session()
             return new Session()
@@ -118,9 +205,118 @@ class Session {
         return newMessageId
         return newMessageId
     }
     }
 
 
-    processEntities(result) {
-        console.log('saving entities')
+    processEntities(tlo) {
+        const entitiesSet = this._entitiesToRows(tlo)
+        for (const e of entitiesSet) {
+            this._entities.add(e)
+        }
+    }
+
+    getEntityRowsByPhone(phone) {
+        for (const e of this._entities) { // id, hash, username, phone, name
+            if (e[3] === phone) {
+                return [e[0], e[1]]
+            }
+        }
     }
     }
+
+    getEntityRowsByName(name) {
+        for (const e of this._entities) { // id, hash, username, phone, name
+            if (e[4] === name) {
+                return [e[0], e[1]]
+            }
+        }
+    }
+
+    getEntityRowsByUsername(username) {
+        for (const e of this._entities) { // id, hash, username, phone, name
+            if (e[2] === username) {
+                return [e[0], e[1]]
+            }
+        }
+    }
+
+    getEntityRowsById(id, exact = true) {
+        if (exact) {
+            for (const e of this._entities) { // id, hash, username, phone, name
+                if (e[0] === id) {
+                    return [e[0], e[1]]
+                }
+            }
+        } else {
+            const ids = [utils.getPeerId(new types.PeerUser({ userId: id })),
+                utils.getPeerId(new types.PeerChat({ chatId: id })),
+                utils.getPeerId(new types.PeerChannel({ channelId: id })),
+            ]
+            for (const e of this._entities) { // id, hash, username, phone, name
+                if (ids.includes(e[0])) {
+                    return [e[0], e[1]]
+                }
+            }
+        }
+    }
+
+    getInputEntity(key) {
+        let exact
+        if (key.SUBCLASS_OF_ID !== undefined) {
+            if ([0xc91c90b6, 0xe669bf46, 0x40f202fd].includes(key.SUBCLASS_OF_ID)) {
+                // hex(crc32(b'InputPeer', b'InputUser' and b'InputChannel'))
+                // We already have an Input version, so nothing else required
+                return key
+            }
+            // Try to early return if this key can be casted as input peer
+            return utils.getInputPeer(key)
+        } else {
+            // Not a TLObject or can't be cast into InputPeer
+            if (key instanceof TLObject) {
+                key = utils.getPeerId(key)
+                exact = true
+            } else {
+                exact = !(typeof key == 'number') || key < 0
+            }
+        }
+        let result = null
+        if (typeof key === 'string') {
+            const phone = utils.parsePhone(key)
+            if (phone) {
+                result = this.getEntityRowsByPhone(phone)
+            } else {
+                const { username, isInvite } = utils.parseUsername(key)
+                if (username && !isInvite) {
+                    result = this.getEntityRowsByUsername(username)
+                } else {
+                    const tup = utils.resolveInviteLink(key)[1]
+                    if (tup) {
+                        result = this.getEntityRowsById(tup, false)
+                    }
+                }
+            }
+        } else if (typeof key === 'number') {
+            result = this.getEntityRowsById(key, exact)
+        }
+        if (!result && typeof key === 'string') {
+            result = this.getEntityRowsByName(key)
+        }
+
+        if (result) {
+            let entityId = result[0] // unpack resulting tuple
+            const entityHash = result[1]
+            const resolved = utils.resolveId(entityId)
+            entityId = resolved[0]
+            const kind = resolved[1]
+            // removes the mark and returns type of entity
+            if (kind === types.PeerUser) {
+                return new types.InputPeerUser({ userId: entityId, accessHash: entityHash })
+            } else if (kind === types.PeerChat) {
+                return new types.InputPeerChat({ chatId: entityId })
+            } else if (kind === types.PeerChannel) {
+                return new types.InputPeerChannel({ channelId: entityId, accessHash: entityHash })
+            }
+        } else {
+            throw new Error('Could not find input entity with key ' + key)
+        }
+    }
+
 }
 }
 
 
 module.exports = Session
 module.exports = Session

+ 1 - 1
gramjs/tl/tlobject.js

@@ -72,7 +72,7 @@ class TLRequest extends TLObject {
         return reader.tgReadObject()
         return reader.tgReadObject()
     }
     }
 
 
-    async resolve(self, client, utils) {}
+    async resolve(client, utils) {}
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 23 - 22
gramjs_generator/generators/tlobject.js

@@ -7,20 +7,20 @@ const { snakeToCamelCase, variableSnakeToCamelCase } = require('../utils')
 const AUTO_GEN_NOTICE = '/*! File generated by TLObjects\' generator. All changes will be ERASED !*/'
 const AUTO_GEN_NOTICE = '/*! File generated by TLObjects\' generator. All changes will be ERASED !*/'
 
 
 const AUTO_CASTS = {
 const AUTO_CASTS = {
-    InputPeer: 'utils.get_input_peer(await client.get_input_entity(%s))',
-    InputChannel: 'utils.get_input_channel(await client.get_input_entity(%s))',
-    InputUser: 'utils.get_input_user(await client.get_input_entity(%s))',
-    InputDialogPeer: 'await client._get_input_dialog(%s)',
-    InputNotifyPeer: 'await client._get_input_notify(%s)',
-    InputMedia: 'utils.get_input_media(%s)',
-    InputPhoto: 'utils.get_input_photo(%s)',
-    InputMessage: 'utils.get_input_message(%s)',
-    InputDocument: 'utils.get_input_document(%s)',
-    InputChatPhoto: 'utils.get_input_chat_photo(%s)',
+    InputPeer: 'utils.getInputPeer(await client.getInputEntity(%s))',
+    InputChannel: 'utils.getInputChannel(await client.getInputEntity(%s))',
+    InputUser: 'utils.getInputUser(await client.getInputEntity(%s))',
+    InputDialogPeer: 'await client._getInputDialog(%s)',
+    InputNotifyPeer: 'await client._getInputNotify(%s)',
+    InputMedia: 'utils.getInputMedia(%s)',
+    InputPhoto: 'utils.getInputPhoto(%s)',
+    InputMessage: 'utils.getInputMessage(%s)',
+    InputDocument: 'utils.getInputDocument(%s)',
+    InputChatPhoto: 'utils.getInputChatPhoto(%s)',
 }
 }
 
 
 const NAMED_AUTO_CASTS = {
 const NAMED_AUTO_CASTS = {
-    'chat_id,int': 'await client.get_peer_id(%s, add_mark=False)',
+    'chatId,int': 'await client.getPeerId(%s, add_mark=False)',
 }
 }
 
 
 // Secret chats have a chat_id which may be negative.
 // Secret chats have a chat_id which may be negative.
@@ -75,7 +75,8 @@ const writeModules = (outDir, depth, kind, namespaceTlobjects, typeConstructors)
 
 
         // Import struct for the .__bytes__(self) serialization
         // Import struct for the .__bytes__(self) serialization
         builder.writeln('const struct = require(\'python-struct\');')
         builder.writeln('const struct = require(\'python-struct\');')
-        builder.writeln(`const Helpers = require('../../utils/Helpers');`)
+        builder.writeln(`const { readBigIntFromBuffer, 
+        readBufferFromBigInt, generateRandomBytes } = require('../../Helpers')`)
 
 
         const typeNames = new Set()
         const typeNames = new Set()
         const typeDefs = []
         const typeDefs = []
@@ -310,7 +311,7 @@ const writeClassConstructor = (tlobject, kind, typeConstructors, builder) => {
             // inferred are those called 'random_id'
             // inferred are those called 'random_id'
         } else if (arg.name === 'random_id') {
         } else if (arg.name === 'random_id') {
             // Endianness doesn't really matter, and 'big' is shorter
             // Endianness doesn't really matter, and 'big' is shorter
-            let code = `Helpers.readBigIntFromBuffer(Helpers.generateRandomBytes(${
+            let code = `readBigIntFromBuffer(generateRandomBytes(${
                 arg.type === 'long' ? 8 : 4
                 arg.type === 'long' ? 8 : 4
             }),false,true)`
             }),false,true)`
 
 
@@ -348,7 +349,7 @@ const writeResolve = (tlobject, builder) => {
             let ac = AUTO_CASTS[arg.type]
             let ac = AUTO_CASTS[arg.type]
 
 
             if (!ac) {
             if (!ac) {
-                ac = NAMED_AUTO_CASTS[`${arg.name},${arg.type}`]
+                ac = NAMED_AUTO_CASTS[`${variableSnakeToCamelCase(arg.name)},${arg.type}`]
 
 
                 if (!ac) {
                 if (!ac) {
                     continue
                     continue
@@ -356,17 +357,18 @@ const writeResolve = (tlobject, builder) => {
             }
             }
 
 
             if (arg.isFlag) {
             if (arg.isFlag) {
-                builder.writeln(`if (this.${arg.name}) {`)
+                builder.writeln(`if (this.${variableSnakeToCamelCase(arg.name)}) {`)
             }
             }
 
 
             if (arg.isVector) {
             if (arg.isVector) {
                 builder.write(`const _tmp = [];`)
                 builder.write(`const _tmp = [];`)
-                builder.writeln(`for (const _x of this.${arg.name}) {`)
+                builder.writeln(`for (const _x of this.${variableSnakeToCamelCase(arg.name)}) {`)
                 builder.writeln(`_tmp.push(%s);`, util.format(ac, '_x'))
                 builder.writeln(`_tmp.push(%s);`, util.format(ac, '_x'))
                 builder.endBlock()
                 builder.endBlock()
-                builder.writeln(`this.${arg.name} = _tmp;`)
+                builder.writeln(`this.${variableSnakeToCamelCase(arg.name)} = _tmp;`)
             } else {
             } else {
-                builder.writeln(`this.${arg.name} = %s`, util.format(ac, `this.${arg.name}`))
+                builder.writeln(`this.${arg.name} = %s`,
+                    util.format(ac, `this.${variableSnakeToCamelCase(arg.name)}`))
             }
             }
 
 
             if (arg.isFlag) {
             if (arg.isFlag) {
@@ -586,11 +588,11 @@ const writeArgToBytes = (builder, arg, args, name = null) => {
     } else if (arg.type === 'int') {
     } else if (arg.type === 'int') {
         builder.write('struct.pack(\'<i\', %s)', name)
         builder.write('struct.pack(\'<i\', %s)', name)
     } else if (arg.type === 'long') {
     } else if (arg.type === 'long') {
-        builder.write('Helpers.readBufferFromBigInt(%s,8,true,true)', name)
+        builder.write('readBufferFromBigInt(%s,8,true,true)', name)
     } else if (arg.type === 'int128') {
     } else if (arg.type === 'int128') {
-        builder.write('Helpers.readBufferFromBigInt(%s,16,true,true)', name)
+        builder.write('readBufferFromBigInt(%s,16,true,true)', name)
     } else if (arg.type === 'int256') {
     } else if (arg.type === 'int256') {
-        builder.write('Helpers.readBufferFromBigInt(%s,32,true,true)', name)
+        builder.write('readBufferFromBigInt(%s,32,true,true)', name)
     } else if (arg.type === 'double') {
     } else if (arg.type === 'double') {
         builder.write('struct.pack(\'<d\', %s.toString())', name)
         builder.write('struct.pack(\'<d\', %s.toString())', name)
     } else if (arg.type === 'string') {
     } else if (arg.type === 'string') {
@@ -757,7 +759,6 @@ const writePatched = (outDir, namespaceTlobjects) => {
         builder.writeln(AUTO_GEN_NOTICE)
         builder.writeln(AUTO_GEN_NOTICE)
         builder.writeln('const struct = require(\'python-struct\');')
         builder.writeln('const struct = require(\'python-struct\');')
         builder.writeln(`const { TLObject, types, custom } = require('..');`)
         builder.writeln(`const { TLObject, types, custom } = require('..');`)
-        builder.writeln(`const Helpers = require('../../utils/Helpers');`)
 
 
         builder.writeln()
         builder.writeln()