Browse Source

Overhaul of the whole codebase
Update to MTProto 2.0

painor 5 years ago
parent
commit
175566e921

+ 0 - 1
.gitignore

@@ -2,7 +2,6 @@
 /.vscode/
 /.vscode/
 /.idea/
 /.idea/
 # Generated code
 # Generated code
-/gramjs/
 /docs/
 /docs/
 /gramjs/tl/functions/
 /gramjs/tl/functions/
 /gramjs/tl/types/
 /gramjs/tl/types/

+ 0 - 4
api/settings

@@ -1,4 +0,0 @@
-api_id=12345
-api_hash=0123456789abcdef0123456789abcdef
-user_phone=+34600000000
-session_name=anonymous

+ 0 - 32
crypto/AuthKey.js

@@ -1,32 +0,0 @@
-const Helpers = require("../utils/Helpers");
-
-class AuthKey {
-    constructor(data) {
-        this.key = data;
-        let offset = 0;
-        let buffer = Helpers.sha1(data);
-        this.auxHash = buffer.readBigUInt64LE(offset);
-        offset = 8 + 4;
-        this.keyId = buffer.readBigUInt64LE(offset);
-
-    }
-
-    /**
-     * Calculates the new nonce hash based on the current class fields' values
-     * @param new_nonce {Buffer}
-     * @param number {number}
-     * @returns {Buffer}
-     */
-    calcNewNonceHash(new_nonce, number) {
-
-        let tempBuffer = Buffer.alloc(1);
-        tempBuffer.writeInt8(number, 0);
-        let secondBuffer = Buffer.alloc(8);
-        secondBuffer.writeBigUInt64LE(this.auxHash, 0);
-        let buffer = Buffer.concat([new_nonce, tempBuffer, secondBuffer]);
-        return Helpers.calcMsgKey(buffer);
-    }
-
-}
-
-module.exports = AuthKey;

+ 0 - 259
errors.js

@@ -1,259 +0,0 @@
-/**
- * Occurs when a read operation was cancelled
- */
-class ReadCancelledError extends Error {
-    constructor() {
-        super("You must run `python3 tl_generator.py` first. #ReadTheDocs!")
-    }
-}
-
-/**
- * Occurs when you should've ran `tl_generator.py`, but you haven't
- */
-class TLGeneratorNotRan extends Error {
-    constructor() {
-        super("You must run `python3 tl_generator.py` first. #ReadTheDocs!")
-    }
-}
-
-/**
- * Occurs when an invalid parameter is given, for example,
- * when either A or B are required but none is given
- */
-class InvalidParameterError extends Error {
-
-}
-
-/**
- * Occurs when a type is not found, for example,
- * when trying to read a TLObject with an invalid constructor code
- */
-class TypeNotFoundError extends Error {
-    constructor(invalidConstructorId) {
-        super('Could not find a matching Constructor ID for the TLObject ' +
-            'that was supposed to be read with ID {}. Most likely, a TLObject ' +
-            'was trying to be read when it should not be read.'.replace("{}",
-                invalidConstructorId.toString("16")));
-        this.invalidConstructorId = invalidConstructorId;
-    }
-}
-
-/**
- * Occurs when a read operation was cancelled
- */
-class InvalidDCError extends Error {
-    constructor(newDC) {
-        super('Your phone number is registered to #{} DC. ' +
-            'This should have been handled automatically; ' +
-            'if it has not, please restart the app.'.replace("{}", newDC));
-        this.newDC = newDC;
-    }
-}
-
-/**
- * Occurs when an invalid checksum is passed
- */
-class InvalidChecksumError extends Error {
-    constructor(checksum, validChecksum) {
-        super('Invalid checksum ({0} when {1} was expected). This packet should be skipped.'
-            .replace("{0}", checksum).replace("{1}", validChecksum));
-        this.checksum = checksum;
-        this.validChecksum = validChecksum;
-
-    }
-}
-
-class RPCError extends Error {
-    static CodeMessages = {
-        303: Array('ERROR_SEE_OTHER', 'The request must be repeated, but directed to a different data center.'),
-
-        400: Array('BAD_REQUEST', 'The query contains errors. In the event that a request was created using a ' +
-            'form and contains user generated data, the user should be notified that the ' +
-            'data must be corrected before the query is repeated.'),
-
-        401: Array('UNAUTHORIZED', 'There was an unauthorized attempt to use functionality available only to ' +
-            'authorized users.'),
-
-        403: Array('FORBIDDEN', 'Privacy violation. For example, an attempt to write a message to someone who ' +
-            'has blacklisted the current user.'),
-
-        404: Array('NOT_FOUND', 'An attempt to invoke a non-existent object, such as a method.'),
-
-        420: Array('FLOOD', 'The maximum allowed number of attempts to invoke the given method with ' +
-            'the given input parameters has been exceeded. For example, in an attempt ' +
-            'to request a large number of text messages (SMS) for the same phone number.'),
-
-        500: Array('INTERNAL', 'An internal server error occurred while a request was being processed; ' +
-            'for example, there was a disruption while accessing a database or file storage.')
-    };
-    static ErrorMessages = {
-        // 303 ERROR_SEE_OTHER
-        'FILE_MIGRATE_(\\d+)': 'The file to be accessed is currently stored in a different data center (#{}).',
-
-        'PHONE_MIGRATE_(\\d+)': 'The phone number a user is trying to use for authorization is associated ' +
-            'with a different data center (#{}).',
-
-        'NETWORK_MIGRATE_(\\d+)': 'The source IP address is associated with a different data center (#{}, ' +
-            'for registration).',
-
-        'USER_MIGRATE_(\\d+)': 'The user whose identity is being used to execute queries is associated with ' +
-            'a different data center  (#{} for registration).',
-
-        // 400 BAD_REQUEST
-        'FIRSTNAME_INVALID': 'The first name is invalid.',
-
-        'LASTNAME_INVALID': 'The last name is invalid.',
-
-        'PHONE_NUMBER_INVALID': 'The phone number is invalid.',
-
-        'PHONE_CODE_HASH_EMPTY': 'phone_code_hash is missing.',
-
-        'PHONE_CODE_EMPTY': 'phone_code is missing.',
-
-        'PHONE_CODE_EXPIRED': 'The confirmation code has expired.',
-
-        'API_ID_INVALID': 'The api_id/api_hash combination is invalid.',
-
-        'PHONE_NUMBER_OCCUPIED': 'The phone number is already in use.',
-
-        'PHONE_NUMBER_UNOCCUPIED': 'The phone number is not yet being used.',
-
-        'USERS_TOO_FEW': 'Not enough users (to create a chat, for example).',
-
-        'USERS_TOO_MUCH': 'The maximum number of users has been exceeded (to create a chat, for example).',
-
-        'TYPE_CONSTRUCTOR_INVALID': 'The type constructor is invalid.',
-
-        'FILE_PART_INVALID': 'The file part number is invalid.',
-
-        'FILE_PARTS_INVALID': 'The number of file parts is invalid.',
-
-        'FILE_PART_(\\d+)_MISSING': 'Part {} of the file is missing from storage.',
-
-        'MD5_CHECKSUM_INVALID': 'The MD5 checksums do not match.',
-
-        'PHOTO_INVALID_DIMENSIONS': 'The photo dimensions are invalid.',
-
-        'FIELD_NAME_INVALID': 'The field with the name FIELD_NAME is invalid.',
-
-        'FIELD_NAME_EMPTY': 'The field with the name FIELD_NAME is missing.',
-
-        'MSG_WAIT_FAILED': 'A waiting call returned an error.',
-
-        'CHAT_ADMIN_REQUIRED': 'Chat admin privileges are required to do that in the specified chat ' +
-            '(for example, to send a message in a channel which is not yours).',
-
-        // 401 UNAUTHORIZED
-        'AUTH_KEY_UNREGISTERED': 'The key is not registered in the system.',
-
-        'AUTH_KEY_INVALID': 'The key is invalid.',
-
-        'USER_DEACTIVATED': 'The user has been deleted/deactivated.',
-
-        'SESSION_REVOKED': 'The authorization has been invalidated, because of the user terminating all sessions.',
-
-        'SESSION_EXPIRED': 'The authorization has expired.',
-
-        'ACTIVE_USER_REQUIRED': 'The method is only available to already activated users.',
-
-        'AUTH_KEY_PERM_EMPTY': 'The method is unavailable for temporary authorization key, not bound to permanent.',
-
-        // 420 FLOOD
-        'FLOOD_WAIT_(\\d+)': 'A wait of {} seconds is required.'
-    };
-
-    constructor(code, message) {
-        let codeMeaning = RPCError.CodeMessages[code];
-        let mustResend = code === 303; // ERROR_SEE_OTHER, "The request must be repeated"
-
-        let calledSuper = false;
-        for (let item in RPCError.ErrorMessages) {
-
-            let key = new RegExp(item);
-            let errorMsg = RPCError.ErrorMessages[item];
-
-            let match = message.match(key);
-
-            if (match) {
-                // Get additionalData if any
-                if (match.length === 2) {
-                    let additionalData = parseInt(match[1]);
-                    super(errorMsg.replace("{}", additionalData));
-
-                    this.additionalData = additionalData;
-                } else {
-                    super(errorMsg);
-                }
-                calledSuper = true;
-                break;
-            }
-
-        }
-        if (!calledSuper) {
-            super("Unknown error message with code {0}: {1}"
-                .replace("{0}", code).replace("{1}", message))
-        }
-        this.code = code;
-        this.errorMessage = message;
-        this.codeMeaning = codeMeaning;
-        this.mustResend = mustResend;
-
-    }
-}
-
-/**
- * Occurs when handling a badMessageNotification
- */
-class BadMessageError extends Error {
-    static ErrorMessages = {
-        16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' +
-            'synchronize it using msg_id notifications and re-send the original message ' +
-            'with the “correct” msg_id or wrap it in a container with a new msg_id if the ' +
-            'original message had waited too long on the client to be transmitted).',
-
-        17: 'msg_id too high (similar to the previous case, the client time has to be ' +
-            'synchronized, and the message re-sent with the correct msg_id).',
-
-        18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' +
-            'to be divisible by 4).',
-
-        19: 'Container msg_id is the same as msg_id of a previously received message ' +
-            '(this must never happen).',
-
-        20: 'Message too old, and it cannot be verified whether the server has received a ' +
-            'message with this msg_id or not.',
-
-        32: 'msg_seqno too low (the server has already received a message with a lower ' +
-            'msg_id but with either a higher or an equal and odd seqno).',
-
-        33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' +
-            'either a lower or an equal and odd seqno).',
-
-        34: 'An even msg_seqno expected (irrelevant message), but odd received.',
-
-        35: 'Odd msg_seqno expected (relevant message), but even received.',
-
-        48: 'Incorrect server salt (in this case, the bad_server_salt response is received with ' +
-            'the correct salt, and the message is to be re-sent with it).',
-
-        64: 'Invalid container.'
-    };
-
-    constructor(code) {
-        super(BadMessageError.ErrorMessages[code] || "Unknown error code (this should not happen): {}."
-            .replace("{}", code));
-    }
-
-}
-
-
-module.exports = {
-    ReadCancelledError,
-    TLGeneratorNotRan,
-    InvalidParameterError,
-    TypeNotFoundError,
-    InvalidDCError,
-    InvalidChecksumError,
-    RPCError,
-    BadMessageError
-};

+ 0 - 0
crypto/AES.js → gramjs/crypto/AES.js


+ 54 - 0
gramjs/crypto/AuthKey.js

@@ -0,0 +1,54 @@
+const Helpers = require("../utils/Helpers");
+const BinaryReader = require("../extensions/BinaryReader");
+
+class AuthKey {
+    constructor(data) {
+        this.key = data;
+
+    }
+
+    set key(value) {
+        if (!value) {
+            this._key = this.auxHash = this.keyId = null;
+            return
+        }
+        if (value instanceof this) {
+            this._key = value._key;
+            this.auxHash = value.auxHash;
+            this.keyId = value.keyId;
+            return
+        }
+        this._key = value;
+        let reader = new BinaryReader(Helpers.sha1(this._key));
+        this.auxHash = reader.readLong(false);
+        reader.read(4);
+        this.keyId = reader.readLong(false);
+    }
+
+    get key() {
+        return this._key;
+    }
+
+
+    // TODO : This doesn't really fit here, it's only used in authentication
+    /**
+     * Calculates the new nonce hash based on the current class fields' values
+     * @param new_nonce {Buffer}
+     * @param number {number}
+     * @returns {Buffer}
+     */
+    calcNewNonceHash(new_nonce, number) {
+        let tempBuffer = Buffer.alloc(1);
+        tempBuffer.writeInt8(number, 0);
+        let secondBuffer = Buffer.alloc(8);
+        secondBuffer.writeBigUInt64LE(this.auxHash, 0);
+        let buffer = Buffer.concat([new_nonce, tempBuffer, secondBuffer]);
+        return Helpers.calcMsgKey(buffer);
+    }
+
+    equals(other) {
+        return (other instanceof this.constructor && other.key === this._key)
+    }
+}
+
+module.exports = AuthKey;

+ 0 - 0
crypto/Factorizator.js → gramjs/crypto/Factorizator.js


+ 0 - 0
crypto/RSA.js → gramjs/crypto/RSA.js


+ 141 - 0
gramjs/errors/Common.js

@@ -0,0 +1,141 @@
+/**
+ * Errors not related to the Telegram API itself
+ */
+
+const {TLRequest} = require("../tl");
+const struct = require("python-struct");
+
+/**
+ * Occurs when a read operation was cancelled.
+ */
+class ReadCancelledError extends Error {
+    constructor() {
+        super("The read operation was cancelled.");
+    }
+}
+
+/**
+ * Occurs when a type is not found, for example,
+ * when trying to read a TLObject with an invalid constructor code.
+ */
+class TypeNotFoundError extends Error {
+
+    constructor(invalidConstructorId, remaining) {
+        super(`Could not find a matching Constructor ID for the TLObject that was supposed to be 
+        read with ID ${invalidConstructorId}. Most likely, a TLObject was trying to be read when
+         it should not be read. Remaining bytes: ${remaining}`);
+        this.invalidConstructorId = invalidConstructorId;
+        this.remaining = remaining;
+    }
+}
+
+/**
+ * Occurs when using the TCP full mode and the checksum of a received
+ * packet doesn't match the expected checksum.
+ */
+class InvalidChecksumError extends Error {
+    constructor(checksum, validChecksum) {
+        super(`Invalid checksum (${checksum} when ${validChecksum} was expected). This packet should be skipped.`);
+        this.checksum = checksum;
+        this.validChecksum = validChecksum;
+    }
+}
+
+/**
+ * Occurs when the buffer is invalid, and may contain an HTTP error code.
+ * For instance, 404 means "forgotten/broken authorization key", while
+ */
+class InvalidBufferError extends Error {
+    constructor(payload) {
+        let code = (-struct.unpack("<i", payload)[0]);
+        if ((payload.length === 4)) {
+            super(`Invalid response buffer (HTTP code ${code})`);
+        } else {
+            this.code = null;
+            super(`Invalid response buffer (too short ${payload})`);
+        }
+        this.code = code;
+        this.payload = payload;
+
+    }
+}
+
+
+/**
+ * Generic security error, mostly used when generating a new AuthKey.
+ */
+class SecurityError extends Error {
+    constructor(...args) {
+        if (!args) {
+            args = ["A security check failed."];
+        }
+        super(...args);
+    }
+}
+
+/**
+ * Occurs when there's a hash mismatch between the decrypted CDN file
+ * and its expected hash.
+ */
+class CdnFileTamperedError extends SecurityError {
+    constructor() {
+        super("The CDN file has been altered and its download cancelled.");
+    }
+}
+
+/**
+ * Occurs when another exclusive conversation is opened in the same chat.
+ */
+class AlreadyInConversationError extends Error {
+    constructor() {
+        super("Cannot open exclusive conversation in a chat that already has one open conversation");
+    }
+}
+
+/**
+ * Occurs when handling a badMessageNotification
+ */
+class BadMessageError extends Error {
+    static ErrorMessages = {
+        16: 'msg_id too low (most likely, client time is wrong it would be worthwhile to ' +
+            'synchronize it using msg_id notifications and re-send the original message ' +
+            'with the “correct” msg_id or wrap it in a container with a new msg_id if the ' +
+            'original message had waited too long on the client to be transmitted).',
+
+        17: 'msg_id too high (similar to the previous case, the client time has to be ' +
+            'synchronized, and the message re-sent with the correct msg_id).',
+
+        18: 'Incorrect two lower order msg_id bits (the server expects client message msg_id ' +
+            'to be divisible by 4).',
+
+        19: 'Container msg_id is the same as msg_id of a previously received message ' +
+            '(this must never happen).',
+
+        20: 'Message too old, and it cannot be verified whether the server has received a ' +
+            'message with this msg_id or not.',
+
+        32: 'msg_seqno too low (the server has already received a message with a lower ' +
+            'msg_id but with either a higher or an equal and odd seqno).',
+
+        33: 'msg_seqno too high (similarly, there is a message with a higher msg_id but with ' +
+            'either a lower or an equal and odd seqno).',
+
+        34: 'An even msg_seqno expected (irrelevant message), but odd received.',
+
+        35: 'Odd msg_seqno expected (relevant message), but even received.',
+
+        48: 'Incorrect server salt (in this case, the bad_server_salt response is received with ' +
+            'the correct salt, and the message is to be re-sent with it).',
+
+        64: 'Invalid container.'
+    };
+
+    constructor(code) {
+        super(BadMessageError.ErrorMessages[code] || `Unknown error code (this should not happen): ${code}.`);
+        this.code = code;
+
+    }
+
+}
+
+// TODO : Support multi errors.

+ 105 - 0
gramjs/errors/RPCBaseErrors.js

@@ -0,0 +1,105 @@
+/**
+ * Base class for all Remote Procedure Call errors.
+ */
+class RPCError extends Error {
+
+
+    constructor(request, message, code = null) {
+        super("RPCError {0}: {1}{2}"
+            .replace("{0}", code)
+            .replace("{1}", message)
+            .replace("{2}", RPCError._fmt_request(request)));
+        this.code = code;
+        this.message = message;
+    }
+
+    static _fmt_request(request) {
+        return ' (caused by {})'.format(request.constructor.name)
+    }
+}
+
+/**
+ * The request must be repeated, but directed to a different data center.
+ */
+class InvalidDCError extends RPCError {
+    code = 303;
+    message = 'ERROR_SEE_OTHER'
+}
+
+
+/**
+ * The query contains errors. In the event that a request was created
+ * using a form and contains user generated data, the user should be
+ * notified that the data must be corrected before the query is repeated.
+ */
+class BadRequestError extends RPCError {
+    code = 400;
+    message = 'BAD_REQUEST'
+}
+
+/**
+ * There was an unauthorized attempt to use functionality available only
+ * to authorized users.
+ */
+class UnauthorizedError extends RPCError {
+    code = 401;
+    message = 'UNAUTHORIZED'
+}
+
+/**
+ * Privacy violation. For example, an attempt to write a message to
+ * someone who has blacklisted the current user.
+ */
+class ForbiddenError extends RPCError {
+    code = 403;
+    message = 'FORBIDDEN'
+}
+
+/**
+ * An attempt to invoke a non-existent object, such as a method.
+ */
+class NotFoundError extends RPCError {
+    code = 404;
+    message = 'NOT_FOUND'
+}
+
+/**
+ * Errors related to invalid authorization key, like
+ * AUTH_KEY_DUPLICATED which can cause the connection to fail.
+ */
+class AuthKeyError extends RPCError {
+    code = 406;
+    message = 'AUTH_KEY'
+}
+
+/**
+ * The maximum allowed number of attempts to invoke the given method
+ * with the given input parameters has been exceeded. For example, in an
+ * attempt to request a large number of text messages (SMS) for the same
+ * phone number.
+ */
+class FloodError extends RPCError {
+    code = 420;
+    message = 'FLOOD'
+
+
+}
+
+/**
+ * An internal server error occurred while a request was being processed
+ * for example, there was a disruption while accessing a database or file
+ * storage
+ */
+class ServerError extends RPCError {
+    code = 500;  // Also witnessed as -500
+    message = 'INTERNAL'
+}
+
+/**
+ * Clicking the inline buttons of bots that never (or take to long to)
+ * call ``answerCallbackQuery`` will result in this "special" RPCError.
+ */
+class TimedOutError extends RPCError {
+    code = 503;  // Only witnessed as -503
+    message = 'Timeout';
+}

+ 266 - 0
gramjs/extensions/BinaryReader.js

@@ -0,0 +1,266 @@
+const unpack = require("python-struct").unpack;
+const BigIntBuffer = require("bigint-buffer");
+
+class BinaryReader {
+
+    /**
+     * Small utility class to read binary data.
+     * @param data {Buffer}
+     */
+    constructor(data) {
+        this.stream = data;
+        this._last = null;
+        this.offset = 0;
+    }
+
+    // region Reading
+
+    // "All numbers are written as little endian."
+    // https://core.telegram.org/mtproto
+    /**
+     * Reads a single byte value.
+     */
+    readByte() {
+        return this.read(1)[0];
+    }
+
+    /**
+     * Reads an integer (4 bytes or 32 bits) value.
+     * @param signed {Boolean}
+     */
+    readInt(signed = true) {
+        let res;
+        if (signed) {
+            res = this.stream.readInt32LE(this.offset);
+        } else {
+            res = this.stream.readUInt32LE(this.offset);
+        }
+        this.offset += 4;
+        return res;
+    }
+
+    /**
+     * Reads a long integer (8 bytes or 64 bits) value.
+     * @param signed
+     * @returns {bigint}
+     */
+    readLong(signed = true) {
+        let res;
+        if (signed) {
+            res = this.stream.readBigInt64LE(this.offset);
+        } else {
+            res = this.stream.readBigUInt64LE(this.offset);
+        }
+        this.offset += 8;
+        return res;
+    }
+
+    /**
+     * Reads a real floating point (4 bytes) value.
+     * @returns {number}
+     */
+    readFloat() {
+        return unpack('<f', this.read(4))[0];
+    }
+
+    /**
+     * Reads a real floating point (8 bytes) value.
+     * @returns {BigInt}
+     */
+    readDouble() {
+        return unpack('<f', this.read(8))[0];
+    }
+
+    /**
+     * Reads a n-bits long integer value.
+     * @param bits
+     * @param signed {Boolean}
+     */
+    readLargeInt(bits, signed = true) {
+        let buffer = this.read(Math.floor(bits / 8));
+        return BigIntBuffer.toBigIntLE(buffer);
+    }
+
+    /**
+     * Read the given amount of bytes, or -1 to read all remaining.
+     * @param length {number}
+     */
+    read(length = -1) {
+        if (length === -1) {
+            length = this.stream.length - this.offset;
+        }
+        let result = this.stream.slice(this.offset, this.offset + length);
+        this.offset += length;
+        if (result.length !== length) {
+            throw Error(`No more data left to read (need ${length}, got ${result.length}: ${result}); last read ${this._last}`)
+        }
+        this._last = result;
+        return result;
+    }
+
+    /**
+     * Gets the byte array representing the current buffer as a whole.
+     * @returns {Buffer}
+     */
+    getBuffer() {
+        return this.stream;
+    }
+
+    // endregion
+
+    // region Telegram custom reading
+    /**
+     * Reads a Telegram-encoded byte array, without the need of
+     * specifying its length.
+     * @returns {Buffer}
+     */
+    tgReadByte() {
+        let firstByte = this.readByte();
+        let padding, length;
+        if (firstByte === 254) {
+            length = this.readByte | this.readByte << 8 | this.readByte << 16;
+            padding = length % 4;
+        } else {
+            length = firstByte;
+            padding = (length + 1) % 4;
+        }
+
+        let data = this.read(length);
+
+        if (padding > 0) {
+            padding = 4 - padding;
+            this.read(padding);
+        }
+
+        return data;
+    }
+
+    /**
+     * Reads a Telegram-encoded string.
+     * @returns {string}
+     */
+    tgReadString() {
+        return this.tgReadByte().toString("utf-8");
+    }
+
+    /**
+     * Reads a Telegram boolean value.
+     * @returns {boolean}
+     */
+    tgReadBool() {
+        let value = this.readInt(false);
+        if (value === 0x997275b5) { // boolTrue
+            return true;
+        } else if (value === 0xbc799737) { //boolFalse
+            return false;
+        } else {
+            throw new Error(`Invalid boolean code ${value.toString("16")}`);
+        }
+    }
+
+    /**
+     * Reads and converts Unix time (used by Telegram)
+     * into a Javascript {Date} object.
+     * @returns {Date}
+     */
+    tgReadDate() {
+        let value = this.readInt();
+        return new Date(value * 1000);
+    }
+
+    /**
+     * Reads a Telegram object.
+     */
+    tgReadObject() {
+        let constructorId = this.readInt(false);
+        let clazz = tlobjects[constructorId];
+        if (clazz === undefined) {
+            /**
+             * The class was None, but there's still a
+             * chance of it being a manually parsed value like bool!
+             */
+            let value = constructorId;
+            if (value === 0x997275b5) { // boolTrue
+                return true
+            } else if (value === 0xbc799737) {  // boolFalse
+                return false;
+            } else if (value === 0x1cb5c415) {  // Vector
+                let temp = [];
+                for (let i = 0; i < this.readInt(); i++) {
+                    temp.push(this.tgReadObject());
+                }
+                return temp;
+            }
+
+            clazz = coreObjects[constructorId];
+            if (clazz === undefined) {
+                // If there was still no luck, give up
+                this.seek(-4); // Go back
+                let pos = this.tellPosition();
+                let error = TypeNotFoundError(constructorId, this.read());
+                this.setPosition(pos);
+                throw new error;
+            }
+
+        }
+        return clazz.fromReader(this);
+
+    }
+
+    /**
+     * Reads a vector (a list) of Telegram objects.
+     * @returns {[Buffer]}
+     */
+    tgReadVector() {
+        if (this.readInt(false) !== 0x1cb5c415) {
+            throw new Error('Invalid constructor code, vector was expected');
+        }
+        let count = this.readInt();
+        let temp = [];
+        for (let i = 0; i < count; i++) {
+            temp.push(this.tgReadObject());
+        }
+        return temp;
+    }
+
+    // endregion
+
+    /**
+     * Closes the reader.
+     */
+    close() {
+        this.stream = null;
+    }
+
+    // region Position related
+
+    /**
+     * Tells the current position on the stream.
+     * @returns {number}
+     */
+    tellPosition() {
+        return this.offset;
+    }
+
+    /**
+     * Sets the current position on the stream.
+     * @param position
+     */
+    sePosition(position) {
+        this.offset = position;
+    }
+
+    /**
+     * Seeks the stream position given an offset from the current position.
+     * The offset may be negative.
+     * @param offset
+     */
+    seek(offset) {
+        this.offset += offset;
+    }
+
+    //endregion
+
+}
+
+module.exports = BinaryReader;

+ 95 - 0
gramjs/network/MTProtoPlainSender.js

@@ -0,0 +1,95 @@
+/**
+ *  This module contains the class used to communicate with Telegram's servers
+ *  in plain text, when no authorization key has been created yet.
+ */
+const Helpers = require("../utils/Helpers");
+const BigIntBuffer = require('bigint-buffer');
+const MTProtoState = require("./MTProtoState");
+const struct = require("python-struct");
+const BinaryReader = require("../extensions/BinaryReader");
+
+/**
+ * MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)
+ */
+
+class MTProtoPlainSender {
+    /**
+     * Initializes the MTProto plain sender.
+     * @param connection connection: the Connection to be used.
+     * @param loggers
+     */
+    constructor(connection, loggers) {
+        this._state = MTProtoState(connection, loggers);
+        this._connection = connection;
+
+    }
+
+    /**
+     * Sends and receives the result for the given request.
+     * @param request
+     */
+    async send(request) {
+        let body = request.toBuffer();
+        let msgId = this._state._getNewMsgId();
+        await this._connection.send(
+            Buffer.concat([
+                struct.pack('<qqi', 0, msgId, body.length),
+                body
+            ])
+        );
+
+        body = await this._connection.recv();
+        if (body.length < 9) {
+            throw InvalidBufferError(body);
+        }
+        let reader = new BinaryReader(body);
+        let authKeyId = reader.readLong();
+        if (authKeyId !== 0n) {
+            throw new Error("Bad authKeyId");
+        }
+        msgId = reader.readLong();
+        if (msgId === 0n) {
+            throw new Error("Bad msgId");
+        }
+        /** ^ We should make sure that the read ``msg_id`` is greater
+         * than our own ``msg_id``. However, under some circumstances
+         * (bad system clock/working behind proxies) this seems to not
+         * be the case, which would cause endless assertion errors.
+         */
+
+        let length = reader.readInt();
+        if (length <= 0) {
+            throw new Error("Bad length");
+        }
+        /**
+         * We could read length bytes and use those in a new reader to read
+         * the next TLObject without including the padding, but since the
+         * reader isn't used for anything else after this, it's unnecessary.
+         */
+        return reader.tgReadObject();
+
+    }
+
+
+    /**
+     * Generates a new message ID based on the current time (in ms) since epoch
+     * @returns {BigInt}
+     */
+    getNewMsgId() {
+        //See https://core.telegram.org/mtproto/description#message-identifier-msg-id
+        let msTime = Date.now();
+        let newMsgId = ((BigInt(Math.floor(msTime / 1000)) << BigInt(32)) | // "must approximately equal unixtime*2^32"
+            (BigInt(msTime % 1000) << BigInt(32)) | // "approximate moment in time the message was created"
+            BigInt(Helpers.getRandomInt(0, 524288)) << BigInt(2));// "message identifiers are divisible by 4"
+        //Ensure that we always return a message ID which is higher than the previous one
+        if (this._lastMsgId >= newMsgId) {
+            newMsgId = this._lastMsgId + 4n
+        }
+        this._lastMsgId = newMsgId;
+        return BigInt(newMsgId);
+
+    }
+
+}
+
+module.exports = MTProtoPlainSender;

+ 1017 - 0
gramjs/network/MTProtoSender.js

@@ -0,0 +1,1017 @@
+const MtProtoPlainSender = require("./MTProtoPlainSender");
+const Helpers = require("../utils/Helpers");
+const {MsgsAck} = require("../gramjs/tl/types");
+const AES = require("../crypto/AES");
+const {RPCError} = require("../errors");
+const format = require('string-format');
+const {TypeNotFoundError} = require("../errors");
+const {BadMessageError} = require("../errors");
+const {InvalidDCError} = require("../errors");
+const {gzip, ungzip} = require('node-gzip');
+//const {tlobjects} = require("../gramjs/tl/alltlobjects");
+format.extend(String.prototype, {});
+
+/**
+ * MTProto Mobile Protocol sender
+ * (https://core.telegram.org/mtproto/description)
+ * This class is responsible for wrapping requests into `TLMessage`'s,
+ * sending them over the network and receiving them in a safe manner.
+ *
+ * Automatic reconnection due to temporary network issues is a concern
+ * for this class as well, including retry of messages that could not
+ * be sent successfully.
+ *
+ * A new authorization key will be generated on connection if no other
+ * key exists yet.
+ */
+class MTProtoSender {
+
+    /**
+     * @param authKey
+     * @param opt
+     */
+    constructor(authKey, opt = {
+        loggers: null,
+        retries: 5,
+        delay: 1,
+        autoReconnect: true,
+        connectTimeout: null,
+        authKeyCallback: null,
+        updateCallback: null,
+        autoReconnectCallback: null
+    }) {
+        this._connection = null;
+        this._loggers = opt.loggers;
+        this._log = opt.loggers[__name__];
+        this._retries = opt.retries;
+        this._delay = opt.delay;
+        this._autoReconnect = opt.autoReconnect;
+        this._connectTimeout = opt.connectTimeout;
+        this._authKeyCallback = opt.authKeyCallback;
+        this._updateCallback = opt.updateCallback;
+        this._autoReconnectCallback = opt.autoReconnectCallback;
+
+        /**
+         * Whether the user has explicitly connected or disconnected.
+         *
+         * If a disconnection happens for any other reason and it
+         * was *not* user action then the pending messages won't
+         * be cleared but on explicit user disconnection all the
+         * pending futures should be cancelled.
+         */
+        this._user_connected = false;
+        this._reconnecting = false;
+        this._disconnected = true;
+
+        /**
+         * We need to join the loops upon disconnection
+         */
+        this._send_loop_handle = null;
+        this._recv_loop_handle = null;
+
+        /**
+         * Preserving the references of the AuthKey and state is important
+         */
+        this.auth_key = authKey || new AuthKey(null);
+        this._state = new MTProtoState(this.auth_key, this._loggers);
+
+        /**
+         * Outgoing messages are put in a queue and sent in a batch.
+         * Note that here we're also storing their ``_RequestState``.
+         */
+        this._send_queue = new MessagePacker(this._state,
+            this._loggers);
+
+        /**
+         * Sent states are remembered until a response is received.
+         */
+        this._pending_state = {};
+
+        /**
+         * Responses must be acknowledged, and we can also batch these.
+         */
+        this._pending_ack = new Set();
+
+        /**
+         * Similar to pending_messages but only for the last acknowledges.
+         * These can't go in pending_messages because no acknowledge for them
+         * is received, but we may still need to resend their state on bad salts.
+         */
+        this._last_acks = [];
+
+        /**
+         * Jump table from response ID to method that handles it
+         */
+
+        this._handlers = {
+            [RPCResult.CONSTRUCTOR_ID]: this._handle_rpc_result,
+            [MessageContainer.CONSTRUCTOR_ID]: this._handle_container,
+            [GzipPacked.CONSTRUCTOR_ID]: this._handle_gzip_packed,
+            [Pong.CONSTRUCTOR_ID]: this._handle_pong,
+            [BadServerSalt.CONSTRUCTOR_ID]: this._handle_bad_server_salt,
+            [BadMsgNotification.CONSTRUCTOR_ID]: this._handle_bad_notification,
+            [MsgDetailedInfo.CONSTRUCTOR_ID]: this._handle_detailed_info,
+            [MsgNewDetailedInfo.CONSTRUCTOR_ID]: this._handle_new_detailed_info,
+            [NewSessionCreated.CONSTRUCTOR_ID]: this._handle_new_session_created,
+            [MsgsAck.CONSTRUCTOR_ID]: this._handle_ack,
+            [FutureSalts.CONSTRUCTOR_ID]: this._handle_future_salts,
+            [MsgsStateReq.CONSTRUCTOR_ID]: this._handle_state_forgotten,
+            [MsgResendReq.CONSTRUCTOR_ID]: this._handle_state_forgotten,
+            [MsgsAllInfo.CONSTRUCTOR_ID]: this._handle_msg_all,
+        }
+
+
+    }
+
+    // Public API
+
+    /**
+     * Connects to the specified given connection using the given auth key.
+     * @param connection
+     * @returns {Promise<boolean>}
+     */
+    async connect(connection) {
+        if (this._user_connected) {
+            this._log.info('User is already connected!');
+            return false;
+        }
+        this._connection = connection;
+        await this._connect();
+        this._user_connected = true;
+        return true;
+    }
+
+    is_connected() {
+        return this._user_connected
+    }
+
+    /**
+     * Cleanly disconnects the instance from the network, cancels
+     * all pending requests, and closes the send and receive loops.
+     */
+    async disconnect() {
+        await this._disconnect();
+    }
+
+    /**
+     *
+     This method enqueues the given request to be sent. Its send
+     state will be saved until a response arrives, and a ``Future``
+     that will be resolved when the response arrives will be returned:
+
+     .. code-block:: javascript
+
+     async def method():
+     # Sending (enqueued for the send loop)
+     future = sender.send(request)
+     # Receiving (waits for the receive loop to read the result)
+     result = await future
+
+     Designed like this because Telegram may send the response at
+     any point, and it can send other items while one waits for it.
+     Once the response for this future arrives, it is set with the
+     received result, quite similar to how a ``receive()`` call
+     would otherwise work.
+
+     Since the receiving part is "built in" the future, it's
+     impossible to await receive a result that was never sent.
+     * @param request
+     * @returns {RequestState}
+     */
+    send(request) {
+        if (!this._user_connected) {
+            throw new Error('Cannot send requests while disconnected')
+        }
+
+        if (!utils.isArrayLike(request)) {
+            let state = new RequestState(request);
+            this._send_queue.push(state);
+            return state;
+        } else {
+            throw new Error("not supported");
+        }
+    }
+
+    /**
+     * Performs the actual connection, retrying, generating the
+     * authorization key if necessary, and starting the send and
+     * receive loops.
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _connect() {
+        this._log.info('Connecting to {0}...'.replace("{0}", this._connection));
+        await this._connection.connect();
+
+        this._log.debug("Connection success!");
+        if (!this.auth_key) {
+            let plain = new MtProtoPlainSender(this._connection, this._loggers);
+            let res = await authenticator.do_authentication(plain);
+            this.auth_key.key = res.key;
+            this._state.time_offset = res.timeOffset;
+
+            /**
+             * This is *EXTREMELY* important since we don't control
+             * external references to the authorization key, we must
+             * notify whenever we change it. This is crucial when we
+             * switch to different data centers.
+             */
+            if (this._authKeyCallback) {
+                this._authKeyCallback(this.auth_key)
+
+            }
+
+        }
+        this._log.debug('Starting send loop');
+        this._send_loop_handle = this._send_loop();
+
+        this._log.debug('Starting receive loop');
+        this._recv_loop_handle = this._recv_loop();
+
+        // _disconnected only completes after manual disconnection
+        // or errors after which the sender cannot continue such
+        // as failing to reconnect or any unexpected error.
+
+        this._log.info('Connection to %s complete!', this._connection)
+
+    }
+
+
+    async _disconnected(error = null) {
+        if (this._connection === null) {
+            this._log.info('Not disconnecting (already have no connection)');
+            return
+        }
+        this._log.info('Disconnecting from %s...', this._connection);
+        this._user_connected = false;
+        this._log.debug('Closing current connection...');
+        await this._connection.disconnect()
+
+    }
+
+    /**
+     * This loop is responsible for popping items off the send
+     * queue, encrypting them, and sending them over the network.
+     * Besides `connect`, only this method ever sends data.
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _send_loop() {
+        while (this._user_connected && !this._reconnecting) {
+            if (this._pending_ack) {
+                let ack = new RequestState(new MsgsAck(Array(this._pending_ack)));
+                this._send_queue.push(ack);
+                this._last_acks.push(ack);
+                this._pending_ack.clear()
+            }
+            this._log.debug('Waiting for messages to send...');
+            // TODO Wait for the connection send queue to be empty?
+            // This means that while it's not empty we can wait for
+            // more messages to be added to the send queue.
+            let {batch, data} = await this._send_queue.get();
+
+            if (!data) {
+                continue
+            }
+            this._log.debug(`Encrypting ${batch.length} message(s) in ${data.length} bytes for sending`);
+
+            data = this._state.encryptMessageData(data);
+            try {
+                await this._connection.send(data);
+            } catch (e) {
+                console.log(e);
+                this._log.info('Connection closed while sending data');
+                return
+            }
+
+        }
+    }
+
+
+    async _recv_loop() {
+        let body, message;
+
+        while (this._user_connected && !this._reconnecting) {
+            this._log.debug('Receiving items from the network...');
+            try {
+                body = await this._connection.recv();
+            } catch (e) {
+                this._log.info('Connection closed while receiving data');
+                return
+            }
+            try {
+                message = this._state.decryptMessageData(body);
+            } catch (e) {
+                console.log(e);
+
+                if (e instanceof TypeNotFoundError) {
+                    // Received object which we don't know how to deserialize
+                    this._log.info(`Type ${e.invalidConstructorId} not found, remaining data ${e.remaining}`);
+                } else if (e instanceof SecurityError) {
+                    // A step while decoding had the incorrect data. This message
+                    // should not be considered safe and it should be ignored.
+                    this._log.warning(`Security error while unpacking a received message: ${e}`);
+                    continue
+                } else if (e instanceof InvalidBufferError) {
+                    this._log.info('Broken authorization key; resetting');
+                    this.auth_key.key = null;
+                    if (this._authKeyCallback) {
+                        this._authKeyCallback(null)
+                    }
+                    return
+                } else {
+                    this._log.exception('Unhandled error while receiving data');
+                    return
+                }
+            }
+        }
+        try {
+            await this._processMessage(message)
+        } catch (e) {
+            this._log.exception('Unhandled error while receiving data');
+
+        }
+
+    }
+
+    // Response Handlers
+
+    /**
+     * Adds the given message to the list of messages that must be
+     * acknowledged and dispatches control to different ``_handle_*``
+     * method based on its type.
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _processMessage(message) {
+        this._pending_ack.add(message.msgId);
+        let handler = this._handlers.get(message.obj.CONSTRUCTOR_ID, this.handleUpdate);
+        await handler(message);
+    }
+
+    /**
+     * Pops the states known to match the given ID from pending messages.
+     * This method should be used when the response isn't specific.
+     * @param msgId
+     * @returns {Promise<[]>}
+     * @private
+     */
+    async _popStates(msgId) {
+        let state = this._pending_state.pop(msgId, null);
+        if (state) {
+            return [state];
+        }
+
+        let to_pop = [];
+
+        for (state of this._pending_state.values()) {
+            if (state.containerId === msgId) {
+                to_pop.push(state.msgId);
+            }
+        }
+
+        if (to_pop) {
+            let temp = [];
+            for (const x of to_pop) {
+                temp.push(this._pending_state.pop(x));
+            }
+            return temp
+        }
+
+        for (let ack of this._last_acks) {
+            if (ack.msgId === msgId)
+                return [ack]
+        }
+
+        return []
+    }
+
+
+    /**
+     * Handles the result for Remote Procedure Calls:
+     * rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult;
+     * This is where the future results for sent requests are set.
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleRPCResult(message) {
+        let RPCResult = message.obj;
+        let state = this._pending_state.pop(RPCResult.reqMsgId, null);
+        this._log.debug(`Handling RPC result for message ${RPCResult.reqMsgId}`);
+
+        if (!state) {
+            // TODO We should not get responses to things we never sent
+            // However receiving a File() with empty bytes is "common".
+            // See #658, #759 and #958. They seem to happen in a container
+            // which contain the real response right after.
+            try {
+                let reader = new BinaryReader(RPCResult.body);
+                if (!(reader.tgReadObject() instanceof upload.File)) {
+                    throw new TypeNotFoundError("Not an upload.File");
+                }
+            } catch (e) {
+                if (e instanceof TypeNotFoundError) {
+                    this._log.info(`Received response without parent request: ${RPCResult.body}`);
+                    return
+                } else {
+                    throw e;
+                }
+            }
+            if (RPCResult.error) {
+                let error = RPCMessageToError(RPCResult.error, state.request);
+                this._send_queue.append(
+                    new RequestState(new MsgsAck([state.msgId]))
+                );
+            } else {
+                let reader = new BinaryReader(RPCResult.body);
+                let result = state.request.readResult(reader);
+            }
+        }
+
+    }
+
+    /**
+     * Processes the inner messages of a container with many of them:
+     * msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleContainer(message) {
+        this._log.debug('Handling container');
+        for (let inner_message in message.obj.messages) {
+            await this._processMessage(inner_message)
+        }
+    }
+
+    /**
+     * Unpacks the data from a gzipped object and processes it:
+     * gzip_packed#3072cfa1 packed_data:bytes = Object;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleGzipPacked(message) {
+        this._log.debug('Handling gzipped data');
+        let reader = new BinaryReader(message.obj.data);
+        message.obj = reader.tgReadObject();
+        await this._processMessage(message)
+    }
+
+    async _handleUpdate(message) {
+        if (message.obj.SUBCLASS_OF_ID !== 0x8af52aac) {  // crc32(b'Updates')
+            this._log.warning(`Note: %s is not an update, not dispatching it ${message.obj}`);
+            return
+        }
+        this._log.debug('Handling update %s', message.obj.constructor.name);
+        if (this._updateCallback) {
+            this._updateCallback(message.obj)
+        }
+    }
+
+    /**
+     * Handles pong results, which don't come inside a ``RPCResult``
+     * but are still sent through a request:
+     * pong#347773c5 msg_id:long ping_id:long = Pong;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handlePong(message) {
+        let pong = message.obj;
+        this._log.debug(`Handling pong for message ${pong.msgId}`);
+        let state = this._pending_state.pop(pong.msgId, null);
+        // Todo Check result
+        if (state) {
+            state.future.set_result(pong)
+        }
+    }
+
+    /**
+     * Corrects the currently used server salt to use the right value
+     * before enqueuing the rejected message to be re-sent:
+     * bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int
+     * error_code:int new_server_salt:long = BadMsgNotification;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleBadServerSalt(message) {
+        let badSalt = message.obj;
+        this._log.debug(`Handling bad salt for message ${badSalt.badMsgId}`);
+        this._state.salt = badSalt.newServerSalt;
+        let states = this._popStates(badSalt.badMsgId);
+        this._send_queue.extend(states);
+        this._log.debug('%d message(s) will be resent', states.length);
+    }
+
+    /**
+     * Adjusts the current state to be correct based on the
+     * received bad message notification whenever possible:
+     * bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int
+     * error_code:int = BadMsgNotification;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleBadNotification(message) {
+        let badMsg = message.obj;
+        let states = this._popStates(badMsg.badMsgId);
+        this._log.debug(`Handling bad msg ${badMsg}`);
+        if ([16, 17].contains(badMsg.errorCode)) {
+            // Sent msg_id too low or too high (respectively).
+            // Use the current msg_id to determine the right time offset.
+            let to = this._state.updateTimeOffset(message.msgId);
+            this._log.info(`System clock is wrong, set time offset to ${to}s`);
+        } else if (badMsg.errorCode === 32) {
+            // msg_seqno too low, so just pump it up by some "large" amount
+            // TODO A better fix would be to start with a new fresh session ID
+            this._state._sequence += 64
+        } else if (badMsg.errorCode === 33) {
+            // msg_seqno too high never seems to happen but just in case
+            this._state._sequence -= 16
+        } else {
+            for (let state of states) {
+                // TODO set errors;
+                /* state.future.set_exception(
+                BadMessageError(state.request, bad_msg.error_code))*/
+            }
+
+            return
+        }
+        // Messages are to be re-sent once we've corrected the issue
+        this._send_queue.extend(states);
+        this._log.debug(`${states.length} messages will be resent due to bad msg`)
+    }
+
+    /**
+     * Updates the current status with the received detailed information:
+     * msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long
+     * bytes:int status:int = MsgDetailedInfo;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleDetailedInfo(message) {
+        // TODO https://goo.gl/VvpCC6
+        let msgId = message.obj.answerMsgId;
+        this._log.debug(`Handling detailed info for message ${msgId}`);
+        this._pending_ack.add(msgId)
+    }
+
+    /**
+     * Updates the current status with the received detailed information:
+     * msg_new_detailed_info#809db6df answer_msg_id:long
+     * bytes:int status:int = MsgDetailedInfo;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleNewDetailedInfo(message) {
+        // TODO https://goo.gl/VvpCC6
+        let msgId = message.obj.answerMsgId;
+        this._log.debug(`Handling new detailed info for message ${msgId}`);
+        this._pending_ack.add(msgId)
+    }
+
+    /**
+     * Updates the current status with the received session information:
+     * new_session_created#9ec20908 first_msg_id:long unique_id:long
+     * server_salt:long = NewSession;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleNewSessionCreated(message) {
+        // TODO https://goo.gl/LMyN7A
+        this._log.debug('Handling new session created');
+        this._state.salt = message.obj.serverSalt;
+    }
+
+    /**
+     * Handles a server acknowledge about our messages. Normally
+     * these can be ignored except in the case of ``auth.logOut``:
+     *
+     *     auth.logOut#5717da40 = Bool;
+     *
+     * Telegram doesn't seem to send its result so we need to confirm
+     * it manually. No other request is known to have this behaviour.
+
+     * Since the ID of sent messages consisting of a container is
+     * never returned (unless on a bad notification), this method
+     * also removes containers messages when any of their inner
+     * messages are acknowledged.
+
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleAck(message) {
+        let ack = message.obj;
+        this._log.debug(`Handling acknowledge for ${ack.msgIds}`);
+        for (let msgId of ack.msgIds) {
+            let state = this._pending_state[msgId];
+            if (state && state.request instanceof LogOutRequest) {
+                delete this._pending_state[msgId];
+                state.future.set_result(true)
+            }
+        }
+
+    }
+
+    /**
+     * Handles future salt results, which don't come inside a
+     * ``rpc_result`` but are still sent through a request:
+     *     future_salts#ae500895 req_msg_id:long now:int
+     *     salts:vector<future_salt> = FutureSalts;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleFutureSalts(message) {
+        // TODO save these salts and automatically adjust to the
+        // correct one whenever the salt in use expires.
+        this._log.debug(`Handling future salts for message ${message.msgId}`);
+        let state = self._pending_state.pop(message.msgId, null);
+        if (state) {
+            state.future.set_result(message.obj)
+        }
+    }
+
+    /**
+     * Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by
+     * enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleStateForgotten(message) {
+        self._send_queue.append(new RequestState(new MsgsStateInfo(
+            message.msgId, String.fromCharCode(1).repeat(message.obj.msgIds))))
+    }
+
+    /**
+     * Handles :tl:`MsgsAllInfo` by doing nothing (yet).
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleMsgAll(message){
+
+    }
+
+    /**
+     * Adds an update handler (a method with one argument, the received
+     * TLObject) that is fired when there are updates available
+     * @param handler {function}
+     */
+    addUpdateHandler(handler) {
+        let firstHandler = Boolean(this.onUpdateHandlers.length);
+        this.onUpdateHandlers.push(handler);
+        // If this is the first added handler,
+        // we must start receiving updates
+        if (firstHandler) {
+            this.setListenForUpdates(true);
+        }
+    }
+
+    /**
+     * Removes an update handler (a method with one argument, the received
+     * TLObject) that is fired when there are updates available
+     * @param handler {function}
+     */
+    removeUpdateHandler(handler) {
+        let index = this.onUpdateHandlers.indexOf(handler);
+        if (index !== -1) this.onUpdateHandlers.splice(index, 1);
+        if (!Boolean(this.onUpdateHandlers.length)) {
+            this.setListenForUpdates(false);
+
+        }
+    }
+
+    /**
+     *
+     * @param confirmed {boolean}
+     * @returns {number}
+     */
+    generateSequence(confirmed) {
+        if (confirmed) {
+            let result = this.session.sequence * 2 + 1;
+            this.session.sequence += 1;
+            return result;
+        } else {
+            return this.session.sequence * 2;
+        }
+    }
+
+    /**
+     * Sends the specified MTProtoRequest, previously sending any message
+     * which needed confirmation. This also pauses the updates thread
+     * @param request {MTProtoRequest}
+     * @param resend
+     */
+    async send(request, resend = false) {
+        let buffer;
+        //If any message needs confirmation send an AckRequest first
+        if (this.needConfirmation.length) {
+            let msgsAck = new MsgsAck(
+                {
+                    msgIds:
+                    this.needConfirmation
+                });
+
+            buffer = msgsAck.onSend();
+            await this.sendPacket(buffer, msgsAck);
+            this.needConfirmation.length = 0;
+        }
+        //Finally send our packed request
+
+        buffer = request.onSend();
+        await this.sendPacket(buffer, request);
+
+        //And update the saved session
+        this.session.save();
+
+    }
+
+    /**
+     *
+     * @param request
+     */
+    async receive(request) {
+        try {
+            //Try until we get an update
+            while (!request.confirmReceived) {
+                let {seq, body} = await this.transport.receive();
+                let {message, remoteMsgId, remoteSequence} = this.decodeMsg(body);
+                console.log("processing msg");
+                await this.processMsg(remoteMsgId, remoteSequence, message, 0, request);
+                console.log("finished processing msg");
+            }
+        } finally {
+            // Todo
+        }
+    }
+
+    // region Low level processing
+    /**
+     * Sends the given packet bytes with the additional
+     * information of the original request.
+     * @param packet
+     * @param request
+     */
+    async sendPacket(packet, request) {
+        request.msgId = this.session.getNewMsgId();
+        // First Calculate plainText to encrypt it
+        let first = Buffer.alloc(8);
+        let second = Buffer.alloc(8);
+        let third = Buffer.alloc(8);
+        let forth = Buffer.alloc(4);
+        let fifth = Buffer.alloc(4);
+        first.writeBigUInt64LE(this.session.salt, 0);
+        second.writeBigUInt64LE(this.session.id, 0);
+        third.writeBigUInt64LE(request.msgId, 0);
+        forth.writeInt32LE(this.generateSequence(request.confirmed), 0);
+        fifth.writeInt32LE(packet.length, 0);
+        let plain = Buffer.concat([
+            first,
+            second,
+            third,
+            forth,
+            fifth,
+            packet
+        ]);
+        let msgKey = Helpers.calcMsgKey(plain);
+        let {key, iv} = Helpers.calcKey(this.session.authKey.key, msgKey, true);
+
+        let cipherText = AES.encryptIge(plain, key, iv);
+
+        //And then finally send the encrypted packet
+
+        first = Buffer.alloc(8);
+        first.writeBigUInt64LE(this.session.authKey.keyId, 0);
+        let cipher = Buffer.concat([
+            first,
+            msgKey,
+            cipherText,
+        ]);
+        await this.transport.send(cipher);
+    }
+
+    /**
+     *
+     * @param body {Buffer}
+     * @returns {{remoteMsgId: number, remoteSequence: BigInt, message: Buffer}}
+     */
+    decodeMsg(body) {
+        if (body.length < 8) {
+            throw Error("Can't decode packet");
+        }
+        let remoteAuthKeyId = body.readBigInt64LE(0);
+        let offset = 8;
+        let msgKey = body.slice(offset, offset + 16);
+        offset += 16;
+        let {key, iv} = Helpers.calcKey(this.session.authKey.key, msgKey, false);
+        let plainText = AES.decryptIge(body.slice(offset, body.length), key, iv);
+        offset = 0;
+        let remoteSalt = plainText.readBigInt64LE(offset);
+        offset += 8;
+        let remoteSessionId = plainText.readBigInt64LE(offset);
+        offset += 8;
+        let remoteMsgId = plainText.readBigInt64LE(offset);
+        offset += 8;
+        let remoteSequence = plainText.readInt32LE(offset);
+        offset += 4;
+        let msgLen = plainText.readInt32LE(offset);
+        offset += 4;
+        let message = plainText.slice(offset, offset + msgLen);
+        return {message, remoteMsgId, remoteSequence}
+    }
+
+    async processMsg(msgId, sequence, reader, offset, request = undefined) {
+        this.needConfirmation.push(msgId);
+        let code = reader.readUInt32LE(offset);
+        console.log("code is ", code);
+        // The following codes are "parsed manually"
+        if (code === 0xf35c6d01) {  //rpc_result, (response of an RPC call, i.e., we sent a request)
+            console.log("got rpc result");
+            return await this.handleRpcResult(msgId, sequence, reader, offset, request);
+        }
+
+        if (code === 0x73f1f8dc) {  //msg_container
+            return this.handleContainer(msgId, sequence, reader, offset, request);
+        }
+        if (code === 0x3072cfa1) {  //gzip_packed
+            return this.handleGzipPacked(msgId, sequence, reader, offset, request);
+        }
+        if (code === 0xedab447b) {  //bad_server_salt
+            return await this.handleBadServerSalt(msgId, sequence, reader, offset, request);
+        }
+        if (code === 0xa7eff811) {  //bad_msg_notification
+            console.log("bad msg notification");
+            return this.handleBadMsgNotification(msgId, sequence, reader, offset);
+        }
+        /**
+         * If the code is not parsed manually, then it was parsed by the code generator!
+         * In this case, we will simply treat the incoming TLObject as an Update,
+         * if we can first find a matching TLObject
+         */
+        console.log("code", code);
+        if (code === 0x9ec20908) {
+            return this.handleUpdate(msgId, sequence, reader, offset);
+        } else {
+
+            if (tlobjects.contains(code)) {
+                return this.handleUpdate(msgId, sequence, reader);
+            }
+        }
+        console.log("Unknown message");
+        return false;
+    }
+
+    // region Message handling
+
+    handleUpdate(msgId, sequence, reader, offset = 0) {
+        let tlobject = Helpers.tgReadObject(reader, offset);
+        for (let handler of this.onUpdateHandlers) {
+            handler(tlobject);
+        }
+        return Float32Array
+    }
+
+    async handleContainer(msgId, sequence, reader, offset, request) {
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let size = reader.readInt32LE(offset);
+        offset += 4;
+        for (let i = 0; i < size; i++) {
+            let innerMsgId = reader.readBigUInt64LE(offset);
+            offset += 8;
+            let innerSequence = reader.readInt32LE(offset);
+            offset += 4;
+            let innerLength = reader.readInt32LE(offset);
+            offset += 4;
+            if (!(await this.processMsg(innerMsgId, sequence, reader, offset, request))) {
+                offset += innerLength;
+            }
+        }
+        return false;
+    }
+
+    async handleBadServerSalt(msgId, sequence, reader, offset, request) {
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let badMsgId = reader.readBigUInt64LE(offset);
+        offset += 8;
+        let badMsgSeqNo = reader.readInt32LE(offset);
+        offset += 4;
+        let errorCode = reader.readInt32LE(offset);
+        offset += 4;
+        let newSalt = reader.readBigUInt64LE(offset);
+        offset += 8;
+        this.session.salt = newSalt;
+
+        if (!request) {
+            throw Error("Tried to handle a bad server salt with no request specified");
+        }
+
+        //Resend
+        await this.send(request, true);
+        return true;
+    }
+
+    handleBadMsgNotification(msgId, sequence, reader, offset) {
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let requestId = reader.readUInt32LE(offset);
+        offset += 4;
+        let requestSequence = reader.readInt32LE(offset);
+        offset += 4;
+        let errorCode = reader.readInt32LE(offset);
+        return new BadMessageError(errorCode);
+    }
+
+    async handleRpcResult(msgId, sequence, reader, offset, request) {
+        if (!request) {
+            throw Error("RPC results should only happen after a request was sent");
+        }
+        let buffer = Buffer.alloc(0);
+        let code = reader.readUInt32LE(offset);
+        offset += 4;
+        let requestId = reader.readUInt32LE(offset);
+        offset += 4;
+        let innerCode = reader.readUInt32LE(offset);
+        offset += 4;
+        if (requestId === request.msgId) {
+            request.confirmReceived = true;
+        }
+
+        if (innerCode === 0x2144ca19) {  // RPC Error
+            console.log("Got an error");
+            let errorCode = reader.readInt32LE(offset);
+            offset += 4;
+            let errorMessage = Helpers.tgReadString(reader, offset);
+            offset = errorMessage.offset;
+            errorMessage = errorMessage.data;
+            let error = new RPCError(errorCode, errorMessage);
+            if (error.mustResend) {
+                request.confirmReceived = false;
+            }
+            if (error.message.startsWith("FLOOD_WAIT_")) {
+                console.log("Should wait {}s. Sleeping until then.".format(error.additionalData));
+                await Helpers.sleep();
+            } else if (error.message.startsWith("PHONE_MIGRATE_")) {
+                throw new InvalidDCError(error.additionalData);
+            } else {
+                throw error;
+            }
+
+        } else {
+            console.log("no errors");
+            if (innerCode === 0x3072cfa1) { //GZip packed
+                console.log("Gzipped data");
+                let res = Helpers.tgReadByte(reader, offset);
+                let unpackedData = await ungzip(res.data);
+                offset = res.offset;
+                res = request.onResponse(unpackedData, offset);
+                buffer = res.data;
+                offset = res.offset;
+            } else {
+                console.log("plain data");
+                offset -= 4;
+                let res = request.onResponse(reader, offset);
+                buffer = res.data;
+                offset = res.offset;
+            }
+        }
+        return {buffer, offset}
+
+    }
+
+    handleGzipPacked(msgId, sequence, reader, offset, request) {
+        throw Error("not implemented");
+        // TODO
+    }
+
+    setListenForUpdates(enabled) {
+
+        if (enabled) {
+            console.log("Enabled updates");
+        } else {
+            console.log("Disabled updates");
+        }
+    }
+
+    updatesListenMethod() {
+        while (true) {
+            let {seq, body} = this.transport.receive();
+            let {message, remoteMsgId, remoteSequence} = this.decodeMsg(body);
+            this.processMsg(remoteMsgId, remoteSequence, message);
+
+        }
+    }
+}
+
+module.exports = MTProtoSender;

+ 263 - 0
gramjs/network/MTProtoState.js

@@ -0,0 +1,263 @@
+const struct = require("python-struct");
+const Helpers = require("../utils/Helpers");
+const AES = require("../crypto/AES");
+const BinaryReader = require("../extensions/BinaryReader");
+
+class MTProtoState {
+    /**
+     *
+     `telethon.network.mtprotosender.MTProtoSender` needs to hold a state
+     in order to be able to encrypt and decrypt incoming/outgoing messages,
+     as well as generating the message IDs. Instances of this class hold
+     together all the required information.
+
+     It doesn't make sense to use `telethon.sessions.abstract.Session` for
+     the sender because the sender should *not* be concerned about storing
+     this information to disk, as one may create as many senders as they
+     desire to any other data center, or some CDN. Using the same session
+     for all these is not a good idea as each need their own authkey, and
+     the concept of "copying" sessions with the unnecessary entities or
+     updates state for these connections doesn't make sense.
+
+     While it would be possible to have a `MTProtoPlainState` that does no
+     encryption so that it was usable through the `MTProtoLayer` and thus
+     avoid the need for a `MTProtoPlainSender`, the `MTProtoLayer` is more
+     focused to efficiency and this state is also more advanced (since it
+     supports gzipping and invoking after other message IDs). There are too
+     many methods that would be needed to make it convenient to use for the
+     authentication process, at which point the `MTProtoPlainSender` is better
+     * @param authKey
+     * @param loggers
+     */
+    constructor(authKey, loggers) {
+        this.authKey = authKey;
+        this._log = loggers;
+        this.timeOffset = 0;
+        this.salt = 0;
+
+        this.id = this._sequence = this._lastMsgId = null;
+        this.reset();
+    }
+
+    /**
+     * Resets the state
+     */
+    reset() {
+        // Session IDs can be random on every connection
+        this.id = Helpers.generateRandomLong(true);
+        this._sequence = 0;
+        this._lastMsgId = 0;
+    }
+
+    /**
+     * Updates the message ID to a new one,
+     * used when the time offset changed.
+     * @param message
+     */
+    updateMessageId(message) {
+        message.msgId = this._getNewMsgId();
+    }
+
+    /**
+     * Calculate the key based on Telegram guidelines, specifying whether it's the client or not
+     * @param authKey
+     * @param msgKey
+     * @param client
+     * @returns {{iv: Buffer, key: Buffer}}
+     */
+    _calcKey(authKey, msgKey, client) {
+        let x = client === true ? 0 : 8;
+        let sha256a = Helpers.sha256(Buffer.concat([
+            msgKey,
+            authKey.slice(x, (x + 36))
+        ]));
+        let sha256b = Helpers.sha256(Buffer.concat([
+            authKey.slice(x + 40, x + 76),
+            msgKey,
+        ]));
+
+        let key = Buffer.concat([
+            sha256a.slice(0, 8),
+            sha256b.slice(8, 24),
+            sha256a.slice(24, 32)]);
+        let iv = Buffer.concat([
+            sha256b.slice(0, 8),
+            sha256a.slice(8, 24),
+            sha256b.slice(24, 32)]);
+        return {key, iv}
+
+    }
+
+    /**
+     * Writes a message containing the given data into buffer.
+     * Returns the message id.
+     * @param buffer
+     * @param data
+     * @param contentRelated
+     * @param opt
+     */
+    writeDataAsMessage(buffer, data, contentRelated, opt = {}) {
+        let msgId = this._getNewMsgId();
+        let seqNo = this._getSeqNo(contentRelated);
+        let body;
+        if (!opt) {
+            body = GzipPacked.gzipIfSmaller(contentRelated, data);
+        } else {
+            body = GzipPacked.gzipIfSmaller(contentRelated,
+                new InvokeAfterMsgRequest(opt.afterId, data).toBuffer()
+            );
+        }
+
+        buffer = Buffer.from([
+            buffer,
+            struct.pack('<qii', msgId, seqNo, body.length),
+            body,
+        ]);
+        return {msgId, buffer}
+    }
+
+    /**
+     * Encrypts the given message data using the current authorization key
+     * following MTProto 2.0 guidelines core.telegram.org/mtproto/description.
+     * @param data
+     */
+    encryptMessageData(data) {
+        data = Buffer.concat([
+            struct.pack('<qq', this.salt, this.id),
+            data,
+        ]);
+        let padding = Helpers.generateRandomBytes(-(data.length + 12) % 16 + 12);
+        // Being substr(what, offset, length); x = 0 for client
+        // "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
+        let msgKeyLarge = Helpers.sha256(
+            Buffer.concat(([
+                this.authKey.key.slice(88, 88 + 32),
+                data,
+                padding
+            ]))
+        );
+        // "msg_key = substr (msg_key_large, 8, 16)"
+        let msgKey = msgKeyLarge.slice(8, 24);
+        let {iv, key} = this._calcKey(this.authKey.key, msgKey, true);
+        let keyId = struct.pack('<Q', this.authKey.keyId);
+        return Buffer.concat([
+            keyId,
+            msgKey,
+            AES.encryptIge(Buffer.concat([
+                    data,
+                    padding
+                ]),
+                key,
+                iv)
+        ]);
+
+    }
+
+    /**
+     * Inverse of `encrypt_message_data` for incoming server messages.
+     * @param body
+     */
+    decryptMessageData(body) {
+        if (body.length < 8) {
+            throw InvalidBufferError(body);
+        }
+
+        // TODO Check salt,sessionId, and sequenceNumber
+        let keyId = struct.unpack('<Q', body.slice(0, 8))[0];
+        if (keyId !== this.authKey.keyId) {
+            throw new SecurityError('Server replied with an invalid auth key');
+        }
+
+        let msgKey = body.slice(8, 24);
+        let {iv, key} = this._calcKey(this.authKey.key, msgKey, false);
+        body = AES.decryptIge(body.slice(24), key, iv);
+
+        // https://core.telegram.org/mtproto/security_guidelines
+        // Sections "checking sha256 hash" and "message length"
+
+        let ourKey = Helpers.sha256(Buffer.concat([
+            this.authKey.key.slice(96, 96 + 32),
+            body
+        ]));
+        if (msgKey !== ourKey.slice(8, 24)) {
+            throw new SecurityError(
+                "Received msg_key doesn't match with expected one")
+        }
+
+        let reader = new BinaryReader(body);
+        reader.readLong(); // removeSalt
+        if (reader.readLong() !== this.id) {
+            throw new SecurityError('Server replied with a wrong session ID');
+        }
+
+        let remoteMsgId = reader.readLong();
+        let remoteSequence = reader.readInt();
+        reader.readInt(); // msgLen for the inner object, padding ignored
+
+        // We could read msg_len bytes and use those in a new reader to read
+        // the next TLObject without including the padding, but since the
+        // reader isn't used for anything else after this, it's unnecessary.
+        let obj = reader.tgReadObject();
+
+        return new TLMessage(remoteMsgId, remoteSequence, obj);
+
+    }
+
+    /**
+     * Generates a new unique message ID based on the current
+     * time (in ms) since epoch, applying a known time offset.
+     * @private
+     */
+    _getNewMsgId() {
+        let now = new Date().getTime() / 1000 + this.timeOffset;
+        let nanoseconds = Math.floor((now - Math.floor(now)) * 1e+9);
+        let newMsgId = (Math.floor(now) << 32n) | (nanoseconds << 2);
+        if (this._lastMsgId >= newMsgId) {
+            newMsgId = this._lastMsgId + 4;
+        }
+        this._lastMsgId = newMsgId;
+        return newMsgId;
+    }
+
+    /**
+     * Updates the time offset to the correct
+     * one given a known valid message ID.
+     * @param correctMsgId
+     */
+    updateTimeOffset(correctMsgId) {
+        let bad = this._getNewMsgId();
+        let old = this.timeOffset;
+        let now = Math.floor(new Date().getTime() / 1000);
+        let correct = correctMsgId >> 32n;
+        this.timeOffset = correct - now;
+
+        if (this.timeOffset !== old) {
+            this._lastMsgId = 0;
+            this._log.debug(
+                `Updated time offset (old offset ${old}, bad ${bad}, good ${correctMsgId}, new ${this.timeOffset})`,
+            )
+        }
+
+        return this.timeOffset;
+    }
+
+    /**
+     * Generates the next sequence number depending on whether
+     * it should be for a content-related query or not.
+     * @param contentRelated
+     * @private
+     */
+    _getSeqNo(contentRelated) {
+        if (contentRelated) {
+            let result = this._sequence * 2 + 1;
+            this._sequence += 1;
+            return result;
+        } else {
+            return this._sequence * 2;
+        }
+    }
+
+}
+
+
+module.exports = MTProtoState;

+ 12 - 0
gramjs/network/RequestState.js

@@ -0,0 +1,12 @@
+class RequestState {
+
+
+    constructor(request, after = null) {
+        this.containerId = null;
+        this.msgId = null;
+        this.request = request;
+        this.data = request.toBuffer();
+        this.after = after
+
+    }
+}

+ 2 - 2
network/TcpClient.js → gramjs/network/TCPClient.js

@@ -1,7 +1,7 @@
 const Socket = require("net").Socket;
 const Socket = require("net").Socket;
 const sleep = require("../utils/Helpers").sleep;
 const sleep = require("../utils/Helpers").sleep;
 
 
-class TcpClient {
+class TCPClient {
     constructor() {
     constructor() {
         this.connected = false;
         this.connected = false;
         this.socket = new Socket();
         this.socket = new Socket();
@@ -71,4 +71,4 @@ class TcpClient {
 
 
 }
 }
 
 
-module.exports = TcpClient;
+module.exports = TCPClient;

+ 3 - 3
network/TcpTransport.js → gramjs/network/TCPTransport.js

@@ -1,7 +1,7 @@
-const TcpClient = require("./TcpClient");
+const TcpClient = require("./TCPClient");
 const crc = require('crc');
 const crc = require('crc');
 
 
-class TcpTransport {
+class TCPTransport {
     constructor(ipAddress, port) {
     constructor(ipAddress, port) {
         this.tcpClient = new TcpClient();
         this.tcpClient = new TcpClient();
         this.sendCounter = 0;
         this.sendCounter = 0;
@@ -85,4 +85,4 @@ class TcpTransport {
 
 
 }
 }
 
 
-module.exports = TcpTransport;
+module.exports = TCPTransport;

+ 6 - 6
network/Authenticator.js → gramjs/network/connection/Authenticator.js

@@ -1,9 +1,9 @@
-const AES = require("../crypto/AES");
-const AuthKey = require("../crypto/AuthKey");
-const Factorizator = require("../crypto/Factorizator");
-const RSA = require("../crypto/RSA");
-const MtProtoPlainSender = require("./MTProtoPlainSender");
-const Helpers = require("../utils/Helpers");
+const AES = require("../../crypto/AES");
+const AuthKey = require("../../crypto/AuthKey");
+const Factorizator = require("../../crypto/Factorizator");
+const RSA = require("../../crypto/RSA");
+const MtProtoPlainSender = require("../MTProtoPlainSender");
+const Helpers = require("../../utils/Helpers");
 const BigIntBuffer = require("bigint-buffer");
 const BigIntBuffer = require("bigint-buffer");
 
 
 /**
 /**

+ 0 - 0
tl/MTProtoRequest.js → gramjs/tl/MTProtoRequest.js


+ 0 - 0
tl/Session.js → gramjs/tl/Session.js


+ 2 - 2
tl/TelegramClient.js → gramjs/tl/TelegramClient.js

@@ -1,8 +1,8 @@
 const Session = require("./Session");
 const Session = require("./Session");
-const doAuthentication = require("../network/authenticator");
+const doAuthentication = require("../network/connection/Authenticator");
 const MtProtoSender = require("../network/mtprotoSender");
 const MtProtoSender = require("../network/mtprotoSender");
 const MTProtoRequest = require("../tl/MTProtoRequest");
 const MTProtoRequest = require("../tl/MTProtoRequest");
-const TcpTransport = require("../network/tcpTransport");
+const TcpTransport = require("../network/TCPTransport");
 
 
 const {InvokeWithLayerRequest, InitConnectionRequest} = require("../gramjs/tl/functions/index");
 const {InvokeWithLayerRequest, InitConnectionRequest} = require("../gramjs/tl/functions/index");
 const {GetConfigRequest} = require("../gramjs/tl/functions/help");
 const {GetConfigRequest} = require("../gramjs/tl/functions/help");

+ 1 - 0
gramjs/tl/index.js

@@ -0,0 +1 @@
+module.exports = require("./tlobject");

+ 92 - 0
gramjs/tl/tlobject.js

@@ -0,0 +1,92 @@
+const struct = require("python-struct");
+
+class TLObject {
+    CONSTRUCTOR_ID = null;
+    SUBCLASS_OF_ID = null;
+
+    static prettyFormat() {
+        // TODO
+    }
+
+    /**
+     * Write bytes by using Telegram guidelines
+     * @param data {Buffer|string}
+     */
+    static serializeBytes(data) {
+        if (!(data instanceof Buffer)) {
+            if (typeof data == "string") {
+                data = Buffer.from(data);
+            } else {
+                throw Error(`Bytes or str expected, not ${data.constructor.name}`);
+            }
+        }
+        let r = [];
+        let padding = 0;
+        if (data.length < 252) {
+            padding = (data.length + 1) % 4;
+            if (padding !== 0) {
+                padding = 4 - padding;
+            }
+            r.push(Buffer.from([data.length]));
+            r.push(data);
+        } else {
+            padding = data.length % 4;
+            if (padding !== 0) {
+                padding = 4 - padding;
+            }
+            r.push(Buffer.from([
+                254,
+                data.length % 256,
+                (data.length >> 8) % 256,
+                (data.length >> 16) % 256,
+
+            ]));
+            r.push(data);
+        }
+        r.push(Buffer.alloc(padding).fill(0));
+        return Buffer.concat(r);
+    }
+
+    static serializeDate(dt) {
+        if (!dt) {
+            return Buffer.alloc(4).fill(0);
+        }
+        if (dt instanceof Date) {
+            dt = Math.floor((Date.now() - dt.getTime()) / 1000);
+            console.log(dt);
+        }
+        if (typeof dt == "number") {
+            return struct.pack('<i', dt)
+        }
+        throw Error(`Cannot interpret "${dt}" as a date`);
+
+    }
+
+    fromReader(reader) {
+        throw Error("not implemented");
+    }
+
+}
+
+/**
+ * Represents a content-related `TLObject` (a request that can be sent).
+ */
+class TLRequest extends TLObject {
+    /**
+     *
+     * @param reader {BinaryReader}
+     * @returns {boolean}
+     */
+    static read_result(reader) {
+        return reader.tgReadObject();
+    }
+
+    async resolve(self, client, utils) {
+
+    }
+}
+
+module.exports = {
+    TLObject,
+    TLRequest
+};

+ 16 - 1
utils/Helpers.js → gramjs/utils/Helpers.js

@@ -135,6 +135,19 @@ class Helpers {
 
 
     }
     }
 
 
+    /**
+     * Calculates the SHA256 digest for the given data
+     * @param data
+     * @returns {Buffer}
+     */
+    static sha256(data) {
+        let shaSum = crypto.createHash('sha256');
+        shaSum.update(data);
+        return shaSum.digest();
+
+    }
+
+
     /**
     /**
      * Reads a Telegram-encoded string
      * Reads a Telegram-encoded string
      * @param buffer {Buffer}
      * @param buffer {Buffer}
@@ -284,4 +297,6 @@ class Helpers {
 
 
 }
 }
 
 
-module.exports = Helpers;
+module.exports = Helpers;
+
+console.log(Helpers.sha256("ok"));

+ 0 - 71
network/MTProtoPlainSender.js

@@ -1,71 +0,0 @@
-const Helpers = require("../utils/Helpers");
-const BigIntBuffer = require('bigint-buffer');
-
-/**
- * MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)
- */
-class MTProtoPlainSender {
-    constructor(transport) {
-        this._sequence = 0;
-        this._timeOffset = 0;
-        this._lastMsgId = 0;
-        this._transport = transport;
-    }
-
-    /**
-     * Sends a plain packet (auth_key_id = 0) containing the given message body (data)
-     * @param data
-     */
-    async send(data) {
-        let packet = Buffer.alloc(20);
-        let offset = 0;
-        packet.writeBigInt64LE(0n, offset);
-        offset += 8;
-        packet.writeBigInt64LE(this.getNewMsgId(), offset);
-        offset += 8;
-        packet.writeInt32LE(data.length, offset);
-        await this._transport.send(Buffer.concat([
-            packet,
-            data,
-        ]));
-    }
-
-    /**
-     * Receives a plain packet, returning the body of the response
-     * @returns {number}
-     */
-    async receive() {
-        let {seq, body} = await this._transport.receive();
-        let offset = 0;
-        let authKeyId = body.readBigInt64LE(0);
-        offset += 8;
-        let msgId = body.readBigInt64LE(offset);
-        offset += 8;
-        let messageLength = body.readInt32LE(offset);
-        offset += 4;
-        return body.slice(offset);
-
-    }
-
-    /**
-     * Generates a new message ID based on the current time (in ms) since epoch
-     * @returns {BigInt}
-     */
-    getNewMsgId() {
-        //See https://core.telegram.org/mtproto/description#message-identifier-msg-id
-        let msTime = Date.now();
-        let newMsgId = ((BigInt(Math.floor(msTime / 1000)) << BigInt(32)) | // "must approximately equal unixtime*2^32"
-            (BigInt(msTime % 1000) << BigInt(32)) | // "approximate moment in time the message was created"
-            BigInt(Helpers.getRandomInt(0, 524288)) << BigInt(2));// "message identifiers are divisible by 4"
-        //Ensure that we always return a message ID which is higher than the previous one
-        if (this._lastMsgId >= newMsgId) {
-            newMsgId = this._lastMsgId + 4n
-        }
-        this._lastMsgId = newMsgId;
-        return BigInt(newMsgId);
-
-    }
-
-}
-
-module.exports = MTProtoPlainSender;

+ 0 - 390
network/MTProtoSender.js

@@ -1,390 +0,0 @@
-const MtProtoPlainSender = require("./MTProtoPlainSender");
-const Helpers = require("../utils/Helpers");
-const {MsgsAck} = require("../gramjs/tl/types");
-const AES = require("../crypto/AES");
-const {RPCError} = require("../errors");
-const format = require('string-format');
-const {BadMessageError} = require("../errors");
-const {InvalidDCError} = require("../errors");
-const {gzip, ungzip} = require('node-gzip');
-//const {tlobjects} = require("../gramjs/tl/alltlobjects");
-format.extend(String.prototype, {});
-
-/**
- * MTProto Mobile Protocol sender (https://core.telegram.org/mtproto/description)
- */
-class MTProtoSender {
-    /**
-     *
-     * @param transport
-     * @param session
-     */
-    constructor(transport, session) {
-        this.transport = transport;
-        this.session = session;
-        this.needConfirmation = Array(); // Message IDs that need confirmation
-        this.onUpdateHandlers = Array();
-
-    }
-
-    /**
-     * Disconnects
-     */
-    disconnect() {
-        this.setListenForUpdates(false);
-        this.transport.close();
-    }
-
-    /**
-     * Adds an update handler (a method with one argument, the received
-     * TLObject) that is fired when there are updates available
-     * @param handler {function}
-     */
-    addUpdateHandler(handler) {
-        let firstHandler = Boolean(this.onUpdateHandlers.length);
-        this.onUpdateHandlers.push(handler);
-        // If this is the first added handler,
-        // we must start receiving updates
-        if (firstHandler) {
-            this.setListenForUpdates(true);
-        }
-    }
-
-    /**
-     * Removes an update handler (a method with one argument, the received
-     * TLObject) that is fired when there are updates available
-     * @param handler {function}
-     */
-    removeUpdateHandler(handler) {
-        let index = this.onUpdateHandlers.indexOf(handler);
-        if (index !== -1) this.onUpdateHandlers.splice(index, 1);
-        if (!Boolean(this.onUpdateHandlers.length)) {
-            this.setListenForUpdates(false);
-
-        }
-    }
-
-    /**
-     *
-     * @param confirmed {boolean}
-     * @returns {number}
-     */
-    generateSequence(confirmed) {
-        if (confirmed) {
-            let result = this.session.sequence * 2 + 1;
-            this.session.sequence += 1;
-            return result;
-        } else {
-            return this.session.sequence * 2;
-        }
-    }
-
-    /**
-     * Sends the specified MTProtoRequest, previously sending any message
-     * which needed confirmation. This also pauses the updates thread
-     * @param request {MTProtoRequest}
-     * @param resend
-     */
-    async send(request, resend = false) {
-        let buffer;
-        //If any message needs confirmation send an AckRequest first
-        if (this.needConfirmation.length) {
-            let msgsAck = new MsgsAck(
-                {
-                    msgIds:
-                    this.needConfirmation
-                });
-
-            buffer = msgsAck.onSend();
-            await this.sendPacket(buffer, msgsAck);
-            this.needConfirmation.length = 0;
-        }
-        //Finally send our packed request
-
-        buffer = request.onSend();
-        await this.sendPacket(buffer, request);
-
-        //And update the saved session
-        this.session.save();
-
-    }
-
-    /**
-     *
-     * @param request
-     */
-    async receive(request) {
-        try {
-            //Try until we get an update
-            while (!request.confirmReceived) {
-                let {seq, body} = await this.transport.receive();
-                let {message, remoteMsgId, remoteSequence} = this.decodeMsg(body);
-                console.log("processing msg");
-                await this.processMsg(remoteMsgId, remoteSequence, message, 0, request);
-                console.log("finished processing msg");
-            }
-        } finally {
-            // Todo
-        }
-    }
-
-    // region Low level processing
-    /**
-     * Sends the given packet bytes with the additional
-     * information of the original request.
-     * @param packet
-     * @param request
-     */
-    async sendPacket(packet, request) {
-        request.msgId = this.session.getNewMsgId();
-        // First Calculate plainText to encrypt it
-        let first = Buffer.alloc(8);
-        let second = Buffer.alloc(8);
-        let third = Buffer.alloc(8);
-        let forth = Buffer.alloc(4);
-        let fifth = Buffer.alloc(4);
-        first.writeBigUInt64LE(this.session.salt, 0);
-        second.writeBigUInt64LE(this.session.id, 0);
-        third.writeBigUInt64LE(request.msgId, 0);
-        forth.writeInt32LE(this.generateSequence(request.confirmed), 0);
-        fifth.writeInt32LE(packet.length, 0);
-        let plain = Buffer.concat([
-            first,
-            second,
-            third,
-            forth,
-            fifth,
-            packet
-        ]);
-        let msgKey = Helpers.calcMsgKey(plain);
-        let {key, iv} = Helpers.calcKey(this.session.authKey.key, msgKey, true);
-
-        let cipherText = AES.encryptIge(plain, key, iv);
-
-        //And then finally send the encrypted packet
-
-        first = Buffer.alloc(8);
-        first.writeBigUInt64LE(this.session.authKey.keyId, 0);
-        let cipher = Buffer.concat([
-            first,
-            msgKey,
-            cipherText,
-        ]);
-        await this.transport.send(cipher);
-    }
-
-    /**
-     *
-     * @param body {Buffer}
-     * @returns {{remoteMsgId: number, remoteSequence: BigInt, message: Buffer}}
-     */
-    decodeMsg(body) {
-        if (body.length < 8) {
-            throw Error("Can't decode packet");
-        }
-        let remoteAuthKeyId = body.readBigInt64LE(0);
-        let offset = 8;
-        let msgKey = body.slice(offset, offset + 16);
-        offset += 16;
-        let {key, iv} = Helpers.calcKey(this.session.authKey.key, msgKey, false);
-        let plainText = AES.decryptIge(body.slice(offset, body.length), key, iv);
-        offset = 0;
-        let remoteSalt = plainText.readBigInt64LE(offset);
-        offset += 8;
-        let remoteSessionId = plainText.readBigInt64LE(offset);
-        offset += 8;
-        let remoteMsgId = plainText.readBigInt64LE(offset);
-        offset += 8;
-        let remoteSequence = plainText.readInt32LE(offset);
-        offset += 4;
-        let msgLen = plainText.readInt32LE(offset);
-        offset += 4;
-        let message = plainText.slice(offset, offset + msgLen);
-        return {message, remoteMsgId, remoteSequence}
-    }
-
-    async processMsg(msgId, sequence, reader, offset, request = undefined) {
-        this.needConfirmation.push(msgId);
-        let code = reader.readUInt32LE(offset);
-        console.log("code is ", code);
-        // The following codes are "parsed manually"
-        if (code === 0xf35c6d01) {  //rpc_result, (response of an RPC call, i.e., we sent a request)
-            console.log("got rpc result");
-            return await this.handleRpcResult(msgId, sequence, reader, offset, request);
-        }
-
-        if (code === 0x73f1f8dc) {  //msg_container
-            return this.handleContainer(msgId, sequence, reader, offset, request);
-        }
-        if (code === 0x3072cfa1) {  //gzip_packed
-            return this.handleGzipPacked(msgId, sequence, reader, offset, request);
-        }
-        if (code === 0xedab447b) {  //bad_server_salt
-            return await this.handleBadServerSalt(msgId, sequence, reader, offset, request);
-        }
-        if (code === 0xa7eff811) {  //bad_msg_notification
-            console.log("bad msg notification");
-            return this.handleBadMsgNotification(msgId, sequence, reader, offset);
-        }
-        /**
-         * If the code is not parsed manually, then it was parsed by the code generator!
-         * In this case, we will simply treat the incoming TLObject as an Update,
-         * if we can first find a matching TLObject
-         */
-        console.log("code", code);
-        if (code === 0x9ec20908) {
-            return this.handleUpdate(msgId, sequence, reader, offset);
-        } else {
-
-            if (tlobjects.contains(code)) {
-                return this.handleUpdate(msgId, sequence, reader);
-            }
-        }
-        console.log("Unknown message");
-        return false;
-    }
-
-    // region Message handling
-
-    handleUpdate(msgId, sequence, reader, offset = 0) {
-        let tlobject = Helpers.tgReadObject(reader,offset);
-        for (let handler of this.onUpdateHandlers) {
-            handler(tlobject);
-        }
-        return Float32Array
-    }
-
-    async handleContainer(msgId, sequence, reader, offset, request) {
-        let code = reader.readUInt32LE(offset);
-        offset += 4;
-        let size = reader.readInt32LE(offset);
-        offset += 4;
-        for (let i = 0; i < size; i++) {
-            let innerMsgId = reader.readBigUInt64LE(offset);
-            offset += 8;
-            let innerSequence = reader.readInt32LE(offset);
-            offset += 4;
-            let innerLength = reader.readInt32LE(offset);
-            offset += 4;
-            if (!(await this.processMsg(innerMsgId, sequence, reader, offset, request))) {
-                offset += innerLength;
-            }
-        }
-        return false;
-    }
-
-    async handleBadServerSalt(msgId, sequence, reader, offset, request) {
-        let code = reader.readUInt32LE(offset);
-        offset += 4;
-        let badMsgId = reader.readBigUInt64LE(offset);
-        offset += 8;
-        let badMsgSeqNo = reader.readInt32LE(offset);
-        offset += 4;
-        let errorCode = reader.readInt32LE(offset);
-        offset += 4;
-        let newSalt = reader.readBigUInt64LE(offset);
-        offset += 8;
-        this.session.salt = newSalt;
-
-        if (!request) {
-            throw Error("Tried to handle a bad server salt with no request specified");
-        }
-
-        //Resend
-        await this.send(request, true);
-        return true;
-    }
-
-    handleBadMsgNotification(msgId, sequence, reader, offset) {
-        let code = reader.readUInt32LE(offset);
-        offset += 4;
-        let requestId = reader.readUInt32LE(offset);
-        offset += 4;
-        let requestSequence = reader.readInt32LE(offset);
-        offset += 4;
-        let errorCode = reader.readInt32LE(offset);
-        return new BadMessageError(errorCode);
-    }
-
-    async handleRpcResult(msgId, sequence, reader, offset, request) {
-        if (!request) {
-            throw Error("RPC results should only happen after a request was sent");
-        }
-        let buffer = Buffer.alloc(0);
-        let code = reader.readUInt32LE(offset);
-        offset += 4;
-        let requestId = reader.readUInt32LE(offset);
-        offset += 4;
-        let innerCode = reader.readUInt32LE(offset);
-        offset += 4;
-        if (requestId === request.msgId) {
-            request.confirmReceived = true;
-        }
-
-        if (innerCode === 0x2144ca19) {  // RPC Error
-            console.log("Got an error");
-            let errorCode = reader.readInt32LE(offset);
-            offset += 4;
-            let errorMessage = Helpers.tgReadString(reader, offset);
-            offset = errorMessage.offset;
-            errorMessage = errorMessage.data;
-            let error = new RPCError(errorCode, errorMessage);
-            if (error.mustResend) {
-                request.confirmReceived = false;
-            }
-            if (error.message.startsWith("FLOOD_WAIT_")) {
-                console.log("Should wait {}s. Sleeping until then.".format(error.additionalData));
-                await Helpers.sleep();
-            } else if (error.message.startsWith("PHONE_MIGRATE_")) {
-                throw new InvalidDCError(error.additionalData);
-            } else {
-                throw error;
-            }
-
-        } else {
-            console.log("no errors");
-            if (innerCode === 0x3072cfa1) { //GZip packed
-                console.log("Gzipped data");
-                let res = Helpers.tgReadByte(reader, offset);
-                let unpackedData = await ungzip(res.data);
-                offset = res.offset;
-                res = request.onResponse(unpackedData, offset);
-                buffer = res.data;
-                offset = res.offset;
-            } else {
-                console.log("plain data");
-                offset -= 4;
-                let res = request.onResponse(reader, offset);
-                buffer = res.data;
-                offset = res.offset;
-            }
-        }
-        return {buffer, offset}
-
-    }
-
-    handleGzipPacked(msgId, sequence, reader, offset, request) {
-        throw Error("not implemented");
-        // TODO
-    }
-
-    setListenForUpdates(enabled) {
-
-        if (enabled) {
-            console.log("Enabled updates");
-        } else {
-            console.log("Disabled updates");
-        }
-    }
-
-    updatesListenMethod() {
-        while (true) {
-            let {seq, body} = this.transport.receive();
-            let {message, remoteMsgId, remoteSequence} = this.decodeMsg(body);
-            this.processMsg(remoteMsgId, remoteSequence, message);
-
-        }
-    }
-}
-
-module.exports = MTProtoSender;

+ 6 - 0
package-lock.json

@@ -750,6 +750,12 @@
       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
       "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
       "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
     },
     },
+    "leemon": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/leemon/-/leemon-6.2.0.tgz",
+      "integrity": "sha512-a5ieuGSGEb5ezCL6UNds5//cVFaKpeexVK0VDCE8/eOF0r0/9Og94LQ33U2Px5dUcHVCDPWQY8gXLgDlDJnyyg==",
+      "dev": true
+    },
     "levn": {
     "levn": {
       "version": "0.3.0",
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",

+ 2 - 1
package.json

@@ -23,6 +23,7 @@
     "string-format": "^2.0.0"
     "string-format": "^2.0.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "bigint-buffer": "^1.1.2"
+    "bigint-buffer": "^1.1.2",
+    "leemon": "^6.2.0"
   }
   }
 }
 }