telegram-mtproto
Version:
Telegram MTProto library
730 lines (650 loc) • 22.6 kB
JavaScript
//@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