UNPKG

telegram-mtproto

Version:
730 lines (650 loc) 22.6 kB
//@flow import Bluebird from 'bluebird' import uuid from 'Util/uuid' import { contains, toPairs } from 'ramda' import { fromEvent } from 'most' import Emitter, { type Emit } from 'eventemitter2' import { smartTimeout, immediate } from 'mtproto-shared' import { tsNow } from '../time-manager' import { NetMessage, NetContainer } from './net-message' import { Serialization, TypeWriter } from '../../tl' import { writeInnerMessage } from '../chain/perform-request' import Config from 'ConfigProvider' import { requestNextSeq } from '../../state/reaction' import { queryAck, queryHomeDc } from '../../state/query' import ApiRequest from '../main/request' import { writeInt, writeBytes, writeLong } from '../../tl/writer' import LongPoll from '../../plugins/long-poll' import { NET, NETWORKER_STATE } from 'Action' import { type ApiConfig } from '../main/index.h' import { type DCNumber, type UID, } from 'Newtype' import { dispatch } from 'State' import L1Cache from '../../l1-cache' import Logger from 'mtproto-logger' const log = Logger`networker` let iii = 0 let akStopped = false type NetOptions = { fileUpload?: boolean, fileDownload?: boolean, notContentRelated?: boolean, afterMessageID?: string, resultType?: string, messageID?: string } type Bytes = number[] const storeIntString = (writer: TypeWriter) => ([field, value]: [string, number | string]) => { switch (typeof value) { case 'string': return writeBytes(writer, value) case 'number': return writeInt(writer, value, field) default: throw new Error(`tl storeIntString field ${field} value type ${typeof value}`) } } function addInitialMessage(serialBox: TypeWriter, appConfig: ApiConfig) { const mapper = storeIntString(serialBox) //$off const pairs: [string, string | number][] = toPairs(appConfig) pairs.forEach(mapper) } function addAfterMessage(serialBox: TypeWriter, id: string) { writeInt(serialBox, 0xcb9f372d, 'invokeAfterMsg') writeLong(serialBox, id, 'msg_id') } function isHomeDC(uid, dc) { return queryHomeDc(uid) .map(x => x === dc) .fold(() => false, x => x) } export class NetworkerThread { threadID: UID = uuid() uid: UID dcID: DCNumber iii: number upload: boolean = false sessionID: Bytes prevSessionID: Bytes connectionInited = false checkConnectionPeriod = 0 checkConnectionPromise: Promise<mixed> emit: Emit lastServerMessages: string[] = [] offline: boolean longPoll: LongPoll nextReq: number nextReqPromise: Promise<mixed> lastResendReq: { req_msg_id: string, resend_msg_ids: string[], } | void constructor(dc: number, uid: UID) { this.uid = uid const emitter = Config.rootEmitter(this.uid) this.emit = emitter.emit //$off this.dcID = dc this.iii = iii++ //$FlowIssue Object.defineProperties(this, { performSheduledRequest: { value : this.performSheduledRequest.bind(this), enumerable: false, writable : false, }, wrapMtpCall: { value : this.wrapMtpCall.bind(this), enumerable: false, } }) Config.fastCache.init(uid, this.dcID) Config.thread.set(uid, this.dcID, this) this.longPoll = new LongPoll(this) // // this.checkLongPollCond = this.checkLongPollCond.bind(this) // this.serverSalt = serverSalt // Bluebird.all([ // storage.set(keyNames.authKey, bytesToHex(authKey)), // storage.set(keyNames.saltKey, bytesToHex(serverSalt)) // ]).then(() => { // }) emitter.emit('new-networker', this) // this.updateSession() setInterval(() => this.checkLongPoll(), 10000) //NOTE make configurable interval // this.checkLongPoll() } get state(): L1Cache { return Config.fastCache.get(this.uid, this.dcID) } // updateSession() { // this.prevSessionID = this.sessionID // this.sessionID = new Array(8) // random(this.sessionID) // } updateSentMessage(sentMessageID: string) { if (!this.state.hasSent(sentMessageID)) { console.log(`no id`, sentMessageID) return false } const sentMessage = this.state.getSent(sentMessageID) const newInner: string[] = [] if (sentMessage instanceof NetContainer) { for (const innerID of sentMessage.inner) { const innerSentMessage = this.updateSentMessage(innerID) if (innerSentMessage) newInner.push(innerSentMessage.msg_id) } } // dispatch(NETWORKER_STATE.SENT.DEL([sentMessage], this.dcID)) this.state.deleteSent(sentMessage) const seq_no = requestNextSeq( this.uid, this.dcID, sentMessage.notContentRelated || sentMessage.container ) const newMessage = sentMessage.clone(seq_no, this.dcID) if (newMessage instanceof NetContainer) { newMessage.inner = newInner } this.state.addSent(newMessage) // dispatch(NETWORKER_STATE.SENT.ADD([newMessage], this.dcID)) return newMessage } wrapMtpCall(method: string, params: Object, options: NetOptions) { const serializer = new Serialization({ mtproto: true }, this.uid) serializer.storeMethod(method, params) const seqNo = requestNextSeq(this.uid, this.dcID) const message = new NetMessage( this.uid, this.dcID, seqNo, serializer.getBytes(true) ) this.pushMessage(message, options) return message.deferred.promise } wrapMtpMessage(object: Object, options: NetOptions = {}) { const serializer = new Serialization({ mtproto: true }, this.uid) serializer.storeObject(object, 'Object', 'wrap_message') const seqNo = requestNextSeq(this.uid, this.dcID, options.notContentRelated) const message = new NetMessage( this.uid, this.dcID, seqNo, serializer.getBytes(true), 'ack/resend' ) this.pushMessage(message, options) return message } wrapApiCall(netReq: ApiRequest): NetMessage { const { data: { method, params, }, options, requestID } = netReq const serializer = new Serialization(options, this.uid) const serialBox = serializer.writer if (!this.connectionInited) { addInitialMessage(serialBox, Config.apiConfig.get(this.uid)) } if (typeof options.afterMessageID === 'string') addAfterMessage(serialBox, options.afterMessageID) options.resultType = serializer.storeMethod(method, params) const seqNo = requestNextSeq(this.uid, this.dcID) const message = new NetMessage( this.uid, this.dcID, seqNo, serializer.getBytes(true), 'api' ) message.isAPI = true message.requestID = requestID this.pushMessage(message, options) return message } checkLongPollCond() { return this.longPoll.pendingTime > tsNow() || !!this.offline || akStopped } checkLongPollAfterDcCond(isClean: boolean) { return isClean && !isHomeDC(this.uid, this.dcID) } pollEvents = (() => { const emitter = new Emitter() return emitter })() runLongPoll = async() => { await this.longPoll.sendLongPool() this.checkLongPoll() } poll = fromEvent('poll', this.pollEvents) .throttle(50) .observe(this.runLongPoll) checkLongPoll() { const isClean = this.cleanupSent() // if (this.checkLongPollCond()) // return false // if (this.checkLongPollAfterDcCond(isClean)) // // console.warn(dTime(), 'Send long-poll for DC is delayed', this.dcID, this.sleepAfter) // return this.pollEvents.emit('poll') } pushMessage(message: NetMessage, options: NetOptions = {}) { message.copyOptions(options) dispatch(NETWORKER_STATE.SENT.ADD([message], this.dcID), this.uid) // dispatch(NETWORKER_STATE.PENDING.ADD([message.msg_id], this.dcID)) options.messageID = message.msg_id //TODO remove mutable operation this.state.addSent(message) this.state.setPending(message.msg_id) if (!options.noShedule) this.sheduleRequest() } pushResend(messageID: string, delay: number = 0) { const value = tsNow() + delay const sentMessage = this.state.getSent(messageID) if (sentMessage instanceof NetContainer) { for (const msg of sentMessage.inner) { this.state.setPending(msg, value) } // dispatch(NETWORKER_STATE.PENDING.ADD(sentMessage.inner, this.dcID)) } else { // dispatch(NETWORKER_STATE.PENDING.ADD([messageID], this.dcID)) this.state.setPending(messageID, value) } this.sheduleRequest(delay) } async checkConnection() { } toggleOffline(enabled: boolean) { // console.log('toggle ', enabled, this.dcID, this.iii) if (!this.offline !== undefined && this.offline == enabled) return false this.offline = enabled if (this.offline) { smartTimeout.cancel(this.nextReqPromise) delete this.nextReq if (this.checkConnectionPeriod < 1.5) this.checkConnectionPeriod = 0 this.checkConnectionPromise = smartTimeout( this.checkConnection, parseInt(this.checkConnectionPeriod * 1000)) this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5) // this.onOnlineCb = this.checkConnection // this.emit('net.offline', this.onOnlineCb) } else { this.longPoll.pendingTime = Date.now() //NOTE check long state was here this.checkLongPoll() this.sheduleRequest() // if (this.onOnlineCb) // this.emit('net.online', this.onOnlineCb) smartTimeout.cancel(this.checkConnectionPromise) } } performResend() { if (this.state.hasResends()) { const resendMsgIDs = [...this.state.getResends()] const resendOpts = { noShedule: true, notContentRelated: true } // console.log('resendReq messages', resendMsgIDs) const msg = this.wrapMtpMessage({ _ : 'msg_resend_req', msg_ids: resendMsgIDs }, resendOpts) this.lastResendReq = { req_msg_id : msg.msg_id, resend_msg_ids: resendMsgIDs, } } } performSheduledRequest() { //TODO extract huge method // console.log(dTime(), 'sheduled', this.dcID, this.iii) if (this.offline || akStopped) { log`Cancel sheduled`(``) return Bluebird.resolve(false) } delete this.nextReq const ackMsgIDs = queryAck(this.uid, this.dcID) if (ackMsgIDs.length > 0) { log`acking messages`(ackMsgIDs) this.wrapMtpMessage({ _ : 'msgs_ack', msg_ids: ackMsgIDs }, { notContentRelated: true, noShedule : true }) //TODO WTF Why we make wrapped message and doesnt use it? // const res = await msg.deferred.promise // log(`AWAITED`, `ack`)(res) dispatch(NET.ACK_DELETE({ dc: this.dcID, ack: ackMsgIDs }), this.uid) } this.performResend() const messages: NetMessage[] = [] //$off let message: NetMessage let messagesByteLen = 0 let lengthOverflow = false const pendingIds = [] for (const [messageID, value] of this.state.pendingIterator()) { if (value && value < tsNow()) continue this.state.deletePending(messageID) pendingIds.push(messageID) if (!this.state.hasSent(messageID)) continue message = this.state.getSent(messageID) const messageByteLength = message.size() + 32 const cond1 = !message.notContentRelated && lengthOverflow const cond2 = !message.notContentRelated && messagesByteLen + messageByteLength > 655360 // 640 Kb if (cond1) continue if (cond2) { lengthOverflow = true continue } messages.push(message) messagesByteLen += messageByteLength } // dispatch(NETWORKER_STATE.PENDING.DEL(pendingIds, this.dcID)) messages.map(msg => this.emit('message-in', msg)) if (!message) return Bluebird.resolve(false) //TODO Why? if (message.isAPI && !message.longPoll) { const serializer = new Serialization({ mtproto: true }, this.uid) const params = { max_delay : 0, wait_after: 100, max_wait : 5000 } serializer.storeMethod('http_wait', params) const netMessage = new NetMessage( this.uid, this.dcID, requestNextSeq(this.uid, this.dcID), serializer.getBytesPlain(), 'polling' ) this.longPoll.writePollTime() messages.push(netMessage) } if (!messages.length) { // console.log('no sheduled messages') return Bluebird.resolve(false) } let noResponseMsgs = [] if (messages.length > 1) { const container = new Serialization({ mtproto: true, startMaxLength: messagesByteLen + 64 }, this.uid) const contBox = container.writer writeInt(contBox, 0x73f1f8dc, 'CONTAINER[id]') writeInt(contBox, messages.length, 'CONTAINER[count]') const { innerMessages, noResponseMessages } = writeInnerMessage({ writer: contBox, messages }) noResponseMsgs = noResponseMessages const innerApi = messages.reduce( (acc: (string | boolean)[], val) => { if (!val.isAPI) return [...acc, false] return [...acc, val.requestID /*::|| '' */] }, []) message = new NetContainer( this.uid, this.dcID, requestNextSeq(this.uid, this.dcID, true), container.getBytes(true), innerMessages, innerApi) } else { if (message.noResponse) noResponseMsgs.push(message.msg_id) } this.state.addSent(message) if (lengthOverflow) this.sheduleRequest() dispatch(NET.SEND({ message, options : {}, threadID: this.threadID, thread : this, noResponseMsgs, }, this.dcID), this.uid) return Bluebird.resolve(true) } getMsgById = ({ req_msg_id }: { req_msg_id: string }) => this.state.getSent(req_msg_id) /* async applyServerSalt(newServerSalt: string) { const serverSalt = longToBytes(newServerSalt) await this.storage.set(`dc${ this.dcID }_server_salt`, bytesToHex(serverSalt)) this.serverSalt = serverSalt return true } */ sheduleRequest(delay: number = 0) { if (this.offline) this.checkConnection() const nextReq = tsNow() + delay if (delay && this.nextReq && this.nextReq <= nextReq) return false smartTimeout.cancel(this.nextReqPromise) if (delay > 0) this.nextReqPromise = smartTimeout( this.performSheduledRequest, delay) else immediate(this.performSheduledRequest) this.nextReq = nextReq } ackMessage(msgID: string) { const ackMsgIDs = queryAck(this.uid, this.dcID) if (contains(msgID, ackMsgIDs)) return dispatch(NET.ACK_ADD({ dc: this.dcID, ack: [msgID] }), this.uid) this.sheduleRequest(30000) } reqResendMessage(msgID: string) { log`Req resend`(msgID) this.state.addResend(msgID) this.sheduleRequest(100) } cleanupSent() { let notEmpty = false // console.log('clean start', this.dcID/*, this.state.sent*/) const sentDel = [] for (const [msgID, message] of this.state.sentIterator()) { let complete = true if (message.notContentRelated && !this.state.hasPending(msgID)) { sentDel.push(message) // console.log('clean notContentRelated', msgID) this.state.deleteSent(message) } else if (message instanceof NetContainer) { for (const inner of message.inner) { if (this.state.hasSent(inner)) { // console.log('clean failed, found', msgID, message.inner[i], // this.state.getSent(message.inner[i]).seq_no) notEmpty = true complete = false break } } // console.log('clean container', msgID) if (complete) { sentDel.push(message) this.state.deleteSent(message) } } else notEmpty = true } dispatch(NETWORKER_STATE.SENT.DEL(sentDel, this.dcID), this.uid) return !notEmpty } processMessageAck = (messageID: string) => { const sentMessage = this.state.getSent(messageID) if (sentMessage && !sentMessage.acked) { delete sentMessage.body sentMessage.acked = true return true } return false } async processMessage(message: *, messageID: string, sessionID: Uint8Array) { if (!isFinite(messageID)) { throw new TypeError(`Message ID should be finite ${messageID} ${typeof messageID}`) } const msgidInt = parseInt(messageID, 10) if (msgidInt % 2) { console.warn('[MT] Server even message id: ', messageID, message) return } switch (message._) { case 'msg_container': { /* for (const inner of message.messages) await this.processMessage(inner, inner.msg_id, sessionID) */ break } case 'bad_server_salt': { // log(`Bad server salt`)(message) // const sentMessage = this.state.getSent(message.bad_msg_id) // if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) { // log(`invalid message`)(message.bad_msg_id, message.bad_msg_seqno) // throw new Error('[MT] Bad server salt for invalid message') // } // await this.applyServerSalt(message.new_server_salt) // this.pushResend(message.bad_msg_id) // this.ackMessage(messageID) break } case 'bad_msg_notification': { /* log(`Bad msg notification`)(message) const sentMessage = this.state.getSent(message.bad_msg_id) if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) { log(`invalid message`)(message.bad_msg_id, message.bad_msg_seqno) throw new Error('[MT] Bad msg notification for invalid message') } if (message.error_code == 16 || message.error_code == 17) { if (applyServerTime( this.uid, rshift32(messageID) )) { log(`Update session`)() this.updateSession() } const badMessage = this.updateSentMessage(message.bad_msg_id) if (badMessage instanceof NetMessage) this.pushResend(badMessage.msg_id) this.ackMessage(messageID) } */ break } case 'message': { /* if (this.lastServerMessages.indexOf(messageID) != -1) { // console.warn('[MT] Server same messageID: ', messageID) this.ackMessage(messageID) return } this.lastServerMessages.push(messageID) if (this.lastServerMessages.length > 100) { this.lastServerMessages.shift() } await this.processMessage(message.body, message.msg_id, sessionID) */ break } case 'new_session_created': { // this.ackMessage(messageID) // this.processMessageAck(message.first_msg_id) // await this.applyServerSalt(message.server_salt) /* this.emit('new-session', { threadID : this.threadID, networkerDC: this.dcID, messageID, message }) */ // const baseDcID = await this.storage.get('dc') // const updateCond = // baseDcID === this.dcID && // !this.upload && // updatesProcessor // if (updateCond) // updatesProcessor(message, true) break } case 'msgs_ack': { /* message.msg_ids.forEach(this.processMessageAck) */ break } case 'msg_detailed_info': { /* if (!this.state.hasSent(message.msg_id)) { this.ackMessage(message.answer_msg_id) break } */ break } case 'msg_new_detailed_info': { /* this.ackMessage(message.answer_msg_id) this.reqResendMessage(message.answer_msg_id) */ break } case 'msgs_state_info': { /* this.ackMessage(message.answer_msg_id) const lastResendReq = this.lastResendReq if (!lastResendReq) break if (lastResendReq.req_msg_id != message.req_msg_id) break // const resendDel = [] for (const badMsgID of lastResendReq.resend_msg_ids) { // resendDel.push(badMsgID) this.state.deleteResent(badMsgID) } */ // dispatch(NETWORKER_STATE.RESEND.DEL(resendDel, this.dcID)) break } case 'rpc_result': { this.ackMessage(messageID) const sentMessageID = message.req_msg_id const sentMessage = this.state.getSent(sentMessageID) this.processMessageAck(sentMessageID) if (!sentMessage) break // dispatch(NETWORKER_STATE.SENT.DEL([sentMessage], this.dcID)) this.state.deleteSent(sentMessage) if (message.result._ == 'rpc_error') { this.emit('rpc-error', { threadID : this.threadID, networkerDC: this.dcID, error : message.result, sentMessage, message }) } else { this.emit('rpc-result', { threadID : this.threadID, networkerDC: this.dcID, message, sentMessage, result : message.result }) if (sentMessage.isAPI) this.connectionInited = true } break } default: { this.ackMessage(messageID) /* this.emit('untyped-message', { threadID : this.threadID, networkerDC: this.dcID, message, messageID, sessionID, result : message.result }) if (updatesProcessor) updatesProcessor(message, true) */ break } } } } export function createThread( dc: DCNumber, uid: UID ): NetworkerThread { return new NetworkerThread(dc, uid) } export default NetworkerThread