|
@@ -13,13 +13,19 @@ const { LAYER } = require('../tl/AllTLObjects')
|
|
|
const { functions, types } = require('../tl')
|
|
|
const { computeCheck } = require('../Password')
|
|
|
const MTProtoSender = require('../network/MTProtoSender')
|
|
|
+const Helpers = require('../Helpers')
|
|
|
const { ConnectionTCPObfuscated } = require('../network/connection/TCPObfuscated')
|
|
|
-
|
|
|
+const { BinaryWriter } = require('../extensions')
|
|
|
const DEFAULT_DC_ID = 4
|
|
|
const DEFAULT_IPV4_IP = '149.154.167.51'
|
|
|
const DEFAULT_IPV6_IP = '[2001:67c:4e8:f002::a]'
|
|
|
const DEFAULT_PORT = 443
|
|
|
|
|
|
+// Chunk sizes for upload.getFile must be multiples of the smallest size
|
|
|
+const MIN_CHUNK_SIZE = 4096
|
|
|
+const MAX_CHUNK_SIZE = 512 * 1024
|
|
|
+
|
|
|
+
|
|
|
class TelegramClient {
|
|
|
static DEFAULT_OPTIONS = {
|
|
|
connection: ConnectionTCPObfuscated,
|
|
@@ -182,6 +188,7 @@ class TelegramClient {
|
|
|
|
|
|
async _getDC(dcId, cdn = false) {
|
|
|
if (!this._config) {
|
|
|
+ console.log('no config getting new')
|
|
|
this._config = await this.invoke(new functions.help.GetConfigRequest())
|
|
|
}
|
|
|
if (cdn && !this._cdnConfig) {
|
|
@@ -214,8 +221,6 @@ class TelegramClient {
|
|
|
if (request.CONSTRUCTOR_ID in this._floodWaitedRequests) {
|
|
|
const due = this._floodWaitedRequests[request.CONSTRUCTOR_ID]
|
|
|
const diff = Math.round(due - new Date().getTime() / 1000)
|
|
|
- console.log('diff is ', diff)
|
|
|
- console.log('limit is ', this.floodSleepLimit)
|
|
|
if (diff <= 3) { // Flood waits below 3 seconds are 'ignored'
|
|
|
delete this._floodWaitedRequests[request.CONSTRUCTOR_ID]
|
|
|
} else if (diff <= this.floodSleepLimit) {
|
|
@@ -234,6 +239,7 @@ class TelegramClient {
|
|
|
for (attempt = 0; attempt < this._requestRetries; attempt++) {
|
|
|
try {
|
|
|
const promise = this._sender.send(request)
|
|
|
+ console.log('promise is ', promise)
|
|
|
const result = await promise
|
|
|
this.session.processEntities(result)
|
|
|
this._entityCache.add(result)
|
|
@@ -258,6 +264,7 @@ class TelegramClient {
|
|
|
if (shouldRaise && await this.isUserAuthorized()) {
|
|
|
throw e
|
|
|
}
|
|
|
+ await Helpers.sleep(1000)
|
|
|
await this._switchDC(e.newDc)
|
|
|
} else {
|
|
|
throw e
|
|
@@ -474,6 +481,7 @@ class TelegramClient {
|
|
|
* @private
|
|
|
*/
|
|
|
_onLogin(user) {
|
|
|
+ console.log('on login')
|
|
|
this._bot = Boolean(user.bot)
|
|
|
this._authorized = true
|
|
|
return user
|
|
@@ -642,6 +650,17 @@ class TelegramClient {
|
|
|
|
|
|
|
|
|
// users region
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
/**
|
|
|
Turns the given entity into its input entity version.
|
|
|
|
|
@@ -847,6 +866,302 @@ class TelegramClient {
|
|
|
return sender
|
|
|
}
|
|
|
|
|
|
+ // end region
|
|
|
+
|
|
|
+ // download region
|
|
|
+
|
|
|
+
|
|
|
+ async downloadFile(inputLocation, file, args = {
|
|
|
+ partSizeKb: null,
|
|
|
+ fileSize: null,
|
|
|
+ progressCallback: null,
|
|
|
+ dcId: null,
|
|
|
+ }) {
|
|
|
+ if (!args.partSizeKb) {
|
|
|
+ if (!args.fileSize) {
|
|
|
+ args.partSizeKb = 64
|
|
|
+ } else {
|
|
|
+ args.partSizeKb = utils.getAppropriatedPartSize(args.fileSize)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const partSize = parseInt(args.partSizeKb * 1024)
|
|
|
+ if (partSize % MIN_CHUNK_SIZE !== 0) {
|
|
|
+ throw new Error('The part size must be evenly divisible by 4096')
|
|
|
+ }
|
|
|
+ const inMemory = !file || file === Buffer
|
|
|
+ let f
|
|
|
+ if (inMemory) {
|
|
|
+ f = new BinaryWriter(Buffer.alloc(0))
|
|
|
+ } else {
|
|
|
+ throw new Error('not supported')
|
|
|
+ }
|
|
|
+ const res = utils.getInputLocation(inputLocation)
|
|
|
+ let exported = res.dcId && this.session.dcId !== res.dc
|
|
|
+ let sender
|
|
|
+ if (exported) {
|
|
|
+ try {
|
|
|
+ sender = await this._borrowExportedSender(res.dcId)
|
|
|
+ } catch (e) {
|
|
|
+ if (e instanceof errors.DcIdInvalidError) {
|
|
|
+ // Can't export a sender for the ID we are currently in
|
|
|
+ sender = this._sender
|
|
|
+ exported = false
|
|
|
+ } else {
|
|
|
+ throw e
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ sender = this._sender
|
|
|
+ }
|
|
|
+
|
|
|
+ this._log.info(`Downloading file in chunks of ${partSize} bytes`)
|
|
|
+
|
|
|
+ try {
|
|
|
+ let offset = 0
|
|
|
+ // eslint-disable-next-line no-constant-condition
|
|
|
+ while (true) {
|
|
|
+ let result
|
|
|
+ try {
|
|
|
+ result = await sender.send(new functions.upload.GetFileRequest({
|
|
|
+ location: res.inputLocation,
|
|
|
+ offset: offset,
|
|
|
+ limit: partSize,
|
|
|
+ }))
|
|
|
+ if (result instanceof types.upload.FileCdnRedirect) {
|
|
|
+ throw new Error('not implemented')
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ if (e instanceof errors.FileMigrateError) {
|
|
|
+ this._log.info('File lives in another DC')
|
|
|
+ sender = await this._borrowExportedSender(e.newDc)
|
|
|
+ exported = true
|
|
|
+ continue
|
|
|
+ } else {
|
|
|
+ throw e
|
|
|
+ }
|
|
|
+ }
|
|
|
+ offset += partSize
|
|
|
+
|
|
|
+ if (!result.bytes.length) {
|
|
|
+ if (inMemory) {
|
|
|
+ return f.getValue()
|
|
|
+ } else {
|
|
|
+ // Todo implement
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this._log.debug(`Saving ${result.bytes.length} more bytes`)
|
|
|
+ f.write(result.bytes)
|
|
|
+ if (args.progressCallback) {
|
|
|
+ await args.progressCallback(f.getValue().length, args.fileSize)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ // TODO
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async downloadMedia(message, file, args = {
|
|
|
+ thumb: null,
|
|
|
+ progressCallback: null,
|
|
|
+ }) {
|
|
|
+ let date
|
|
|
+ let media
|
|
|
+ if (message instanceof types.Message) {
|
|
|
+ date = message.date
|
|
|
+ media = message.media
|
|
|
+ } else {
|
|
|
+ date = new Date().getTime()
|
|
|
+ media = message
|
|
|
+ }
|
|
|
+ if (typeof media == 'string') {
|
|
|
+ throw new Error('not implemented')
|
|
|
+ }
|
|
|
+
|
|
|
+ if (media instanceof types.MessageMediaWebPage) {
|
|
|
+ if (media.webpage instanceof types.WebPage) {
|
|
|
+ media = media.webpage.document || media.webpage.photo
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (media instanceof types.MessageMediaPhoto || media instanceof types.Photo) {
|
|
|
+ console.log('is a photo')
|
|
|
+ return await this._downloadPhoto(media, file, date, args.thumb, args.progressCallback)
|
|
|
+ } else if (media instanceof types.MessageMediaDocument || media instanceof types.Document) {
|
|
|
+ return await this._downloadDocument(media, file, date, args.thumb, args.progressCallback)
|
|
|
+ } else if (media instanceof types.MessageMediaContact && args.thumb == null) {
|
|
|
+ return this._downloadContact(media, file)
|
|
|
+ } else if ((media instanceof types.WebDocument || media instanceof types.WebDocumentNoProxy) && args.thumb == null) {
|
|
|
+ return await this._downloadWebDocument(media, file, args.progressCallback)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async downloadProfilePhoto(entity, file, downloadBig = true) {
|
|
|
+ // ('User', 'Chat', 'UserFull', 'ChatFull')
|
|
|
+ const ENTITIES = [0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697]
|
|
|
+ // ('InputPeer', 'InputUser', 'InputChannel')
|
|
|
+ // const INPUTS = [0xc91c90b6, 0xe669bf46, 0x40f202fd]
|
|
|
+ // Todo account for input methods
|
|
|
+ const thumb = downloadBig ? -1 : 0
|
|
|
+
|
|
|
+ let photo
|
|
|
+ if (!(entity.SUBCLASS_OF_ID in ENTITIES)) {
|
|
|
+ photo = entity
|
|
|
+ } else {
|
|
|
+ if (!entity.photo) {
|
|
|
+ // Special case: may be a ChatFull with photo:Photo
|
|
|
+ if (!entity.chatPhoto) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ return await this._downloadPhoto(
|
|
|
+ entity.chatPhoto, file, null, thumb, null,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ photo = entity.photo
|
|
|
+ }
|
|
|
+ let dcId
|
|
|
+ let which
|
|
|
+ let loc
|
|
|
+ if (photo instanceof types.UserProfilePhoto || photo instanceof types.ChatPhoto) {
|
|
|
+ dcId = photo.dcId
|
|
|
+ which = downloadBig ? photo.photoBig : photo.photoSmall
|
|
|
+ loc = new types.InputPeerPhotoFileLocation({
|
|
|
+ peer: await this.getInputEntity({ entity: entity }),
|
|
|
+ localId: which.localId,
|
|
|
+ volumeId: which.volumeId,
|
|
|
+ big: downloadBig,
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ // It doesn't make any sense to check if `photo` can be used
|
|
|
+ // as input location, because then this method would be able
|
|
|
+ // to "download the profile photo of a message", i.e. its
|
|
|
+ // media which should be done with `download_media` instead.
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ file = file ? file : Buffer
|
|
|
+ try {
|
|
|
+ const result = await this.downloadFile(loc, file, {
|
|
|
+ dcId: dcId,
|
|
|
+ })
|
|
|
+ return result
|
|
|
+ } catch (e) {
|
|
|
+ if (e instanceof errors.LocationInvalidError) {
|
|
|
+ const ie = await this.getInputEntity(entity)
|
|
|
+ if (ie instanceof types.InputPeerChannel) {
|
|
|
+ const full = await this.invoke(new functions.channels.GetFullChannelRequest({
|
|
|
+ channel: ie,
|
|
|
+ }))
|
|
|
+ return await this._downloadPhoto(full.fullChat.chatPhoto, file, null, null, thumb)
|
|
|
+ } else {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw e
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ _getThumb(thumbs, thumb) {
|
|
|
+ if (thumb === null || thumb === undefined) {
|
|
|
+ return thumbs[thumbs.length - 1]
|
|
|
+ } else if (typeof thumb === 'number') {
|
|
|
+ return thumbs[thumb]
|
|
|
+ } else if (thumb instanceof types.PhotoSize ||
|
|
|
+ thumb instanceof types.PhotoCachedSize ||
|
|
|
+ thumb instanceof types.PhotoStrippedSize) {
|
|
|
+ return thumb
|
|
|
+ } else {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ _downloadCachedPhotoSize(size, file) {
|
|
|
+ // No need to download anything, simply write the bytes
|
|
|
+ let data
|
|
|
+ if (size instanceof types.PhotoStrippedSize) {
|
|
|
+ data = utils.strippedPhotoToJpg(size.bytes)
|
|
|
+ } else {
|
|
|
+ data = size.bytes
|
|
|
+ }
|
|
|
+ return data
|
|
|
+ }
|
|
|
+
|
|
|
+ async _downloadPhoto(photo, file, date, thumb, progressCallback) {
|
|
|
+ if (photo instanceof types.MessageMediaPhoto) {
|
|
|
+ photo = photo.photo
|
|
|
+ }
|
|
|
+ if (!(photo instanceof types.Photo)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const size = this._getThumb(photo.sizes, thumb)
|
|
|
+ if (!size || (size instanceof types.PhotoSizeEmpty)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ file = file ? file : Buffer
|
|
|
+ if (size instanceof types.PhotoCachedSize || size instanceof types.PhotoStrippedSize) {
|
|
|
+ return this._downloadCachedPhotoSize(size, file)
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await this.downloadFile(
|
|
|
+ new types.InputPhotoFileLocation({
|
|
|
+ id: photo.id,
|
|
|
+ accessHash: photo.accessHash,
|
|
|
+ fileReference: photo.fileReference,
|
|
|
+ thumbSize: size.type,
|
|
|
+ }),
|
|
|
+ file,
|
|
|
+ {
|
|
|
+ fileSize: size.size,
|
|
|
+ progressCallback: progressCallback,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ async _downloadDocument(document, file, date, thumb, progressCallback) {
|
|
|
+ if (document instanceof types.MessageMediaPhoto) {
|
|
|
+ document = document.document
|
|
|
+ }
|
|
|
+ if (!(document instanceof types.Document)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let size
|
|
|
+ file = file ? file : Buffer
|
|
|
+
|
|
|
+ if (thumb === null || thumb === undefined) {
|
|
|
+ size = null
|
|
|
+ } else {
|
|
|
+ size = this._getThumb(document.thumbs, thumb)
|
|
|
+ if (size instanceof types.PhotoCachedSize || size instanceof types.PhotoStrippedSize) {
|
|
|
+ return this._downloadCachedPhotoSize(size, file)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const result = await this.downloadFile(
|
|
|
+ new types.InputDocumentFileLocation({
|
|
|
+ id: document.id,
|
|
|
+ accessHash: document.accessHash,
|
|
|
+ fileReference: document.fileReference,
|
|
|
+ thumbSize: size ? size.type : '',
|
|
|
+ }),
|
|
|
+ file,
|
|
|
+ {
|
|
|
+ fileSize: size ? size.size : document.size,
|
|
|
+ progressCallback: progressCallback,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ return result
|
|
|
+ }
|
|
|
+
|
|
|
+ _downloadContact(media, file) {
|
|
|
+ throw new Error('not implemented')
|
|
|
+ }
|
|
|
+
|
|
|
+ // endregion
|
|
|
+ async _downloadWebDocument(media, file, progressCallback) {
|
|
|
+ throw new Error('not implemented')
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
module.exports = TelegramClient
|