From ac0702fb1255b4e6d338f6385061417835e592d6 Mon Sep 17 00:00:00 2001 From: goodmind Date: Wed, 15 Mar 2017 18:46:46 +0500 Subject: [PATCH 01/15] use func declaration for getUserID --- src/service/updates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/updates.js b/src/service/updates.js index 73414f6..7158ce4 100644 --- a/src/service/updates.js +++ b/src/service/updates.js @@ -24,7 +24,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { let myID = 0 getUserID().then(id => myID = id) - const getUserID = async () => { + async function getUserID() { const auth = await api.storage.get('user_auth') return auth.id || 0 } From 2cf643a2b33e1b198dce5d2fc1f44415f4c62917 Mon Sep 17 00:00:00 2001 From: goodmind Date: Wed, 15 Mar 2017 21:58:12 +0500 Subject: [PATCH 02/15] update types --- index.d.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/index.d.ts b/index.d.ts index 9b9cb0e..820d8cf 100644 --- a/index.d.ts +++ b/index.d.ts @@ -38,6 +38,7 @@ declare module 'telegram-mtproto' { interface ApiManagerInstance { readonly storage: AsyncStorage + readonly updates: any (method: string): Promise (method: string, params: Object): Promise (method: string, params: Object, options: Object): Promise @@ -48,16 +49,19 @@ declare module 'telegram-mtproto' { new (): ApiManagerInstance new ({ server, api, app, schema, mtSchema }: Config): ApiManagerInstance } - export var ApiManager: IApiManager + export const ApiManager: IApiManager class ApiManagerClass { readonly storage: AsyncStorage setUserAuth(dc: number, userAuth: T): void on(event: string|string[], handler: Function) } export interface AsyncStorage { - get(...keys: string[]): Promise - set(obj: Object): Promise + get(key: string): Promise + set(key: string, val: any): Promise remove(...keys: string[]): Promise clear(): Promise<{}> } + + function MTProto({ server, api, app, schema, mtSchema }: Config): ApiManagerInstance + export default MTProto } \ No newline at end of file From b67b2b5c04da5f780459fd52bedb3e036db1a0cf Mon Sep 17 00:00:00 2001 From: goodmind Date: Wed, 15 Mar 2017 22:00:34 +0500 Subject: [PATCH 03/15] remove clutter code We should return update message handling (maybe in app layer) --- src/service/updates.js | 90 +++++++++++------------------------------- 1 file changed, 22 insertions(+), 68 deletions(-) diff --git a/src/service/updates.js b/src/service/updates.js index 7158ce4..7a955fa 100644 --- a/src/service/updates.js +++ b/src/service/updates.js @@ -8,8 +8,8 @@ import { setUpdatesProcessor } from './networker' import type { ApiManagerInstance } from './api-manager/index.h' import type { UpdatesState, CurState } from './updates.h' -const AppPeersManager = null -const AppUsersManager = null +// const AppPeersManager = null +// const AppUsersManager = null const AppChatsManager = null const UpdatesManager = (api: ApiManagerInstance) => { @@ -36,8 +36,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { return false } const updates = pendingUpdatesData.updates - for (const update of updates) - saveUpdate(update) + updates.forEach(saveUpdate) updatesState.seq = pendingUpdatesData.seq if (pendingUpdatesData.date && updatesState.date < pendingUpdatesData.date) { updatesState.date = pendingUpdatesData.date @@ -136,32 +135,20 @@ const UpdatesManager = (api: ApiManagerInstance) => { const toID = updateMessage.chat_id ? -updateMessage.chat_id : isOut ? updateMessage.user_id : myID - - processUpdate({ - _ : 'updateNewMessage', - message: { - _ : 'message', - flags : updateMessage.flags, - pFlags : updateMessage.pFlags, - id : updateMessage.id, - from_id : fromID, - to_id : AppPeersManager.getOutputPeer(toID), - date : updateMessage.date, - message : updateMessage.message, - fwd_from : updateMessage.fwd_from, - reply_to_msg_id: updateMessage.reply_to_msg_id, - entities : updateMessage.entities - }, - pts : updateMessage.pts, - pts_count: updateMessage.pts_count - }, processOpts) + + api.emit('updateShortMessage', { + processUpdate, + processOpts, + updateMessage, + fromID, + toID + }) } break case 'updatesCombined': - case 'updates': - AppUsersManager.saveApiUsers(updateMessage.users) - AppChatsManager.saveApiChats(updateMessage.chats) + case 'updates': + api.emit('apiUpdate', updateMessage) updateMessage.updates.forEach(update => { processUpdate(update, processOpts) @@ -195,12 +182,11 @@ const UpdatesManager = (api: ApiManagerInstance) => { updatesState.date = differenceResult.date updatesState.seq = differenceResult.seq updatesState.syncLoading = false - api.on('stateSynchronized') + api.emit('stateSynchronized') return false } - AppUsersManager.saveApiUsers(differenceResult.users) - AppChatsManager.saveApiChats(differenceResult.chats) + api.emit('difference', differenceResult) // Should be first because of updateMessageID // console.log(dT(), 'applying', differenceResult.other_updates.length, 'other updates') @@ -238,7 +224,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { getDifference() } else { // console.log(dT(), 'finished get diff') - api.on('stateSynchronized') + api.emit('stateSynchronized') updatesState.syncLoading = false } } @@ -266,7 +252,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { if (differenceResult._ == 'updates.channelDifferenceEmpty') { debug('apply channel empty diff')(differenceResult) channelState.syncLoading = false - api.on('stateSynchronized') + api.emit('stateSynchronized') return false } @@ -278,8 +264,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { return false } - AppUsersManager.saveApiUsers(differenceResult.users) - AppChatsManager.saveApiChats(differenceResult.chats) + api.emit('difference', differenceResult) // Should be first because of updateMessageID debug('applying')(differenceResult.other_updates.length, 'channel other updates') @@ -302,7 +287,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { getChannelDifference(channelID) } else { debug('finished channel get diff')() - api.on('stateSynchronized') + api.emit('stateSynchronized') channelState.syncLoading = false } } @@ -335,7 +320,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { switch (update._) { case 'updateNewChannelMessage': case 'updateEditChannelMessage': - channelID = -AppPeersManager.getPeerID(update.message.to_id) + channelID = update.message.to_id.channel_id || update.message.to_id.chat_id break case 'updateDeleteChannelMessages': channelID = update.channel_id @@ -361,39 +346,13 @@ const UpdatesManager = (api: ApiManagerInstance) => { return false } - if (update._ == 'updateNewMessage' || - update._ == 'updateEditMessage' || - update._ == 'updateNewChannelMessage' || - update._ == 'updateEditChannelMessage') { - const message = update.message - const toPeerID = AppPeersManager.getPeerID(message.to_id) - const fwdHeader = message.fwd_from || {} - if (message.from_id && !AppUsersManager.hasUser(message.from_id, message.pFlags.post) || - fwdHeader.from_id && !AppUsersManager.hasUser(fwdHeader.from_id, !!fwdHeader.channel_id) || - fwdHeader.channel_id && !AppChatsManager.hasChat(fwdHeader.channel_id, true) || - toPeerID > 0 && !AppUsersManager.hasUser(toPeerID) || - toPeerID < 0 && !AppChatsManager.hasChat(-toPeerID)) { - debug('Not enough data for message update')(message) - if (channelID && AppChatsManager.hasChat(channelID)) { - getChannelDifference(channelID) - } else { - forceGetDifference() - } - return false - } - } - else if (channelID && !AppChatsManager.hasChat(channelID)) { - // console.log(dT(), 'skip update, missing channel', channelID, update) - return false - } - let popPts let popSeq if (update.pts) { const newPts = curState.pts + (update.pts_count || 0) if (newPts < update.pts) { - debug('Pts hole')(curState, update, channelID && AppChatsManager.getChat(channelID)) + // debug('Pts hole')(curState, update, channelID && AppChatsManager.getChat(channelID)) curState.pendingPtsUpdates.push(update) if (!curState.syncPending) { curState.syncPending = { @@ -469,7 +428,7 @@ const UpdatesManager = (api: ApiManagerInstance) => { } function saveUpdate(update: any) { - // api.on('apiUpdate', update) + api.emit('apiUpdate', update) } async function attach() { @@ -481,11 +440,6 @@ const UpdatesManager = (api: ApiManagerInstance) => { setTimeout(() => { updatesState.syncLoading = false }, 1000) - - // updatesState.seq = 1 - // updatesState.pts = stateResult.pts - 5000 - // updatesState.date = 1 - // getDifference() } return { From 4e75f9b14c828e51d5e4202b89eef1abc5a48890 Mon Sep 17 00:00:00 2001 From: goodmind Date: Wed, 15 Mar 2017 22:00:52 +0500 Subject: [PATCH 04/15] implements emitter --- src/service/api-manager/index.h.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/service/api-manager/index.h.js b/src/service/api-manager/index.h.js index 6efd34c..abb1aae 100644 --- a/src/service/api-manager/index.h.js +++ b/src/service/api-manager/index.h.js @@ -1,5 +1,7 @@ //@flow +import type { Emit, On } from '../main/index.h' + export type Bytes = number[] export type PublicKey = { //TODO remove this @@ -45,5 +47,6 @@ export type ApiManagerInstance = { (method: string, params: Object, options: Object): Promise, storage: AsyncStorage, setUserAuth(dc: number, userAuth: any): void, - on: (event: string | string[], handler: Function) => void + on: On, + emit: Emit } From 3e15f1aedff8b97bb9b1e277ef6d0825fc0687c6 Mon Sep 17 00:00:00 2001 From: goodmind Date: Wed, 15 Mar 2017 22:01:03 +0500 Subject: [PATCH 05/15] add update manager --- src/service/api-manager/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index 2348bd3..8c553b2 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -1,6 +1,7 @@ //@flow import Promise from 'bluebird' +import UpdatesManager from '../updates' import isNil from 'ramda/src/isNil' import is from 'ramda/src/is' @@ -64,6 +65,7 @@ export class ApiManager { mtSchema: TLSchema keyManager: Args networkFabric: any + updatesManager: any auth: any on: On emit: Emit @@ -97,8 +99,12 @@ export class ApiManager { const apiManager = this.mtpInvokeApi apiManager.setUserAuth = this.setUserAuth apiManager.on = this.on + apiManager.emit = this.emit apiManager.storage = storage + this.updatesManager = UpdatesManager(apiManager) + apiManager.updates = this.updatesManager + return apiManager } networkSetter = (dc: number, options: LeftOptions) => From f16a92a34b8662e74e03eed4b6fc18bfa79b6abb Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Thu, 23 Mar 2017 20:29:51 +0300 Subject: [PATCH 06/15] Async logger, layer generator Signed-off-by: Dmitry Boldyrev --- .babelrc | 3 + .eslintignore | 3 + CHANGELOG.md | 34 +++ package.json | 16 +- src/error.js | 2 +- src/service/api-manager/index.js | 2 +- src/service/authorizer/index.js | 65 +++--- src/service/main/index.h.js | 2 +- src/service/main/index.js | 3 + src/service/main/invoke-layer-generator.js | 36 +++ src/service/time-manager.js | 10 +- src/tl/index.js | 105 ++++----- src/tl/type-buffer.js | 257 +++++++++++++++++++++ src/tl/types.js | 131 ----------- src/util/dtime.js | 7 + src/util/log.js | 69 +++++- test/node.test.js | 14 +- 17 files changed, 507 insertions(+), 252 deletions(-) create mode 100644 .eslintignore create mode 100644 CHANGELOG.md create mode 100644 src/service/main/invoke-layer-generator.js create mode 100644 src/tl/type-buffer.js delete mode 100644 src/tl/types.js create mode 100644 src/util/dtime.js diff --git a/.babelrc b/.babelrc index 84e5f58..e6ab419 100644 --- a/.babelrc +++ b/.babelrc @@ -7,6 +7,9 @@ // ["fast-async", { // "useRuntimeModule":true // }], + ["transform-es2015-for-of", { + "loose": true + }], "transform-class-properties", "transform-async-to-generator", // ["transform-async-to-module-method", { diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..153dadb --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +lib/** +es/** +dist/** diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e4447f8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# 2.2.1 + +* *invokeWithLayer* api field now may detects internally and don't required (but still valid) + +# 2.2.0 + +* **breaking** Instance now creates without `new` +* **breaking** Rename module exports from `ApiManager` to `MTProto` + +# =<2.1.0 + +Several early alpha versions based on new architechture + +--- + +# 1.1.0 *(beta)* + +* **breaking** Remove all functions from response. Just use the field values. +* Remove logger from response +* Add changelog.md + +# 1.0.6 *(beta)* + +* Https connection. Usage: +```javascript +const { network } = require('telegram-mtproto') +const connection = network.http({ host: 'ip', port: '80', protocol: 'https' }) +``` +* Websockets connection. Usage: +```javascript +const connection = network.wc({ host: 'ip', port: '80' }) +``` +* Precision timing +* Major performance boost \ No newline at end of file diff --git a/package.json b/package.json index 008c377..de49024 100644 --- a/package.json +++ b/package.json @@ -29,35 +29,39 @@ "ajv": "^4.11.5", "ajv-keywords": "^1.5.1", "axios": "^0.15.3", - "bluebird": "^3.4.7", + "bluebird": "^3.5.0", "debug": "^2.6.3", "detect-node": "^2.0.3", "eventemitter2": "^3.0.2", + "memoizee": "^0.4.4", "pako": "^1.0.4", + "pretty-format": "^19.0.0", "ramda": "^0.23.0", "randombytes": "^2.0.3", "rusha": "^0.8.5", "worker-loader": "^0.8.0" }, "devDependencies": { + "@types/memoizee": "^0.4.0", "@types/debug": "0.0.29", "babel-cli": "^6.24.0", "babel-core": "^6.24.0", - "babel-eslint": "^7.1.1", - "babel-loader": "^6.4.0", + "babel-eslint": "^7.2.0", + "babel-loader": "^6.4.1", "babel-plugin-closure-elimination": "^1.1.14", "babel-plugin-transform-async-to-generator": "^6.22.0", "babel-plugin-transform-async-to-module-method": "^6.22.0", "babel-plugin-transform-class-properties": "^6.23.0", + "babel-plugin-transform-es2015-for-of": "^6.23.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-plugin-transform-object-rest-spread": "^6.23.0", "cross-env": "^3.2.4", - "eslint": "^3.17.1", + "eslint": "^3.18.0", "eslint-plugin-babel": "^4.1.1", - "eslint-plugin-flowtype": "^2.30.3", + "eslint-plugin-flowtype": "^2.30.4", "eslint-plugin-promise": "^3.5.0", - "flow-bin": "^0.41.0", + "flow-bin": "^0.42.0", "hirestime": "^3.1.0", "nightmare": "^2.10.0", "prompt": "^1.0.0", diff --git a/src/error.js b/src/error.js index f834f0d..46e10c7 100644 --- a/src/error.js +++ b/src/error.js @@ -1,6 +1,6 @@ //@flow -import type { TypeBuffer } from './tl/types' +import type { TypeBuffer } from './tl/type-buffer' export class MTError extends Error { static getMessage(code: number, type: string, message: string) { diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index 8c553b2..d4ea30d 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -26,7 +26,7 @@ import { AuthKeyError } from '../../error' import { bytesFromHex, bytesToHex } from '../../bin' import type { TLFabric } from '../../tl' -import type { TLSchema } from '../../tl/types' +import type { TLSchema } from '../../tl/type-buffer' import { switchErrors } from './error-cases' import { delayedCall } from '../../util/smart-timeout' diff --git a/src/service/authorizer/index.js b/src/service/authorizer/index.js index 8c01f87..b1ee4c4 100644 --- a/src/service/authorizer/index.js +++ b/src/service/authorizer/index.js @@ -15,6 +15,10 @@ import { bytesCmp, bytesToHex, sha1BytesSync, nextRandomInt, import { bpe, str2bigInt, one, dup, sub_, sub, greater } from '../../vendor/leemon' +import Logger from '../../util/log' + +const log = Logger`auth` + // import { ErrorBadResponse } from '../../error' import SendPlainReq from './send-plain-req' @@ -29,12 +33,6 @@ const primeHex = 'c71caeb9c6b1c9048e6c522f70f13f73980d40238e3e21c14934d037563d93 'a3928fef5b9ae4e418fc15e83ebea0f87fa9ff5eed70050ded2849f47bf959d956850ce929851' + 'f0d8115f635b105ee2e4e15d04b2454bf6f4fadf034b10403119cd8e3b92fcc5b' -const asyncLog = (...data) => { - const time = dTime() - console.log(time, ...data) - // setTimeout(() => console.log(time, ...data), 300) -} - const concat = (e1, e2) => [...e1, ...e2] const tmpAesKey = (serverNonce, newNonce) => { @@ -97,12 +95,26 @@ type AuthBasic = { serverSalt: Bytes } +const minSize = Math.ceil(64 / bpe) + 1 + +const getTwoPow = () => { //Dirty hack to count 2^(2048 - 64) + //This number contains 496 zeroes in hex + const arr = Array(496) + .fill('0') + arr.unshift('1') + const hex = arr.join('') + const res = str2bigInt(hex, 16, minSize) + return res +} + +const leemonTwoPow = getTwoPow() + export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, prepare }: Args) => { const sendPlainReq = SendPlainReq({ Serialization, Deserialization }) function mtpSendReqPQ(auth: AuthBasic) { const deferred = auth.deferred - asyncLog('Send req_pq', bytesToHex(auth.nonce)) + log('Send req_pq')(bytesToHex(auth.nonce)) const request = Serialization({ mtproto: true }) @@ -113,18 +125,18 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre : Promise.reject(new Error('[MT] No public key found')) const factorizeThunk = () => { - asyncLog('PQ factorization start', auth.pq) + log('PQ factorization start')(auth.pq) return CryptoWorker.factorize(auth.pq) } const factDone = ([ p, q, it ]) => { auth.p = p auth.q = q - asyncLog('PQ factorization done', it) + log('PQ factorization done')(it) return mtpSendReqDhParams(auth) } const factFail = error => { - asyncLog('Worker error', error, error.stack) + log('Worker error')(error, error.stack) deferred.reject(error) } @@ -141,7 +153,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre auth.pq = response.pq auth.fingerprints = response.server_public_key_fingerprints - asyncLog('Got ResPQ', bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints) + log('Got ResPQ')(bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints) return select(auth.fingerprints) .then(keyFoundCheck) @@ -227,7 +239,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre mtpSendSetClientDhParams(auth) } - asyncLog('Send req_DH_params') + log('afterReqDH')('Send req_DH_params') return sendPlainReq(auth.dcUrl, request.getBuffer()) .then(afterReqDH, deferred.reject) } @@ -257,7 +269,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre if (!bytesCmp(auth.serverNonce, response.server_nonce)) throw new Error('[MT] server_DH_inner_data serverNonce mismatch') - asyncLog('Done decrypting answer') + log('DecryptServerDhDataAnswer')('Done decrypting answer') auth.g = response.g auth.dhPrime = response.dh_prime auth.gA = response.g_a @@ -275,27 +287,14 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre applyServerTime(auth.serverTime, auth.localTime) } - const minSize = Math.ceil(64 / bpe) + 1 - - const getTwoPow = () => { //Dirty hack to count 2^(2048 - 64) - //This number contains 496 zeroes in hex - const arr = Array(496) - .fill('0') - arr.unshift('1') - const hex = arr.join('') - const res = str2bigInt(hex, 16, minSize) - return res - } - - const leemonTwoPow = getTwoPow() - function mtpVerifyDhParams(g, dhPrime, gA) { - asyncLog('Verifying DH params') + const innerLog = log('VerifyDhParams') + innerLog('begin') const dhPrimeHex = bytesToHex(dhPrime) if (g !== 3 || dhPrimeHex !== primeHex) // The verified value is from https://core.telegram.org/mtproto/security_guidelines throw new Error('[MT] DH params are not verified: unknown dhPrime') - asyncLog('dhPrime cmp OK') + innerLog('dhPrime cmp OK') // const gABigInt = new BigInteger(bytesToHex(gA), 16) // const dhPrimeBigInt = new BigInteger(dhPrimeHex, 16) @@ -334,7 +333,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}') if (case4) throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}') - asyncLog('2^{2048-64} < gA < dhPrime-2^{2048-64} OK') + innerLog('2^{2048-64} < gA < dhPrime-2^{2048-64} OK') return true } @@ -356,7 +355,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre authKeyAux = authKeyHash.slice(0, 8), authKeyID = authKeyHash.slice(-8) - asyncLog('Got Set_client_DH_params_answer', response._) + log('Got Set_client_DH_params_answer')(response._) switch (response._) { case 'dh_gen_ok': { const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16) @@ -438,7 +437,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre encrypted_data: encryptedData }) - asyncLog('Send set_client_DH_params') + log('onGb')('Send set_client_DH_params') return sendPlainReq(auth.dcUrl, request.getBuffer()) .then(afterPlainRequest) } @@ -450,7 +449,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre function mtpAuth(dcID: number, cached: Cached, dcUrl: string) { if (cached[dcID]) return cached[dcID].promise - asyncLog('mtpAuth') + log('mtpAuth', 'dcID', 'dcUrl')(dcID, dcUrl) const nonce = [] for (let i = 0; i < 16; i++) nonce.push(nextRandomInt(0xFF)) diff --git a/src/service/main/index.h.js b/src/service/main/index.h.js index 66299fd..84e2114 100644 --- a/src/service/main/index.h.js +++ b/src/service/main/index.h.js @@ -1,6 +1,6 @@ //@flow -import type { TLSchema } from '../../tl/types' +import type { TLSchema } from '../../tl/type-buffer' export type ApiConfig = { invokeWithLayer?: number, diff --git a/src/service/main/index.js b/src/service/main/index.js index 809b05c..0b485a3 100644 --- a/src/service/main/index.js +++ b/src/service/main/index.js @@ -8,6 +8,7 @@ import { PureStorage } from '../../store' import TL from '../../tl' import configValidator from './config-validation' +import generateInvokeLayer from './invoke-layer-generator' import type { TLFabric } from '../../tl' import type { ApiConfig, ConfigType, StrictConfig, Emit, On, PublicKey } from './index.h' @@ -58,6 +59,8 @@ const configNormalization = (config: ConfigType): StrictConfig => { mtSchema = mtproto57, } = config const apiNormalized = { ...apiConfig, ...api } + const invokeLayer = generateInvokeLayer(apiNormalized.layer) + apiNormalized.invokeWithLayer = invokeLayer const fullCfg = { server, api: apiNormalized, diff --git a/src/service/main/invoke-layer-generator.js b/src/service/main/invoke-layer-generator.js new file mode 100644 index 0000000..7296135 --- /dev/null +++ b/src/service/main/invoke-layer-generator.js @@ -0,0 +1,36 @@ +//@flow + +/** + * Defines the parameter required for authorization based on the level number + * + * Values were taken from here + * https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/scheme.tl + * + * @param {number} apiLevel + * @returns + */ +function generateInvokeLayer(apiLevel: number) { + switch (apiLevel) { + case 1: return 0x53835315 + case 2: return 0x289dd1f6 + case 3: return 0xb7475268 + case 4: return 0xdea0d430 + case 5: return 0x417a57ae + case 6: return 0x3a64d54d + case 7: return 0xa5be56d3 + case 8: return 0xe9abd9fd + case 9: return 0x76715a63 + case 10: return 0x39620c41 + case 11: return 0xa6b88fdf + case 12: return 0xdda60d3c + case 13: return 0x427c8ea2 + case 14: return 0x2b9b08fa + case 15: return 0xb4418b64 + case 16: return 0xcf5f0987 + case 17: return 0x50858a19 + case 18: return 0x1c900537 + default: return 0xda9b0d0d + } +} + +export default generateInvokeLayer \ No newline at end of file diff --git a/src/service/time-manager.js b/src/service/time-manager.js index 77f9b53..7435f1e 100644 --- a/src/service/time-manager.js +++ b/src/service/time-manager.js @@ -3,6 +3,10 @@ import isNode from 'detect-node' import { TimeOffset } from '../store' import { nextRandomInt, lshift32 } from '../bin' +import Logger from '../util/log' + +const log = Logger`time-manager` + export const tsNow = seconds => { let t = +new Date() //eslint-disable-next-line @@ -12,9 +16,7 @@ export const tsNow = seconds => { : t } -const logTimer = (new Date()).getTime() - -export const dTime = () => `[${(((new Date()).getTime() -logTimer) / 1000).toFixed(3)}]` +export { dTime } from '../util/dtime' let lastMessageID = [0, 0] let timerOffset = 0 @@ -48,7 +50,7 @@ export const applyServerTime = (serverTime, localTime) => { lastMessageID = [0, 0] timerOffset = newTimeOffset - console.log(dTime(), 'Apply server time', serverTime, localTime, newTimeOffset, changed) + log('Apply server time')(serverTime, localTime, newTimeOffset, changed) return changed } diff --git a/src/tl/index.js b/src/tl/index.js index d6e6f45..2c7f07c 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -10,7 +10,8 @@ import Logger from '../util/log' const debug = Logger`tl` import { readInt, TypeBuffer, getNakedType, - getPredicate, getString, getTypeConstruct } from './types' + getPredicate, getString, getTypeConstruct } from './type-buffer' +import type { TLSchema } from './type-buffer' const PACKED = 0x3072cfa1 @@ -21,7 +22,9 @@ export class Serialization { intView: Int32Array byteView: Uint8Array mtproto: boolean - constructor({ mtproto, startMaxLength }, api, mtApi) { + api: TLSchema + mtApi: TLSchema + constructor({ mtproto, startMaxLength }, api: TLSchema, mtApi: TLSchema) { this.api = api this.mtApi = mtApi @@ -90,19 +93,19 @@ export class Serialization { this.offset += 4 } - storeIntString = (value, field) => { - switch (true) { - case is(String, value): return this.storeString(value, field) - case is(Number, value): return this.storeInt(value, field) + storeIntString = (value: number | string, field: string) => { + switch (typeof value) { + case 'string': return this.storeString(value, field) + case 'number': return this.storeInt(value, field) default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) } } - storeInt = (i, field = '') => { + storeInt = (i: number, field: string = '') => { this.writeInt(i, `${ field }:int`) } - storeBool(i, field = '') { + storeBool(i: boolean, field: string = '') { if (i) { this.writeInt(0x997275b5, `${ field }:bool`) } else { @@ -110,12 +113,12 @@ export class Serialization { } } - storeLongP(iHigh, iLow, field) { + storeLongP(iHigh, iLow, field: string) { this.writeInt(iLow, `${ field }:long[low]`) this.writeInt(iHigh, `${ field }:long[high]`) } - storeLong(sLong, field = '') { + storeLong(sLong, field: string = '') { if (is(Array, sLong)) return sLong.length === 2 ? this.storeLongP(sLong[0], sLong[1], field) @@ -130,7 +133,7 @@ export class Serialization { this.writeInt(int1, `${ field }:long[high]`) } - storeDouble(f, field = '') { + storeDouble(f, field: string = '') { const buffer = new ArrayBuffer(8) const intView = new Int32Array(buffer) const doubleView = new Float64Array(buffer) @@ -224,13 +227,13 @@ export class Serialization { this.offset += len } - storeMethod(methodName, params) { + storeMethod(methodName: string, params) { const schema = selectSchema(this.mtproto, this.api, this.mtApi) let methodData = false - for (let i = 0; i < schema.methods.length; i++) { - if (schema.methods[i].method == methodName) { - methodData = schema.methods[i] + for (const method of schema.methods) { + if (method.method == methodName) { + methodData = method break } } @@ -240,13 +243,10 @@ export class Serialization { this.storeInt(intToUint(methodData.id), `${methodName }[id]`) - let param, type let condType let fieldBit - const len = methodData.params.length - for (let i = 0; i < len; i++) { - param = methodData.params[i] - type = param.type + for (const param of methodData.params) { + let type = param.type if (type.indexOf('?') !== -1) { condType = type.split('?') fieldBit = condType[0].split('.') @@ -262,21 +262,21 @@ export class Serialization { if (!stored) throw new Error(`Method ${methodName}.`+ ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`)*/ - this.storeObject(stored, type, `${methodName }[${ paramName }]`) + this.storeObject(stored, type, `f{${methodName}}(${paramName})`) } return methodData.type } - emptyOfType(ofType, schema) { + /*emptyOfType(ofType, schema: TLSchema) { const resultConstruct = schema.constructors.find( - ({ type, predicate }) => + ({ type, predicate }: TLConstruct) => type === ofType && predicate.indexOf('Empty') !== -1) return resultConstruct ? { _: resultConstruct.predicate } : null - } - storeObject(obj, type, field) { + }*/ + storeObject(obj, type: string, field: string) { switch (type) { case '#': case 'int': @@ -326,13 +326,13 @@ export class Serialization { const predicate = obj['_'] let isBare = false let constructorData = false - - if (isBare = type.charAt(0) == '%') + isBare = type.charAt(0) == '%' + if (isBare) type = type.substr(1) - for (let i = 0; i < schema.constructors.length; i++) { - if (schema.constructors[i].predicate == predicate) { - constructorData = schema.constructors[i] + for (const tlConst of schema.constructors) { + if (tlConst.predicate == predicate) { + constructorData = tlConst break } } @@ -343,14 +343,11 @@ export class Serialization { isBare = true if (!isBare) - this.writeInt(intToUint(constructorData.id), `${field }[${ predicate }][id]`) + this.writeInt(intToUint(constructorData.id), `${field }.${ predicate }[id]`) - let param let condType let fieldBit - const len = constructorData.params.length - for (let i = 0; i < len; i++) { - param = constructorData.params[i] + for (const param of constructorData.params) { type = param.type if (type.indexOf('?') !== -1) { condType = type.split('?') @@ -361,7 +358,7 @@ export class Serialization { type = condType[1] } - this.storeObject(obj[param.name], type, `${field }[${ predicate }][${ param.name }]`) + this.storeObject(obj[param.name], type, `${field}.${ predicate }.${ param.name }`) } return constructorData.type @@ -373,7 +370,9 @@ export class Deserialization { typeBuffer: TypeBuffer override: Object mtproto: boolean - constructor(buffer: Buffer, { mtproto, override }: DConfig, api, mtApi) { + api: TLSchema + mtApi: TLSchema + constructor(buffer: Buffer, { mtproto, override }: DConfig, api: TLSchema, mtApi: TLSchema) { this.api = api this.mtApi = mtApi this.override = override @@ -384,11 +383,11 @@ export class Deserialization { readInt = readInt(this) - fetchInt(field = '') { + fetchInt(field: string = '') { return this.readInt(`${ field }:int`) } - fetchDouble(field = '') { + fetchDouble(field: string = '') { const buffer = new ArrayBuffer(8) const intView = new Int32Array(buffer) const doubleView = new Float64Array(buffer) @@ -399,27 +398,15 @@ export class Deserialization { return doubleView[0] } - fetchLong(field = '') { + fetchLong(field: string = '') { const iLow = this.readInt(`${ field }:long[low]`) const iHigh = this.readInt(`${ field }:long[high]`) const res = lshift32(iHigh, iLow) - // const longDec = bigint(iHigh) - // .shiftLeft(32) - // .add(bigint(iLow)) - // .toString() - - - // debug`long, iLow, iHigh`(strDecToHex(iLow.toString()), - // strDecToHex(iHigh.toString())) - // debug`long, leemon`(res, strDecToHex(res.toString())) - // debug`long, bigint`(longDec, strDecToHex(longDec.toString())) - - return res } - fetchBool(field = '') { + fetchBool(field: string = '') { const i = this.readInt(`${ field }:bool`) switch (i) { case 0x997275b5: return true @@ -431,7 +418,7 @@ export class Deserialization { } } - fetchString(field = '') { + fetchString(field: string = '') { let len = this.typeBuffer.nextByte() if (len == 254) { @@ -454,7 +441,7 @@ export class Deserialization { return s } - fetchBytes(field = '') { + fetchBytes(field: string = '') { let len = this.typeBuffer.nextByte() if (len == 254) { @@ -471,7 +458,7 @@ export class Deserialization { return bytes } - fetchIntBytes(bits, field = '') { + fetchIntBytes(bits, field: string = '') { if (bits % 32) throw new Error(`Invalid bits: ${bits}`) @@ -484,7 +471,7 @@ export class Deserialization { return bytes } - fetchRawBytes(len, field = '') { + fetchRawBytes(len, field: string = '') { if (len === false) { len = this.readInt(`${ field }_length`) if (len > this.typeBuffer.byteView.byteLength) @@ -641,7 +628,7 @@ export class Deserialization { } -const selectSchema = (mtproto: boolean, api, mtApi) => mtproto +const selectSchema = (mtproto: boolean, api: TLSchema, mtApi: TLSchema) => mtproto ? mtApi : api @@ -668,7 +655,7 @@ export type TLFabric = { Deserialization: DeserializationFabric } -export const TL = (api, mtApi) => ({ +export const TL = (api: TLSchema, mtApi: TLSchema) => ({ Serialization: ({ mtproto = false, startMaxLength = 2048 /* 2Kb */ } = {}) => new Serialization({ mtproto, startMaxLength }, api, mtApi), Deserialization: (buffer: Buffer, { mtproto = false, override = {} }: DConfig = {}) => diff --git a/src/tl/type-buffer.js b/src/tl/type-buffer.js new file mode 100644 index 0000000..b64291f --- /dev/null +++ b/src/tl/type-buffer.js @@ -0,0 +1,257 @@ +//@flow + +import isNode from 'detect-node' + +import Logger from '../util/log' +const log = Logger('tl', 'type-buffer') + +import { immediate } from '../util/smart-timeout' +import { TypeBufferIntError } from '../error' + +// import { bigint, uintToInt, intToUint, bytesToHex, +// gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' + +type TLParam = { + name: string, + type: string +} + +export type TLConstruct = { + id: string, + type: string, + predicate: string, + params: TLParam[] +} + +type TLMethod = { + id: string, + type: string, + method: string, + params: TLParam[] +} + +export type TLSchema = { + constructors: TLConstruct[], + methods: TLMethod[], + constructorsIndex?: number[] +} + +type HasTypeBuffer = { + typeBuffer: TypeBuffer +} + +export const readInt = (ctx: HasTypeBuffer) => (field: string) => { + const i = ctx.typeBuffer.nextInt() + // log('int')(field, i.toString(16), i) + return i +} + +function findType(val: TLConstruct) { + return val.type == this +} + +function findPred(val: TLConstruct) { + return val.predicate == this +} + +function findId(val: TLConstruct) { + return val.id == this +} + +export const getNakedType = (type: string, schema: TLSchema) => { + const checkType = type.substr(1) + const result = schema.constructors.find(findType, checkType) + if (!result) + throw new Error(`Constructor not found for type: ${type}`) + return result +} + +export const getPredicate = (type: string, schema: TLSchema) => { + const result = schema.constructors.find(findPred, type) + if (!result) + throw new Error(`Constructor not found for predicate: ${type}`) + return result +} + +export const getTypeConstruct = + (construct: number, schema: TLSchema) => + schema.constructors.find(findId, construct) + +const getChar = (e: number) => String.fromCharCode(e) + +export const getString = (length: number, buffer: TypeBuffer) => { + const bytes = buffer.next(length) + + const result = [...bytes].map(getChar).join('') + buffer.addPadding() + return result +} + +const countNewLength = (maxLength: number, need: number, offset: number) => { + const e1 = maxLength * 2 + const e2 = offset + need + 16 + const max = Math.max(e1, e2) / 4 + const rounded = Math.ceil(max) * 4 + return rounded +} + +const writeIntLogger = log('writeInt') + +const writeIntLog = (i: number, field: string) => { + const hex = i && i.toString(16) || 'UNDEF' + writeIntLogger(hex, i, field) +} + +export class TypeWriter { + offset: number = 0 + buffer: ArrayBuffer + intView: Int32Array + byteView: Uint8Array + maxLength: number + constructor(startMaxLength: number) { + this.maxLength = startMaxLength + this.reset() + } + reset() { + this.buffer = new ArrayBuffer(this.maxLength) + this.intView = new Int32Array(this.buffer) + this.byteView = new Uint8Array(this.buffer) + } + next(data: number) { + this.byteView[this.offset] = data + this.offset++ + } + checkLength(needBytes: number) { + if (this.offset + needBytes < this.maxLength) { + return + } + log('Increase buffer')(this.offset, needBytes, this.maxLength) + this.maxLength = countNewLength( + this.maxLength, + needBytes, + this.offset + ) + const previousBuffer = this.buffer + const previousArray = new Int32Array(previousBuffer) + + this.reset() + + new Int32Array(this.buffer).set(previousArray) + } + writeInt(i: number) { + // immediate(writeIntLog, i, field) + + this.checkLength(4) + this.intView[this.offset / 4] = i + this.offset += 4 + } + addPadding() { + while (this.offset % 4) + this.next(0) + } + writeArray(list?: ArrayBuffer | string | number[]) { + let iter, length + + if (list === undefined) + iter = [] + else if (typeof list === 'string') + iter = stringToNums(list) + // else if (list instanceof ArrayBuffer) + // iter = new Uint8Array(list) + else iter = list + + if (list instanceof ArrayBuffer) + length = list.byteLength + else + length = iter.length + + this.checkLength(length + 8) + + if (length <= 253) + this.next(length) + else { + this.next(254) + this.next(length & 0xFF) + this.next((length & 0xFF00) >> 8) + this.next((length & 0xFF0000) >> 16) + } + + this.set(iter, length) + + this.addPadding() + } + set(list: number[] | Uint8Array, length: number) { + this.byteView.set(list, this.offset) + this.offset += length + } + writeBytes(bytes: number[] | ArrayBuffer, cond: (ln: number) => void) { + let iter + if (bytes instanceof ArrayBuffer) + iter = new Uint8Array(bytes) + else + iter = bytes + const length = iter.length + cond(length) + this.checkLength(length) + + this.set(iter, length) + } +} + +const stringToNums = (str: string) => [...str].map(e => e.charCodeAt(0)) + +export class TypeBuffer { + offset: number = 0 + buffer: Buffer + intView: Uint32Array + byteView: Uint8Array + constructor(buffer: Buffer) { + this.buffer = buffer + this.intView = toUint32(buffer) + this.byteView = new Uint8Array(buffer) + } + + nextByte() { + return this.byteView[this.offset++] + } + nextInt() { + if (this.offset >= this.intView.length * 4) + throw new TypeBufferIntError(this) + const int = this.intView[this.offset / 4] + this.offset += 4 + return int + } + next(length: number) { + const result = this.byteView.subarray(this.offset, this.offset + length) + this.offset += length + return result + } + isEnd() { + return this.offset === this.byteView.length + } + addPadding() { + const offset = this.offset % 4 + if (offset > 0) + this.offset += 4 - offset + } +} + +const toUint32 = (buf: Buffer) => { + let ln, res + if (!isNode) //TODO browser behavior not equals, why? + return new Uint32Array( buf ) + if (buf.readUInt32LE) { + ln = buf.byteLength / 4 + res = new Uint32Array( ln ) + for (let i = 0; i < ln; i++) + res[i] = buf.readUInt32LE( i*4 ) + } else { + //$FlowIssue + const data = new DataView( buf ) + ln = data.byteLength / 4 + res = new Uint32Array( ln ) + for (let i = 0; i < ln; i++) + res[i] = data.getUint32( i*4, true ) + } + return res +} diff --git a/src/tl/types.js b/src/tl/types.js deleted file mode 100644 index 30f83d1..0000000 --- a/src/tl/types.js +++ /dev/null @@ -1,131 +0,0 @@ -//@flow - -import isNode from 'detect-node' -import Debug from 'debug' - -const debug = Debug('telegram-mtproto:tl:types') - -import { TypeBufferIntError } from '../error' - -// import { bigint, uintToInt, intToUint, bytesToHex, -// gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' - -type TLParam = { - name: string, - type: string -} - -type TLConstruct = { - id: string, - type: string, - predicate: string, - param: TLParam[] -} - -export type TLSchema = { - constructors: TLConstruct[], - methods: any[], - constructorsIndex?: number[] -} - -export const readInt = (ctx: any) => (field: string) => { - const i = ctx.typeBuffer.nextInt() - debug('[int]', field, i.toString(16), i) - return i -} - -function findType(val: TLConstruct) { - return val.type == this -} - -function findPred(val: TLConstruct) { - return val.predicate == this -} - -function findId(val: TLConstruct) { - return val.id == this -} - -export const getNakedType = (type: string, schema: TLSchema) => { - const checkType = type.substr(1) - const result = schema.constructors.find(findType, checkType) - if (!result) - throw new Error(`Constructor not found for type: ${type}`) - return result -} - -export const getPredicate = (type: string, schema: TLSchema) => { - const result = schema.constructors.find(findPred, type) - if (!result) - throw new Error(`Constructor not found for predicate: ${type}`) - return result -} - -export const getTypeConstruct = - (construct: number, schema: TLSchema) => - schema.constructors.find(findId, construct) - -const getChar = (e: number) => String.fromCharCode(e) - -export const getString = (length: number, buffer: TypeBuffer) => { - const bytes = buffer.next(length) - - const result = [...bytes].map(getChar).join('') - buffer.addPadding() - return result -} - -export class TypeBuffer { - offset: number = 0 - buffer: Buffer - intView: Uint32Array - byteView: Uint8Array - constructor(buffer: Buffer) { - this.buffer = buffer - this.intView = toUint32(buffer) - this.byteView = new Uint8Array(buffer) - } - nextByte() { - return this.byteView[this.offset++] - } - nextInt() { - if (this.offset >= this.intView.length * 4) - throw new TypeBufferIntError(this) - const int = this.intView[this.offset / 4] - this.offset += 4 - return int - } - next(length: number) { - const result = this.byteView.subarray(this.offset, this.offset + length) - this.offset += length - return result - } - isEnd() { - return this.offset === this.byteView.length - } - addPadding() { - const offset = this.offset % 4 - if (offset > 0) - this.offset += 4 - offset - } -} - -const toUint32 = (buf: Buffer) => { - let ln, res - if (!isNode) //TODO browser behavior not equals, why? - return new Uint32Array( buf ) - if (buf.readUInt32LE) { - ln = buf.byteLength / 4 - res = new Uint32Array( ln ) - for (let i = 0; i < ln; i++) - res[i] = buf.readUInt32LE( i*4 ) - } else { - //$FlowIssue - const data = new DataView( buf ) - ln = data.byteLength / 4 - res = new Uint32Array( ln ) - for (let i = 0; i < ln; i++) - res[i] = data.getUint32( i*4, true ) - } - return res -} diff --git a/src/util/dtime.js b/src/util/dtime.js new file mode 100644 index 0000000..2d16cdf --- /dev/null +++ b/src/util/dtime.js @@ -0,0 +1,7 @@ +//@flow + +const logTimer = (new Date()).getTime() + +export const dTime = () => `[${(((new Date()).getTime() -logTimer) / 1000).toFixed(3)}]` + +export default dTime \ No newline at end of file diff --git a/src/util/log.js b/src/util/log.js index f680731..1b674fc 100644 --- a/src/util/log.js +++ b/src/util/log.js @@ -1,5 +1,8 @@ //@flow +// import memoize from 'memoizee' +// import pretty from 'pretty-format' + import Debug from 'debug' import trim from 'ramda/src/trim' @@ -18,7 +21,8 @@ import unapply from 'ramda/src/unapply' import unnest from 'ramda/src/unnest' import tail from 'ramda/src/tail' -import { dTime } from '../service/time-manager' +import dTime from './dtime' +import { immediate } from './smart-timeout' type VariString = string | string[] @@ -40,11 +44,62 @@ const fullNormalize: FullNormalize = pipe( const stringNormalize = when( both(is(String), e => e.length > 50), - take(50) + take(150) ) +// const isSimple = either( +// is(String), +// is(Number) +// ) + +// const prettify = unless( +// isSimple, +// pretty +// ) + +const genericLogger = Debug('telegram-mtproto') + +class LogEvent { + log: typeof genericLogger + values: mixed[] + constructor(log: typeof genericLogger, values: mixed[]) { + this.log = log + this.values = values + } + print() { + this.log(...this.values) + } +} + +class Sheduler { + queue: LogEvent[][] = [] + buffer: LogEvent[] = [] + add = (log: typeof genericLogger, time: string, tagStr: string, values: mixed[]) => { + const results = values.map(stringNormalize) + const first = results[0] || '' + const other = tail(results) + const firstLine = [tagStr, time, first].join(' ') + this.buffer.push(new LogEvent(log, [firstLine, ...other])) + } + sheduleBuffer = () => { + this.queue.push(this.buffer) + this.buffer = [] + } + print = () => { + for (const buffer of this.queue) + for (const logEvent of buffer) + logEvent.print() + this.queue = [] + } + constructor() { + setInterval(this.sheduleBuffer, 50) + setInterval(this.print, 300) + } +} + +const sheduler = new Sheduler -const Logger = (moduleName: VariString, ...thanksFlow: string[]) => { - const fullModule: string[] = arrify(moduleName, ...thanksFlow) +const Logger = (moduleName: VariString, ...rest: string[]) => { + const fullModule: string[] = arrify(moduleName, ...rest) fullModule.unshift('telegram-mtproto') const fullname = fullModule.join(':') const debug = Debug(fullname) @@ -52,11 +107,7 @@ const Logger = (moduleName: VariString, ...thanksFlow: string[]) => { const tagStr = fullNormalize(tags) return (...objects: any[]) => { const time = dTime() - const results = objects.map(stringNormalize) - const first = results[0] || '' - const other = tail(results) - const firstLine = [tagStr, time, first].join(' ') - setTimeout(debug, 200, firstLine, ...other) + immediate(sheduler.add, debug, time, tagStr, objects) } } return logger diff --git a/test/node.test.js b/test/node.test.js index cfc3745..df5468c 100644 --- a/test/node.test.js +++ b/test/node.test.js @@ -1,4 +1,4 @@ -const test = require('tap').test +const { test } = require('tap') const { MTProto } = require('../lib') const phone = { @@ -7,12 +7,12 @@ const phone = { } const api = { - invokeWithLayer: 0xda9b0d0d, - layer : 57, - initConnection : 0x69796de9, - api_id : 49631, - app_version : '1.0.1', - lang_code : 'en' + // invokeWithLayer: 0xda9b0d0d, + layer : 57, + initConnection: 0x69796de9, + api_id : 49631, + app_version : '1.0.1', + lang_code : 'en' } const server = { dev : true, From b42758e13c03cceb487cf377e7e7d2c341b23fed Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Fri, 24 Mar 2017 03:34:58 +0300 Subject: [PATCH 07/15] Move methods from Serialize to TypeWriter Signed-off-by: Dmitry Boldyrev --- .flowconfig | 3 +- src/bin.js | 12 +- src/service/api-manager/index.h.js | 2 +- src/service/api-manager/index.js | 2 +- src/service/main/index.h.js | 2 +- src/service/rsa-keys-manger.js | 19 ++- src/tl/index.h.js | 28 ++++ src/tl/index.js | 235 ++++++++++++++--------------- src/tl/type-buffer.js | 69 +++++---- 9 files changed, 204 insertions(+), 168 deletions(-) create mode 100644 src/tl/index.h.js diff --git a/.flowconfig b/.flowconfig index 83de378..dfedf54 100644 --- a/.flowconfig +++ b/.flowconfig @@ -13,4 +13,5 @@ [options] suppress_comment=\\(.\\|\n\\)*\\$FlowIssue -suppress_type=$FlowIssue \ No newline at end of file +suppress_type=$FlowIssue +unsafe.enable_getters_and_setters=true \ No newline at end of file diff --git a/src/bin.js b/src/bin.js index 9c24f73..7380407 100644 --- a/src/bin.js +++ b/src/bin.js @@ -14,6 +14,16 @@ import { eGCD_, greater, divide_, str2bigInt, equalsInt, const rushaInstance = new Rusha(1024 * 1024) + + +export function stringToChars(str: string) { + const ln = str.length + const result: number[] = Array(ln) + for (let i = 0; i < ln; ++i) + result[i] = str.charCodeAt(i) + return result +} + export const strDecToHex = str => toLower( bigInt2str( str2bigInt(str, 10, 0), 16 @@ -27,7 +37,7 @@ export function bytesToHex(bytes = []) { return arr.join('') } -export function bytesFromHex(hexString) { +export function bytesFromHex(hexString: string) { const len = hexString.length let start = 0 const bytes = [] diff --git a/src/service/api-manager/index.h.js b/src/service/api-manager/index.h.js index abb1aae..28c8420 100644 --- a/src/service/api-manager/index.h.js +++ b/src/service/api-manager/index.h.js @@ -25,7 +25,7 @@ export type AsyncStorage = { //TODO remove this noPrefix(): void } -type Cached = { +export type Cached = { [id: number]: Model } diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index d4ea30d..7d1fdef 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -26,7 +26,7 @@ import { AuthKeyError } from '../../error' import { bytesFromHex, bytesToHex } from '../../bin' import type { TLFabric } from '../../tl' -import type { TLSchema } from '../../tl/type-buffer' +import type { TLSchema } from '../../tl/index.h' import { switchErrors } from './error-cases' import { delayedCall } from '../../util/smart-timeout' diff --git a/src/service/main/index.h.js b/src/service/main/index.h.js index 84e2114..d020d8b 100644 --- a/src/service/main/index.h.js +++ b/src/service/main/index.h.js @@ -1,6 +1,6 @@ //@flow -import type { TLSchema } from '../../tl/type-buffer' +import type { TLSchema } from '../../tl/index.h' export type ApiConfig = { invokeWithLayer?: number, diff --git a/src/service/rsa-keys-manger.js b/src/service/rsa-keys-manger.js index 324c1fc..a2f7cf1 100644 --- a/src/service/rsa-keys-manger.js +++ b/src/service/rsa-keys-manger.js @@ -1,13 +1,20 @@ +//@flow + +import type { PublicKey } from './main/index.h' +import type { Cached } from './api-manager/index.h' + import Promise from 'bluebird' import { bytesToHex, sha1BytesSync, bytesFromHex, strDecToHex } from '../bin' import type { SerializationFabric } from '../tl' -export const KeyManager = (Serialization: SerializationFabric, publisKeysHex, publicKeysParsed) => { +export const KeyManager = (Serialization: SerializationFabric, + publisKeysHex: PublicKey[], + publicKeysParsed: Cached) => { let prepared = false - const mapPrepare = ({ modulus, exponent }) => { + const mapPrepare = ({ modulus, exponent }: PublicKey) => { const RSAPublicKey = Serialization() RSAPublicKey.storeBytes(bytesFromHex(modulus), 'n') RSAPublicKey.storeBytes(bytesFromHex(exponent), 'e') @@ -31,15 +38,15 @@ export const KeyManager = (Serialization: SerializationFabric, publisKeysHex, pu prepared = true } - async function selectRsaKeyByFingerPrint(fingerprints) { + async function selectRsaKeyByFingerPrint(fingerprints: string[]) { await prepareRsaKeys() let fingerprintHex, foundKey - for (let i = 0; i < fingerprints.length; i++) { - fingerprintHex = strDecToHex(fingerprints[i]) + for (const fingerprint of fingerprints) { + fingerprintHex = strDecToHex(fingerprint) foundKey = publicKeysParsed[fingerprintHex] if (foundKey) - return { fingerprint: fingerprints[i], ...foundKey } + return { fingerprint, ...foundKey } } return false } diff --git a/src/tl/index.h.js b/src/tl/index.h.js new file mode 100644 index 0000000..716806f --- /dev/null +++ b/src/tl/index.h.js @@ -0,0 +1,28 @@ +//@flow + +export type BinaryData = number[] | Uint8Array + +type TLParam = { + name: string, + type: string +} + +export type TLConstruct = { + id: string, + type: string, + predicate: string, + params: TLParam[] +} + +type TLMethod = { + id: string, + type: string, + method: string, + params: TLParam[] +} + +export type TLSchema = { + constructors: TLConstruct[], + methods: TLMethod[], + constructorsIndex?: number[] +} \ No newline at end of file diff --git a/src/tl/index.js b/src/tl/index.js index 2c7f07c..67a112a 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -3,99 +3,124 @@ import is from 'ramda/src/is' import { uintToInt, intToUint, bytesToHex, - gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' + gzipUncompress, bytesToArrayBuffer, longToInts, lshift32, stringToChars } from '../bin' import Logger from '../util/log' const debug = Logger`tl` -import { readInt, TypeBuffer, getNakedType, +import { readInt, TypeBuffer, TypeWriter, getNakedType, getPredicate, getString, getTypeConstruct } from './type-buffer' -import type { TLSchema } from './type-buffer' +import type { BinaryData, TLSchema } from './index.h' const PACKED = 0x3072cfa1 +type SerialConstruct = { + mtproto: boolean, + startMaxLength: number +} + +const normalizeBytes = (bytes?: number[] | ArrayBuffer | string) => { + let list, length + if (bytes instanceof ArrayBuffer) { + list = new Uint8Array(bytes) + length = bytes.byteLength + } else if (bytes === undefined) { + list = [] + length = 0 + } else if (typeof bytes === 'string') { + list = + stringToChars( + unescape( + encodeURIComponent( + bytes))) + length = list.length + } else { + list = bytes + length = bytes.length + } + return { + list, + length + } +} + +const binaryDataGuard = (bytes: BinaryData | ArrayBuffer) => { + let list + if (bytes instanceof ArrayBuffer) + list = new Uint8Array(bytes) + else + list = bytes + return list +} + export class Serialization { - maxLength: number - offset: number = 0 // in bytes - buffer: ArrayBuffer - intView: Int32Array - byteView: Uint8Array + writer: TypeWriter = new TypeWriter() mtproto: boolean api: TLSchema mtApi: TLSchema - constructor({ mtproto, startMaxLength }, api: TLSchema, mtApi: TLSchema) { + constructor({ mtproto, startMaxLength }: SerialConstruct, api: TLSchema, mtApi: TLSchema) { this.api = api this.mtApi = mtApi this.maxLength = startMaxLength - this.createBuffer() + this.writer.reset() this.mtproto = mtproto } - createBuffer() { - this.buffer = new ArrayBuffer(this.maxLength) - this.intView = new Int32Array(this.buffer) - this.byteView = new Uint8Array(this.buffer) + get maxLength(): number { + return this.writer.maxLength } - - getArray() { - const resultBuffer = new ArrayBuffer(this.offset) - const resultArray = new Int32Array(resultBuffer) - - resultArray.set(this.intView.subarray(0, this.offset / 4)) - - return resultArray + set maxLength(maxLength: number) { + this.writer.maxLength = maxLength } - getBuffer() { - return this.getArray().buffer + get offset(): number { + return this.writer.offset + } + set offset(offset: number) { + this.writer.offset = offset } - getBytes(typed) { - if (typed) { - const resultBuffer = new ArrayBuffer(this.offset) - const resultArray = new Uint8Array(resultBuffer) - - resultArray.set(this.byteView.subarray(0, this.offset)) - - return resultArray - } + get buffer(): ArrayBuffer { + return this.writer.buffer + } - const bytes = [] - for (let i = 0; i < this.offset; i++) { - bytes.push(this.byteView[i]) - } - return bytes + get intView(): Int32Array { + return this.writer.intView } - checkLength(needBytes) { - if (this.offset + needBytes < this.maxLength) { - return - } + get byteView(): Uint8Array { + return this.writer.byteView + } - console.trace('Increase buffer', this.offset, needBytes, this.maxLength) - this.maxLength = Math.ceil(Math.max(this.maxLength * 2, this.offset + needBytes + 16) / 4) * 4 - const previousBuffer = this.buffer - const previousArray = new Int32Array(previousBuffer) + getArray() { + return this.writer.getArray() + } - this.createBuffer() + getBuffer() { + return this.writer.getArray().buffer + } - new Int32Array(this.buffer).set(previousArray) + getBytes(typed: boolean) { + if (typed) + return this.writer.getBytesTyped() + else + return this.writer.getBytesPlain() } - writeInt(i, field) { - // this.debug && console.log('>>>', i.toString(16), i, field) + checkLength(needBytes: number) { + this.writer.checkLength(needBytes) + } - this.checkLength(4) - this.intView[this.offset / 4] = i - this.offset += 4 + writeInt(i: number, field: string) { + this.writer.writeInt(i, field) } storeIntString = (value: number | string, field: string) => { switch (typeof value) { - case 'string': return this.storeString(value, field) + case 'string': return this.storeBytes(value, `${field}:string`) case 'number': return this.storeInt(value, field) default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) } @@ -119,7 +144,7 @@ export class Serialization { } storeLong(sLong, field: string = '') { - if (is(Array, sLong)) + if (Array.isArray(sLong)) return sLong.length === 2 ? this.storeLongP(sLong[0], sLong[1], field) : this.storeIntBytes(sLong, 64, field) @@ -144,87 +169,45 @@ export class Serialization { this.writeInt(intView[1], `${ field }:double[high]`) } - storeString(s, field = '') { - // this.debug && console.log('>>>', s, `${ field }:string`) - - if (s === undefined) - s = '' - const sUTF8 = unescape(encodeURIComponent(s)) - - this.checkLength(sUTF8.length + 8) - - const len = sUTF8.length - if (len <= 253) { - this.byteView[this.offset++] = len - } else { - this.byteView[this.offset++] = 254 - this.byteView[this.offset++] = len & 0xFF - this.byteView[this.offset++] = (len & 0xFF00) >> 8 - this.byteView[this.offset++] = (len & 0xFF0000) >> 16 - } - for (let i = 0; i < len; i++) - this.byteView[this.offset++] = sUTF8.charCodeAt(i) - - // Padding - while (this.offset % 4) - this.byteView[this.offset++] = 0 - } - - storeBytes(bytes, field = '') { - if (bytes instanceof ArrayBuffer) { - bytes = new Uint8Array(bytes) - } - else if (bytes === undefined) - bytes = [] + storeBytes(bytes?: number[] | ArrayBuffer | string, field: string = '') { + const { list, length } = normalizeBytes(bytes) // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:bytes`) - const len = bytes.byteLength || bytes.length - this.checkLength(len + 8) - if (len <= 253) { - this.byteView[this.offset++] = len + this.checkLength(length + 8) + if (length <= 253) { + this.writer.next(length) } else { - this.byteView[this.offset++] = 254 - this.byteView[this.offset++] = len & 0xFF - this.byteView[this.offset++] = (len & 0xFF00) >> 8 - this.byteView[this.offset++] = (len & 0xFF0000) >> 16 + this.writer.next(254) + this.writer.next(length & 0xFF) + this.writer.next((length & 0xFF00) >> 8) + this.writer.next((length & 0xFF0000) >> 16) } - this.byteView.set(bytes, this.offset) - this.offset += len - - // Padding - while (this.offset % 4) { - this.byteView[this.offset++] = 0 - } + this.writer.set(list, length) + this.writer.addPadding() } - storeIntBytes(bytes, bits, field = '') { - if (bytes instanceof ArrayBuffer) { - bytes = new Uint8Array(bytes) - } - const len = bytes.length + storeIntBytes(bytes: BinaryData | ArrayBuffer, bits: number, field: string = '') { + const data = binaryDataGuard(bytes) + const len = data.length if (bits % 32 || len * 8 != bits) { - throw new Error(`Invalid bits: ${ bits }, ${ bytes.length}`) + throw new Error(`Invalid bits: ${ bits }, ${len}`) } // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:int${ bits}`) this.checkLength(len) - this.byteView.set(bytes, this.offset) - this.offset += len + this.writer.set(data, len) } - storeRawBytes(bytes, field = '') { - if (bytes instanceof ArrayBuffer) { - bytes = new Uint8Array(bytes) - } - const len = bytes.length + storeRawBytes(bytes: BinaryData | ArrayBuffer, field: string = '') { + const data = binaryDataGuard(bytes) + const len = data.length // this.debug && console.log('>>>', bytesToHex(bytes), field) this.checkLength(len) - this.byteView.set(bytes, this.offset) - this.offset += len + this.writer.set(data, len) } storeMethod(methodName: string, params) { @@ -262,7 +245,7 @@ export class Serialization { if (!stored) throw new Error(`Method ${methodName}.`+ ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`)*/ - this.storeObject(stored, type, `f{${methodName}}(${paramName})`) + this.storeObject(stored, type, `f ${methodName}(${paramName})`) } return methodData.type @@ -290,7 +273,7 @@ export class Serialization { case 'int512': return this.storeIntBytes(obj, 512, field) case 'string': - return this.storeString(obj, field) + return this.storeBytes(obj, `${field}:string`) case 'bytes': return this.storeBytes(obj, field) case 'double': @@ -302,9 +285,8 @@ export class Serialization { } if (is(Array, obj)) { - if (type.substr(0, 6) == 'Vector') { - this.writeInt(0x1cb5c415, `${field }[id]`) - } + if (type.substr(0, 6) == 'Vector') + this.writeInt(0x1cb5c415, `${field}[id]`) else if (type.substr(0, 6) != 'vector') { throw new Error(`Invalid vector type ${ type}`) } @@ -431,7 +413,7 @@ export class Deserialization { let s try { - s = decodeURIComponent(escape(sUTF8)) + s = decodeURIComponent(encodeURIComponent(sUTF8)) } catch (e) { s = sUTF8 } @@ -487,7 +469,12 @@ export class Deserialization { const compressed = this.fetchBytes(`${field}[packed_string]`) const uncompressed = gzipUncompress(compressed) const buffer = bytesToArrayBuffer(uncompressed) - const newDeserializer = new Deserialization(buffer, { mtproto: this.mtproto, override: this.override }, this.api, this.mtApi) + const newDeserializer = new Deserialization( + buffer, { + mtproto : this.mtproto, + override: this.override + }, + this.api, this.mtApi) return newDeserializer.fetchObject(type, field) } diff --git a/src/tl/type-buffer.js b/src/tl/type-buffer.js index b64291f..f8d6e93 100644 --- a/src/tl/type-buffer.js +++ b/src/tl/type-buffer.js @@ -11,30 +11,8 @@ import { TypeBufferIntError } from '../error' // import { bigint, uintToInt, intToUint, bytesToHex, // gzipUncompress, bytesToArrayBuffer, longToInts, lshift32 } from '../bin' -type TLParam = { - name: string, - type: string -} - -export type TLConstruct = { - id: string, - type: string, - predicate: string, - params: TLParam[] -} +import type { BinaryData, TLConstruct, TLSchema } from './index.h' -type TLMethod = { - id: string, - type: string, - method: string, - params: TLParam[] -} - -export type TLSchema = { - constructors: TLConstruct[], - methods: TLMethod[], - constructorsIndex?: number[] -} type HasTypeBuffer = { typeBuffer: TypeBuffer @@ -103,20 +81,24 @@ const writeIntLog = (i: number, field: string) => { } export class TypeWriter { - offset: number = 0 + offset: number = 0 // in bytes buffer: ArrayBuffer intView: Int32Array byteView: Uint8Array maxLength: number - constructor(startMaxLength: number) { - this.maxLength = startMaxLength - this.reset() + constructor(/*startMaxLength: number*/) { + // this.maxLength = startMaxLength + // this.reset() } reset() { this.buffer = new ArrayBuffer(this.maxLength) this.intView = new Int32Array(this.buffer) this.byteView = new Uint8Array(this.buffer) } + set(list: BinaryData, length: number) { + this.byteView.set(list, this.offset) + this.offset += length + } next(data: number) { this.byteView[this.offset] = data this.offset++ @@ -138,8 +120,31 @@ export class TypeWriter { new Int32Array(this.buffer).set(previousArray) } - writeInt(i: number) { - // immediate(writeIntLog, i, field) + getArray() { + const resultBuffer = new ArrayBuffer(this.offset) + const resultArray = new Int32Array(resultBuffer) + + resultArray.set(this.intView.subarray(0, this.offset / 4)) + + return resultArray + } + getBytesTyped() { + const resultBuffer = new ArrayBuffer(this.offset) + const resultArray = new Uint8Array(resultBuffer) + + resultArray.set(this.byteView.subarray(0, this.offset)) + + return resultArray + } + getBytesPlain() { + const bytes = [] + for (let i = 0; i < this.offset; i++) { + bytes.push(this.byteView[i]) + } + return bytes + } + writeInt(i: number, field: string) { + immediate(writeIntLog, i, field) this.checkLength(4) this.intView[this.offset / 4] = i @@ -149,7 +154,7 @@ export class TypeWriter { while (this.offset % 4) this.next(0) } - writeArray(list?: ArrayBuffer | string | number[]) { + /*writeArray(list?: ArrayBuffer | string | number[]) { let iter, length if (list === undefined) @@ -195,11 +200,9 @@ export class TypeWriter { this.checkLength(length) this.set(iter, length) - } + }*/ } -const stringToNums = (str: string) => [...str].map(e => e.charCodeAt(0)) - export class TypeBuffer { offset: number = 0 buffer: Buffer From aa82fd565379ae26dc4cff06020812ae6895732e Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Fri, 24 Mar 2017 06:42:26 +0300 Subject: [PATCH 08/15] Simplify Serializer Signed-off-by: Dmitry Boldyrev --- src/bin.js | 2 +- src/service/authorizer/index.js | 14 +- src/service/authorizer/send-plain-req.js | 11 +- src/service/networker/index.js | 59 ++++--- src/service/rsa-keys-manger.js | 14 +- src/tl/index.js | 191 ++++------------------- src/tl/mediator.js | 120 ++++++++++++++ src/tl/type-buffer.js | 54 +------ 8 files changed, 214 insertions(+), 251 deletions(-) create mode 100644 src/tl/mediator.js diff --git a/src/bin.js b/src/bin.js index 7380407..6335017 100644 --- a/src/bin.js +++ b/src/bin.js @@ -173,7 +173,7 @@ const dividerLem = str2bigInt('100000000', 16, 4) // () => console.log(`Timer L ${timeL} B ${timeB}`, ...a, ...b, n || ''), // 100) -export function longToInts(sLong) { +export function longToInts(sLong: string) { const lemNum = str2bigInt(sLong, 10, 6) const div = new Array(lemNum.length) const rem = new Array(lemNum.length) diff --git a/src/service/authorizer/index.js b/src/service/authorizer/index.js index b1ee4c4..a6f15b0 100644 --- a/src/service/authorizer/index.js +++ b/src/service/authorizer/index.js @@ -117,7 +117,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre log('Send req_pq')(bytesToHex(auth.nonce)) const request = Serialization({ mtproto: true }) - + const reqBox = request.writer request.storeMethod('req_pq', { nonce: auth.nonce }) const keyFoundCheck = key => key @@ -161,7 +161,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre .then(factDone, factFail) } - const sendPlainThunk = () => sendPlainReq(auth.dcUrl, request.getBuffer()) + const sendPlainThunk = () => sendPlainReq(auth.dcUrl, reqBox.getBuffer()) return prepare() .then(sendPlainThunk) @@ -178,6 +178,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre random(auth.newNonce) const data = Serialization({ mtproto: true }) + const dataBox = data.writer data.storeObject({ _ : 'p_q_inner_data', pq : auth.pq, @@ -188,9 +189,10 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre new_nonce : auth.newNonce }, 'P_Q_inner_data', 'DECRYPTED_DATA') - const dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()) + const dataWithHash = sha1BytesSync(dataBox.getBuffer()).concat(data.getBytes()) const request = Serialization({ mtproto: true }) + const reqBox = request.writer request.storeMethod('req_DH_params', { nonce : auth.nonce, server_nonce : auth.serverNonce, @@ -240,7 +242,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre } log('afterReqDH')('Send req_DH_params') - return sendPlainReq(auth.dcUrl, request.getBuffer()) + return sendPlainReq(auth.dcUrl, reqBox.getBuffer()) .then(afterReqDH, deferred.reject) } @@ -426,7 +428,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre g_b : gB }, 'Client_DH_Inner_Data') - const dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes()) + const dataWithHash = sha1BytesSync(data.writer.getBuffer()).concat(data.getBytes()) const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv) @@ -438,7 +440,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre }) log('onGb')('Send set_client_DH_params') - return sendPlainReq(auth.dcUrl, request.getBuffer()) + return sendPlainReq(auth.dcUrl, request.writer.getBuffer()) .then(afterPlainRequest) } diff --git a/src/service/authorizer/send-plain-req.js b/src/service/authorizer/send-plain-req.js index 65ab68d..48ea896 100644 --- a/src/service/authorizer/send-plain-req.js +++ b/src/service/authorizer/send-plain-req.js @@ -9,6 +9,7 @@ import allPass from 'ramda/src/allPass' import httpClient from '../../http' import { ErrorBadResponse, ErrorNotFound } from '../../error' import { generateID } from '../time-manager' +import { WriteMediator } from '../../tl' import type { TLFabric } from '../../tl' @@ -21,11 +22,13 @@ const SendPlain = ({ Serialization, Deserialization }: TLFabric) => { requestArray = new Int32Array(requestBuffer) const header = Serialization() - header.storeLongP(0, 0, 'auth_key_id') // Auth key - header.storeLong(generateID(), 'msg_id') // Msg_id - header.storeInt(requestLength, 'request_length') + const headBox = header.writer - const headerBuffer: ArrayBuffer = header.getBuffer(), + WriteMediator.longP(headBox, 0, 0, 'auth_key_id') // Auth key + WriteMediator.long(headBox, generateID(), 'msg_id') // Msg_id + WriteMediator.int(headBox, requestLength, 'request_length') + + const headerBuffer: ArrayBuffer = headBox.getBuffer(), headerArray = new Int32Array(headerBuffer) const headerLength = headerBuffer.byteLength diff --git a/src/service/networker/index.js b/src/service/networker/index.js index 063aa31..9124385 100644 --- a/src/service/networker/index.js +++ b/src/service/networker/index.js @@ -26,6 +26,7 @@ import { convertToUint8Array, convertToArrayBuffer, sha1BytesSync, bytesToArrayBuffer, longToBytes, uintToInt, rshift32 } from '../../bin' import type { TLFabric, SerializationFabric, DeserializationFabric } from '../../tl' +import { WriteMediator, TypeWriter } from '../../tl' import type { Emit } from '../main/index.h' let updatesProcessor @@ -50,6 +51,14 @@ type ContextConfig = { emit: Emit } +const storeIntString = (writer: TypeWriter) => (value: number | string, field: string) => { + switch (typeof value) { + case 'string': return WriteMediator.bytes(writer, value, `${field}:string`) + case 'number': return WriteMediator.int(writer, value, field) + default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) + } +} + export class NetworkerThread { dcID: number authKey: string @@ -187,7 +196,7 @@ export class NetworkerThread { wrapApiCall(method: string, params: Object, options: NetOptions) { const serializer = this.Serialization(options) - + const serialBox = serializer.writer if (!this.connectionInited) { // serializer.storeInt(0xda9b0d0d, 'invokeWithLayer') // serializer.storeInt(Config.Schema.API.layer, 'layer') @@ -197,12 +206,13 @@ export class NetworkerThread { // serializer.storeString(navigator.platform || 'Unknown Platform', 'system_version') // serializer.storeString(Config.App.version, 'app_version') // serializer.storeString(navigator.language || 'en', 'lang_code') - mapObjIndexed(serializer.storeIntString, this.appConfig) + const mapper = storeIntString(serialBox) + mapObjIndexed(mapper, this.appConfig) } if (options.afterMessageID) { - serializer.storeInt(0xcb9f372d, 'invokeAfterMsg') - serializer.storeLong(options.afterMessageID, 'msg_id') + WriteMediator.int(serialBox, 0xcb9f372d, 'invokeAfterMsg') + WriteMediator.long(serialBox, options.afterMessageID, 'msg_id') } options.resultType = serializer.storeMethod(method, params) @@ -499,16 +509,17 @@ export class NetworkerThread { if (messages.length > 1) { const container = this.Serialization({ mtproto: true, startMaxLength: messagesByteLen + 64 }) - container.storeInt(0x73f1f8dc, 'CONTAINER[id]') - container.storeInt(messages.length, 'CONTAINER[count]') + const contBox = container.writer + WriteMediator.int(contBox, 0x73f1f8dc, 'CONTAINER[id]') + WriteMediator.int(contBox, messages.length, 'CONTAINER[count]') const innerMessages = [] let i = 0 for (const msg of messages) { - container.storeLong(msg.msg_id, `CONTAINER[${i}][msg_id]`) + WriteMediator.long(contBox, msg.msg_id, `CONTAINER[${i}][msg_id]`) innerMessages.push(msg.msg_id) - container.storeInt(msg.seq_no, `CONTAINER[${i}][seq_no]`) - container.storeInt(msg.body.length, `CONTAINER[${i}][bytes]`) - container.storeRawBytes(msg.body, `CONTAINER[${i}][body]`) + WriteMediator.int(contBox, msg.seq_no, `CONTAINER[${i}][seq_no]`) + WriteMediator.int(contBox, msg.body.length, `CONTAINER[${i}][bytes]`) + WriteMediator.intBytes(contBox, msg.body, false, `CONTAINER[${i}][body]`) if (msg.noResponse) noResponseMsgs.push(msg.msg_id) i++ @@ -580,19 +591,18 @@ export class NetworkerThread { // console.log(dTime(), 'Send encrypted'/*, message*/) // console.trace() const data = this.Serialization({ startMaxLength: message.body.length + 64 }) + const dataBox = data.writer + WriteMediator.intBytes(dataBox, this.serverSalt, 64, 'salt') + WriteMediator.intBytes(dataBox, this.sessionID, 64, 'session_id') + WriteMediator.long(dataBox, message.msg_id, 'message_id') + WriteMediator.int(dataBox, message.seq_no, 'seq_no') - data.storeIntBytes(this.serverSalt, 64, 'salt') - data.storeIntBytes(this.sessionID, 64, 'session_id') - - data.storeLong(message.msg_id, 'message_id') - data.storeInt(message.seq_no, 'seq_no') - - data.storeInt(message.body.length, 'message_data_length') - data.storeRawBytes(message.body, 'message_data') + WriteMediator.int(dataBox, message.body.length, 'message_data_length') + WriteMediator.intBytes(dataBox, message.body, false, 'message_data') const url = this.chooseServer(this.dcID, this.upload) - const bytes = data.getBuffer() + const bytes = dataBox.getBuffer() const bytesHash = await CryptoWorker.sha1Hash(bytes) const msgKey = new Uint8Array(bytesHash).subarray(4, 20) @@ -600,13 +610,14 @@ export class NetworkerThread { const encryptedBytes = await CryptoWorker.aesEncrypt(bytes, keyIv[0], keyIv[1]) const request = this.Serialization({ startMaxLength: encryptedBytes.byteLength + 256 }) - request.storeIntBytes(this.authKeyID, 64, 'auth_key_id') - request.storeIntBytes(msgKey, 128, 'msg_key') - request.storeRawBytes(encryptedBytes, 'encrypted_data') + const requestBox = request.writer + WriteMediator.intBytes(requestBox, this.authKeyID, 64, 'auth_key_id') + WriteMediator.intBytes(requestBox, msgKey, 128, 'msg_key') + WriteMediator.intBytes(requestBox, encryptedBytes, false, 'encrypted_data') const requestData = xhrSendBuffer - ? request.getBuffer() - : request.getArray() + ? requestBox.getArray().buffer + : requestBox.getArray() options = { responseType: 'arraybuffer', ...options } diff --git a/src/service/rsa-keys-manger.js b/src/service/rsa-keys-manger.js index a2f7cf1..0ca851d 100644 --- a/src/service/rsa-keys-manger.js +++ b/src/service/rsa-keys-manger.js @@ -1,13 +1,16 @@ //@flow +import Promise from 'bluebird' + import type { PublicKey } from './main/index.h' import type { Cached } from './api-manager/index.h' +import type { SerializationFabric } from '../tl' + +import { WriteMediator } from '../tl' -import Promise from 'bluebird' import { bytesToHex, sha1BytesSync, bytesFromHex, strDecToHex } from '../bin' -import type { SerializationFabric } from '../tl' export const KeyManager = (Serialization: SerializationFabric, publisKeysHex: PublicKey[], @@ -16,10 +19,11 @@ export const KeyManager = (Serialization: SerializationFabric, const mapPrepare = ({ modulus, exponent }: PublicKey) => { const RSAPublicKey = Serialization() - RSAPublicKey.storeBytes(bytesFromHex(modulus), 'n') - RSAPublicKey.storeBytes(bytesFromHex(exponent), 'e') + const rsaBox = RSAPublicKey.writer + WriteMediator.bytes(rsaBox, bytesFromHex(modulus), 'n') + WriteMediator.bytes(rsaBox, bytesFromHex(exponent), 'e') - const buffer = RSAPublicKey.getBuffer() + const buffer = rsaBox.getBuffer() const fingerprintBytes = sha1BytesSync(buffer).slice(-8) fingerprintBytes.reverse() diff --git a/src/tl/index.js b/src/tl/index.js index 67a112a..4368999 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -5,14 +5,15 @@ import is from 'ramda/src/is' import { uintToInt, intToUint, bytesToHex, gzipUncompress, bytesToArrayBuffer, longToInts, lshift32, stringToChars } from '../bin' -import Logger from '../util/log' - -const debug = Logger`tl` +import { WriteMediator } from './mediator' import { readInt, TypeBuffer, TypeWriter, getNakedType, getPredicate, getString, getTypeConstruct } from './type-buffer' import type { BinaryData, TLSchema } from './index.h' +import Logger from '../util/log' +const debug = Logger`tl` + const PACKED = 0x3072cfa1 type SerialConstruct = { @@ -20,7 +21,7 @@ type SerialConstruct = { startMaxLength: number } -const normalizeBytes = (bytes?: number[] | ArrayBuffer | string) => { +const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) => { let list, length if (bytes instanceof ArrayBuffer) { list = new Uint8Array(bytes) @@ -45,15 +46,6 @@ const normalizeBytes = (bytes?: number[] | ArrayBuffer | string) => { } } -const binaryDataGuard = (bytes: BinaryData | ArrayBuffer) => { - let list - if (bytes instanceof ArrayBuffer) - list = new Uint8Array(bytes) - else - list = bytes - return list -} - export class Serialization { writer: TypeWriter = new TypeWriter() mtproto: boolean @@ -63,153 +55,19 @@ export class Serialization { this.api = api this.mtApi = mtApi - this.maxLength = startMaxLength + this.writer.maxLength = startMaxLength this.writer.reset() this.mtproto = mtproto } - get maxLength(): number { - return this.writer.maxLength - } - set maxLength(maxLength: number) { - this.writer.maxLength = maxLength - } - - get offset(): number { - return this.writer.offset - } - set offset(offset: number) { - this.writer.offset = offset - } - - get buffer(): ArrayBuffer { - return this.writer.buffer - } - - get intView(): Int32Array { - return this.writer.intView - } - - get byteView(): Uint8Array { - return this.writer.byteView - } - - getArray() { - return this.writer.getArray() - } - - getBuffer() { - return this.writer.getArray().buffer - } - - getBytes(typed: boolean) { + getBytes(typed?: boolean) { if (typed) return this.writer.getBytesTyped() else return this.writer.getBytesPlain() } - checkLength(needBytes: number) { - this.writer.checkLength(needBytes) - } - - writeInt(i: number, field: string) { - this.writer.writeInt(i, field) - } - - storeIntString = (value: number | string, field: string) => { - switch (typeof value) { - case 'string': return this.storeBytes(value, `${field}:string`) - case 'number': return this.storeInt(value, field) - default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) - } - } - - storeInt = (i: number, field: string = '') => { - this.writeInt(i, `${ field }:int`) - } - - storeBool(i: boolean, field: string = '') { - if (i) { - this.writeInt(0x997275b5, `${ field }:bool`) - } else { - this.writeInt(0xbc799737, `${ field }:bool`) - } - } - - storeLongP(iHigh, iLow, field: string) { - this.writeInt(iLow, `${ field }:long[low]`) - this.writeInt(iHigh, `${ field }:long[high]`) - } - - storeLong(sLong, field: string = '') { - if (Array.isArray(sLong)) - return sLong.length === 2 - ? this.storeLongP(sLong[0], sLong[1], field) - : this.storeIntBytes(sLong, 64, field) - - if (typeof sLong !== 'string') - sLong = sLong - ? sLong.toString() - : '0' - const [int1, int2] = longToInts(sLong) - this.writeInt(int2, `${ field }:long[low]`) - this.writeInt(int1, `${ field }:long[high]`) - } - - storeDouble(f, field: string = '') { - const buffer = new ArrayBuffer(8) - const intView = new Int32Array(buffer) - const doubleView = new Float64Array(buffer) - - doubleView[0] = f - - this.writeInt(intView[0], `${ field }:double[low]`) - this.writeInt(intView[1], `${ field }:double[high]`) - } - - storeBytes(bytes?: number[] | ArrayBuffer | string, field: string = '') { - const { list, length } = normalizeBytes(bytes) - // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:bytes`) - - this.checkLength(length + 8) - if (length <= 253) { - this.writer.next(length) - } else { - this.writer.next(254) - this.writer.next(length & 0xFF) - this.writer.next((length & 0xFF00) >> 8) - this.writer.next((length & 0xFF0000) >> 16) - } - - this.writer.set(list, length) - this.writer.addPadding() - } - - storeIntBytes(bytes: BinaryData | ArrayBuffer, bits: number, field: string = '') { - const data = binaryDataGuard(bytes) - const len = data.length - if (bits % 32 || len * 8 != bits) { - throw new Error(`Invalid bits: ${ bits }, ${len}`) - } - - // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:int${ bits}`) - this.checkLength(len) - - this.writer.set(data, len) - } - - storeRawBytes(bytes: BinaryData | ArrayBuffer, field: string = '') { - const data = binaryDataGuard(bytes) - const len = data.length - - // this.debug && console.log('>>>', bytesToHex(bytes), field) - this.checkLength(len) - - this.writer.set(data, len) - } - storeMethod(methodName: string, params) { const schema = selectSchema(this.mtproto, this.api, this.mtApi) let methodData = false @@ -223,8 +81,9 @@ export class Serialization { if (!methodData) { throw new Error(`No method ${ methodName } found`) } - - this.storeInt(intToUint(methodData.id), `${methodName }[id]`) + WriteMediator.int(this.writer, + intToUint(methodData.id), + `${methodName }[id]`) let condType let fieldBit @@ -263,35 +122,35 @@ export class Serialization { switch (type) { case '#': case 'int': - return this.storeInt(obj, field) + return WriteMediator.int(this.writer, obj, field) case 'long': - return this.storeLong(obj, field) + return WriteMediator.long(this.writer, obj, field) case 'int128': - return this.storeIntBytes(obj, 128, field) + return WriteMediator.intBytes(this.writer, obj, 128, field) case 'int256': - return this.storeIntBytes(obj, 256, field) + return WriteMediator.intBytes(this.writer, obj, 256, field) case 'int512': - return this.storeIntBytes(obj, 512, field) + return WriteMediator.intBytes(this.writer, obj, 512, field) case 'string': - return this.storeBytes(obj, `${field}:string`) + return WriteMediator.bytes(this.writer, obj, `${field}:string`) case 'bytes': - return this.storeBytes(obj, field) + return WriteMediator.bytes(this.writer, obj, field) case 'double': - return this.storeDouble(obj, field) + return WriteMediator.double(this.writer, obj, field) case 'Bool': - return this.storeBool(obj, field) + return WriteMediator.bool(this.writer, obj, field) case 'true': return } if (is(Array, obj)) { if (type.substr(0, 6) == 'Vector') - this.writeInt(0x1cb5c415, `${field}[id]`) + WriteMediator.int(this.writer, 0x1cb5c415, `${field}[id]`) else if (type.substr(0, 6) != 'vector') { throw new Error(`Invalid vector type ${ type}`) } const itemType = type.substr(7, type.length - 8) // for "Vector" - this.writeInt(obj.length, `${field }[count]`) + WriteMediator.int(this.writer, obj.length, `${field}[count]`) for (let i = 0; i < obj.length; i++) { this.storeObject(obj[i], itemType, `${field }[${ i }]`) } @@ -319,13 +178,15 @@ export class Serialization { } } if (!constructorData) - throw new Error(`No predicate ${ predicate } found`) + throw new Error(`No predicate ${predicate} found`) if (predicate == type) isBare = true if (!isBare) - this.writeInt(intToUint(constructorData.id), `${field }.${ predicate }[id]`) + WriteMediator.int(this.writer, + intToUint(constructorData.id), + `${field}.${predicate}[id]`) let condType let fieldBit @@ -649,4 +510,6 @@ export const TL = (api: TLSchema, mtApi: TLSchema) => ({ new Deserialization(buffer, { mtproto, override }, api, mtApi) }) +export * from './mediator' +export { TypeWriter } from './type-buffer' export default TL \ No newline at end of file diff --git a/src/tl/mediator.js b/src/tl/mediator.js new file mode 100644 index 0000000..c8e13bf --- /dev/null +++ b/src/tl/mediator.js @@ -0,0 +1,120 @@ +//@flow + +import { TypeWriter } from './type-buffer' +import { longToInts, stringToChars } from '../bin' + +import Logger from '../util/log' +const log = Logger`tl, mediator` + +import type { BinaryData } from './index.h' + +export const WriteMediator = { + int(ctx: TypeWriter, i: number, field: string = '') { + ctx.writeInt(i, `${ field }:int`) + }, + bool(ctx: TypeWriter, i: boolean, field: string = '') { + if (i) { + ctx.writeInt(0x997275b5, `${ field }:bool`) + } else { + ctx.writeInt(0xbc799737, `${ field }:bool`) + } + }, + longP(ctx: TypeWriter, + iHigh: number, + iLow: number, + field: string) { + ctx.writePair(iLow, iHigh, + `${ field }:long[low]`, + `${ field }:long[high]`) + }, + long(ctx: TypeWriter, + sLong?: number[] | string | number, + field: string = '') { + if (Array.isArray(sLong)) + return sLong.length === 2 + ? this.longP(ctx, sLong[0], sLong[1], field) + : this.intBytes(ctx, sLong, 64, field) + let str + if (typeof sLong !== 'string') + str = sLong + ? sLong.toString() + : '0' + else str = sLong + const [int1, int2] = longToInts(str) + ctx.writePair(int2, int1, + `${ field }:long[low]`, + `${ field }:long[high]`) + }, + double(ctx: TypeWriter, f: number, field: string = '') { + const buffer = new ArrayBuffer(8) + const intView = new Int32Array(buffer) + const doubleView = new Float64Array(buffer) + + doubleView[0] = f + + const [int1, int2] = intView + ctx.writePair(int2, int1, + `${ field }:double[low]`, + `${ field }:double[high]`) + }, + bytes(ctx: TypeWriter, + bytes?: number[] | ArrayBuffer | string, + field: string = '') { + const { list, length } = binaryDataGuard(bytes) + // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:bytes`) + + ctx.checkLength(length + 8) + if (length <= 253) { + ctx.next(length) + } else { + ctx.next(254) + ctx.next(length & 0xFF) + ctx.next((length & 0xFF00) >> 8) + ctx.next((length & 0xFF0000) >> 16) + } + + ctx.set(list, length) + ctx.addPadding() + }, + intBytes(ctx: TypeWriter, + bytes: BinaryData | ArrayBuffer | string, + bits: number | false, + field: string = '') { + const { list, length } = binaryDataGuard(bytes) + + if (bits) { + if (bits % 32 || length * 8 != bits) { + throw new Error(`Invalid bits: ${ bits }, ${length}`) + } + } + // this.debug && console.log('>>>', bytesToHex(bytes), `${ field }:int${ bits}`) + ctx.checkLength(length) + ctx.set(list, length) + } +} + + +const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) => { + let list, length + if (bytes instanceof ArrayBuffer) { + list = new Uint8Array(bytes) + length = bytes.byteLength + } else if (bytes === undefined) { + list = [] + length = 0 + } else if (typeof bytes === 'string') { + list = + stringToChars( + unescape( + encodeURIComponent( + bytes))) + length = list.length + } else { + list = bytes + length = bytes.length + } + return { + list, + length + } +} diff --git a/src/tl/type-buffer.js b/src/tl/type-buffer.js index f8d6e93..f12c46c 100644 --- a/src/tl/type-buffer.js +++ b/src/tl/type-buffer.js @@ -128,6 +128,9 @@ export class TypeWriter { return resultArray } + getBuffer() { + return this.getArray().buffer + } getBytesTyped() { const resultBuffer = new ArrayBuffer(this.offset) const resultArray = new Uint8Array(resultBuffer) @@ -150,57 +153,14 @@ export class TypeWriter { this.intView[this.offset / 4] = i this.offset += 4 } + writePair(n1: number, n2: number, field1: string, field2: string) { + this.writeInt(n1, field1) + this.writeInt(n2, field2) + } addPadding() { while (this.offset % 4) this.next(0) } - /*writeArray(list?: ArrayBuffer | string | number[]) { - let iter, length - - if (list === undefined) - iter = [] - else if (typeof list === 'string') - iter = stringToNums(list) - // else if (list instanceof ArrayBuffer) - // iter = new Uint8Array(list) - else iter = list - - if (list instanceof ArrayBuffer) - length = list.byteLength - else - length = iter.length - - this.checkLength(length + 8) - - if (length <= 253) - this.next(length) - else { - this.next(254) - this.next(length & 0xFF) - this.next((length & 0xFF00) >> 8) - this.next((length & 0xFF0000) >> 16) - } - - this.set(iter, length) - - this.addPadding() - } - set(list: number[] | Uint8Array, length: number) { - this.byteView.set(list, this.offset) - this.offset += length - } - writeBytes(bytes: number[] | ArrayBuffer, cond: (ln: number) => void) { - let iter - if (bytes instanceof ArrayBuffer) - iter = new Uint8Array(bytes) - else - iter = bytes - const length = iter.length - cond(length) - this.checkLength(length) - - this.set(iter, length) - }*/ } export class TypeBuffer { From 796c8ac28715f76a77205c336cf6ac06d71e1a5d Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Fri, 24 Mar 2017 22:32:56 +0300 Subject: [PATCH 09/15] Add schema layout parser Signed-off-by: Dmitry Boldyrev --- .babelrc | 7 +- package.json | 6 +- src/bin.js | 21 ++-- src/layout/index.h.js | 29 +++++ src/layout/index.js | 211 +++++++++++++++++++++++++++++++++++++ src/layout/schema-model.js | 88 ++++++++++++++++ src/tl/index.h.js | 4 +- src/tl/index.js | 29 +---- test/layout.test.js | 183 ++++++++++++++++++++++++++++++++ 9 files changed, 531 insertions(+), 47 deletions(-) create mode 100644 src/layout/index.h.js create mode 100644 src/layout/index.js create mode 100644 src/layout/schema-model.js create mode 100644 test/layout.test.js diff --git a/.babelrc b/.babelrc index e6ab419..a2a86d4 100644 --- a/.babelrc +++ b/.babelrc @@ -4,9 +4,7 @@ ], "plugins": [ "transform-flow-strip-types", - // ["fast-async", { - // "useRuntimeModule":true - // }], + "transform-exponentiation-operator", ["transform-es2015-for-of", { "loose": true }], @@ -16,6 +14,9 @@ // "module": "bluebird", // "method": "coroutine" // }], + ["transform-es2015-block-scoping", { + "throwIfClosureRequired": true + }], ["transform-object-rest-spread", { "useBuiltIns": true }], "closure-elimination" ], diff --git a/package.json b/package.json index de49024..aab2e2c 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "author": "Zero Bias", "dependencies": { "@goodmind/node-cryptojs-aes": "^0.5.0", + "@types/uuid": "^2.0.29", "ajv": "^4.11.5", "ajv-keywords": "^1.5.1", "axios": "^0.15.3", @@ -39,11 +40,12 @@ "ramda": "^0.23.0", "randombytes": "^2.0.3", "rusha": "^0.8.5", + "uuid": "^3.0.1", "worker-loader": "^0.8.0" }, "devDependencies": { - "@types/memoizee": "^0.4.0", "@types/debug": "0.0.29", + "@types/memoizee": "^0.4.0", "babel-cli": "^6.24.0", "babel-core": "^6.24.0", "babel-eslint": "^7.2.0", @@ -52,8 +54,10 @@ "babel-plugin-transform-async-to-generator": "^6.22.0", "babel-plugin-transform-async-to-module-method": "^6.22.0", "babel-plugin-transform-class-properties": "^6.23.0", + "babel-plugin-transform-es2015-block-scoping": "^6.23.0", "babel-plugin-transform-es2015-for-of": "^6.23.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", + "babel-plugin-transform-exponentiation-operator": "^6.22.0", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-plugin-transform-object-rest-spread": "^6.23.0", "cross-env": "^3.2.4", diff --git a/src/bin.js b/src/bin.js index 6335017..eb27f0f 100644 --- a/src/bin.js +++ b/src/bin.js @@ -205,22 +205,15 @@ export const rshift32 = str => { return bigInt2str(num, 10) } -// export function longFromLem(high, low) { -// const highNum = int2bigInt(high, 96, 0) -// leftShift_(highNum, 32) - -// addInt_(highNum, low) -// const res = bigInt2str(highNum, 10) -// return res -// } - -export function intToUint(val) { - val = parseInt(val) //TODO PERF parseInt is a perfomance issue - if (val < 0) - val = val + 0x100000000 - return val +export function intToUint(val: string) { + let result = ~~val + if (result < 0) + result = result + 0x100000000 + return result } + const middle = 0x100000000 / 2 - 1 + export function uintToInt(val) { if (val > middle) val = val - 0x100000000 diff --git a/src/layout/index.h.js b/src/layout/index.h.js new file mode 100644 index 0000000..5ad67ae --- /dev/null +++ b/src/layout/index.h.js @@ -0,0 +1,29 @@ +//@flow + +export type { TLMethod, TLConstruct, TLSchema } from '../tl/index.h' + +export type SchemaParam = { + name: string, + type: string +} + +export type SchemaElement = { + id: string, + type: string, + params: SchemaParam[] +} + +export type TLParam = { + name: string, + typeClass: string, + isVector: boolean, + isFlag: boolean, + flagIndex: number +} + +export type TLCreator = { + id: number, + name: string, + hasFlags: boolean, + params: TLParam[] +} \ No newline at end of file diff --git a/src/layout/index.js b/src/layout/index.js new file mode 100644 index 0000000..0b930fb --- /dev/null +++ b/src/layout/index.js @@ -0,0 +1,211 @@ +//@flow + +import has from 'ramda/src/has' +import flip from 'ramda/src/flip' +import contains from 'ramda/src/contains' + +import type { TLParam, TLCreator, SchemaElement, TLMethod, TLConstruct, TLSchema } from './index.h' + +class TypeClass { + name: string + + types: Set = new Set + constructor(name: string) { + this.name = name + } + isTypeOf(value: any): boolean { + return false + } +} + +class Argument { + id: string + name: string + typeClass: string + isVector: boolean + isFlag: boolean + flagIndex: number + fullType: string + constructor(id: string, + name: string, + typeClass: string, + isVector: boolean = false, + isFlag: boolean = false, + flagIndex: number = NaN) { + this.name = name + this.typeClass = typeClass + this.isVector = isVector + this.isFlag = isFlag + this.flagIndex = flagIndex + this.id = id + + this.fullType = Argument.fullType(this) + } + static fullType(obj: Argument) { + const { typeClass, isVector, isFlag, flagIndex } = obj + let result = typeClass + if (isVector) + result = `Vector<${result}>` + if (isFlag) + result = `flags.${flagIndex}?${result}` + return result + } +} + +class Creator { + id: number + name: string //predicate or method + hasFlags: boolean + params: Argument[] + constructor(id: number, + name: string, + hasFlags: boolean, + params: Argument[]) { + this.id = id + this.name = name + this.hasFlags = hasFlags + this.params = params + } +} + +class Method extends Creator { + returns: string + constructor(id: number, + name: string, + hasFlags: boolean, + params: Argument[], + returns: string) { + super(id, name, hasFlags, params) + this.returns = returns + } +} + +class Type extends Creator { + typeClass: string + constructor(id: number, + name: string, + hasFlags: boolean, + params: Argument[], + typeClass: string) { + super(id, name, hasFlags, params) + this.typeClass = typeClass + } +} + + +export class Layout { + typeClasses: Map = new Map + creators: Set = new Set + args: Map = new Map + funcs: Map = new Map + types: Map = new Map + schema: TLSchema + makeCreator(elem: SchemaElement, + name: string, + sign: string, + Construct: typeof Method | typeof Type) { + const args: Argument[] = [] + let hasFlags = false + for (const [ i, param ] of elem.params.entries()) { + const id = `${name}.${param.name}/${i}` + const { typeClass, isVector, isFlag, flagIndex } = getTypeProps(param.type) + if (isFlag) hasFlags = true + this.pushTypeClass(typeClass) + const arg = new Argument(id, param.name, typeClass, isVector, isFlag, flagIndex) + args.push(arg) + this.args.set(id, arg) + } + const id = parseInt(elem.id, 10) + const creator = new Construct(id, name, hasFlags, args, sign) + this.creators.add(name) + if (creator instanceof Method) + this.funcs.set(name, creator) + else if (creator instanceof Type) + this.types.set(name, creator) + } + makeMethod(elem: TLMethod) { + const name = elem.method + const returns = elem.type + this.pushTypeClass(returns, name) + this.makeCreator(elem, name, returns, Method) + } + pushTypeClass(typeClass: string, type?: string) { + let instance + if (this.typeClasses.has(typeClass)) + instance = this.typeClasses.get(typeClass) + else { + instance = new TypeClass(typeClass) + this.typeClasses.set(typeClass, instance) + } + if (type && instance) + instance.types.add(type) + } + makeType(elem: TLConstruct) { + const name = elem.predicate + const typeClass = elem.type + this.pushTypeClass(typeClass, name) + this.makeCreator(elem, name, typeClass, Type) + } + makeLayout(schema: TLSchema) { + const { methods, constructors } = schema + constructors.map(this.makeType) + methods.map(this.makeMethod) + } + constructor(schema: TLSchema) { + //$FlowIssue + this.makeType = this.makeType.bind(this) + //$FlowIssue + this.makeMethod = this.makeMethod.bind(this) + // this.schema = schema + this.makeLayout(schema) + } +} +const hasQuestion = contains('?') +const hasVector = contains('<') + +const getTypeProps = (rawType: string) => { + const result = { + typeClass: rawType, + isVector : false, + isFlag : false, + flagIndex: NaN + } + if (hasQuestion(rawType)) { + const [ prefix, rest ] = rawType.split('?') + const [ , index ] = prefix.split('.') + result.isFlag = true + result.flagIndex = +index + result.typeClass = rest + } + if (hasVector(result.typeClass)) { + result.isVector = true + result.typeClass = result.typeClass.slice(7, -1) + } + return result +} + +const isSimpleType: (type: string) => boolean = + flip(contains)( + ['int', 'long', 'string', 'double', 'true', 'bytes']) + + +const getFlagsRed = + (data: Object) => + (acc: number, { name, flagIndex }: TLParam) => + has(name, data) + ? acc + 2 ** flagIndex + : acc + +export const getFlags = ({ params }: TLCreator) => { + const flagsParams = + params.filter( + (e: TLParam) => e.isFlag) + + return (data: Object) => + flagsParams + .reduce( + getFlagsRed(data), + 0) +} + +export default Layout \ No newline at end of file diff --git a/src/layout/schema-model.js b/src/layout/schema-model.js new file mode 100644 index 0000000..b9e278e --- /dev/null +++ b/src/layout/schema-model.js @@ -0,0 +1,88 @@ +//@flow + +import uuid from 'uuid/v1' +import contains from 'ramda/src/contains' +import split from 'ramda/src/split' + +import type { TLSchema, TLConstruct, TLMethod, TLParam } from '../tl/index.h' + +class SchemaFunc { + id: string + type: string + method: string + params: TLParam[] + + hasFlags: boolean + flags: TLParam[] +} + +class TLType { + name: string + + creators: string[] + + isTypeOf(value: any): boolean { + return false + } +} + +class TLArg { + name: string + type: string + isVector: boolean + isFlag: boolean + flagIndex: number + + link: string = uuid() + constructor(name: string, + type: string, + isVector: boolean = false, + isFlag: boolean = false, + flagIndex: number = NaN) { + this.name = name + this.type = type + this.isVector = isVector + this.isFlag = isFlag + this.flagIndex = flagIndex + } + toString() { + let result = this.type + if (this.isVector) + result = TLArg.toVector(result) + if (this.isFlag) + result = TLArg.toFlags(this.flagIndex, result) + return result + } + static toVector(type: string) { + return `Vector<${type}>` + } + static toFlags(index: number, type: string) { + return `flags.${index}?${type}` + } +} + +class TLCreator { + id: string //NOTE Maybe number will better? + name: string //predicate or method + type: string + params: TLArg[] +} + +class ParsedSchema { + types: Map = new Map + creators: Map = new Map + funcs: Map = new Map +} + +const parse = (schema: TLSchema) => { + const result = new ParsedSchema + for (const creator of schema.constructors) { + + } +} + + +const hasQuestion = contains('?') +const hasVector = contains('<') +const splitQuestion = split('?') +const splitDot = split('.') \ No newline at end of file diff --git a/src/tl/index.h.js b/src/tl/index.h.js index 716806f..f26dc45 100644 --- a/src/tl/index.h.js +++ b/src/tl/index.h.js @@ -2,7 +2,7 @@ export type BinaryData = number[] | Uint8Array -type TLParam = { +export type TLParam = { name: string, type: string } @@ -14,7 +14,7 @@ export type TLConstruct = { params: TLParam[] } -type TLMethod = { +export type TLMethod = { id: string, type: string, method: string, diff --git a/src/tl/index.js b/src/tl/index.js index 4368999..dfdada2 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -9,7 +9,7 @@ import { WriteMediator } from './mediator' import { readInt, TypeBuffer, TypeWriter, getNakedType, getPredicate, getString, getTypeConstruct } from './type-buffer' -import type { BinaryData, TLSchema } from './index.h' +import type { TLSchema } from './index.h' import Logger from '../util/log' const debug = Logger`tl` @@ -21,31 +21,6 @@ type SerialConstruct = { startMaxLength: number } -const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) => { - let list, length - if (bytes instanceof ArrayBuffer) { - list = new Uint8Array(bytes) - length = bytes.byteLength - } else if (bytes === undefined) { - list = [] - length = 0 - } else if (typeof bytes === 'string') { - list = - stringToChars( - unescape( - encodeURIComponent( - bytes))) - length = list.length - } else { - list = bytes - length = bytes.length - } - return { - list, - length - } -} - export class Serialization { writer: TypeWriter = new TypeWriter() mtproto: boolean @@ -143,7 +118,7 @@ export class Serialization { return } - if (is(Array, obj)) { + if (Array.isArray(obj)) { if (type.substr(0, 6) == 'Vector') WriteMediator.int(this.writer, 0x1cb5c415, `${field}[id]`) else if (type.substr(0, 6) != 'vector') { diff --git a/test/layout.test.js b/test/layout.test.js new file mode 100644 index 0000000..60d552b --- /dev/null +++ b/test/layout.test.js @@ -0,0 +1,183 @@ +const { test } = require('tap') +const { getFlags, Layout } = require('../lib/layout') +const apiSchema = require('../schema/api-57.json') + +const { has } = require('ramda') + +const methodRaw = { + 'id' : '-1137057461', + 'method': 'messages.saveDraft', + 'params': [{ + 'name': 'flags', + 'type': '#' + }, { + 'name': 'no_webpage', + 'type': 'flags.1?true' + }, { + 'name': 'reply_to_msg_id', + 'type': 'flags.0?int' + }, { + 'name': 'peer', + 'type': 'InputPeer' + }, { + 'name': 'message', + 'type': 'string' + }, { + 'name': 'entities', + 'type': 'flags.3?Vector' + }], + 'type': 'Bool' +} + + +const methodModel = { + id : -1137057461, + name : 'messages.saveDraft', + returns : 'Bool', + hasFlags: true, + params : [ + { + name : 'flags', + type : '#', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'no_webpage', + type : 'true', + isVector : false, + isFlag : true, + flagIndex: 1 + }, { + name : 'reply_to_msg_id', + type : 'int', + isVector : false, + isFlag : true, + flagIndex: 0 + }, { + name : 'peer', + type : 'InputPeer', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'message', + type : 'string', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'entities', + type : 'MessageEntity', + isVector : true, + isFlag : true, + flagIndex: 3 + }, + ] +} + +const MessageEntity = { + name : 'MessageEntity', + creators: [ + 'messageEntityUnknown', + 'messageEntityMention', + 'messageEntityHashtag' + ] +} + +const entityCreator1 = { + 'id' : '-1148011883', + 'predicate': 'messageEntityUnknown', + 'params' : [ + { + 'name': 'offset', + 'type': 'int' + }, { + 'name': 'length', + 'type': 'int' + } + ], + 'type': 'MessageEntity' +} +const entityCreator2 = { + 'id' : '-100378723', + 'predicate': 'messageEntityMention', + 'params' : [ + { + 'name': 'offset', + 'type': 'int' + }, { + 'name': 'length', + 'type': 'int' + } + ], + 'type': 'MessageEntity' +} +const entityCreator3 = { + 'id' : '1868782349', + 'predicate': 'messageEntityHashtag', + 'params' : [ + { + 'name': 'offset', + 'type': 'int' + }, { + 'name': 'length', + 'type': 'int' + } + ], + 'type': 'MessageEntity' +} + + +const entityCreatorModel = { + id : -1148011883, + name : 'messageEntityUnknown', + type : 'MessageEntity', + hasFlags: false, + params : [ + { + name : 'offset', + type : 'int', + isVector : false, + isFlag : false, + flagIndex: NaN + }, { + name : 'length', + type : 'int', + isVector : false, + isFlag : false, + flagIndex: NaN + }, + ] +} + +/*const method = Layout.method('auth.sendCode') +for (const param of method.params) { + +}*/ +test('flags counter', t => { + const flagTests = [ + [{}, 0], + [{ + no_webpage: true + }, 2], + [{ + no_webpage : true, + reply_to_msg_id: 1234556 + }, 3], + [{ + entities : [{}], + reply_to_msg_id: 1234556 + }, 9], + ] + + + let getter + t.notThrow(() => getter = getFlags(methodModel), 'getFlags') + for (const [ obj, result ] of flagTests) + t.equal(getter(obj), result, `flags test`) + let layout + t.notThrow(() => layout = new Layout(apiSchema), 'make new layout') + console.log(layout) + t.end() +}) \ No newline at end of file From 71adec9c97996fb56ceaab6ec271e10f9c78b185 Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Sat, 25 Mar 2017 06:05:51 +0300 Subject: [PATCH 10/15] TL refactoring Signed-off-by: Dmitry Boldyrev --- .babelrc | 5 -- CHANGELOG.md | 8 ++- examples/chat-history.js | 6 +- examples/index.js | 8 ++- examples/login.js | 2 +- examples/update-profile.js | 11 ++++ package.json | 5 +- src/layout/index.h.js | 7 -- src/layout/index.js | 41 +++++++++--- src/layout/schema-model.js | 88 ------------------------- src/service/api-manager/index.js | 8 +-- src/service/networker/index.js | 2 +- src/tl/index.js | 109 ++++++++++++++++++++----------- src/tl/mediator.js | 18 ++++- src/tl/type-buffer.js | 11 ---- src/util/log.js | 1 - test/layout.test.js | 2 +- 17 files changed, 152 insertions(+), 180 deletions(-) create mode 100644 examples/update-profile.js delete mode 100644 src/layout/schema-model.js diff --git a/.babelrc b/.babelrc index a2a86d4..ce0e720 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,5 @@ { "presets": [ - // ["es2015", { "modules": false }] ], "plugins": [ "transform-flow-strip-types", @@ -10,10 +9,6 @@ }], "transform-class-properties", "transform-async-to-generator", - // ["transform-async-to-module-method", { - // "module": "bluebird", - // "method": "coroutine" - // }], ["transform-es2015-block-scoping", { "throwIfClosureRequired": true }], diff --git a/CHANGELOG.md b/CHANGELOG.md index e4447f8..38caaa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # 2.2.1 - -* *invokeWithLayer* api field now may detects internally and don't required (but still valid) +* Flag (optional type) fields now acts like common fields. +* Zeroes, empty strings and empty types now can be omited. write only useful fields. +* *invokeWithLayer* api field now may detects internally and don't required (but still valid). +* Type check argument fields +* Fix auth race condition +* Add batch async logger # 2.2.0 diff --git a/examples/chat-history.js b/examples/chat-history.js index b631d55..1b39f34 100644 --- a/examples/chat-history.js +++ b/examples/chat-history.js @@ -1,13 +1,11 @@ -const { pluck, last } = require('ramda') +const { pluck } = require('ramda') const { inputField } = require('./fixtures') const telegram = require('./init') const getChat = async () => { const dialogs = await telegram('messages.getDialogs', { - offset : 0, - limit : 50, - offset_peer: { _: 'inputPeerEmpty' } + limit: 50, }) const { chats } = dialogs const selectedChat = await selectChat(chats) diff --git a/examples/index.js b/examples/index.js index e15dd2f..d536381 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,10 +1,12 @@ const login = require('./login') const { getChat, chatHistory } = require('./chat-history') +const updateProfile = require('./update-profile') const run = async () => { - await login() - const chat = await getChat() - await chatHistory(chat) + const first_name = await login() + await updateProfile(first_name) + // const chat = await getChat() + // await chatHistory(chat) } run() \ No newline at end of file diff --git a/examples/login.js b/examples/login.js index 7637cec..4331328 100644 --- a/examples/login.js +++ b/examples/login.js @@ -34,7 +34,7 @@ const login = async () => { username = '' } = user console.log('signIn', first_name, username, user.phone) - return telegram + return first_name } catch (error) { console.error(error) } diff --git a/examples/update-profile.js b/examples/update-profile.js new file mode 100644 index 0000000..11bc324 --- /dev/null +++ b/examples/update-profile.js @@ -0,0 +1,11 @@ +const telegram = require('./init') + +const updateProfile = async (currentName) => { + const result = await telegram('account.updateProfile', { + first_name: currentName + 'test' + }) + console.log('updateProfile', result) + return result +} + +module.exports = updateProfile \ No newline at end of file diff --git a/package.json b/package.json index aab2e2c..55062a3 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "author": "Zero Bias", "dependencies": { "@goodmind/node-cryptojs-aes": "^0.5.0", - "@types/uuid": "^2.0.29", "ajv": "^4.11.5", "ajv-keywords": "^1.5.1", "axios": "^0.15.3", @@ -36,11 +35,9 @@ "eventemitter2": "^3.0.2", "memoizee": "^0.4.4", "pako": "^1.0.4", - "pretty-format": "^19.0.0", "ramda": "^0.23.0", "randombytes": "^2.0.3", "rusha": "^0.8.5", - "uuid": "^3.0.1", "worker-loader": "^0.8.0" }, "devDependencies": { @@ -107,5 +104,5 @@ "type": "git", "url": "git+https://github.com/zerobias/telegram-mtproto.git" }, - "version": "2.2.0" + "version": "2.2.1" } diff --git a/src/layout/index.h.js b/src/layout/index.h.js index 5ad67ae..4d8e336 100644 --- a/src/layout/index.h.js +++ b/src/layout/index.h.js @@ -19,11 +19,4 @@ export type TLParam = { isVector: boolean, isFlag: boolean, flagIndex: number -} - -export type TLCreator = { - id: number, - name: string, - hasFlags: boolean, - params: TLParam[] } \ No newline at end of file diff --git a/src/layout/index.js b/src/layout/index.js index 0b930fb..3103e3b 100644 --- a/src/layout/index.js +++ b/src/layout/index.js @@ -4,7 +4,7 @@ import has from 'ramda/src/has' import flip from 'ramda/src/flip' import contains from 'ramda/src/contains' -import type { TLParam, TLCreator, SchemaElement, TLMethod, TLConstruct, TLSchema } from './index.h' +import type { TLParam, SchemaElement, TLMethod, TLConstruct, TLSchema, SchemaParam } from './index.h' class TypeClass { name: string @@ -23,6 +23,7 @@ class Argument { name: string typeClass: string isVector: boolean + isBare: boolean isFlag: boolean flagIndex: number fullType: string @@ -30,11 +31,13 @@ class Argument { name: string, typeClass: string, isVector: boolean = false, + isBare: boolean = false, isFlag: boolean = false, flagIndex: number = NaN) { this.name = name this.typeClass = typeClass this.isVector = isVector + this.isBare = isBare this.isFlag = isFlag this.flagIndex = flagIndex this.id = id @@ -92,6 +95,10 @@ class Type extends Creator { } } +const isFlagItself = + (param: SchemaParam) => + param.name === 'flags' && + param.type === '#' export class Layout { typeClasses: Map = new Map @@ -99,6 +106,7 @@ export class Layout { args: Map = new Map funcs: Map = new Map types: Map = new Map + typeDefaults: Map = new Map schema: TLSchema makeCreator(elem: SchemaElement, name: string, @@ -107,11 +115,15 @@ export class Layout { const args: Argument[] = [] let hasFlags = false for (const [ i, param ] of elem.params.entries()) { + if (isFlagItself(param)) { + hasFlags = true + continue + } const id = `${name}.${param.name}/${i}` - const { typeClass, isVector, isFlag, flagIndex } = getTypeProps(param.type) + const { typeClass, isVector, isFlag, flagIndex, isBare } = getTypeProps(param.type) if (isFlag) hasFlags = true this.pushTypeClass(typeClass) - const arg = new Argument(id, param.name, typeClass, isVector, isFlag, flagIndex) + const arg = new Argument(id, param.name, typeClass, isVector, isBare, isFlag, flagIndex) args.push(arg) this.args.set(id, arg) } @@ -150,6 +162,9 @@ export class Layout { const { methods, constructors } = schema constructors.map(this.makeType) methods.map(this.makeMethod) + for (const [ key, type ] of this.types.entries()) + if (hasEmpty(key)) + this.typeDefaults.set(type.typeClass, { _: key }) } constructor(schema: TLSchema) { //$FlowIssue @@ -160,15 +175,18 @@ export class Layout { this.makeLayout(schema) } } +const hasEmpty = contains('Empty') const hasQuestion = contains('?') const hasVector = contains('<') +const hasBare = contains('%') const getTypeProps = (rawType: string) => { const result = { typeClass: rawType, isVector : false, isFlag : false, - flagIndex: NaN + flagIndex: NaN, + isBare : false } if (hasQuestion(rawType)) { const [ prefix, rest ] = rawType.split('?') @@ -181,25 +199,28 @@ const getTypeProps = (rawType: string) => { result.isVector = true result.typeClass = result.typeClass.slice(7, -1) } + if (hasBare(result.typeClass)) { + result.isBare = true + result.typeClass = result.typeClass.slice(1) + } return result } -const isSimpleType: (type: string) => boolean = +export const isSimpleType: (type: string) => boolean = flip(contains)( - ['int', 'long', 'string', 'double', 'true', 'bytes']) - + ['int', /*'long',*/ 'string', /*'double', */'true', /*'bytes'*/]) const getFlagsRed = (data: Object) => - (acc: number, { name, flagIndex }: TLParam) => + (acc: number, { name, flagIndex }: Argument) => has(name, data) ? acc + 2 ** flagIndex : acc -export const getFlags = ({ params }: TLCreator) => { +export const getFlags = ({ params }: Creator) => { const flagsParams = params.filter( - (e: TLParam) => e.isFlag) + (e: Argument) => e.isFlag) return (data: Object) => flagsParams diff --git a/src/layout/schema-model.js b/src/layout/schema-model.js deleted file mode 100644 index b9e278e..0000000 --- a/src/layout/schema-model.js +++ /dev/null @@ -1,88 +0,0 @@ -//@flow - -import uuid from 'uuid/v1' -import contains from 'ramda/src/contains' -import split from 'ramda/src/split' - -import type { TLSchema, TLConstruct, TLMethod, TLParam } from '../tl/index.h' - -class SchemaFunc { - id: string - type: string - method: string - params: TLParam[] - - hasFlags: boolean - flags: TLParam[] -} - -class TLType { - name: string - - creators: string[] - - isTypeOf(value: any): boolean { - return false - } -} - -class TLArg { - name: string - type: string - isVector: boolean - isFlag: boolean - flagIndex: number - - link: string = uuid() - constructor(name: string, - type: string, - isVector: boolean = false, - isFlag: boolean = false, - flagIndex: number = NaN) { - this.name = name - this.type = type - this.isVector = isVector - this.isFlag = isFlag - this.flagIndex = flagIndex - } - toString() { - let result = this.type - if (this.isVector) - result = TLArg.toVector(result) - if (this.isFlag) - result = TLArg.toFlags(this.flagIndex, result) - return result - } - static toVector(type: string) { - return `Vector<${type}>` - } - static toFlags(index: number, type: string) { - return `flags.${index}?${type}` - } -} - -class TLCreator { - id: string //NOTE Maybe number will better? - name: string //predicate or method - type: string - params: TLArg[] -} - -class ParsedSchema { - types: Map = new Map - creators: Map = new Map - funcs: Map = new Map -} - -const parse = (schema: TLSchema) => { - const result = new ParsedSchema - for (const creator of schema.constructors) { - - } -} - - -const hasQuestion = contains('?') -const hasVector = contains('<') -const splitQuestion = split('?') -const splitDot = split('.') \ No newline at end of file diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index 7d1fdef..c0f1885 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -194,14 +194,14 @@ export class ApiManager { if (!options.noErrorBox) { //TODO weird code. `error` changed after `.reject`? - //$FlowIssue - err.input = method - //$FlowIssue + + /*err.input = method + err.stack = stack || hasPath(['originalError', 'stack'], error) || error.stack || - (new Error()).stack + (new Error()).stack*/ this.emit('error.invoke', error) } } diff --git a/src/service/networker/index.js b/src/service/networker/index.js index 9124385..f57c23d 100644 --- a/src/service/networker/index.js +++ b/src/service/networker/index.js @@ -633,7 +633,7 @@ export class NetworkerThread { getMsgById = ({ req_msg_id }) => this.state.getSent(req_msg_id) - async parseResponse(responseBuffer) { + async parseResponse(responseBuffer: Uint8Array) { // console.log(dTime(), 'Start parsing response') // const self = this diff --git a/src/tl/index.js b/src/tl/index.js index dfdada2..90b4264 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -1,15 +1,16 @@ //@flow import is from 'ramda/src/is' +import has from 'ramda/src/has' import { uintToInt, intToUint, bytesToHex, gzipUncompress, bytesToArrayBuffer, longToInts, lshift32, stringToChars } from '../bin' -import { WriteMediator } from './mediator' - +import { WriteMediator, ReadMediator } from './mediator' +import Layout, { getFlags, isSimpleType } from '../layout' import { readInt, TypeBuffer, TypeWriter, getNakedType, getPredicate, getString, getTypeConstruct } from './type-buffer' -import type { TLSchema } from './index.h' +import type { TLSchema, TLConstruct } from './index.h' import Logger from '../util/log' const debug = Logger`tl` @@ -21,6 +22,9 @@ type SerialConstruct = { startMaxLength: number } +let apiLayer: Layout +let mtLayer: Layout + export class Serialization { writer: TypeWriter = new TypeWriter() mtproto: boolean @@ -34,6 +38,10 @@ export class Serialization { this.writer.reset() this.mtproto = mtproto + if (!apiLayer) + apiLayer = new Layout(api) + if (!mtLayer) + mtLayer = new Layout(mtApi) } getBytes(typed?: boolean) { @@ -44,23 +52,54 @@ export class Serialization { } storeMethod(methodName: string, params) { - const schema = selectSchema(this.mtproto, this.api, this.mtApi) - let methodData = false + const layer = this.mtproto + ? mtLayer + : apiLayer + const pred = layer.funcs.get(methodName) + if (!pred) throw new Error(`No method name ${methodName} found`) - for (const method of schema.methods) { - if (method.method == methodName) { - methodData = method - break - } + WriteMediator.int(this.writer, + intToUint(`${pred.id}`), + `${methodName}[id]`) + if (pred.hasFlags) { + const flags = getFlags(pred)(params) + this.storeObject(flags, '#', `f ${methodName} #flags ${flags}`) } - if (!methodData) { - throw new Error(`No method ${ methodName } found`) + for (const param of pred.params) { + const paramName = param.name + const typeClass = param.typeClass + let fieldObj + if (!has(paramName, params)) { + if (param.isFlag) continue + else if (layer.typeDefaults.has(typeClass)) + fieldObj = layer.typeDefaults.get(typeClass) + else if (isSimpleType(typeClass)) { + switch (typeClass) { + case 'int': fieldObj = 0; break + // case 'long': fieldObj = 0; break + case 'string': fieldObj = ' '; break + // case 'double': fieldObj = 0; break + case 'true': fieldObj = true; break + // case 'bytes': fieldObj = [0]; break + } + } + else throw new Error(`Method ${methodName} doesnt recieve required argument ${paramName}`) + } else { + fieldObj = params[paramName] + } + if (param.isVector) { + if (!Array.isArray(fieldObj)) + throw new TypeError(`Vector argument ${paramName} in ${methodName} required Array,` + + //$FlowIssue + ` got ${fieldObj} ${typeof fieldObj}`) + WriteMediator.int(this.writer, 0x1cb5c415, `${paramName}[id]`) + WriteMediator.int(this.writer, fieldObj.length, `${paramName}[count]`) + for (const [ i, elem ] of fieldObj.entries()) + this.storeObject(elem, param.typeClass, `${paramName}[${i}]`) + } else + this.storeObject(fieldObj, param.typeClass, `f ${methodName}(${paramName})`) } - WriteMediator.int(this.writer, - intToUint(methodData.id), - `${methodName }[id]`) - - let condType + /*let condType let fieldBit for (const param of methodData.params) { let type = param.type @@ -74,15 +113,15 @@ export class Serialization { } const paramName = param.name const stored = params[paramName] - /*if (!stored) + if (!stored) stored = this.emptyOfType(type, schema) if (!stored) throw new Error(`Method ${methodName}.`+ - ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`)*/ + ` No value of field ${ param.name } recieved and no Empty of type ${ param.type }`) this.storeObject(stored, type, `f ${methodName}(${paramName})`) - } + }*/ - return methodData.type + return pred.returns } /*emptyOfType(ofType, schema: TLSchema) { const resultConstruct = schema.constructors.find( @@ -139,6 +178,7 @@ export class Serialization { throw new Error(`Invalid object for type ${ type}`) const schema = selectSchema(this.mtproto, this.api, this.mtApi) + const predicate = obj['_'] let isBare = false let constructorData = false @@ -146,12 +186,14 @@ export class Serialization { if (isBare) type = type.substr(1) + for (const tlConst of schema.constructors) { if (tlConst.predicate == predicate) { constructorData = tlConst break } } + if (!constructorData) throw new Error(`No predicate ${predicate} found`) @@ -165,6 +207,7 @@ export class Serialization { let condType let fieldBit + for (const param of constructorData.params) { type = param.type if (type.indexOf('?') !== -1) { @@ -199,23 +242,15 @@ export class Deserialization { this.mtproto = mtproto } - readInt = readInt(this) + readInt = (field: string) => { + // log('int')(field, i.toString(16), i) + return ReadMediator.int(this.typeBuffer, field) + } fetchInt(field: string = '') { return this.readInt(`${ field }:int`) } - fetchDouble(field: string = '') { - const buffer = new ArrayBuffer(8) - const intView = new Int32Array(buffer) - const doubleView = new Float64Array(buffer) - - intView[0] = this.readInt(`${ field }:double[low]`) - intView[1] = this.readInt(`${ field }:double[high]`) - - return doubleView[0] - } - fetchLong(field: string = '') { const iLow = this.readInt(`${ field }:long[low]`) const iHigh = this.readInt(`${ field }:long[high]`) @@ -301,7 +336,7 @@ export class Deserialization { return bytes } - fetchPacked(type, field) { + fetchPacked(type, field: string = '') { const compressed = this.fetchBytes(`${field}[packed_string]`) const uncompressed = gzipUncompress(compressed) const buffer = bytesToArrayBuffer(uncompressed) @@ -315,7 +350,7 @@ export class Deserialization { return newDeserializer.fetchObject(type, field) } - fetchVector(type, field) { + fetchVector(type, field: string = '') { if (type.charAt(0) === 'V') { const constructor = this.readInt(`${field}[id]`) const constructorCmp = uintToInt(constructor) @@ -336,7 +371,7 @@ export class Deserialization { return result } - fetchObject(type, field) { + fetchObject(type, field: string = '') { switch (type) { case '#': case 'int': @@ -354,7 +389,7 @@ export class Deserialization { case 'bytes': return this.fetchBytes(field) case 'double': - return this.fetchDouble(field) + return ReadMediator.double(this.typeBuffer, field) case 'Bool': return this.fetchBool(field) case 'true': diff --git a/src/tl/mediator.js b/src/tl/mediator.js index c8e13bf..70c2aba 100644 --- a/src/tl/mediator.js +++ b/src/tl/mediator.js @@ -1,6 +1,6 @@ //@flow -import { TypeWriter } from './type-buffer' +import { TypeWriter, TypeBuffer } from './type-buffer' import { longToInts, stringToChars } from '../bin' import Logger from '../util/log' @@ -93,6 +93,22 @@ export const WriteMediator = { } } +export const ReadMediator = { + int(ctx: TypeBuffer, field: string) { + // log('int')(field, i.toString(16), i) + return ctx.nextInt() + }, + double(ctx: TypeBuffer, field: string) { + const buffer = new ArrayBuffer(8) + const intView = new Int32Array(buffer) + const doubleView = new Float64Array(buffer) + + intView[0] = this.int(ctx, `${ field }:double[low]`) + intView[1] = this.int(ctx, `${ field }:double[high]`) + + return doubleView[0] + } +} const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) => { let list, length diff --git a/src/tl/type-buffer.js b/src/tl/type-buffer.js index f12c46c..f5df726 100644 --- a/src/tl/type-buffer.js +++ b/src/tl/type-buffer.js @@ -13,17 +13,6 @@ import { TypeBufferIntError } from '../error' import type { BinaryData, TLConstruct, TLSchema } from './index.h' - -type HasTypeBuffer = { - typeBuffer: TypeBuffer -} - -export const readInt = (ctx: HasTypeBuffer) => (field: string) => { - const i = ctx.typeBuffer.nextInt() - // log('int')(field, i.toString(16), i) - return i -} - function findType(val: TLConstruct) { return val.type == this } diff --git a/src/util/log.js b/src/util/log.js index 1b674fc..15bb3ca 100644 --- a/src/util/log.js +++ b/src/util/log.js @@ -1,7 +1,6 @@ //@flow // import memoize from 'memoizee' -// import pretty from 'pretty-format' import Debug from 'debug' diff --git a/test/layout.test.js b/test/layout.test.js index 60d552b..99db44f 100644 --- a/test/layout.test.js +++ b/test/layout.test.js @@ -178,6 +178,6 @@ test('flags counter', t => { t.equal(getter(obj), result, `flags test`) let layout t.notThrow(() => layout = new Layout(apiSchema), 'make new layout') - console.log(layout) + // console.log(layout) t.end() }) \ No newline at end of file From c942cfeb645793a7b26c28dc77cab2aa0b97ba8b Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Sat, 25 Mar 2017 07:12:20 +0300 Subject: [PATCH 11/15] Update readme, add usage example Signed-off-by: Dmitry Boldyrev --- README.md | 1 - examples/from-readme.js | 36 ++++++++++++++++++++++++++++++++ examples/index.js | 4 ++-- src/service/api-manager/index.js | 6 +++--- 4 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 examples/from-readme.js diff --git a/README.md b/README.md index c2c0dba..e095d79 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,6 @@ const phone = { } const api = { - invokeWithLayer: 0xda9b0d0d, layer : 57, initConnection : 0x69796de9, api_id : 49631 diff --git a/examples/from-readme.js b/examples/from-readme.js new file mode 100644 index 0000000..27139b4 --- /dev/null +++ b/examples/from-readme.js @@ -0,0 +1,36 @@ +const { MTProto } = require('../lib') + +const phone = { + num : '+9996620001', + code: '22222' +} + +const api = { + layer : 57, + initConnection : 0x69796de9, + api_id : 49631 +} + +const server = { + dev: true //We will connect to the test server. +} //Any empty configurations fields can just not be specified + +const client = MTProto({ server, api }) + +async function connect(){ + const { phone_code_hash } = await client('auth.sendCode', { + phone_number : phone.num, + current_number: false, + api_id : 49631, + api_hash : 'fb050b8f6771e15bfda5df2409931569' + }) + const { user } = await client('auth.signIn', { + phone_number : phone.num, + phone_code_hash: phone_code_hash, + phone_code : phone.code + }) + + console.log('signed as ', user) +} + +connect() \ No newline at end of file diff --git a/examples/index.js b/examples/index.js index d536381..5f78638 100644 --- a/examples/index.js +++ b/examples/index.js @@ -5,8 +5,8 @@ const updateProfile = require('./update-profile') const run = async () => { const first_name = await login() await updateProfile(first_name) - // const chat = await getChat() - // await chatHistory(chat) + const chat = await getChat() + await chatHistory(chat) } run() \ No newline at end of file diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index c0f1885..147e049 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -1,7 +1,7 @@ //@flow import Promise from 'bluebird' -import UpdatesManager from '../updates' +// import UpdatesManager from '../updates' import isNil from 'ramda/src/isNil' import is from 'ramda/src/is' @@ -102,8 +102,8 @@ export class ApiManager { apiManager.emit = this.emit apiManager.storage = storage - this.updatesManager = UpdatesManager(apiManager) - apiManager.updates = this.updatesManager + // this.updatesManager = UpdatesManager(apiManager) + // apiManager.updates = this.updatesManager return apiManager } From e46217e6c987e98e5598f1a9e8916f01a07d86ed Mon Sep 17 00:00:00 2001 From: andretshurotshka Date: Sun, 26 Mar 2017 19:45:15 +0500 Subject: [PATCH 12/15] Fixes #24 --- src/service/api-manager/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index 2348bd3..f15f1ec 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -167,9 +167,10 @@ export class ApiManager { const networker = await this.mtpGetNetworker(baseDc, opts) const nearestDc = await networker.wrapApiCall( 'help.getNearestDc', {}, opts) - const { nearest_dc } = nearestDc + const { nearest_dc, this_dc } = nearestDc await this.storage.set('dc', nearest_dc) debug(`nearest Dc`)('%O', nearestDc) + if (nearest_dc !== this_dc) await this.mtpGetNetworker(nearest_dc, { createNetworker: true }) } } mtpInvokeApi = async (method: string, params: Object, options: LeftOptions = {}) => { @@ -279,4 +280,4 @@ const isAnyNetworker = (ctx: ApiManager) => Object.keys(ctx.cache.downloader).le const netError = error => { console.log('Get networker error', error, error.stack) return Promise.reject(error) -} \ No newline at end of file +} From 164a7e05c4a50db1b92a1922a0f58e8064b1d08c Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Sun, 26 Mar 2017 19:47:08 +0300 Subject: [PATCH 13/15] "Bug fixes, slight refactoring Signed-off-by: Dmitry Boldyrev --- src/bin.js | 2 +- src/layout/index.js | 7 ++- src/service/api-manager/index.js | 3 - src/service/authorizer/send-plain-req.js | 9 +-- src/service/networker/index.js | 18 +++--- src/tl/index.js | 73 +++++++++++------------- src/tl/mediator.js | 20 +++++-- src/tl/type-buffer.js | 5 ++ src/util/log.js | 4 ++ 9 files changed, 77 insertions(+), 64 deletions(-) diff --git a/src/bin.js b/src/bin.js index eb27f0f..c0cfc9e 100644 --- a/src/bin.js +++ b/src/bin.js @@ -214,7 +214,7 @@ export function intToUint(val: string) { const middle = 0x100000000 / 2 - 1 -export function uintToInt(val) { +export function uintToInt(val: number): number { if (val > middle) val = val - 0x100000000 return val diff --git a/src/layout/index.js b/src/layout/index.js index 3103e3b..fbb73e3 100644 --- a/src/layout/index.js +++ b/src/layout/index.js @@ -106,6 +106,7 @@ export class Layout { args: Map = new Map funcs: Map = new Map types: Map = new Map + typesById: Map = new Map typeDefaults: Map = new Map schema: TLSchema makeCreator(elem: SchemaElement, @@ -132,8 +133,10 @@ export class Layout { this.creators.add(name) if (creator instanceof Method) this.funcs.set(name, creator) - else if (creator instanceof Type) + else if (creator instanceof Type) { this.types.set(name, creator) + this.typesById.set(id, creator) + } } makeMethod(elem: TLMethod) { const name = elem.method @@ -180,7 +183,7 @@ const hasQuestion = contains('?') const hasVector = contains('<') const hasBare = contains('%') -const getTypeProps = (rawType: string) => { +export const getTypeProps = (rawType: string) => { const result = { typeClass: rawType, isVector : false, diff --git a/src/service/api-manager/index.js b/src/service/api-manager/index.js index 9513578..ec1e4cb 100644 --- a/src/service/api-manager/index.js +++ b/src/service/api-manager/index.js @@ -180,9 +180,6 @@ export class ApiManager { } } mtpInvokeApi = async (method: string, params: Object, options: LeftOptions = {}) => { - // const self = this - const defError = new Error() - const stack = defError.stack || 'empty stack' const deferred = blueDefer() const rejectPromise = (error: any) => { let err diff --git a/src/service/authorizer/send-plain-req.js b/src/service/authorizer/send-plain-req.js index 48ea896..963e11d 100644 --- a/src/service/authorizer/send-plain-req.js +++ b/src/service/authorizer/send-plain-req.js @@ -9,7 +9,7 @@ import allPass from 'ramda/src/allPass' import httpClient from '../../http' import { ErrorBadResponse, ErrorNotFound } from '../../error' import { generateID } from '../time-manager' -import { WriteMediator } from '../../tl' +import { WriteMediator, ReadMediator } from '../../tl' import type { TLFabric } from '../../tl' @@ -71,9 +71,10 @@ const SendPlain = ({ Serialization, Deserialization }: TLFabric) => { let deserializer try { deserializer = Deserialization(req.data, { mtproto: true }) - deserializer.fetchLong('auth_key_id') - deserializer.fetchLong('msg_id') - deserializer.fetchInt('msg_len') + const ctx = deserializer.typeBuffer + ReadMediator.long(ctx, 'auth_key_id') + ReadMediator.long(ctx, 'msg_id') + ReadMediator.int(ctx, 'msg_len') } catch (e) { return Promise.reject(new ErrorBadResponse(url, e)) } diff --git a/src/service/networker/index.js b/src/service/networker/index.js index f57c23d..d522000 100644 --- a/src/service/networker/index.js +++ b/src/service/networker/index.js @@ -26,7 +26,7 @@ import { convertToUint8Array, convertToArrayBuffer, sha1BytesSync, bytesToArrayBuffer, longToBytes, uintToInt, rshift32 } from '../../bin' import type { TLFabric, SerializationFabric, DeserializationFabric } from '../../tl' -import { WriteMediator, TypeWriter } from '../../tl' +import { WriteMediator, ReadMediator, TypeWriter } from '../../tl' import type { Emit } from '../main/index.h' let updatesProcessor @@ -656,7 +656,7 @@ export class NetworkerThread { deserializer.fetchIntBytes(64, 'salt') const sessionID = deserializer.fetchIntBytes(64, 'session_id') - const messageID = deserializer.fetchLong('message_id') + const messageID = ReadMediator.long( deserializer.typeBuffer, 'message_id') const isInvalidSession = !bytesCmp(sessionID, this.sessionID) && ( @@ -979,10 +979,10 @@ const getDeserializeOpts = msgGetter => ({ mtproto : true, override: { mt_message(result, field) { - result.msg_id = this.fetchLong(`${ field }[msg_id]`) - result.seqno = this.fetchInt(`${ field }[seqno]`) + result.msg_id = ReadMediator.long( this.typeBuffer, `${ field }[msg_id]`) + result.seqno = ReadMediator.int( this.typeBuffer, `${ field }[seqno]`) //TODO WARN! Why everywhere seqno is seq_no and only there its seqno?!? - result.bytes = this.fetchInt(`${ field }[bytes]`) + result.bytes = ReadMediator.int( this.typeBuffer, `${ field }[bytes]`) const offset = this.getOffset() @@ -992,15 +992,15 @@ const getDeserializeOpts = msgGetter => ({ console.error(dTime(), 'parse error', e.message, e.stack) result.body = { _: 'parse_error', error: e } } - if (this.offset != offset + result.bytes) { + if (this.typeBuffer.offset != offset + result.bytes) { // console.warn(dTime(), 'set offset', this.offset, offset, result.bytes) // console.log(dTime(), result) - this.offset = offset + result.bytes + this.typeBuffer.offset = offset + result.bytes } // console.log(dTime(), 'override message', result) }, - mt_rpc_result(result, field) { - result.req_msg_id = this.fetchLong(`${ field }[req_msg_id]`) + mt_rpc_result(result, field: string) { + result.req_msg_id = ReadMediator.long( this.typeBuffer, `${ field }[req_msg_id]`) const sentMessage = msgGetter(result) const type = sentMessage && sentMessage.resultType || 'Object' diff --git a/src/tl/index.js b/src/tl/index.js index 90b4264..b4d45aa 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -7,9 +7,9 @@ import { uintToInt, intToUint, bytesToHex, gzipUncompress, bytesToArrayBuffer, longToInts, lshift32, stringToChars } from '../bin' import { WriteMediator, ReadMediator } from './mediator' -import Layout, { getFlags, isSimpleType } from '../layout' -import { readInt, TypeBuffer, TypeWriter, getNakedType, - getPredicate, getString, getTypeConstruct } from './type-buffer' +import Layout, { getFlags, isSimpleType, getTypeProps } from '../layout' +import { TypeBuffer, TypeWriter, getNakedType, + getString, getTypeConstruct } from './type-buffer' import type { TLSchema, TLConstruct } from './index.h' import Logger from '../util/log' @@ -227,6 +227,8 @@ export class Serialization { } +const getChar = (e: number) => String.fromCharCode(e) + export class Deserialization { typeBuffer: TypeBuffer override: Object @@ -251,14 +253,6 @@ export class Deserialization { return this.readInt(`${ field }:int`) } - fetchLong(field: string = '') { - const iLow = this.readInt(`${ field }:long[low]`) - const iHigh = this.readInt(`${ field }:long[high]`) - - const res = lshift32(iHigh, iLow) - return res - } - fetchBool(field: string = '') { const i = this.readInt(`${ field }:bool`) switch (i) { @@ -271,7 +265,7 @@ export class Deserialization { } } - fetchString(field: string = '') { + fetchBytes(field: string = '') { let len = this.typeBuffer.nextByte() if (len == 254) { @@ -280,37 +274,32 @@ export class Deserialization { this.typeBuffer.nextByte() << 16 } - const sUTF8 = getString(len, this.typeBuffer) + const bytes = this.typeBuffer.next(len) + this.typeBuffer.addPadding() + + debug(`read, bytes`)(bytesToHex(bytes), `${ field }:bytes`) + + return bytes + } + + fetchString(field: string) { + const bytes = this.fetchBytes(`${field}:string`) + const sUTF8 = [...bytes] + .map(getChar) + .join('') let s try { - s = decodeURIComponent(encodeURIComponent(sUTF8)) + s = decodeURIComponent(escape(sUTF8)) } catch (e) { s = sUTF8 } - debug(`string`)(s, `${field}:string`) + debug(`read, string`)(s, `${field}:string`) return s } - fetchBytes(field: string = '') { - let len = this.typeBuffer.nextByte() - - if (len == 254) { - len = this.typeBuffer.nextByte() | - this.typeBuffer.nextByte() << 8 | - this.typeBuffer.nextByte() << 16 - } - - const bytes = this.typeBuffer.next(len) - this.typeBuffer.addPadding() - - debug(`bytes`)(bytesToHex(bytes), `${ field }:bytes`) - - return bytes - } - fetchIntBytes(bits, field: string = '') { if (bits % 32) throw new Error(`Invalid bits: ${bits}`) @@ -350,7 +339,8 @@ export class Deserialization { return newDeserializer.fetchObject(type, field) } - fetchVector(type, field: string = '') { + fetchVector(type: string, field: string = '') { + const typeProps = getTypeProps(type) if (type.charAt(0) === 'V') { const constructor = this.readInt(`${field}[id]`) const constructorCmp = uintToInt(constructor) @@ -377,7 +367,7 @@ export class Deserialization { case 'int': return this.fetchInt(field) case 'long': - return this.fetchLong(field) + return ReadMediator.long(this.typeBuffer, field) case 'int128': return this.fetchIntBytes(128, field) case 'int256': @@ -397,17 +387,22 @@ export class Deserialization { } let fallback field = field || type || 'Object' - if (type.substr(0, 6).toLowerCase() === 'vector') + + const layer = this.mtproto + ? mtLayer + : apiLayer + const typeProps = getTypeProps(type) + // layer.typesById + + if (typeProps.isVector) return this.fetchVector(type, field) const schema = selectSchema(this.mtproto, this.api, this.mtApi) let predicate = false let constructorData = false - if (type.charAt(0) === '%') + if (typeProps.isBare) constructorData = getNakedType(type, schema) - else if (type.charAt(0) >= 97 && type.charAt(0) <= 122) - constructorData = getPredicate(type, schema) else { const constructor = this.readInt(`${field}[id]`) const constructorCmp = uintToInt(constructor) @@ -522,4 +517,4 @@ export const TL = (api: TLSchema, mtApi: TLSchema) => ({ export * from './mediator' export { TypeWriter } from './type-buffer' -export default TL \ No newline at end of file +export default TL diff --git a/src/tl/mediator.js b/src/tl/mediator.js index 70c2aba..038a817 100644 --- a/src/tl/mediator.js +++ b/src/tl/mediator.js @@ -1,7 +1,7 @@ //@flow import { TypeWriter, TypeBuffer } from './type-buffer' -import { longToInts, stringToChars } from '../bin' +import { longToInts, stringToChars, lshift32 } from '../bin' import Logger from '../util/log' const log = Logger`tl, mediator` @@ -95,8 +95,16 @@ export const WriteMediator = { export const ReadMediator = { int(ctx: TypeBuffer, field: string) { - // log('int')(field, i.toString(16), i) - return ctx.nextInt() + const result = ctx.nextInt() + log('read, int')(field, result) + return result + }, + long(ctx: TypeBuffer, field: string ){ + const iLow = this.int(ctx, `${ field }:long[low]`) + const iHigh = this.int(ctx, `${ field }:long[high]`) + + const res = lshift32(iHigh, iLow) + return res }, double(ctx: TypeBuffer, field: string) { const buffer = new ArrayBuffer(8) @@ -115,9 +123,6 @@ const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) = if (bytes instanceof ArrayBuffer) { list = new Uint8Array(bytes) length = bytes.byteLength - } else if (bytes === undefined) { - list = [] - length = 0 } else if (typeof bytes === 'string') { list = stringToChars( @@ -125,6 +130,9 @@ const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) = encodeURIComponent( bytes))) length = list.length + } else if (bytes === undefined) { + list = [] + length = 0 } else { list = bytes length = bytes.length diff --git a/src/tl/type-buffer.js b/src/tl/type-buffer.js index f5df726..29eb4c0 100644 --- a/src/tl/type-buffer.js +++ b/src/tl/type-buffer.js @@ -173,6 +173,11 @@ export class TypeBuffer { this.offset += 4 return int } + readPair(field1: string, field2: string) { + const int1 = this.nextInt(field1) + const int2 = this.nextInt(field2) + return [ int1, int2 ] + } next(length: number) { const result = this.byteView.subarray(this.offset, this.offset + length) this.offset += length diff --git a/src/util/log.js b/src/util/log.js index 15bb3ca..8147bfe 100644 --- a/src/util/log.js +++ b/src/util/log.js @@ -112,4 +112,8 @@ const Logger = (moduleName: VariString, ...rest: string[]) => { return logger } +export const setLogger = (customLogger: Function) => { + Debug.log = customLogger +} + export default Logger \ No newline at end of file From b351e3e629961ef1be06ac75aea87a390b291d5e Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Sun, 26 Mar 2017 22:04:46 +0300 Subject: [PATCH 14/15] Optimize performance, add usage examples Signed-off-by: Dmitry Boldyrev --- .babelrc | 4 +- examples/chat-history.js | 19 +- examples/index.js | 7 +- examples/update-profile.js | 2 +- src/index.js | 1 + src/service/authorizer/index.js | 339 +++++++++++++++++--------------- src/service/networker/index.js | 1 - src/tl/index.js | 58 +----- src/tl/mediator.js | 39 +++- 9 files changed, 248 insertions(+), 222 deletions(-) diff --git a/.babelrc b/.babelrc index ce0e720..362aec7 100644 --- a/.babelrc +++ b/.babelrc @@ -12,8 +12,8 @@ ["transform-es2015-block-scoping", { "throwIfClosureRequired": true }], - ["transform-object-rest-spread", { "useBuiltIns": true }], - "closure-elimination" + ["transform-object-rest-spread", { "useBuiltIns": true }] + ,"closure-elimination" ], "env": { "commonjs": { diff --git a/examples/chat-history.js b/examples/chat-history.js index 1b39f34..50b71f0 100644 --- a/examples/chat-history.js +++ b/examples/chat-history.js @@ -68,7 +68,24 @@ const printMessages = messages => { return formatted } + +const searchUsers = async () => { + const results = await telegram('contacts.search', { + q : '@goodmind', + limit: 100, + }) + const results2 = await telegram('contacts.search', { + q : 'goodmind', + limit: 100, + }) + return { + results, + results2 + } +} + module.exports = { getChat, - chatHistory + chatHistory, + searchUsers } \ No newline at end of file diff --git a/examples/index.js b/examples/index.js index 5f78638..26f25ba 100644 --- a/examples/index.js +++ b/examples/index.js @@ -1,12 +1,13 @@ const login = require('./login') -const { getChat, chatHistory } = require('./chat-history') +const { getChat, chatHistory, searchUsers } = require('./chat-history') const updateProfile = require('./update-profile') const run = async () => { const first_name = await login() + const res = await searchUsers() await updateProfile(first_name) - const chat = await getChat() - await chatHistory(chat) + // const chat = await getChat() + // await chatHistory(chat) } run() \ No newline at end of file diff --git a/examples/update-profile.js b/examples/update-profile.js index 11bc324..ec4c80f 100644 --- a/examples/update-profile.js +++ b/examples/update-profile.js @@ -2,7 +2,7 @@ const telegram = require('./init') const updateProfile = async (currentName) => { const result = await telegram('account.updateProfile', { - first_name: currentName + 'test' + first_name: 'lam'//currentName + 'test' }) console.log('updateProfile', result) return result diff --git a/src/index.js b/src/index.js index c6e01dd..f6db04b 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import MTProto from './service/main/wrap' export { CryptoWorker } from './crypto' export { bin } from './bin' export { ApiManager } from './service/api-manager/index' +export { setLogger } from './util/log' import * as MtpTimeManager from './service/time-manager' export { MtpTimeManager } diff --git a/src/service/authorizer/index.js b/src/service/authorizer/index.js index a6f15b0..f5fc461 100644 --- a/src/service/authorizer/index.js +++ b/src/service/authorizer/index.js @@ -3,7 +3,7 @@ import Promise from 'bluebird' import blueDefer from '../../util/defer' -import { smartTimeout } from '../../util/smart-timeout' +import { immediate } from '../../util/smart-timeout' import CryptoWorker from '../../crypto' import random from '../secure-random' @@ -112,7 +112,7 @@ const leemonTwoPow = getTwoPow() export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, prepare }: Args) => { const sendPlainReq = SendPlainReq({ Serialization, Deserialization }) - function mtpSendReqPQ(auth: AuthBasic) { + async function mtpSendReqPQ(auth: AuthBasic) { const deferred = auth.deferred log('Send req_pq')(bytesToHex(auth.nonce)) @@ -120,58 +120,62 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre const reqBox = request.writer request.storeMethod('req_pq', { nonce: auth.nonce }) - const keyFoundCheck = key => key - ? auth.publicKey = key - : Promise.reject(new Error('[MT] No public key found')) - const factorizeThunk = () => { - log('PQ factorization start')(auth.pq) - return CryptoWorker.factorize(auth.pq) - } - const factDone = ([ p, q, it ]) => { - auth.p = p - auth.q = q - log('PQ factorization done')(it) - return mtpSendReqDhParams(auth) - } - - const factFail = error => { - log('Worker error')(error, error.stack) - deferred.reject(error) + let deserializer + try { + await prepare() + deserializer = await sendPlainReq(auth.dcUrl, reqBox.getBuffer()) + } catch (err) { + console.error(dTime(), 'req_pq error', err.message) + deferred.reject(err) + throw err } - const factorizer = (deserializer) => { - const response = deserializer.fetchObject('ResPQ') - - if (response._ !== 'resPQ') - throw new Error(`[MT] resPQ response invalid: ${ response._}`) - - if (!bytesCmp(auth.nonce, response.nonce)) - throw new Error('[MT] resPQ nonce mismatch') + try { + const response = deserializer.fetchObject('ResPQ', 'ResPQ') + if (response._ !== 'resPQ') { + const error = new Error(`[MT] resPQ response invalid: ${ response._}`) + deferred.reject(error) + return Promise.reject(error) + } + if (!bytesCmp(auth.nonce, response.nonce)) { + const error = new Error('[MT] resPQ nonce mismatch') + deferred.reject(error) + return Promise.reject(error) + } auth.serverNonce = response.server_nonce auth.pq = response.pq auth.fingerprints = response.server_public_key_fingerprints log('Got ResPQ')(bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints) - return select(auth.fingerprints) - .then(keyFoundCheck) - .then(factorizeThunk) - .then(factDone, factFail) + const key = await select(auth.fingerprints) + + if (key) + auth.publicKey = key + else { + const error = new Error('[MT] No public key found') + deferred.reject(error) + return Promise.reject(error) + } + log('PQ factorization start')(auth.pq) + const [ p, q, it ] = await CryptoWorker.factorize(auth.pq) + + auth.p = p + auth.q = q + log('PQ factorization done')(it) + } catch (error) { + log('Worker error')(error, error.stack) + deferred.reject(error) + throw error } - const sendPlainThunk = () => sendPlainReq(auth.dcUrl, reqBox.getBuffer()) - return prepare() - .then(sendPlainThunk) - .then(factorizer, error => { - console.error(dTime(), 'req_pq error', error.message) - deferred.reject(error) - }) + return auth } - function mtpSendReqDhParams(auth: AuthBasic) { + async function mtpSendReqDhParams(auth: AuthBasic) { const deferred = auth.deferred auth.newNonce = new Array(32) @@ -203,47 +207,57 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre }) - const afterReqDH = (deserializer) => { - const response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE') + log('afterReqDH')('Send req_DH_params') - if (response._ !== 'server_DH_params_fail' && response._ !== 'server_DH_params_ok') { - deferred.reject(new Error(`[MT] Server_DH_Params response invalid: ${ response._}`)) - return false - } + let deserializer + try { + deserializer = await sendPlainReq(auth.dcUrl, reqBox.getBuffer()) + } catch (error) { + deferred.reject(error) + throw error + } - if (!bytesCmp(auth.nonce, response.nonce)) { - deferred.reject(new Error('[MT] Server_DH_Params nonce mismatch')) - return false - } - if (!bytesCmp(auth.serverNonce, response.server_nonce)) { - deferred.reject(new Error('[MT] Server_DH_Params server_nonce mismatch')) - return false - } + const response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE') - if (response._ === 'server_DH_params_fail') { - const newNonceHash = sha1BytesSync(auth.newNonce).slice(-16) - if (!bytesCmp(newNonceHash, response.new_nonce_hash)) { - deferred.reject(new Error('[MT] server_DH_params_fail new_nonce_hash mismatch')) - return false - } - deferred.reject(new Error('[MT] server_DH_params_fail')) - return false - } + if (response._ !== 'server_DH_params_fail' && response._ !== 'server_DH_params_ok') { + const error = new Error(`[MT] Server_DH_Params response invalid: ${ response._}`) + deferred.reject(error) + throw error + } - // try { - mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer) - // } catch (e) { - // deferred.reject(e) - // return false - // } + if (!bytesCmp(auth.nonce, response.nonce)) { + const error = new Error('[MT] Server_DH_Params nonce mismatch') + deferred.reject(error) + throw error + } - mtpSendSetClientDhParams(auth) + if (!bytesCmp(auth.serverNonce, response.server_nonce)) { + const error = new Error('[MT] Server_DH_Params server_nonce mismatch') + deferred.reject(error) + throw error } - log('afterReqDH')('Send req_DH_params') - return sendPlainReq(auth.dcUrl, reqBox.getBuffer()) - .then(afterReqDH, deferred.reject) + if (response._ === 'server_DH_params_fail') { + const newNonceHash = sha1BytesSync(auth.newNonce).slice(-16) + if (!bytesCmp(newNonceHash, response.new_nonce_hash)) { + const error = new Error('[MT] server_DH_params_fail new_nonce_hash mismatch') + deferred.reject(error) + throw error + } + const error = new Error('[MT] server_DH_params_fail') + deferred.reject(error) + throw error + } + + // try { + mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer) + // } catch (e) { + // deferred.reject(e) + // return false + // } + + return auth } function mtpDecryptServerDhDataAnswer(auth: AuthBasic, encryptedAnswer) { @@ -260,7 +274,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre const buffer = bytesToArrayBuffer(answerWithPadding) const deserializer = Deserialization(buffer, { mtproto: true }) - const response = deserializer.fetchObject('Server_DH_inner_data') + const response = deserializer.fetchObject('Server_DH_inner_data', 'server_dh') if (response._ !== 'server_DH_inner_data') throw new Error(`[MT] server_DH_inner_data response invalid`) @@ -340,114 +354,113 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre return true } - function mtpSendSetClientDhParams(auth: AuthBasic) { + async function mtpSendSetClientDhParams(auth: AuthBasic) { const deferred = auth.deferred const gBytes = bytesFromHex(auth.g.toString(16)) auth.b = new Array(256) random(auth.b) + const gB = await CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime) + const data = Serialization({ mtproto: true }) + + data.storeObject({ + _ : 'client_DH_inner_data', + nonce : auth.nonce, + server_nonce: auth.serverNonce, + retry_id : [0, auth.retry++], + g_b : gB + }, 'Client_DH_Inner_Data', 'client_DH') + + const dataWithHash = sha1BytesSync(data.writer.getBuffer()).concat(data.getBytes()) + const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv) - const afterPlainRequest = (deserializer) => { - const response = deserializer.fetchObject('Set_client_DH_params_answer') - - const onAnswer = (authKey) => { - const authKeyHash = sha1BytesSync(authKey), - authKeyAux = authKeyHash.slice(0, 8), - authKeyID = authKeyHash.slice(-8) - - log('Got Set_client_DH_params_answer')(response._) - switch (response._) { - case 'dh_gen_ok': { - const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16) - - if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch')) - return false - } - - const serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8)) - // console.log('Auth successfull!', authKeyID, authKey, serverSalt) - - auth.authKeyID = authKeyID - auth.authKey = authKey - auth.serverSalt = serverSalt - - deferred.resolve(auth) - break - } - case 'dh_gen_retry': { - const newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16) - if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch')) - return false - } - - return mtpSendSetClientDhParams(auth) - } - case 'dh_gen_fail': { - const newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16) - if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch')) - return false - } - - deferred.reject(new Error('[MT] Set_client_DH_params_answer fail')) - return false - } + const request = Serialization({ mtproto: true }) + + request.storeMethod('set_client_DH_params', { + nonce : auth.nonce, + server_nonce : auth.serverNonce, + encrypted_data: encryptedData + }) + + log('onGb')('Send set_client_DH_params') + + const deserializer = await sendPlainReq(auth.dcUrl, request.writer.getBuffer()) + + const response = deserializer.fetchObject('Set_client_DH_params_answer', 'client_dh') + + if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') { + const error = new Error(`[MT] Set_client_DH_params_answer response invalid: ${ response._}`) + deferred.reject(error) + throw error + } + + if (!bytesCmp(auth.nonce, response.nonce)) { + const error = new Error('[MT] Set_client_DH_params_answer nonce mismatch') + deferred.reject(error) + throw error + } + + if (!bytesCmp(auth.serverNonce, response.server_nonce)) { + const error = new Error('[MT] Set_client_DH_params_answer server_nonce mismatch') + deferred.reject(error) + throw error + } + + const authKey = await CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime) + + const authKeyHash = sha1BytesSync(authKey), + authKeyAux = authKeyHash.slice(0, 8), + authKeyID = authKeyHash.slice(-8) + + log('Got Set_client_DH_params_answer')(response._) + switch (response._) { + case 'dh_gen_ok': { + const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16) + + if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) { + deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch')) + return false } - } - if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') { - deferred.reject(new Error(`[MT] Set_client_DH_params_answer response invalid: ${ response._}`)) - return false + const serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8)) + // console.log('Auth successfull!', authKeyID, authKey, serverSalt) + + auth.authKeyID = authKeyID + auth.authKey = authKey + auth.serverSalt = serverSalt + + deferred.resolve(auth) + break } + case 'dh_gen_retry': { + const newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16) + if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) { + deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch')) + return false + } - if (!bytesCmp(auth.nonce, response.nonce)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer nonce mismatch')) - return false + return mtpSendSetClientDhParams(auth) } + case 'dh_gen_fail': { + const newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16) + if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) { + deferred.reject(new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch')) + return false + } - if (!bytesCmp(auth.serverNonce, response.server_nonce)) { - deferred.reject(new Error('[MT] Set_client_DH_params_answer server_nonce mismatch')) + deferred.reject(new Error('[MT] Set_client_DH_params_answer fail')) return false } - - return CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime) - .then(onAnswer) - } - - const onGb = (gB) => { - const data = Serialization({ mtproto: true }) - data.storeObject({ - _ : 'client_DH_inner_data', - nonce : auth.nonce, - server_nonce: auth.serverNonce, - retry_id : [0, auth.retry++], - g_b : gB - }, 'Client_DH_Inner_Data') - - const dataWithHash = sha1BytesSync(data.writer.getBuffer()).concat(data.getBytes()) - - const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv) - - const request = Serialization({ mtproto: true }) - request.storeMethod('set_client_DH_params', { - nonce : auth.nonce, - server_nonce : auth.serverNonce, - encrypted_data: encryptedData - }) - - log('onGb')('Send set_client_DH_params') - return sendPlainReq(auth.dcUrl, request.writer.getBuffer()) - .then(afterPlainRequest) } - - return CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime) - .then(onGb) } + const authChain = (auth: AuthBasic) => + mtpSendReqPQ(auth) + .then(mtpSendReqDhParams) + .then(mtpSendSetClientDhParams) + function mtpAuth(dcID: number, cached: Cached, dcUrl: string) { if (cached[dcID]) return cached[dcID].promise @@ -467,7 +480,7 @@ export const Auth = ({ Serialization, Deserialization }: TLFabric, { select, pre deferred: blueDefer() } - smartTimeout.immediate(() => mtpSendReqPQ(auth)) + immediate(authChain, auth) cached[dcID] = auth.deferred diff --git a/src/service/networker/index.js b/src/service/networker/index.js index d522000..cd66044 100644 --- a/src/service/networker/index.js +++ b/src/service/networker/index.js @@ -981,7 +981,6 @@ const getDeserializeOpts = msgGetter => ({ mt_message(result, field) { result.msg_id = ReadMediator.long( this.typeBuffer, `${ field }[msg_id]`) result.seqno = ReadMediator.int( this.typeBuffer, `${ field }[seqno]`) - //TODO WARN! Why everywhere seqno is seq_no and only there its seqno?!? result.bytes = ReadMediator.int( this.typeBuffer, `${ field }[bytes]`) const offset = this.getOffset() diff --git a/src/tl/index.js b/src/tl/index.js index b4d45aa..488c885 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -227,8 +227,6 @@ export class Serialization { } -const getChar = (e: number) => String.fromCharCode(e) - export class Deserialization { typeBuffer: TypeBuffer override: Object @@ -264,43 +262,7 @@ export class Deserialization { } } } - - fetchBytes(field: string = '') { - let len = this.typeBuffer.nextByte() - - if (len == 254) { - len = this.typeBuffer.nextByte() | - this.typeBuffer.nextByte() << 8 | - this.typeBuffer.nextByte() << 16 - } - - const bytes = this.typeBuffer.next(len) - this.typeBuffer.addPadding() - - debug(`read, bytes`)(bytesToHex(bytes), `${ field }:bytes`) - - return bytes - } - - fetchString(field: string) { - const bytes = this.fetchBytes(`${field}:string`) - const sUTF8 = [...bytes] - .map(getChar) - .join('') - - let s - try { - s = decodeURIComponent(escape(sUTF8)) - } catch (e) { - s = sUTF8 - } - - debug(`read, string`)(s, `${field}:string`) - - return s - } - - fetchIntBytes(bits, field: string = '') { + fetchIntBytes(bits: number, field: string = '') { if (bits % 32) throw new Error(`Invalid bits: ${bits}`) @@ -313,7 +275,7 @@ export class Deserialization { return bytes } - fetchRawBytes(len, field: string = '') { + fetchRawBytes(len: number | false, field: string = '') { if (len === false) { len = this.readInt(`${ field }_length`) if (len > this.typeBuffer.byteView.byteLength) @@ -326,7 +288,7 @@ export class Deserialization { } fetchPacked(type, field: string = '') { - const compressed = this.fetchBytes(`${field}[packed_string]`) + const compressed = ReadMediator.bytes( this.typeBuffer, `${field}[packed_string]`) const uncompressed = gzipUncompress(compressed) const buffer = bytesToArrayBuffer(uncompressed) const newDeserializer = new Deserialization( @@ -375,9 +337,9 @@ export class Deserialization { case 'int512': return this.fetchIntBytes(512, field) case 'string': - return this.fetchString(field) + return ReadMediator.string(this.typeBuffer, field) case 'bytes': - return this.fetchBytes(field) + return ReadMediator.bytes(this.typeBuffer, field) case 'double': return ReadMediator.double(this.typeBuffer, field) case 'Bool': @@ -388,9 +350,9 @@ export class Deserialization { let fallback field = field || type || 'Object' - const layer = this.mtproto - ? mtLayer - : apiLayer + // const layer = this.mtproto + // ? mtLayer + // : apiLayer const typeProps = getTypeProps(type) // layer.typesById @@ -443,9 +405,7 @@ export class Deserialization { if (this.override[overrideKey]) { this.override[overrideKey].apply(this, [result, `${field}[${predicate}]`]) } else { - const len = constructorData.params.length - for (let i = 0; i < len; i++) { - const param = constructorData.params[i] + for (const param of constructorData.params) { type = param.type // if (type === '#' && isNil(result.pFlags)) // result.pFlags = {} diff --git a/src/tl/mediator.js b/src/tl/mediator.js index 038a817..e58ca99 100644 --- a/src/tl/mediator.js +++ b/src/tl/mediator.js @@ -1,10 +1,10 @@ //@flow import { TypeWriter, TypeBuffer } from './type-buffer' -import { longToInts, stringToChars, lshift32 } from '../bin' +import { longToInts, stringToChars, lshift32, bytesToHex } from '../bin' import Logger from '../util/log' -const log = Logger`tl, mediator` +const log = Logger`tl:mediator` import type { BinaryData } from './index.h' @@ -115,6 +115,39 @@ export const ReadMediator = { intView[1] = this.int(ctx, `${ field }:double[high]`) return doubleView[0] + }, + string(ctx: TypeBuffer, field: string) { + const bytes = this.bytes(ctx, `${field}:string`) + const sUTF8 = [...bytes] + .map(getChar) + .join('') + + let s + try { + s = decodeURIComponent(escape(sUTF8)) + } catch (e) { + s = sUTF8 + } + + log(`read, string`)(s, `${field}:string`) + + return s + }, + bytes(ctx: TypeBuffer, field: string) { + let len = ctx.nextByte() + + if (len == 254) { + len = ctx.nextByte() | + ctx.nextByte() << 8 | + ctx.nextByte() << 16 + } + + const bytes = ctx.next(len) + ctx.addPadding() + + log(`read, bytes`)(bytesToHex(bytes), `${ field }:bytes`) + + return bytes } } @@ -142,3 +175,5 @@ const binaryDataGuard = (bytes?: number[] | ArrayBuffer | Uint8Array | string) = length } } + +const getChar = (e: number) => String.fromCharCode(e) From 32687138d9d5c7ea52beadb38456821a322f75c5 Mon Sep 17 00:00:00 2001 From: Dmitry Boldyrev Date: Sun, 26 Mar 2017 22:19:45 +0300 Subject: [PATCH 15/15] Fix typo, add changes, bump version Signed-off-by: Dmitry Boldyrev --- CHANGELOG.md | 10 +++++++++- examples/chat-history.js | 13 +++---------- examples/from-readme.js | 12 ++++++------ package.json | 3 +-- src/tl/index.js | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38caaa5..e8d6564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2.2.2 +* Fixed bug with cyrillic text encoding +* Fixed bug with several broken methods invocations +* Optimized authorization performance +* Added fix for automatic base data center selection by [@goodmind][] + # 2.2.1 * Flag (optional type) fields now acts like common fields. * Zeroes, empty strings and empty types now can be omited. write only useful fields. @@ -35,4 +41,6 @@ const connection = network.http({ host: 'ip', port: '80', protocol: 'https' }) const connection = network.wc({ host: 'ip', port: '80' }) ``` * Precision timing -* Major performance boost \ No newline at end of file +* Major performance boost + +[@goodmind]: https://github.com/goodmind/ \ No newline at end of file diff --git a/examples/chat-history.js b/examples/chat-history.js index 50b71f0..9e6da64 100644 --- a/examples/chat-history.js +++ b/examples/chat-history.js @@ -69,19 +69,12 @@ const printMessages = messages => { } -const searchUsers = async () => { +const searchUsers = async (username) => { const results = await telegram('contacts.search', { - q : '@goodmind', + q : username, limit: 100, }) - const results2 = await telegram('contacts.search', { - q : 'goodmind', - limit: 100, - }) - return { - results, - results2 - } + return results } module.exports = { diff --git a/examples/from-readme.js b/examples/from-readme.js index 27139b4..3f1fa85 100644 --- a/examples/from-readme.js +++ b/examples/from-readme.js @@ -6,9 +6,9 @@ const phone = { } const api = { - layer : 57, - initConnection : 0x69796de9, - api_id : 49631 + layer : 57, + initConnection: 0x69796de9, + api_id : 49631 } const server = { @@ -25,9 +25,9 @@ async function connect(){ api_hash : 'fb050b8f6771e15bfda5df2409931569' }) const { user } = await client('auth.signIn', { - phone_number : phone.num, - phone_code_hash: phone_code_hash, - phone_code : phone.code + phone_number: phone.num, + phone_code_hash, + phone_code : phone.code }) console.log('signed as ', user) diff --git a/package.json b/package.json index 55062a3..ec6a902 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "babel-loader": "^6.4.1", "babel-plugin-closure-elimination": "^1.1.14", "babel-plugin-transform-async-to-generator": "^6.22.0", - "babel-plugin-transform-async-to-module-method": "^6.22.0", "babel-plugin-transform-class-properties": "^6.23.0", "babel-plugin-transform-es2015-block-scoping": "^6.23.0", "babel-plugin-transform-es2015-for-of": "^6.23.0", @@ -104,5 +103,5 @@ "type": "git", "url": "git+https://github.com/zerobias/telegram-mtproto.git" }, - "version": "2.2.1" + "version": "2.2.2" } diff --git a/src/tl/index.js b/src/tl/index.js index 488c885..bfdbdbb 100644 --- a/src/tl/index.js +++ b/src/tl/index.js @@ -83,7 +83,7 @@ export class Serialization { // case 'bytes': fieldObj = [0]; break } } - else throw new Error(`Method ${methodName} doesnt recieve required argument ${paramName}`) + else throw new Error(`Method ${methodName} did not receive required argument ${paramName}`) } else { fieldObj = params[paramName] }