Pārlūkot izejas kodu

Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger

painor 5 gadi atpakaļ
vecāks
revīzija
45161b5c98

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+node_modules
+dist
+.cache
+.env
+src/lib/gramjs/build/
+build-contest/
+.idea
+*.iml

+ 33 - 0
src/api/gramjs/builders/common.ts

@@ -0,0 +1,33 @@
+import { ApiFileLocation } from '../../types';
+
+export function buildApiPhotoLocations(entity: MTP.user | MTP.chat): {
+  small: ApiFileLocation;
+  big: ApiFileLocation;
+} | undefined {
+  if (!entity.photo) {
+    return undefined;
+  }
+
+  const { photoSmall, photoBig, dcId } = entity.photo as (MTPNext.UserProfilePhoto | MTPNext.ChatPhoto);
+
+  return {
+    small: {
+      ...photoSmall,
+      dcId,
+    },
+    big: {
+      ...photoBig,
+      dcId,
+    },
+  };
+}
+
+export function bytesToDataUri(bytes: Uint8Array, shouldOmitPrefix = false, mimeType: string = 'image/jpg') {
+  const prefix = shouldOmitPrefix ? '' : `data:${mimeType};base64,`;
+
+  return `${prefix}${btoa(
+    bytes.reduce((data, byte) => {
+      return data + String.fromCharCode(byte);
+    }, ''),
+  )}`;
+}

+ 190 - 0
src/api/gramjs/builders/messages.ts

@@ -0,0 +1,190 @@
+import * as gramJsApi from '../../../lib/gramjs/tl/types';
+import { strippedPhotoToJpg } from '../../../lib/gramjs/Utils';
+import {
+  ApiMessage, ApiMessageForwardInfo, ApiPhoto, ApiPhotoCachedSize, ApiPhotoSize, ApiSticker,
+} from '../../types';
+import { OmitFlags } from '../types/types';
+
+import { getApiChatIdFromMtpPeer } from './chats';
+import { isPeerUser } from './peers';
+import { bytesToDataUri } from './common';
+
+// TODO Maybe we do not need it.
+const DEFAULT_USER_ID = 0;
+
+export function buildApiMessage(mtpMessage: OmitFlags<MTP.message>): ApiMessage {
+  const isPrivateToMe = mtpMessage.out !== true && isPeerUser(mtpMessage.toId);
+  const chatId = isPrivateToMe
+    ? (mtpMessage.fromId || DEFAULT_USER_ID)
+    : getApiChatIdFromMtpPeer(mtpMessage.toId);
+
+  return buildApiMessageWithChatId(chatId, mtpMessage);
+}
+
+export function buildApiMessageFromShort(
+  mtpMessage: OmitFlags<MTP.updateShortMessage>,
+): ApiMessage {
+  const chatId = getApiChatIdFromMtpPeer({ userId: mtpMessage.userId });
+
+  return buildApiMessageWithChatId(chatId, {
+    ...mtpMessage,
+    // TODO Current user ID needed here.
+    fromId: mtpMessage.out ? DEFAULT_USER_ID : mtpMessage.userId,
+  });
+}
+
+export function buildApiMessageFromShortChat(
+  mtpMessage: OmitFlags<MTP.updateShortChatMessage>,
+): ApiMessage {
+  const chatId = getApiChatIdFromMtpPeer({ chatId: mtpMessage.chatId });
+
+  return buildApiMessageWithChatId(chatId, mtpMessage);
+}
+
+export function buildApiMessageWithChatId(
+  chatId: number,
+  mtpMessage: Pick<MTP.message, 'id' | 'out' | 'message' | 'date' | 'fromId' | 'fwdFrom' | 'replyToMsgId' | 'media'>,
+): ApiMessage {
+  const sticker = mtpMessage.media && buildSticker(mtpMessage.media);
+  const photo = mtpMessage.media && buildPhoto(mtpMessage.media);
+  const textContent = mtpMessage.message && {
+    '@type': 'formattedText' as 'formattedText',
+    text: mtpMessage.message,
+  };
+  const caption = textContent && photo ? textContent : null;
+  const text = textContent && !photo ? textContent : null;
+
+  return {
+    id: mtpMessage.id,
+    chat_id: chatId,
+    is_outgoing: mtpMessage.out === true,
+    content: {
+      '@type': 'message',
+      ...(text && { text }),
+      ...(sticker && { sticker }),
+      ...(photo && { photo }),
+      ...(caption && { caption }),
+    },
+    date: mtpMessage.date,
+    sender_user_id: mtpMessage.fromId || DEFAULT_USER_ID,
+    reply_to_message_id: mtpMessage.replyToMsgId,
+    ...(mtpMessage.fwdFrom && { forward_info: buildApiMessageForwardInfo(mtpMessage.fwdFrom) }),
+  };
+}
+
+function buildApiMessageForwardInfo(fwdFrom: MTP.messageFwdHeader): ApiMessageForwardInfo {
+  return {
+    '@type': 'messageForwardInfo',
+    from_chat_id: fwdFrom.fromId,
+    origin: {
+      '@type': 'messageForwardOriginUser',
+      // TODO Handle when empty `fromId`.
+      sender_user_id: fwdFrom.fromId!,
+      // TODO @gramjs Not supported?
+      // sender_user_name: fwdFrom.fromName,
+    },
+  };
+}
+
+function buildSticker(media: MTP.MessageMedia): ApiSticker | null {
+  if (!(media instanceof gramJsApi.MessageMediaDocument)) {
+    return null;
+  }
+
+  const stickerAttribute = media.document.attributes
+    .find((attr: any) => attr instanceof gramJsApi.DocumentAttributeSticker);
+
+  if (!stickerAttribute) {
+    return null;
+  }
+
+  const emoji = stickerAttribute.alt;
+  const isAnimated = media.document.mimeType === 'application/x-tgsticker';
+  const thumb = media.document.thumbs.find((s: any) => s instanceof gramJsApi.PhotoCachedSize);
+  const thumbnail = thumb && buildApiPhotoCachedSize(thumb);
+  const { width, height } = thumbnail || {};
+
+  return {
+    '@type': 'sticker',
+    emoji,
+    is_animated: isAnimated,
+    width,
+    height,
+    thumbnail,
+  };
+}
+
+function buildPhoto(media: MTP.MessageMedia): ApiPhoto | null {
+  if (!(media instanceof gramJsApi.MessageMediaPhoto)) {
+    return null;
+  }
+
+  const hasStickers = media.photo.has_stickers;
+  const thumb = media.photo.sizes.find((s: any) => s instanceof gramJsApi.PhotoStrippedSize);
+  const mSize = media.photo.sizes.find((s: any) => s.type === 'm');
+  const { width, height } = mSize;
+  const minithumbnail: ApiPhoto['minithumbnail'] = thumb && {
+    '@type': 'minithumbnail',
+    data: bytesToDataUri(strippedPhotoToJpg(thumb.bytes as Buffer), true),
+    width,
+    height,
+  };
+  const sizes = media.photo.sizes.filter((s: any) => s instanceof gramJsApi.PhotoSize).map(buildApiPhotoSize);
+
+  return {
+    '@type': 'photo',
+    has_stickers: hasStickers,
+    minithumbnail,
+    sizes,
+  };
+}
+
+function buildApiPhotoCachedSize(photoSize: MTP.photoCachedSize): ApiPhotoCachedSize {
+  const {
+    w, h, type, bytes,
+  } = photoSize;
+  const dataUri = bytesToDataUri(strippedPhotoToJpg(bytes as Buffer));
+
+  return {
+    '@type': 'photoCachedSize',
+    width: w,
+    height: h,
+    type: type as ('m' | 'x' | 'y'),
+    dataUri,
+  };
+}
+
+function buildApiPhotoSize(photoSize: MTP.photoSize): ApiPhotoSize {
+  const { w, h, type } = photoSize;
+
+  return {
+    '@type': 'photoSize',
+    width: w,
+    height: h,
+    type: type as ('m' | 'x' | 'y'),
+  };
+}
+
+// We only support 100000 local pending messages here and expect it will not interfere with real IDs.
+let localMessageCounter = -1;
+export function buildLocalMessage(chatId: number, text: string): ApiMessage {
+  const localId = localMessageCounter--;
+
+  return {
+    id: localId,
+    chat_id: chatId,
+    content: {
+      '@type': 'message',
+      text: {
+        '@type': 'formattedText',
+        text,
+      },
+    },
+    date: Math.round(Date.now() / 1000),
+    is_outgoing: true,
+    sender_user_id: DEFAULT_USER_ID, // TODO
+    sending_state: {
+      '@type': 'messageSendingStatePending',
+    },
+  };
+}

+ 21 - 0
src/api/gramjs/client.ts

@@ -5,6 +5,7 @@ import {
   SupportedUploadRequests,
 } from './types/types';
 import * as apiRequests from '../../lib/gramjs/tl/functions';
+import { Logger as GramJsLogger } from '../../lib/gramjs/extensions';
 
 import { TelegramClient, session } from '../../lib/gramjs';
 import { DEBUG } from '../../config';
@@ -15,6 +16,11 @@ import { onGramJsUpdate } from './onGramJsUpdate';
 import localDb from './localDb';
 import { buildInputPeerPhotoFileLocation } from './inputHelpers';
 import { ApiFileLocation } from '../types';
+<<<<<<< HEAD
+=======
+
+GramJsLogger.getLogger().level = 'debug';
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
 
 let client: any;
 
@@ -97,14 +103,29 @@ export async function invokeRequest(data: InvokeRequestPayload) {
   return result;
 }
 
+<<<<<<< HEAD
 export function downloadFile(id: number, fileLocation: ApiFileLocation, dcId?: number) {
   return client.downloadFile(
     buildInputPeerPhotoFileLocation({ id, fileLocation }),
     true,
+=======
+export function downloadFile(chatOrUserId: number, fileLocation: ApiFileLocation) {
+  const { dcId, volumeId, localId } = fileLocation;
+
+  return client.downloadFile(
+    buildInputPeerPhotoFileLocation(chatOrUserId, volumeId, localId),
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
     { dcId },
   );
 }
 
+<<<<<<< HEAD
+=======
+export function downloadMessageImage(message: MTP.message) {
+  return client.downloadMedia(message, { sizeType: 'x' });
+}
+
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
 function postProcess(name: string, anyResult: any, args: AnyLiteral) {
   switch (name) {
     case 'GetDialogsRequest': {

+ 28 - 0
src/api/gramjs/connectors/files.ts

@@ -1,6 +1,12 @@
 import { ApiFileLocation } from '../../types';
 
+<<<<<<< HEAD
 import { downloadFile } from '../client';
+=======
+import { downloadFile, downloadMessageImage } from '../client';
+import localDb from '../localDb';
+import { bytesToDataUri } from '../builders/common';
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
 
 export function init() {
 }
@@ -11,9 +17,31 @@ export async function loadFile(id: any, fileLocation: ApiFileLocation): Promise<
   return fileBuffer ? bytesToUrl(fileBuffer) : null;
 }
 
+<<<<<<< HEAD
 function bytesToUrl(bytes: Uint8Array, mimeType?: string) {
   if (!mimeType) {
     mimeType = 'image/jpg';
+=======
+export function loadMessageMedia(message: MTP.message): Promise<string | null> {
+  const messageId = message.id;
+
+  if (!localDb.mediaRequests[messageId]) {
+    localDb.mediaRequests[messageId] = downloadMessageImage(message)
+      .then(
+        (fileBuffer: Buffer) => {
+          if (fileBuffer) {
+            return bytesToDataUri(fileBuffer);
+          } else {
+            delete localDb.mediaRequests[messageId];
+            return null;
+          }
+        },
+        () => {
+          delete localDb.mediaRequests[messageId];
+          return null;
+        },
+      );
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
   }
 
   return `data:${mimeType};base64,${btoa(

+ 120 - 0
src/api/gramjs/connectors/messages.ts

@@ -0,0 +1,120 @@
+import * as gramJsApi from '../../../lib/gramjs/tl/types';
+
+import { ApiMessage } from '../../types';
+import { OnApiUpdate } from '../types/types';
+
+import { invokeRequest } from '../client';
+import { buildApiMessage, buildLocalMessage } from '../builders/messages';
+import { buildApiUser } from '../builders/users';
+import { buildInputPeer, generateRandomBigInt } from '../inputHelpers';
+import localDb from '../localDb';
+import { loadMessageMedia } from './files';
+
+let onUpdate: OnApiUpdate;
+
+export function init(_onUpdate: OnApiUpdate) {
+  onUpdate = _onUpdate;
+}
+
+export async function fetchMessages({ chatId, fromMessageId, limit }: {
+  chatId: number;
+  fromMessageId: number;
+  limit: number;
+}): Promise<{ messages: ApiMessage[] } | null> {
+  const result = await invokeRequest({
+    namespace: 'messages',
+    name: 'GetHistoryRequest',
+    args: {
+      offsetId: fromMessageId,
+      limit,
+      peer: buildInputPeer(chatId),
+    },
+  }) as MTP.messages$Messages;
+
+  if (!result || !result.messages) {
+    return null;
+  }
+
+  (result.users as MTP.user[]).forEach((mtpUser) => {
+    const user = buildApiUser(mtpUser);
+
+    onUpdate({
+      '@type': 'updateUser',
+      id: user.id,
+      user,
+    });
+  });
+
+  const messages = (result.messages as MTP.message[])
+    .map((mtpMessage) => {
+      if (isMessageWithImage(mtpMessage)) {
+        loadMessageMedia(mtpMessage).then((dataUri) => {
+          if (!dataUri) {
+            return;
+          }
+
+          onUpdate({
+            '@type': 'updateMessageImage',
+            message_id: mtpMessage.id,
+            data_uri: dataUri,
+          });
+        });
+      }
+
+      return buildApiMessage(mtpMessage);
+    });
+
+  return {
+    messages,
+  };
+}
+
+export function sendMessage(chatId: number, text: string) {
+  const localMessage = buildLocalMessage(chatId, text);
+  onUpdate({
+    '@type': 'updateMessage',
+    id: localMessage.id,
+    chat_id: chatId,
+    message: localMessage,
+  });
+
+  onUpdate({
+    '@type': 'updateChat',
+    id: chatId,
+    chat: {
+      last_message: localMessage,
+    },
+  });
+
+  const randomId = generateRandomBigInt();
+
+  localDb.localMessages[randomId.toString()] = localMessage;
+
+  void invokeRequest({
+    namespace: 'messages',
+    name: 'SendMessageRequest',
+    args: {
+      message: text,
+      peer: buildInputPeer(chatId),
+      randomId,
+    },
+  });
+}
+
+function isMessageWithImage(message: MTP.message) {
+  const { media } = message;
+
+  if (!media) {
+    return false;
+  }
+
+  if (media instanceof gramJsApi.MessageMediaPhoto) {
+    return true;
+  }
+
+  if (media instanceof gramJsApi.MessageMediaDocument) {
+    return media.document.attributes.some((attr: any) => attr instanceof gramJsApi.DocumentAttributeSticker);
+  }
+
+  return false;
+}

+ 16 - 0
src/api/gramjs/localDb.ts

@@ -0,0 +1,16 @@
+import { ApiMessage } from '../types';
+
+export default <{
+  chats: Record<number, MTP.chat | MTP.channel>;
+  users: Record<number, MTP.user>;
+  localMessages: Record<string, ApiMessage>;
+  // TODO Replace with persistent storage for all downloads.
+  avatarRequests: Record<number, Promise<string | null>>;
+  mediaRequests: Record<number, Promise<string | null>>;
+}> {
+  chats: {},
+  users: {},
+  localMessages: {},
+  avatarRequests: {},
+  mediaRequests: {},
+};

+ 83 - 0
src/api/types/updates.ts

@@ -0,0 +1,83 @@
+import { ApiChat } from './chats';
+import { ApiMessage } from './messages';
+import { ApiUser } from './users';
+
+export type ApiUpdateAuthorizationStateType = (
+  'authorizationStateLoggingOut' |
+  'authorizationStateWaitTdlibParameters' |
+  'authorizationStateWaitEncryptionKey' |
+  'authorizationStateWaitPhoneNumber' |
+  'authorizationStateWaitCode' |
+  'authorizationStateWaitPassword' |
+  'authorizationStateWaitRegistration' |
+  'authorizationStateReady' |
+  'authorizationStateClosing' |
+  'authorizationStateClosed'
+);
+
+export type ApiUpdateAuthorizationState = {
+  '@type': 'updateAuthorizationState';
+  authorization_state: {
+    '@type': ApiUpdateAuthorizationStateType;
+  };
+  session_id?: string;
+};
+
+export type ApiUpdateChats = {
+  '@type': 'chats';
+  chats: ApiChat[];
+};
+
+export type ApiUpdateChat = {
+  '@type': 'updateChat';
+  id: number;
+  chat: Partial<ApiChat>;
+};
+
+export type ApiUpdateMessage = {
+  '@type': 'updateMessage';
+  chat_id: number;
+  id: number;
+  message: Partial<ApiMessage>;
+};
+
+export type ApiUpdateMessageSendSucceeded = {
+  '@type': 'updateMessageSendSucceeded';
+  chat_id: number;
+  old_message_id: number;
+  message: ApiMessage;
+};
+
+export type ApiUpdateMessageSendFailed = {
+  '@type': 'updateMessageSendFailed';
+  chat_id: number;
+  old_message_id: number;
+  sending_state: {
+    '@type': 'messageSendingStateFailed';
+  };
+};
+
+export type ApiUpdateUsers = {
+  '@type': 'users';
+  users: ApiUser[];
+};
+
+export type ApiUpdateUser = {
+  '@type': 'updateUser';
+  id: number;
+  user: Partial<ApiUser>;
+};
+
+export type ApiUpdateMessageImage = {
+  '@type': 'updateMessageImage';
+  message_id: number;
+  data_uri: string;
+};
+
+export type ApiUpdate = (
+  ApiUpdateAuthorizationState |
+  ApiUpdateChats | ApiUpdateChat |
+  ApiUpdateMessage | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed |
+  ApiUpdateUsers | ApiUpdateUser |
+  ApiUpdateMessageImage
+);

+ 103 - 79
src/lib/gramjs/client/TelegramClient.js

@@ -158,7 +158,6 @@ class TelegramClient {
     async _updateLoop() {
         while (this.isConnected()) {
             const rnd = Helpers.getRandomInt(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
-            console.log('rnd is ', rnd)
             await Helpers.sleep(1000 * 60)
             // We don't care about the result we just want to send it every
             // 60 seconds so telegram doesn't stop the connection
@@ -242,7 +241,8 @@ class TelegramClient {
 
     async _createExportedSender(dcId, retries) {
         const dc = await this._getDC(dcId)
-        const sender = new MTProtoSender(null, { logger: this._log })
+        const sender = new MTProtoSender(this.session.dcId === dcId ? this._sender.authKey : null,
+            { logger: this._log })
         for (let i = 0; i < retries; i++) {
             try {
                 await sender.connect(new this._connection(
@@ -251,13 +251,15 @@ class TelegramClient {
                     dcId,
                     this._log,
                 ))
-                this._log.info(`Exporting authorization for data center ${dc.ipAddress}`)
-                const auth = await this.invoke(new functions.auth.ExportAuthorizationRequest({ dcId: dcId }))
-                const req = this._initWith(new functions.auth.ImportAuthorizationRequest({
-                        id: auth.id, bytes: auth.bytes,
-                    },
-                ))
-                await sender.send(req)
+                if (this.session.dcId !== dcId) {
+                    this._log.info(`Exporting authorization for data center ${dc.ipAddress}`)
+                    const auth = await this.invoke(new functions.auth.ExportAuthorizationRequest({ dcId: dcId }))
+                    const req = this._initWith(new functions.auth.ImportAuthorizationRequest({
+                            id: auth.id, bytes: auth.bytes,
+                        },
+                    ))
+                    await sender.send(req)
+                }
                 sender.dcId = dcId
                 return sender
             } catch (e) {
@@ -275,20 +277,23 @@ class TelegramClient {
     /**
      * Complete flow to download a file.
      * @param inputLocation {types.InputFileLocation}
-     * @param [saveToMemory=true {boolean}] Whether or not to return a buffer.
      * @param [args[partSizeKb] {number}]
      * @param [args[fileSize] {number}]
      * @param [args[progressCallback] {Function}]
      * @param [args[dcId] {number}]
      * @returns {Promise<Buffer>}
      */
-    async downloadFile(inputLocation, saveToMemory = true, args = {}) {
+    async downloadFile(inputLocation, args = {}) {
         let { partSizeKb, fileSize } = args
         const { dcId } = args
 
         if (!partSizeKb) {
             if (!fileSize) {
+<<<<<<< HEAD
                 partSizeKb = 64
+=======
+                partSizeKb = DEFAULT_CHUNK_SIZE
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
             } else {
                 partSizeKb = utils.getAppropriatedPartSize(partSizeKb)
             }
@@ -298,12 +303,7 @@ class TelegramClient {
             throw new Error('The part size must be evenly divisible by 4096')
         }
 
-        let f
-        if (saveToMemory) {
-            f = new BinaryWriter(Buffer.alloc(0))
-        } else {
-            throw new Error('not supported')
-        }
+        const fileWriter = new BinaryWriter(Buffer.alloc(0))
         const res = utils.getInputLocation(inputLocation)
         let exported = dcId && this.session.dcId !== dcId
 
@@ -355,21 +355,16 @@ class TelegramClient {
                 if (result.bytes.length) {
                     this._log.debug(`Saving ${result.bytes.length} more bytes`)
 
-                    f.write(result.bytes)
+                    fileWriter.write(result.bytes)
 
                     if (args.progressCallback) {
-                        await args.progressCallback(f.getValue().length, fileSize)
+                        await args.progressCallback(fileWriter.getValue().length, fileSize)
                     }
                 }
 
                 // Last chunk.
                 if (result.bytes.length < partSize) {
-                    if (saveToMemory) {
-                        return f.getValue()
-                    } else {
-                        // Todo implement
-                        throw new Error('Saving to files is not implemented yet')
-                    }
+                    return fileWriter.getValue()
                 }
             }
         } finally {
@@ -377,8 +372,8 @@ class TelegramClient {
         }
     }
 
-    async downloadMedia(message, saveToMemory, args = {
-        thumb: null,
+    async downloadMedia(message, args = {
+        sizeType: null,
         progressCallback: null,
     }) {
         let date
@@ -400,6 +395,7 @@ class TelegramClient {
             }
         }
         if (media instanceof types.MessageMediaPhoto || media instanceof types.Photo) {
+<<<<<<< HEAD
             return await this._downloadPhoto(media, saveToMemory, date, args.thumb, args.progressCallback)
         } else if (media instanceof types.MessageMediaDocument || media instanceof types.Document) {
             return await this._downloadDocument(media, saveToMemory, date, args.thumb, args.progressCallback, media.dcId)
@@ -407,16 +403,25 @@ class TelegramClient {
             return this._downloadContact(media, saveToMemory)
         } else if ((media instanceof types.WebDocument || media instanceof types.WebDocumentNoProxy) && args.thumb == null) {
             return await this._downloadWebDocument(media, saveToMemory, args.progressCallback)
+=======
+            return this._downloadPhoto(media, args)
+        } else if (media instanceof types.MessageMediaDocument || media instanceof types.Document) {
+            return this._downloadDocument(media, args, media.dcId)
+        } else if (media instanceof types.MessageMediaContact) {
+            return this._downloadContact(media, args)
+        } else if (media instanceof types.WebDocument || media instanceof types.WebDocumentNoProxy) {
+            return this._downloadWebDocument(media, args)
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
         }
     }
 
-    async downloadProfilePhoto(entity, saveToMemory, downloadBig = false) {
+    async downloadProfilePhoto(entity, isBig = false) {
         // ('User', 'Chat', 'UserFull', 'ChatFull')
-        const ENTITIES = [ 0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697 ]
+        const ENTITIES = [0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697]
         // ('InputPeer', 'InputUser', 'InputChannel')
         // const INPUTS = [0xc91c90b6, 0xe669bf46, 0x40f202fd]
         // Todo account for input methods
-        const thumb = downloadBig ? -1 : 0
+        const sizeType = isBig ? 'x' : 'm'
         let photo
         if (!(ENTITIES.includes(entity.SUBCLASS_OF_ID))) {
             photo = entity
@@ -427,26 +432,29 @@ class TelegramClient {
                     return null
                 }
 
+<<<<<<< HEAD
                 return await this._downloadPhoto(
                     entity.chatPhoto, saveToMemory, null, thumb, null,
+=======
+                return this._downloadPhoto(
+                    entity.chatPhoto, { sizeType },
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
                 )
             }
             photo = entity.photo
         }
         let dcId
-        let which
         let loc
         if (photo instanceof types.UserProfilePhoto || photo instanceof types.ChatPhoto) {
             console.log('i am ere')
             dcId = photo.dcId
-            which = downloadBig ? photo.photoBig : photo.photoSmall
+            const size = isBig ? photo.photoBig : photo.photoSmall
             loc = new types.InputPeerPhotoFileLocation({
                 peer: await this.getInputEntity(entity),
-                localId: which.localId,
-                volumeId: which.volumeId,
-                big: downloadBig,
+                localId: size.localId,
+                volumeId: size.volumeId,
+                big: isBig,
             })
-            console.log(loc)
         } else {
             // It doesn't make any sense to check if `photo` can be used
             // as input location, because then this method would be able
@@ -455,7 +463,11 @@ class TelegramClient {
             return null
         }
         try {
+<<<<<<< HEAD
             const result = await this.downloadFile(loc, saveToMemory, {
+=======
+            return this.downloadFile(loc, {
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
                 dcId: dcId,
             })
             return result
@@ -466,7 +478,11 @@ class TelegramClient {
                     const full = await this.invoke(new functions.channels.GetFullChannelRequest({
                         channel: ie,
                     }))
+<<<<<<< HEAD
                     return await this._downloadPhoto(full.fullChat.chatPhoto, saveToMemory, null, null, thumb)
+=======
+                    return this._downloadPhoto(full.fullChat.chatPhoto, { sizeType })
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
                 } else {
                     return null
                 }
@@ -478,21 +494,16 @@ class TelegramClient {
 
     }
 
-    _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
+    _pickFileSize(sizes, sizeType) {
+        if (!sizeType || !sizes || !sizes.length) {
+            return null;
         }
+
+        return sizes.find((s) => s.type === sizeType);
     }
 
-    _downloadCachedPhotoSize(size, saveToMemory) {
+
+    _downloadCachedPhotoSize(size) {
         // No need to download anything, simply write the bytes
         let data
         if (size instanceof types.PhotoStrippedSize) {
@@ -503,21 +514,21 @@ class TelegramClient {
         return data
     }
 
-    async _downloadPhoto(photo, saveToMemory, date, thumb, progressCallback) {
+    async _downloadPhoto(photo, args) {
         if (photo instanceof types.MessageMediaPhoto) {
             photo = photo.photo
         }
         if (!(photo instanceof types.Photo)) {
             return
         }
-        const size = this._getThumb(photo.sizes, thumb)
+        const size = this._pickFileSize(photo.sizes, args.sizeType)
         if (!size || (size instanceof types.PhotoSizeEmpty)) {
             return
         }
 
 
         if (size instanceof types.PhotoCachedSize || size instanceof types.PhotoStrippedSize) {
-            return this._downloadCachedPhotoSize(size, saveToMemory)
+            return this._downloadCachedPhotoSize(size)
         }
 
         const result = await this.downloadFile(
@@ -527,55 +538,62 @@ class TelegramClient {
                 fileReference: photo.fileReference,
                 thumbSize: size.type,
             }),
-            saveToMemory,
             {
                 dcId: photo.dcId,
                 fileSize: size.size,
-                progressCallback: progressCallback,
+                progressCallback: args.progressCallback,
             },
         )
         return result
     }
 
+<<<<<<< HEAD
     async _downloadDocument(doc, saveToMemory, date, thumb, progressCallback, dcId) {
         if (doc instanceof types.MessageMediaPhoto) {
             doc = document.document
+=======
+    async _downloadDocument(media, args) {
+        if (!(media instanceof types.MessageMediaDocument)) {
+            return
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
         }
+
+        const doc = media.document
+
         if (!(doc instanceof types.Document)) {
             return
         }
-        let size
 
-        if (thumb === null || thumb === undefined) {
-            size = null
-        } else {
-            size = this._getThumb(doc.thumbs, thumb)
-            if (size instanceof types.PhotoCachedSize || size instanceof types.PhotoStrippedSize) {
-                return this._downloadCachedPhotoSize(size, saveToMemory)
-            }
+        const size = doc.thumbs ? this._pickFileSize(doc.thumbs, args.sizeType) : null;
+        if (size && (size instanceof types.PhotoCachedSize || size instanceof types.PhotoStrippedSize)) {
+            return this._downloadCachedPhotoSize(size)
         }
+<<<<<<< HEAD
         const result = await this.downloadFile(
+=======
+
+        return this.downloadFile(
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
             new types.InputDocumentFileLocation({
                 id: doc.id,
                 accessHash: doc.accessHash,
                 fileReference: doc.fileReference,
                 thumbSize: size ? size.type : '',
             }),
-            saveToMemory,
             {
                 fileSize: size ? size.size : doc.size,
-                progressCallback: progressCallback,
-                dcId,
+                progressCallback: args.progressCallback,
+                dcId: doc.dcId,
             },
         )
         return result
     }
 
-    _downloadContact(media, saveToMemory) {
+    _downloadContact(media, args) {
         throw new Error('not implemented')
     }
 
-    async _downloadWebDocument(media, saveToMemory, progressCallback) {
+    _downloadWebDocument(media, args) {
         throw new Error('not implemented')
     }
 
@@ -648,8 +666,11 @@ class TelegramClient {
         let attempt = 0
         for (attempt = 0; attempt < this._requestRetries; attempt++) {
             try {
+                console.log("invoking",request)
+
                 const promise = this._sender.send(request)
                 const result = await promise
+                console.log("got answer",promise)
                 this.session.processEntities(result)
                 this._entityCache.add(result)
                 return result
@@ -684,7 +705,7 @@ class TelegramClient {
 
     async getMe() {
         const me = (await this.invoke(new functions.users
-            .GetUsersRequest({ id: [ new types.InputUserSelf() ] })))[0]
+            .GetUsersRequest({ id: [new types.InputUserSelf()] })))[0]
         return me
     }
 
@@ -829,7 +850,7 @@ class TelegramClient {
         if (args.phone && !args.code && !args.password) {
             return await this.sendCodeRequest(args.phone)
         } else if (args.code) {
-            const [ phone, phoneCodeHash ] =
+            const [phone, phoneCodeHash] =
                 this._parsePhoneAndHash(args.phone, args.phoneCodeHash)
             // May raise PhoneCodeEmptyError, PhoneCodeExpiredError,
             // PhoneCodeHashEmptyError or PhoneCodeInvalidError.
@@ -877,7 +898,7 @@ class TelegramClient {
             throw new Error('You also need to provide a phone_code_hash.')
         }
 
-        return [ phone, phoneHash ]
+        return [phone, phoneHash]
     }
 
     // endregion
@@ -950,7 +971,7 @@ class TelegramClient {
 
     // event region
     addEventHandler(callback, event) {
-        this._eventBuilders.push([ event, callback ])
+        this._eventBuilders.push([event, callback])
     }
 
     _handleUpdate(update) {
@@ -960,7 +981,7 @@ class TelegramClient {
         if (update instanceof types.Updates || update instanceof types.UpdatesCombined) {
             // TODO deal with entities
             const entities = {}
-            for (const x of [ ...update.users, ...update.chats ]) {
+            for (const x of [...update.users, ...update.chats]) {
                 entities[utils.getPeerId(x)] = x
             }
             for (const u of update.updates) {
@@ -1020,8 +1041,13 @@ class TelegramClient {
                 }
                 throw e
             }
+<<<<<<< HEAD
         } else if ([ 'me', 'this' ].includes(string.toLowerCase())) {
             return await this.getMe()
+=======
+        } else if (['me', 'this'].includes(string.toLowerCase())) {
+            return this.getMe()
+>>>>>>> dda7e47e... Fix images loading; Various Gram JS fixes; Refactor Gram JS Logger
         } else {
             const { username, isJoinChat } = utils.parseUsername(string)
             if (isJoinChat) {
@@ -1133,8 +1159,6 @@ class TelegramClient {
      * @returns {Promise<>}
      */
     async getInputEntity(peer) {
-        console.log('trying to read ', peer)
-        console.log('trying to read ', peer)
         // Short-circuit if the input parameter directly maps to an InputPeer
         try {
             return utils.getInputPeer(peer)
@@ -1153,7 +1177,7 @@ class TelegramClient {
         } catch (e) {
         }
         // Then come known strings that take precedence
-        if ([ 'me', 'this' ].includes(peer)) {
+        if (['me', 'this'].includes(peer)) {
             return new types.InputPeerSelf()
         }
         // No InputPeer, cached peer, or known string. Fetch from disk cache
@@ -1173,9 +1197,9 @@ class TelegramClient {
         peer = utils.getPeer(peer)
         if (peer instanceof types.PeerUser) {
             const users = await this.invoke(new functions.users.GetUsersRequest({
-                id: [ new types.InputUser({
+                id: [new types.InputUser({
                     userId: peer.userId, accessHash: 0,
-                }) ],
+                })],
             }))
             if (users && !(users[0] instanceof types.UserEmpty)) {
                 // If the user passed a valid ID they expect to work for
@@ -1194,10 +1218,10 @@ class TelegramClient {
         } else if (peer instanceof types.PeerChannel) {
             try {
                 const channels = await this.invoke(new functions.channels.GetChannelsRequest({
-                    id: [ new types.InputChannel({
+                    id: [new types.InputChannel({
                         channelId: peer.channelId,
                         accessHash: 0,
-                    }) ],
+                    })],
                 }))
 
                 return utils.getInputPeer(channels.chats[0])
@@ -1223,7 +1247,7 @@ class TelegramClient {
         channelId: null,
         ptsDate: null,
     }) {
-        for (const [ builder, callback ] of this._eventBuilders) {
+        for (const [builder, callback] of this._eventBuilders) {
             const event = builder.build(args.update)
             if (event) {
                 await callback(event)

+ 6 - 3
src/lib/gramjs/extensions/Logger.js

@@ -1,10 +1,10 @@
 let logger = null
 
 class Logger {
-    static levels = ['debug', 'info', 'warn', 'error']
+    static levels = ['error', 'warn', 'info', 'debug']
 
     constructor(level) {
-        this.level = level
+        this.level = level || 'debug'
         this.isBrowser = typeof process === 'undefined' ||
             process.type === 'renderer' ||
             process.browser === true ||
@@ -37,7 +37,7 @@ class Logger {
      * @returns {boolean}
      */
     canSend(level) {
-        return (Logger.levels.indexOf(this.level) <= Logger.levels.indexOf(level))
+        return (Logger.levels.indexOf(this.level) >= Logger.levels.indexOf(level))
     }
 
     /**
@@ -87,6 +87,9 @@ class Logger {
      * @param color {string}
      */
     _log(level, message, color) {
+        if (!logger){
+            return
+        }
         if (this.canSend(level)) {
             if (!this.isBrowser) {
                 console.log(color + this.format(message, level) + this.colors.end)

+ 93 - 0
src/lib/gramjs/extensions/MessagePacker.js

@@ -0,0 +1,93 @@
+const MessageContainer = require('../tl/core/MessageContainer')
+const TLMessage = require('../tl/core/TLMessage')
+const { TLRequest } = require('../tl/tlobject')
+const BinaryWriter = require('../extensions/BinaryWriter')
+const struct = require('python-struct')
+
+class MessagePacker {
+    constructor(state, logger) {
+        this._state = state
+        this._queue = []
+        this._ready = new Promise(((resolve) => {
+            this.setReady = resolve
+        }))
+        this._log = logger
+    }
+
+    values() {
+        return this._queue
+    }
+
+    append(state) {
+        this._queue.push(state)
+        this.setReady(true)
+    }
+
+    extend(states) {
+        for (const state of states) {
+            this._queue.push(state)
+        }
+        this.setReady(true)
+    }
+
+    async get() {
+        if (!this._queue.length) {
+            this._ready = new Promise(((resolve) => {
+                this.setReady = resolve
+            }))
+            await this._ready
+        }
+        let data
+        let buffer = new BinaryWriter(Buffer.alloc(0))
+
+        const batch = []
+        let size = 0
+
+        while (this._queue.length && batch.length <= MessageContainer.MAXIMUM_LENGTH) {
+            const state = this._queue.shift()
+            size += state.data.length + TLMessage.SIZE_OVERHEAD
+            if (size <= MessageContainer.MAXIMUM_SIZE) {
+                let afterId
+                if (state.after) {
+                    afterId = state.after.msgId
+                }
+                state.msgId = await this._state.writeDataAsMessage(
+                    buffer, state.data, state.request instanceof TLRequest,
+                    afterId,
+                )
+
+                this._log.debug(`Assigned msgId = ${state.msgId} to ${state.request.constructor.name}`)
+                batch.push(state)
+                continue
+            }
+            if (batch.length) {
+                this._queue.unshift(state)
+                break
+            }
+            this._log.warn(`Message payload for ${state.request.constructor.name} is too long ${state.data.length} and cannot be sent`)
+            state.promise.reject('Request Payload is too big')
+            size = 0
+            continue
+        }
+        if (!batch.length) {
+            return null
+        }
+        if (batch.length > 1) {
+            data = Buffer.concat([struct.pack(
+                '<Ii', MessageContainer.CONSTRUCTOR_ID, batch.length,
+            ), buffer.getValue()])
+            buffer = new BinaryWriter(Buffer.alloc(0))
+            const containerId = await this._state.writeDataAsMessage(
+                buffer, data, false,
+            )
+            for (const s of batch) {
+                s.containerId = containerId
+            }
+        }
+
+        data = buffer.getValue()
+        return { batch, data }
+    }
+}
+
+module.exports = MessagePacker

+ 19 - 6
src/lib/gramjs/extensions/PromisedWebSockets.js

@@ -13,6 +13,19 @@ class PromisedWebSockets {
         this.closed = true
     }
 
+    // TODO This hangs in certain situations (issues with big files) and breaks subsequent calls.
+    // async readExactly(number) {
+    //     let readData = Buffer.alloc(0)
+    //     while (true) {
+    //         const thisTime = await this.read(number)
+    //         readData = Buffer.concat([readData, thisTime])
+    //         number = number - thisTime.length
+    //         if (!number) {
+    //             return readData
+    //         }
+    //     }
+    // }
+
     async read(number) {
         if (this.closed) {
             console.log('couldn\'t read')
@@ -58,18 +71,18 @@ class PromisedWebSockets {
         this.canRead = new Promise((resolve) => {
             this.resolveRead = resolve
         })
-        this.closed  = false
+        this.closed = false
         this.website = this.getWebSocketLink(ip, port)
         this.client = new WebSocketClient(this.website, 'binary')
-        return new Promise(function(resolve, reject) {
-            this.client.onopen = function() {
+        return new Promise(function (resolve, reject) {
+            this.client.onopen = function () {
                 this.receive()
                 resolve(this)
             }.bind(this)
-            this.client.onerror = function(error) {
+            this.client.onerror = function (error) {
                 reject(error)
             }
-            this.client.onclose = function() {
+            this.client.onclose = function () {
                 if (this.client.closed) {
                     this.resolveRead(false)
                     this.closed = true
@@ -92,7 +105,7 @@ class PromisedWebSockets {
     }
 
     async receive() {
-        this.client.onmessage = async function(message) {
+        this.client.onmessage = async function (message) {
             let data
             if (this.isBrowser) {
                 data = Buffer.from(await new Response(message.data).arrayBuffer())

+ 2 - 1
src/lib/gramjs/index.js

@@ -9,8 +9,9 @@ const events = require('./events')
 const utils = require('./Utils')
 const errors = require('./errors')
 const session = require('./sessions')
+const extensions = require('./extensions')
 
 module.exports = {
-    TelegramClient, session, connection,
+    TelegramClient, session, connection, extensions,
     tl, version, events, utils, errors,
 }

+ 766 - 0
src/lib/gramjs/network/MTProtoSender.js

@@ -0,0 +1,766 @@
+const MtProtoPlainSender = require('./MTProtoPlainSender')
+const MTProtoState = require('./MTProtoState')
+const Helpers = require('../Helpers')
+const AuthKey = require('../crypto/AuthKey')
+const doAuthentication = require('./Authenticator')
+const RPCResult = require('../tl/core/RPCResult')
+const MessageContainer = require('../tl/core/MessageContainer')
+const { TLRequest } = require('../tl/tlobject')
+const GZIPPacked = require('../tl/core/GZIPPacked')
+const RequestState = require('./RequestState')
+const format = require('string-format')
+const { MsgsAck, File, MsgsStateInfo, Pong } = require('../tl/types')
+const MessagePacker = require('../extensions/MessagePacker')
+const BinaryReader = require('../extensions/BinaryReader')
+const {
+    BadServerSalt,
+    BadMsgNotification,
+    MsgDetailedInfo,
+    MsgNewDetailedInfo,
+    NewSessionCreated,
+    FutureSalts,
+    MsgsStateReq,
+    MsgResendReq,
+    MsgsAllInfo,
+} = require('../tl/types')
+const { SecurityError } = require('../errors/Common')
+const { InvalidBufferError } = require('../errors/Common')
+const { LogOutRequest } = require('../tl/functions/auth')
+const { RPCMessageToError } = require('../errors')
+const { TypeNotFoundError } = require('../errors/Common')
+
+// const { tlobjects } = require("../gramjs/tl/alltlobjects");
+format.extend(String.prototype, {})
+
+/**
+ * MTProto Mobile Protocol sender
+ * (https://core.telegram.org/mtproto/description)
+ * This class is responsible for wrapping requests into `TLMessage`'s,
+ * sending them over the network and receiving them in a safe manner.
+ *
+ * Automatic reconnection due to temporary network issues is a concern
+ * for this class as well, including retry of messages that could not
+ * be sent successfully.
+ *
+ * A new authorization key will be generated on connection if no other
+ * key exists yet.
+ */
+class MTProtoSender {
+    static DEFAULT_OPTIONS = {
+        logger: null,
+        retries: 5,
+        delay: 1,
+        autoReconnect: true,
+        connectTimeout: null,
+        authKeyCallback: null,
+        updateCallback: null,
+        autoReconnectCallback: null,
+    }
+
+    /**
+     * @param authKey
+     * @param opts
+     */
+    constructor(authKey, opts) {
+        const args = { ...MTProtoSender.DEFAULT_OPTIONS, ...opts }
+        this._connection = null
+        this._log = args.logger
+        this._retries = args.retries
+        this._delay = args.delay
+        this._autoReconnect = args.autoReconnect
+        this._connectTimeout = args.connectTimeout
+        this._authKeyCallback = args.authKeyCallback
+        this._updateCallback = args.updateCallback
+        this._autoReconnectCallback = args.autoReconnectCallback
+
+        /**
+         * Whether the user has explicitly connected or disconnected.
+         *
+         * If a disconnection happens for any other reason and it
+         * was *not* user action then the pending messages won't
+         * be cleared but on explicit user disconnection all the
+         * pending futures should be cancelled.
+         */
+        this._user_connected = false
+        this._reconnecting = false
+        this._disconnected = true
+
+        /**
+         * We need to join the loops upon disconnection
+         */
+        this._send_loop_handle = null
+        this._recv_loop_handle = null
+
+        /**
+         * Preserving the references of the AuthKey and state is important
+         */
+        this.authKey = authKey || new AuthKey(null)
+        this._state = new MTProtoState(this.authKey, this._log)
+
+        /**
+         * Outgoing messages are put in a queue and sent in a batch.
+         * Note that here we're also storing their ``_RequestState``.
+         */
+        this._send_queue = new MessagePacker(this._state, this._log)
+
+        /**
+         * Sent states are remembered until a response is received.
+         */
+        this._pending_state = {}
+
+        /**
+         * Responses must be acknowledged, and we can also batch these.
+         */
+        this._pending_ack = new Set()
+
+        /**
+         * Similar to pending_messages but only for the last acknowledges.
+         * These can't go in pending_messages because no acknowledge for them
+         * is received, but we may still need to resend their state on bad salts.
+         */
+        this._last_acks = []
+
+        /**
+         * Jump table from response ID to method that handles it
+         */
+
+        this._handlers = {
+            [RPCResult.CONSTRUCTOR_ID]: this._handleRPCResult.bind(this),
+            [MessageContainer.CONSTRUCTOR_ID]: this._handleContainer.bind(this),
+            [GZIPPacked.CONSTRUCTOR_ID]: this._handleGzipPacked.bind(this),
+            [Pong.CONSTRUCTOR_ID]: this._handlePong.bind(this),
+            [BadServerSalt.CONSTRUCTOR_ID]: this._handleBadServerSalt.bind(this),
+            [BadMsgNotification.CONSTRUCTOR_ID]: this._handleBadNotification.bind(this),
+            [MsgDetailedInfo.CONSTRUCTOR_ID]: this._handleDetailedInfo.bind(this),
+            [MsgNewDetailedInfo.CONSTRUCTOR_ID]: this._handleNewDetailedInfo.bind(this),
+            [NewSessionCreated.CONSTRUCTOR_ID]: this._handleNewSessionCreated.bind(this),
+            [MsgsAck.CONSTRUCTOR_ID]: this._handleAck.bind(this),
+            [FutureSalts.CONSTRUCTOR_ID]: this._handleFutureSalts.bind(this),
+            [MsgsStateReq.CONSTRUCTOR_ID]: this._handleStateForgotten.bind(this),
+            [MsgResendReq.CONSTRUCTOR_ID]: this._handleStateForgotten.bind(this),
+            [MsgsAllInfo.CONSTRUCTOR_ID]: this._handleMsgAll.bind(this),
+        }
+    }
+
+    // Public API
+
+    /**
+     * Connects to the specified given connection using the given auth key.
+     * @param connection
+     * @returns {Promise<boolean>}
+     */
+    async connect(connection) {
+        if (this._user_connected) {
+            this._log.info('User is already connected!')
+            return false
+        }
+        this._connection = connection
+        await this._connect()
+        return true
+    }
+
+    isConnected() {
+        return this._user_connected
+    }
+
+    /**
+     * Cleanly disconnects the instance from the network, cancels
+     * all pending requests, and closes the send and receive loops.
+     */
+    async disconnect() {
+        await this._disconnect()
+    }
+
+    /**
+     *
+     This method enqueues the given request to be sent. Its send
+     state will be saved until a response arrives, and a ``Future``
+     that will be resolved when the response arrives will be returned:
+
+     .. code-block:: javascript
+
+     async def method():
+     # Sending (enqueued for the send loop)
+     future = sender.send(request)
+     # Receiving (waits for the receive loop to read the result)
+     result = await future
+
+     Designed like this because Telegram may send the response at
+     any point, and it can send other items while one waits for it.
+     Once the response for this future arrives, it is set with the
+     received result, quite similar to how a ``receive()`` call
+     would otherwise work.
+
+     Since the receiving part is "built in" the future, it's
+     impossible to await receive a result that was never sent.
+     * @param request
+     * @returns {RequestState}
+     */
+    send(request) {
+        if (!this._user_connected) {
+            throw new Error('Cannot send requests while disconnected')
+        }
+
+        if (!Helpers.isArrayLike(request)) {
+            const state = new RequestState(request)
+            this._send_queue.append(state)
+            return state.promise
+        } else {
+            throw new Error('not supported')
+        }
+    }
+
+    /**
+     * Performs the actual connection, retrying, generating the
+     * authorization key if necessary, and starting the send and
+     * receive loops.
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _connect() {
+        this._log.info('Connecting to {0}...'.replace('{0}', this._connection))
+        await this._connection.connect()
+        this._log.debug('Connection success!')
+        if (!this.authKey._key) {
+            const plain = new MtProtoPlainSender(this._connection, this._log)
+            this._log.debug('New auth_key attempt ...')
+            const res = await doAuthentication(plain, this._log)
+            this._log.debug('Generated new auth_key successfully')
+            this.authKey.key = res.authKey
+            this._state.time_offset = res.timeOffset
+
+            /**
+             * This is *EXTREMELY* important since we don't control
+             * external references to the authorization key, we must
+             * notify whenever we change it. This is crucial when we
+             * switch to different data centers.
+             */
+            if (this._authKeyCallback) {
+                await this._authKeyCallback(this.authKey)
+            }
+        } else {
+            this._log.debug('Already have an auth key ...')
+        }
+        this._user_connected = true
+        this._reconnecting = false
+
+        this._log.debug('Starting send loop')
+        this._send_loop_handle = this._sendLoop()
+
+        this._log.debug('Starting receive loop')
+        this._recv_loop_handle = this._recvLoop()
+
+        // _disconnected only completes after manual disconnection
+        // or errors after which the sender cannot continue such
+        // as failing to reconnect or any unexpected error.
+
+        this._log.info('Connection to %s complete!'.replace('%s', this._connection.toString()))
+    }
+
+    async _disconnect(error = null) {
+        if (this._connection === null) {
+            this._log.info('Not disconnecting (already have no connection)')
+            return
+        }
+        this._log.info('Disconnecting from %s...'.replace('%s', this._connection.toString()))
+        this._user_connected = false
+        this._log.debug('Closing current connection...')
+        await this._connection.disconnect()
+    }
+
+    /**
+     * This loop is responsible for popping items off the send
+     * queue, encrypting them, and sending them over the network.
+     * Besides `connect`, only this method ever sends data.
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _sendLoop() {
+        while (this._user_connected && !this._reconnecting) {
+            if (this._pending_ack.size) {
+                const ack = new RequestState(new MsgsAck({ msgIds: Array(...this._pending_ack) }))
+                this._send_queue.append(ack)
+                this._last_acks.push(ack)
+                this._pending_ack.clear()
+            }
+            this._log.debug('Waiting for messages to send...')
+            // TODO Wait for the connection send queue to be empty?
+            // This means that while it's not empty we can wait for
+            // more messages to be added to the send queue.
+            const res = await this._send_queue.get()
+
+            if (this._reconnecting) {
+                return;
+            }
+
+            if (!res) {
+                continue
+            }
+            let data = res.data
+            const batch = res.batch
+            this._log.debug(`Encrypting ${batch.length} message(s) in ${data.length} bytes for sending`)
+
+            data = this._state.encryptMessageData(data)
+
+            try {
+                await this._connection.send(data)
+            } catch (e) {
+                console.log(e)
+                this._log.info('Connection closed while sending data')
+                return
+            }
+            for (const state of batch) {
+                if (!Array.isArray(state)) {
+                    if (state.request instanceof TLRequest) {
+                        this._pending_state[state.msgId] = state
+                    }
+                } else {
+                    for (const s of state) {
+                        if (s.request instanceof TLRequest) {
+                            this._pending_state[s.msgId] = s
+                        }
+                    }
+                }
+            }
+            this._log.debug('Encrypted messages put in a queue to be sent')
+        }
+    }
+
+    async _recvLoop() {
+        let body
+        let message
+
+        while (this._user_connected && !this._reconnecting) {
+            // this._log.debug('Receiving items from the network...');
+            this._log.debug('Receiving items from the network...')
+            try {
+                body = await this._connection.recv()
+            } catch (e) {
+                // this._log.info('Connection closed while receiving data');
+                this._log.warn('Connection closed while receiving data')
+                return
+            }
+            try {
+                message = await this._state.decryptMessageData(body)
+            } catch (e) {
+                console.log(e)
+
+                if (e instanceof TypeNotFoundError) {
+                    // Received object which we don't know how to deserialize
+                    this._log.info(`Type ${e.invalidConstructorId} not found, remaining data ${e.remaining}`)
+                    continue
+                } else if (e instanceof SecurityError) {
+                    // A step while decoding had the incorrect data. This message
+                    // should not be considered safe and it should be ignored.
+                    this._log.warn(`Security error while unpacking a received message: ${e}`)
+
+                    // TODO Reconnecting does not work properly: all subsequent requests hang.
+                    // this.authKey.key = null
+                    // if (this._authKeyCallback) {
+                    //     await this._authKeyCallback(null)
+                    // }
+                    // this._startReconnect()
+                    // return
+                } else if (e instanceof InvalidBufferError) {
+                    this._log.info('Broken authorization key; resetting')
+                    this.authKey.key = null
+                    if (this._authKeyCallback) {
+                        await this._authKeyCallback(null)
+                    }
+
+                    this._startReconnect()
+                    return
+                } else {
+                    this._log.error('Unhandled error while receiving data')
+                    return
+                }
+            }
+            try {
+                await this._processMessage(message)
+            } catch (e) {
+                console.log(e)
+                this._log.error('Unhandled error while receiving data')
+            }
+        }
+    }
+
+    // Response Handlers
+
+    /**
+     * Adds the given message to the list of messages that must be
+     * acknowledged and dispatches control to different ``_handle_*``
+     * method based on its type.
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _processMessage(message) {
+        this._pending_ack.add(message.msgId)
+        // eslint-disable-next-line require-atomic-updates
+        message.obj = await message.obj
+        let handler = this._handlers[message.obj.CONSTRUCTOR_ID]
+        if (!handler) {
+            handler = this._handleUpdate.bind(this)
+        }
+
+        await handler(message)
+    }
+
+    /**
+     * Pops the states known to match the given ID from pending messages.
+     * This method should be used when the response isn't specific.
+     * @param msgId
+     * @returns {*[]}
+     * @private
+     */
+    _popStates(msgId) {
+        let state = this._pending_state[msgId]
+        if (state) {
+            delete this._pending_state[msgId]
+            return [state]
+        }
+
+        const toPop = []
+
+        for (state in this._pending_state) {
+            if (state.containerId === msgId) {
+                toPop.push(state.msgId)
+            }
+        }
+
+        if (toPop.length) {
+            const temp = []
+            for (const x of toPop) {
+                temp.push(this._pending_state[x])
+                delete this._pending_state[x]
+            }
+            return temp
+        }
+
+        for (const ack of this._last_acks) {
+            if (ack.msgId === msgId) {
+                return [ack]
+            }
+        }
+
+        return []
+    }
+
+    /**
+     * Handles the result for Remote Procedure Calls:
+     * rpc_result#f35c6d01 req_msg_id:long result:bytes = RpcResult;
+     * This is where the future results for sent requests are set.
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleRPCResult(message) {
+        const RPCResult = message.obj
+        const state = this._pending_state[RPCResult.reqMsgId]
+        if (state) {
+            delete this._pending_state[RPCResult.reqMsgId]
+        }
+        this._log.debug(`Handling RPC result for message ${RPCResult.reqMsgId}`)
+
+        if (!state) {
+            // TODO We should not get responses to things we never sent
+            // However receiving a File() with empty bytes is "common".
+            // See #658, #759 and #958. They seem to happen in a container
+            // which contain the real response right after.
+            try {
+                const reader = new BinaryReader(RPCResult.body)
+                if (!(reader.tgReadObject() instanceof File)) {
+                    throw new TypeNotFoundError('Not an upload.File')
+                }
+            } catch (e) {
+                console.log(e)
+                if (e instanceof TypeNotFoundError) {
+                    this._log.info(`Received response without parent request: ${RPCResult.body}`)
+                    return
+                } else {
+                    throw e
+                }
+            }
+        }
+        if (RPCResult.error) {
+            // eslint-disable-next-line new-cap
+            const error = RPCMessageToError(RPCResult.error, state.request)
+            this._send_queue.append(new RequestState(new MsgsAck({ msgIds: [state.msgId] })))
+            state.reject(error)
+        } else {
+            const reader = new BinaryReader(RPCResult.body)
+            const read = await state.request.readResult(reader)
+            state.resolve(read)
+        }
+    }
+
+    /**
+     * Processes the inner messages of a container with many of them:
+     * msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleContainer(message) {
+        this._log.debug('Handling container')
+        for (const innerMessage of message.obj.messages) {
+            await this._processMessage(innerMessage)
+        }
+    }
+
+    /**
+     * Unpacks the data from a gzipped object and processes it:
+     * gzip_packed#3072cfa1 packed_data:bytes = Object;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleGzipPacked(message) {
+        this._log.debug('Handling gzipped data')
+        const reader = new BinaryReader(message.obj.data)
+        message.obj = reader.tgReadObject()
+        await this._processMessage(message)
+    }
+
+    async _handleUpdate(message) {
+        if (message.obj.SUBCLASS_OF_ID !== 0x8af52aac) {
+            // crc32(b'Updates')
+            this._log.warn(`Note: ${message.obj.constructor.name} is not an update, not dispatching it`)
+            return
+        }
+        this._log.debug('Handling update ' + message.obj.constructor.name)
+        if (this._updateCallback) {
+            this._updateCallback(message.obj)
+        }
+    }
+
+    /**
+     * Handles pong results, which don't come inside a ``RPCResult``
+     * but are still sent through a request:
+     * pong#347773c5 msg_id:long ping_id:long = Pong;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handlePong(message) {
+        const pong = message.obj
+        this._log.debug(`Handling pong for message ${pong.msgId}`)
+        const state = this._pending_state[pong.msgId]
+        delete this._pending_state[pong.msgId]
+
+        // Todo Check result
+        if (state) {
+            state.resolve(pong)
+        }
+    }
+
+    /**
+     * Corrects the currently used server salt to use the right value
+     * before enqueuing the rejected message to be re-sent:
+     * bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int
+     * error_code:int new_server_salt:long = BadMsgNotification;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleBadServerSalt(message) {
+        const badSalt = message.obj
+        this._log.debug(`Handling bad salt for message ${badSalt.badMsgId}`)
+        this._state.salt = badSalt.newServerSalt
+        const states = this._popStates(badSalt.badMsgId)
+        this._send_queue.extend(states)
+        this._log.debug(`${states.length} message(s) will be resent`)
+    }
+
+    /**
+     * Adjusts the current state to be correct based on the
+     * received bad message notification whenever possible:
+     * bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int
+     * error_code:int = BadMsgNotification;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleBadNotification(message) {
+        const badMsg = message.obj
+        const states = this._popStates(badMsg.badMsgId)
+        this._log.debug(`Handling bad msg ${badMsg}`)
+        if ([16, 17].includes(badMsg.errorCode)) {
+            // Sent msg_id too low or too high (respectively).
+            // Use the current msg_id to determine the right time offset.
+            const to = this._state.updateTimeOffset(message.msgId)
+            this._log.info(`System clock is wrong, set time offset to ${to}s`)
+        } else if (badMsg.errorCode === 32) {
+            // msg_seqno too low, so just pump it up by some "large" amount
+            // TODO A better fix would be to start with a new fresh session ID
+            this._state._sequence += 64
+        } else if (badMsg.errorCode === 33) {
+            // msg_seqno too high never seems to happen but just in case
+            this._state._sequence -= 16
+        } else {
+            // for (const state of states) {
+            // TODO set errors;
+            /* state.future.set_exception(
+            BadMessageError(state.request, bad_msg.error_code))*/
+            // }
+
+            return
+        }
+        // Messages are to be re-sent once we've corrected the issue
+        this._send_queue.extend(states)
+        this._log.debug(`${states.length} messages will be resent due to bad msg`)
+    }
+
+    /**
+     * Updates the current status with the received detailed information:
+     * msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long
+     * bytes:int status:int = MsgDetailedInfo;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleDetailedInfo(message) {
+        // TODO https://goo.gl/VvpCC6
+        const msgId = message.obj.answerMsgId
+        this._log.debug(`Handling detailed info for message ${msgId}`)
+        this._pending_ack.add(msgId)
+    }
+
+    /**
+     * Updates the current status with the received detailed information:
+     * msg_new_detailed_info#809db6df answer_msg_id:long
+     * bytes:int status:int = MsgDetailedInfo;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleNewDetailedInfo(message) {
+        // TODO https://goo.gl/VvpCC6
+        const msgId = message.obj.answerMsgId
+        this._log.debug(`Handling new detailed info for message ${msgId}`)
+        this._pending_ack.add(msgId)
+    }
+
+    /**
+     * Updates the current status with the received session information:
+     * new_session_created#9ec20908 first_msg_id:long unique_id:long
+     * server_salt:long = NewSession;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleNewSessionCreated(message) {
+        // TODO https://goo.gl/LMyN7A
+        this._log.debug('Handling new session created')
+        this._state.salt = message.obj.serverSalt
+    }
+
+    /**
+     * Handles a server acknowledge about our messages. Normally
+     * these can be ignored except in the case of ``auth.logOut``:
+     *
+     *     auth.logOut#5717da40 = Bool;
+     *
+     * Telegram doesn't seem to send its result so we need to confirm
+     * it manually. No other request is known to have this behaviour.
+
+     * Since the ID of sent messages consisting of a container is
+     * never returned (unless on a bad notification), this method
+     * also removes containers messages when any of their inner
+     * messages are acknowledged.
+
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleAck(message) {
+        const ack = message.obj
+        this._log.debug(`Handling acknowledge for ${ack.msgIds}`)
+        for (const msgId of ack.msgIds) {
+            const state = this._pending_state[msgId]
+            if (state && state.request instanceof LogOutRequest) {
+                delete this._pending_state[msgId]
+                state.resolve(true)
+            }
+        }
+    }
+
+    /**
+     * Handles future salt results, which don't come inside a
+     * ``rpc_result`` but are still sent through a request:
+     *     future_salts#ae500895 req_msg_id:long now:int
+     *     salts:vector<future_salt> = FutureSalts;
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleFutureSalts(message) {
+        // TODO save these salts and automatically adjust to the
+        // correct one whenever the salt in use expires.
+        this._log.debug(`Handling future salts for message ${message.msgId}`)
+        const state = this._pending_state[message.msgId]
+
+        if (state) {
+            delete this._pending_state[message]
+            state.resolve(message.obj)
+        }
+    }
+
+    /**
+     * Handles both :tl:`MsgsStateReq` and :tl:`MsgResendReq` by
+     * enqueuing a :tl:`MsgsStateInfo` to be sent at a later point.
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleStateForgotten(message) {
+        this._send_queue.append(
+            new RequestState(new MsgsStateInfo(message.msgId, String.fromCharCode(1).repeat(message.obj.msgIds))),
+        )
+    }
+
+    /**
+     * Handles :tl:`MsgsAllInfo` by doing nothing (yet).
+     * @param message
+     * @returns {Promise<void>}
+     * @private
+     */
+    async _handleMsgAll(message) {
+    }
+
+    async _startReconnect() {
+        if (this._user_connected && !this._reconnecting) {
+            this._reconnecting = true
+            // TODO Should we set this?
+            // this._user_connected = false
+            await this._reconnect()
+        }
+    }
+
+    async _reconnect() {
+        this._log.debug('Closing current connection...')
+        try {
+            await this._connection.disconnect()
+        } catch (err) {
+            console.warn(err)
+        }
+        this._state.reset()
+        const retries = this._retries
+        for (let attempt = 0; attempt < retries; attempt++) {
+            try {
+                await this._connect()
+                this._send_queue.extend(Object.values(this._pending_state))
+                this._pending_state = {}
+                if (this._autoReconnectCallback) {
+                    await this._autoReconnectCallback()
+                }
+                break
+            } catch (e) {
+                this._log.error(e)
+                await Helpers.sleep(this._delay)
+            }
+        }
+    }
+}
+
+module.exports = MTProtoSender

+ 0 - 1
src/lib/gramjs/network/MTProtoState.js

@@ -133,7 +133,6 @@ class MTProtoState {
 
         // TODO Check salt,sessionId, and sequenceNumber
         const keyId = Helpers.readBigIntFromBuffer(body.slice(0, 8))
-
         if (keyId.neq(this.authKey.keyId)) {
             throw new SecurityError('Server replied with an invalid auth key')
         }

+ 1 - 1
src/lib/gramjs/network/connection/TCPAbridged.js

@@ -41,7 +41,7 @@ class AbridgedPacketCodec extends PacketCodec {
  * 508 bytes (127 << 2, which is very common).
  */
 class ConnectionTCPAbridged extends Connection {
-    packetCode = AbridgedPacketCodec
+    PacketCodecClass = AbridgedPacketCodec
 }
 
 module.exports = {

+ 77 - 0
src/lib/gramjs/network/connection/TCPObfuscated.js

@@ -0,0 +1,77 @@
+const { generateRandomBytes } = require('../../Helpers')
+const { ObfuscatedConnection } = require('./Connection')
+const { AbridgedPacketCodec } = require('./TCPAbridged')
+const AESModeCTR = require('../../crypto/AESCTR')
+
+class ObfuscatedIO {
+    header = null
+
+    constructor(connection) {
+        this.connection = connection.socket
+        const res = this.initHeader(connection.PacketCodecClass)
+        this.header = res.random
+
+        this._encrypt = res.encryptor
+        this._decrypt = res.decryptor
+    }
+
+    initHeader(packetCodec) {
+        // Obfuscated messages secrets cannot start with any of these
+        const keywords = [Buffer.from('50567247', 'hex'), Buffer.from('474554', 'hex'),
+            Buffer.from('504f5354', 'hex'), Buffer.from('eeeeeeee', 'hex')]
+        let random
+
+        // eslint-disable-next-line no-constant-condition
+        while (true) {
+            random = generateRandomBytes(64)
+            if (random[0] !== 0xef && !(random.slice(4, 8).equals(Buffer.alloc(4)))) {
+                let ok = true
+                for (const key of keywords) {
+                    if (key.equals(random.slice(0, 4))) {
+                        ok = false
+                        break
+                    }
+                }
+                if (ok) {
+                    break
+                }
+            }
+        }
+        random = random.toJSON().data
+
+        const randomReversed = Buffer.from(random.slice(8, 56)).reverse()
+        // Encryption has "continuous buffer" enabled
+        const encryptKey = Buffer.from(random.slice(8, 40))
+        const encryptIv = Buffer.from(random.slice(40, 56))
+        const decryptKey = Buffer.from(randomReversed.slice(0, 32))
+        const decryptIv = Buffer.from(randomReversed.slice(32, 48))
+        const encryptor = new AESModeCTR(encryptKey, encryptIv)
+        const decryptor = new AESModeCTR(decryptKey, decryptIv)
+
+        random = Buffer.concat([
+            Buffer.from(random.slice(0, 56)), packetCodec.obfuscateTag, Buffer.from(random.slice(60)),
+        ])
+        random = Buffer.concat([
+            Buffer.from(random.slice(0, 56)), Buffer.from(encryptor.encrypt(random).slice(56, 64)),Buffer.from(random.slice(64)) ,
+        ])
+        return { random, encryptor, decryptor }
+    }
+
+    async read(n) {
+        const data = await this.connection.read(n)
+        return this._decrypt.encrypt(data)
+    }
+
+    write(data) {
+        this.connection.write(this._encrypt.encrypt(data))
+    }
+}
+
+class ConnectionTCPObfuscated extends ObfuscatedConnection {
+    ObfuscatedIO = ObfuscatedIO
+    PacketCodecClass = AbridgedPacketCodec
+}
+
+module.exports = {
+    ConnectionTCPObfuscated,
+}

+ 34 - 0
src/modules/gramjs/updaters/files.ts

@@ -0,0 +1,34 @@
+import { getGlobal, setGlobal } from '../../../lib/teactn';
+
+import { ApiUpdate } from '../../../api/types';
+
+export function onUpdate(update: ApiUpdate) {
+  switch (update['@type']) {
+    case 'updateMessageImage': {
+      const { message_id, data_uri } = update;
+
+      const fileKey = `msg${message_id}`;
+      updateFile(fileKey, data_uri);
+
+      break;
+    }
+  }
+}
+
+function updateFile(fileId: string, dataUri: string) {
+  const global = getGlobal();
+
+  setGlobal({
+    ...global,
+    files: {
+      ...global.files,
+      byKey: {
+        ...global.files.byKey,
+        [fileId]: {
+          ...(global.files.byKey[fileId] || {}),
+          dataUri,
+        },
+      },
+    },
+  });
+}