UNPKG

telegram-mtproto

Version:
803 lines (749 loc) 19.8 kB
//@flow import { append, reject, isEmpty, chain, filter, pipe, lensPath, over, defaultTo, } from 'ramda' import { cache } from 'fluture' // import { Just } from 'folktale/maybe' import { Maybe } from 'apropos' const { Just, Nothing } = Maybe import { type IncomingType, type SystemMessage, type MessageUnit, type ᐸPatchProcessAck, type ᐸPatchAck, type ᐸPatchHome, type ᐸPatchAuthKey, type ᐸPatchReqResend, type ᐸPatchResend, type ᐸPatchLastMesages, type ᐸPatchSalt, type ᐸPatchSession, type ᐸPatchSummary, } from './index.h' import { dispatch } from 'State' import describeProtocolError from './describe-protocol-error' import { MAIN, NETWORKER_STATE } from 'Action' import { longToBytes, rshift32 } from 'Bin' import guard from 'Util/match-spec' import warning from 'Util/warning' import random from '../service/secure-random' import { toDCNumber } from 'Newtype' import { type ᐸMTNewSessionCreated, type ᐸMTBadSalt, type ᐸMTBadNotification, type ᐸMTRpcResult, } from 'Mtp' import { queryRequest, queryAck } from '../state/query' import Logger from 'mtproto-logger' import { applyServerTime } from '../service/time-manager' import invoke from '../service/invoke' import { NetMessage } from '../service/networker/net-message' import Config from 'ConfigProvider' const log = Logger`single-handler` //eslint-disable-next-line const appendRO = (() => { /*:: declare function appendReadOnly<T>( value: T, list: $ReadOnlyArray<T> ): $ReadOnlyArray<T> */ return (append /*:: , appendReadOnly */) })() /*:: interface Writer<+T> { set next(x: T): void, read(): $ReadOnlyArray<T>, } */ class WriterFacade<T> implements Writer<T> { state: $ReadOnlyArray<T> = [] set next(x: T) { this.state = appendRO(x, this.state) } read(): $ReadOnlyArray<T> { return this.state } } type Selector = <A, B>( check: (((x: Flags) => ?true) & ((x: A) => B[])) ) => (x: A) => B[] const getFlags = (e): Flags => e.flags const selector: Selector = (select) => pipe( filter(pipe(getFlags, select, e => !!e)), chain(select) ) const noEmpty = reject(isEmpty) export default function singleHandler( ctx: IncomingType, message: MessageUnit ): { message: MessageUnit, summary: ᐸPatchSummary, } { const { flags } = message /*:: const inners = handleInner(ctx, message) const unrels = handleUnrelated(ctx, message) */ type Saved = ᐸPatchSummary const patches: Writer<ᐸPatchSummary> = new WriterFacade let result = message if (flags.inner) { patches.next = handleInner(ctx, message) } if (isUnrelatedBody(flags)) { const unrel = handleUnrelated(ctx, message) if (unrel !== void 0) patches.next = unrel } if (flags.error) { const { info, patch } = handleError(ctx, message) patches.next = patch result = info } const collected = patches.read() // .map(({ flags, ...e }) => ({ // flags: { // ...emptyPatch().flags, // ...flags, // }, // ...e // })) // collected.forEach(e => log`patches`(e)) const summary = makeSummary(collected) //$off log`summary`(noEmpty(summary)) return { message: result, summary, } } const isUnrelatedBody = guard({ api : false, container: false, body : true, }) type Flags = { +net?: true, +processAck?: true, +ack?: true, +home?: true, +authKey?: true, +reqResend?: true, +resend?: true, // as pushResend +lastServerMessages?: true, +salt?: true, +session?: true, } //$off const processAckChain = selector(e => e.processAck) //$off const ackChain = selector(e => e.ack) //$off // const homeChain = selector(e => e.home) // //$off // const authChain = selector(e => e.authKey) //$off const reqResendChain = selector(e => e.reqResend) // //$off // const resendChain = selector(e => e.resend) // //$off // const lastMessagesChain = selector(e => e.lastServerMessages) // //$off // const saltChain = selector(e => e.salt) // //$off // const sessionChain = selector(e => e.session) function makeSummary(collected): ᐸPatchSummary { const processAck: ᐸPatchProcessAck[] = processAckChain(collected) const ack: ᐸPatchAck[] = ackChain(collected) // const home: ᐸPatchᐳHome[] = homeChain(collected) // const auth: ᐸPatchᐳAuthKey[] = authChain(collected) const reqResend: ᐸPatchReqResend[] = reqResendChain(collected) // const resend: ᐸPatchᐳResend[] = resendChain(collected) // const lastMessages: ᐸPatchᐳLastMesages[] = lastMessagesChain(collected) // const salt: ᐸPatchᐳSalt[] = saltChain(collected) // const session: ᐸPatchᐳSession[] = sessionChain(collected) const result = { processAck, ack, // home, // auth, reqResend, // resend, // lastMessages, // salt, // session, } return result } const patchState = (() => { const defArray = defaultTo([]) // const lensProcessAck = lensPath(['processAck']) // const lensAck = lensPath(['ack']) // const lensReqResend = lensPath(['reqResend']) // const lensFlags = lensPath(['flags']) class PatchState { value: ᐸPatchSummary constructor(value: ᐸPatchᐳSummary) { this.value = value } ack(data) { return new PatchState({ // flags: { ...flags, ack: true }, ...this.value, ack : [...defArray(this.value.ack), ...data] }) } processAck(data) { return new PatchState({ // flags : { ...flags, processAck: true }, ...this.value, processAck: [...defArray(this.value.processAck), ...data] }) } reqResend(data) { return new PatchState({ // flags : { ...flags, reqResend: true }, ...this.value, reqResend: [...defArray(this.value.reqResend), ...data] }) } } return () => new PatchState(emptyPatch()) })() function handleUnrelated(ctx: IncomingType, message: MessageUnit) { const { thread, uid, dc } = ctx //$off const cast: typeof message & { body: SystemMessage } = message const { body } = cast const { id } = cast switch (body._) { case 'msgs_ack': { // body.msg_ids.forEach(thread.processMessageAck) const msg_ids: string[] = body.msg_ids return patchState() .processAck(msg_ids.map(msg => ({ dc, id: msg }))) .value } case 'msg_detailed_info': { if (!Config.fastCache.get(uid, dc).hasSent(body.msg_id)) { const id: string = body.answer_msg_id thread.ackMessage(id) return patchState() .ack([{ dc, id }]) .value } return emptyPatch() } case 'msg_new_detailed_info': { const { answer_msg_id: id } = body let state = patchState() if (queryAck(uid, dc).indexOf(id) === -1) state = state.reqResend([{ dc, id }]) return state // .ack([{ dc, id }]) .value } case 'msgs_state_info': { const { answer_msg_id } = body // thread.ackMessage(answer_msg_id) const lastResendReq = thread.lastResendReq if (!lastResendReq) break if (lastResendReq.req_msg_id != body.req_msg_id) break // const resendDel = [] for (const badMsgID of lastResendReq.resend_msg_ids) { // resendDel.push(badMsgID) Config.fastCache.get(uid, dc).deleteResent(badMsgID) } const aId: string = answer_msg_id return patchState() .ack([{ dc, id: aId }]) .reqResend([{ dc, id }]) .value // dispatch(NETWORKER_STATE.RESEND.DEL(resendDel, this.dcID)) } case 'rpc_result': { return handleRpcResult(ctx, message) } case 'new_session_created': { // thread.emit('new-session', { // threadID : thread.threadID, // networkerDC: message.dc, // messageID : message.id, // message : body // }) return handleNewSession(ctx, message) } case 'bad_server_salt': { return handleBadSalt(ctx, message) } case 'bad_msg_notification': { return handleBadNotify(ctx, message) } default: { const { id } = message thread.ackMessage(message.id) thread.emit('untyped-message', { threadID : thread.threadID, networkerDC: message.dc, message : body, messageID : message.id, sessionID : Config.session.get(ctx.thread.uid, message.dc), result : message, }) return patchState() .ack([{ dc, id }]) .value } } } function handleInner(ctx: IncomingType, message: MessageUnit) { const { thread } = ctx const { id, dc } = message if (thread.lastServerMessages.indexOf(id) != -1) { // console.warn('[MT] Server same messageID: ', messageID) // thread.ackMessage(id) return patchState() .ack([{ dc, id }]) .value } else { thread.lastServerMessages.push(id) if (thread.lastServerMessages.length > 100) { thread.lastServerMessages.shift() } return { flags: { net : true, lastServerMessages: true, }, net: [{ dc, lastServerMessages: [id], }], lastServerMessages: [{ dc, id }] } } } const migrateRegexp = /^(PHONE_MIGRATE_|NETWORK_MIGRATE_|USER_MIGRATE_)(\d+)/ const fileMigrateRegexp = /^(FILE_MIGRATE_)(\d+)/ const floodWaitRegexp = /^(FLOOD_WAIT_)(\d+)/ function handleError(ctx: IncomingType, data: MessageUnit) { const err: { code: number, message: string, handled: boolean, //$off } = data.error const { code, message, } = err if (floodWaitRegexp.test(message)) { return handleFloodWait(message, data, code, ctx) } else if (fileMigrateRegexp.test(message)) { return handleFileMigrate(message, data, code, ctx) } else if (migrateRegexp.test(message)) { return handleMigrateError(message, data, code, ctx) } else { switch (message) { case 'AUTH_KEY_UNREGISTERED': return handleAuthUnreg(ctx, message, data, code) case 'AUTH_RESTART': return handleAuthRestart(message, data, code, ) } } return { info: data, patch: emptyPatch() } } function numberFromError(message, regexp): Maybe<number> { const matched = message.match(regexp) if (!matched || matched.length < 2) return Nothing() const [ , , numStr] = matched if (!isFinite(numStr)) return Nothing() const num = parseInt(numStr, 10) return Just(num) } const patchNothing = data => () => ({ info : data, patch: emptyPatch(), }) const floodWarning = warning({ isIssue: false, message: ['Flood wait! Too many requests, you should wait', 'seconds before new requests'] }) function handleFloodWait(message, data, code, ctx) { return numberFromError(message, floodWaitRegexp) .fold( patchNothing(data), waitTime => { floodWarning(waitTime) const info = { ...data, error: { code, message, handled: true } } return { info, patch: emptyPatch() } }) } function handleFileMigrate(message, data, code, ctx) { const { uid, dc } = ctx return numberFromError(message, fileMigrateRegexp) /*:: .map(toDCNumber) */ .pred(dc => data.flags.methodResult) .chain(newDc => queryRequest(uid, dc, data.methodResult.outID).map(req => ({ req, newDc, }))) .fold( patchNothing(data), ({ req, newDc }) => { req.dc = Just(newDc) const futureAuth = Config.authRequest.get(uid, newDc) if (!futureAuth) { const authReq = cache(invoke(uid, 'auth.exportAuthorization', { dc_id: newDc }) .map(resp => (console.log(resp), resp)) .map((resp: mixed) => { if (typeof resp === 'object' && resp != null) { if (typeof resp.id === 'number') { const { id } = resp if (resp.bytes != null) { const { bytes } = resp return { id, bytes: [...bytes] } } } } console.error('incorrect', resp) return resp }) .chain(resp => invoke(uid, 'auth.importAuthorization', resp, { dcID: newDc }))) Config.authRequest.set(uid, newDc, authReq) authReq.promise() } const info = { ...data, error: { code, message, handled: true } } return { info, patch: emptyPatch() } }) } function handleMigrateError(message, data, code, ctx) { const { uid, dc } = ctx return numberFromError(message, migrateRegexp) /*:: .map(toDCNumber) */ .fold( patchNothing(data), newDc => { dispatch(MAIN.RECOVERY_MODE({ halt : dc, recovery: newDc, uid, }), uid) Config.fastCache.init(uid, dc) Config.seq.set(uid, dc, 0) Config.halt.set(uid, dc, true) Config.halt.set(uid, newDc, false) //$off Config.session.set(uid, ctx.dc, null) Promise.all([ Config.storageAdapter.set.dc(uid, newDc), Config.storageAdapter.set.nearestDC(uid, newDc) ]).then(() => { dispatch(MAIN.DC_DETECTED({ dc: newDc, uid, }), uid) }) const patch = { flags: { net : true, home: true, }, net: [{ dc : data.dc, home: false, }, { dc : newDc, home: true, }], home: [newDc], } const info = { ...data, error: { code, message, handled: true } } return { info, patch } }) } function handleAuthRestart(message, data, code) { const { dc } = data // dispatch(MAIN.AUTH_UNREG(dc)) const patch = { flags: { net : true, authKey: true, }, net: [{ dc, authKey: [], }], authKey: [{ dc, authKey: false, }] } const info = { ...data, error: { code, message, handled: true } } return { info, patch } } function handleAuthUnreg(ctx: IncomingType, message, data, code) { const { dc, uid } = ctx dispatch(MAIN.AUTH_UNREG(dc), uid) const patch = { flags: { net : true, authKey: true, }, net: [{ dc, authKey: [], }], authKey: [{ dc, authKey: false, }] } const info = { ...data, error: { code, message, handled: true } } return { info, patch } } //$off const emptyPatch = (): ᐸPatchSummary => ({ flags: { /*:: net: true, */ } }) function handleNewSession(ctx: IncomingType, message: MessageUnit) { const body: ᐸMTNewSessionCreated = message.body const { first_msg_id, server_salt } = body const salt = longToBytes(server_salt) const { dc, id } = message // const session = new Array(8) // random(session) // Config.seq.set(ctx.thread.uid, dc, 0) return { flags: { net : true, // session : true, salt : true, ack : true, processAck: true, }, net: [{ dc, salt, // session, seq : 0, first: first_msg_id, // Refers to outcoming api message }], // session: [{ // dc, // session, // seq : 0, // first: first_msg_id, // }], salt: [{ dc, salt }], ack : [{ dc, id }], processAck: [{ dc, id: first_msg_id }], } } function handleBadNotify(ctx: IncomingType, message: MessageUnit) { const body: ᐸMTBadNotification = message.body const { dc, uid } = ctx log`Bad msg notification`(message) const { bad_msg_id: badMsg, bad_msg_seqno: seq, error_code: code, } = body const sentMessage = Config.fastCache.get(uid, dc).getSent(badMsg) const error = describeProtocolError(code || 0) errorPrint: { log`protocol error, code`(error.code) log`protocol error, message`(error.message) log`protocol error, description`(error.description) } if (!sentMessage || sentMessage.seq_no != seq) { log`Bad msg notification, seq`(badMsg, seq) // throw error } const { id } = message let flags = { /*:: ack: true */ } let data = {} if (code === 16 || code === 17) { if (applyServerTime( ctx.thread.uid, rshift32(id) )) { const session = new Array(8) random(session) flags = { ...flags, session: true } data = { ...data, session: [{ dc, session, seq : 0, first: badMsg, }], } const badMessage = ctx.thread.updateSentMessage(badMsg) if (badMessage instanceof NetMessage) { flags = { ...flags, resend: true } data = { ...data, resend: [{ dc, id: badMsg }] } } flags = { ...flags, ack: true } data = { ...data, ack: [{ dc, id }], } } } return { ...data, flags, } } function handleBadSalt(ctx: IncomingType, message: MessageUnit) { const body: ᐸMTBadSalt = message.body log`Bad server salt`(message) const { bad_msg_id: badMsg, bad_msg_seqno: seq, error_code: code, new_server_salt: newSalt, } = body const { dc, uid } = ctx const sentMessage = Config.fastCache.get(uid, dc).getSent(badMsg) const error = describeProtocolError(code || 0) errorPrint: { log`protocol error, code`(error.code) log`protocol error, message`(error.message) log`protocol error, description`(error.description) } if (!sentMessage || sentMessage.seq_no != seq) { log`invalid message, seq`(badMsg, seq) // throw error } const salt = longToBytes(newSalt) const { id } = message const session = new Array(8) random(session) ctx.thread.pushResend(badMsg) return { flags: { net : true, session: true, salt : true, ack : true, resend : true, }, net: [{ dc, salt, session, seq : 0, first: badMsg, }], session: [{ dc, session, seq : 0, first: badMsg, }], salt: [{ dc, salt }], ack : [{ dc, id }], resend: [{ dc, id: badMsg }] } } function handleRpcResult(ctx: IncomingType, message: MessageUnit) { const { thread, dc, uid } = ctx const { id } = message const body: ᐸMTRpcResult = message.body thread.ackMessage(id) const sentMessageID = body.req_msg_id const sentMessage = Config.fastCache.get(uid, dc).getSent(sentMessageID) // thread.processMessageAck(sentMessageID) if (!sentMessage) { console.warn('No sent message!', sentMessageID, message) return emptyPatch() } dispatch(NETWORKER_STATE.SENT.DEL([sentMessage], dc), uid) Config.fastCache.get(uid, dc).deleteSent(sentMessage) if (body.result) { if (body.result._ == 'rpc_error') { thread.emit('rpc-error', { threadID : thread.threadID, networkerDC: dc, error : body.result, sentMessage, message }) } else { thread.emit('rpc-result', { threadID : thread.threadID, networkerDC: dc, message, sentMessage, result : body.result }) } } else { console.warn('No result!', sentMessageID, message) } if (sentMessage.isAPI) thread.connectionInited = true return emptyPatch() }