Bladeren bron

Add support for String and Memory session

painor 5 jaren geleden
bovenliggende
commit
056897be58
4 gewijzigde bestanden met toevoegingen van 494 en 0 verwijderingen
  1. 157 0
      gramjs/sessions/Abstract.js
  2. 0 0
      gramjs/sessions/JSONSession.js
  3. 252 0
      gramjs/sessions/Memory.js
  4. 85 0
      gramjs/sessions/StringSession.js

+ 157 - 0
gramjs/sessions/Abstract.js

@@ -0,0 +1,157 @@
+class Session {
+    constructor() {
+
+    }
+
+    /**
+     * Creates a clone of this session file
+     * @param toInstance {Session|null}
+     * @returns {Session}
+     */
+    clone(toInstance = null) {
+        return toInstance || new this.constructor()
+    }
+
+    /**
+     * Sets the information of the data center address and port that
+     * the library should connect to, as well as the data center ID,
+     * which is currently unused.
+     * @param dcId {number}
+     * @param serverAddress {string}
+     * @param port {number}
+     */
+    setDC(dcId, serverAddress, port) {
+        throw new Error('Not implemented')
+    }
+
+    /**
+     * Returns the currently-used data center ID.
+     */
+    get dcId() {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Returns the server address where the library should connect to.
+     */
+    get serverAddress() {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Returns the port to which the library should connect to.
+     */
+    get port() {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Returns an ``AuthKey`` instance associated with the saved
+     * data center, or `None` if a new one should be generated.
+     */
+    get authKey() {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Sets the ``AuthKey`` to be used for the saved data center.
+     * @param value
+     */
+    set authKey(value) {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Returns an ID of the takeout process initialized for this session,
+     * or `None` if there's no were any unfinished takeout requests.
+     */
+    get takeoutId() {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Sets the ID of the unfinished takeout process for this session.
+     * @param value
+     */
+    set takeoutId(value) {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Returns the ``UpdateState`` associated with the given `entity_id`.
+     * If the `entity_id` is 0, it should return the ``UpdateState`` for
+     * no specific channel (the "general" state). If no state is known
+     * it should ``return None``.
+     * @param entityId
+     */
+    getUpdateState(entityId) {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Sets the given ``UpdateState`` for the specified `entity_id`, which
+     * should be 0 if the ``UpdateState`` is the "general" state (and not
+     * for any specific channel).
+     * @param entityId
+     * @param state
+     */
+    setUpdateState(entityId, state) {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Called on client disconnection. Should be used to
+     * free any used resources. Can be left empty if none.
+     */
+    close() {
+
+    }
+
+    /**
+     * called whenever important properties change. It should
+     * make persist the relevant session information to disk.
+     */
+    save() {
+        throw new Error('Not Implemented')
+
+    }
+
+    /**
+     * Called upon client.log_out(). Should delete the stored
+     * information from disk since it's not valid anymore.
+     */
+    delete() {
+        throw new Error('Not Implemented')
+
+    }
+
+    /**
+     * Lists available sessions. Not used by the library itself.
+     */
+    listSessions() {
+        throw new Error('Not Implemented')
+
+    }
+
+    /**
+     * Processes the input ``TLObject`` or ``list`` and saves
+     * whatever information is relevant (e.g., ID or access hash).
+     * @param tlo
+     */
+    processEntities(tlo) {
+        throw new Error('Not Implemented')
+    }
+
+    /**
+     * Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``).
+     * The library uses this method whenever an ``InputPeer`` is needed
+     * to suit several purposes (e.g. user only provided its ID or wishes
+     * to use a cached username to avoid extra RPC).
+     */
+    getInputEntity(key) {
+        throw new Error('Not Implemented')
+
+    }
+}
+
+module.exports = Session

+ 0 - 0
gramjs/sessions/Session.js → gramjs/sessions/JSONSession.js


+ 252 - 0
gramjs/sessions/Memory.js

@@ -0,0 +1,252 @@
+const { TLObject } = require('../tl/tlobject')
+const utils = require('../Utils')
+const types = require('../tl/types')
+const Session = require('./Abstract')
+
+class MemorySession extends Session {
+    constructor() {
+        super()
+
+        this._serverAddress = null
+        this._dcId = 0
+        this._port = null
+        this._authKey = null
+        this._takeoutId = null
+
+        this._entities = new Set()
+        this._updateStates = {}
+    }
+
+    setDC(dcId, serverAddress, port) {
+        this._dcId = dcId | 0
+        this._serverAddress = serverAddress
+        this._port = port
+    }
+
+    get dcId() {
+        return this._dcId
+    }
+
+    get serverAddress() {
+        return this._serverAddress
+    }
+
+    get port() {
+        return this._port
+    }
+
+    get authKey() {
+        return this._authKey
+    }
+
+    set authKey(value) {
+        this._authKey = value
+    }
+
+    get takeoutId() {
+        return this._takeoutId
+    }
+
+    set takeoutId(value) {
+        this._takeoutId = value
+    }
+
+
+    getUpdateState(entityId) {
+        return this._updateStates[entityId]
+    }
+
+    setUpdateState(entityId, state) {
+        return this._updateStates[entityId] = state
+    }
+
+    close() {
+    }
+
+    save() {
+    }
+
+    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
+    }
+
+    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]]
+            }
+        }
+    }
+
+    getEntityRowsByUsername(username) {
+        for (const e of this._entities) { // id, hash, username, phone, name
+            if (e[2] === username) {
+                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]]
+            }
+        }
+    }
+
+    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 = MemorySession

+ 85 - 0
gramjs/sessions/StringSession.js

@@ -0,0 +1,85 @@
+const MemorySession = require('./Memory')
+const AuthKey = require('../crypto/AuthKey')
+const BinaryReader = require('../extensions/BinaryReader')
+const CURRENT_VERSION = '1'
+
+
+class StringSession extends MemorySession {
+    /**
+     * This session file can be easily saved and loaded as a string. According
+     * to the initial design, it contains only the data that is necessary for
+     * successful connection and authentication, so takeout ID is not stored.
+
+     * It is thought to be used where you don't want to create any on-disk
+     * files but would still like to be able to save and load existing sessions
+     * by other means.
+
+     * You can use custom `encode` and `decode` functions, if present:
+
+     * `encode` definition must be ``function encode(value: Buffer) -> string:``.
+     * `decode` definition must be ``function decode(value: string) -> Buffer:``.
+     * @param session {string|null}
+     */
+    constructor(session = null) {
+        super()
+        if (session) {
+            if (session[0] !== CURRENT_VERSION) {
+                throw new Error('Not a valid string')
+            }
+            session = session.slice(1)
+            const ipLen = session.length === 352 ? 4 : 16
+            const r = StringSession.decode(session)
+            const reader = new BinaryReader(r)
+            this._dcId = reader.read(1).readUInt8()
+            const ip = reader.read(ipLen)
+            this._port = reader.read(2).readInt16BE()
+            const key = reader.read(-1)
+            this._serverAddress = ip.readUInt8(0) + '.' +
+                ip.readUInt8(1) + '.' + ip.readUInt8(2) +
+                '.' + ip.readUInt8(3)
+            if (key) {
+                this._authKey = new AuthKey(key)
+            }
+        }
+    }
+
+    /**
+     * @param x {Buffer}
+     * @returns {string}
+     */
+    static encode(x) {
+        return x.toString('base64')
+    }
+
+    /**
+     * @param x {string}
+     * @returns {Buffer}
+     */
+    static decode(x) {
+        return Buffer.from(x, 'base64')
+    }
+
+    save() {
+        if (!this.authKey) {
+            return ''
+        }
+        const ip = this.serverAddress.split('.')
+        const dcBuffer = Buffer.from([this.dcId])
+        const ipBuffer = Buffer.alloc(4)
+        const portBuffer = Buffer.alloc(2)
+        portBuffer.writeInt16BE(this.port)
+        for (let i = 0; i < ip.length; i++) {
+            ipBuffer.writeUInt8(parseInt(ip[i]), i)
+        }
+
+
+        return CURRENT_VERSION + StringSession.encode(Buffer.concat([
+            dcBuffer,
+            ipBuffer,
+            portBuffer,
+            this.authKey.key,
+        ]))
+    }
+}
+
+module.exports = StringSession