Przeglądaj źródła

GramJS: Log out on `InvalidBufferError` (#481)

painor 5 lat temu
rodzic
commit
cb137b37f9

+ 535 - 0
src/api/gramjs/updater.ts

@@ -0,0 +1,535 @@
+import { Api as GramJs, connection } from '../../lib/gramjs';
+import { ApiMessage, ApiUpdateConnectionStateType, OnApiUpdate } from '../types';
+
+import { pick } from '../../util/iteratees';
+import {
+  buildApiMessage,
+  buildApiMessageFromShort,
+  buildApiMessageFromShortChat,
+  buildMessageMediaContent,
+  buildMessageTextContent,
+  resolveMessageApiChatId,
+  buildPoll,
+  buildPollResults,
+  buildApiMessageFromNotification,
+} from './apiBuilders/messages';
+import {
+  getApiChatIdFromMtpPeer,
+  buildChatMember,
+  buildChatMembers,
+  buildChatTypingStatus,
+  buildAvatar,
+  buildApiChatFromPreview,
+} from './apiBuilders/chats';
+import { buildApiUser, buildApiUserStatus } from './apiBuilders/users';
+import {
+  buildMessageFromUpdateShortSent,
+  isMessageWithMedia,
+  buildChatPhotoForLocalDb,
+  buildNotificationMessageForLocalDb,
+} from './gramjsBuilders';
+import localDb from './localDb';
+import { omitVirtualClassFields } from './apiBuilders/helpers';
+import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
+
+type Update = (
+  (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
+) | typeof connection.UpdateConnectionState;
+
+let onUpdate: OnApiUpdate;
+let currentUserId: number | undefined;
+
+export function init(_onUpdate: OnApiUpdate) {
+  onUpdate = _onUpdate;
+}
+
+export function setUpdaterCurrentUserId(_currentUserId: number) {
+  currentUserId = _currentUserId;
+}
+
+export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
+  if (update instanceof connection.UpdateConnectionState) {
+    let connectionState: ApiUpdateConnectionStateType;
+
+    switch (update.state) {
+      case connection.UpdateConnectionState.states.disconnected:
+        connectionState = 'connectionStateConnecting';
+        break;
+      case connection.UpdateConnectionState.states.broken:
+        connectionState = 'connectionStateBroken';
+        break;
+      case connection.UpdateConnectionState.states.connected:
+      default:
+        connectionState = 'connectionStateReady';
+        break;
+    }
+
+    onUpdate({
+      '@type': 'updateConnectionState',
+      connectionState,
+    });
+
+    // Messages
+  } else if (
+    update instanceof GramJs.UpdateNewMessage
+    || update instanceof GramJs.UpdateNewChannelMessage
+    || update instanceof GramJs.UpdateShortChatMessage
+    || update instanceof GramJs.UpdateShortMessage
+    || update instanceof GramJs.UpdateServiceNotification
+  ) {
+    let message: ApiMessage | undefined;
+
+    if (update instanceof GramJs.UpdateShortChatMessage) {
+      message = buildApiMessageFromShortChat(update);
+    } else if (update instanceof GramJs.UpdateShortMessage) {
+      message = buildApiMessageFromShort(update, currentUserId!);
+    } else if (update instanceof GramJs.UpdateServiceNotification) {
+      const currentDate = Date.now();
+      message = buildApiMessageFromNotification(update, currentDate);
+
+      if (isMessageWithMedia(update)) {
+        localDb.messages[SERVICE_NOTIFICATIONS_USER_ID] = buildNotificationMessageForLocalDb(
+          update,
+          message.id,
+          currentDate,
+          currentUserId!,
+        );
+      }
+    } else {
+      if (update.message instanceof GramJs.Message && isMessageWithMedia(update.message)) {
+        const messageFullId = `${resolveMessageApiChatId(update.message)}-${update.message.id}`;
+        localDb.messages[messageFullId] = update.message;
+      }
+
+      message = buildApiMessage(update.message)!;
+    }
+
+    // eslint-disable-next-line no-underscore-dangle
+    const entities = update._entities;
+    if (entities && entities.length) {
+      entities
+        .filter((e) => e instanceof GramJs.User)
+        .map(buildApiUser)
+        .forEach((user) => {
+          if (!user) {
+            return;
+          }
+
+          onUpdate({
+            '@type': 'updateUser',
+            id: user.id,
+            user,
+          });
+        });
+    }
+
+    onUpdate({
+      '@type': 'newMessage',
+      id: message.id,
+      chatId: message.chatId,
+      message,
+    });
+
+    // Some updates to a Chat/Channel don't have a dedicated update class.
+    // We can get info on some updates from Service Messages.
+    if (update.message instanceof GramJs.MessageService) {
+      const { action } = update.message;
+
+      if (action instanceof GramJs.MessageActionChatEditTitle) {
+        onUpdate({
+          '@type': 'updateChat',
+          id: message.chatId,
+          chat: {
+            title: action.title,
+          },
+        });
+      } else if (action instanceof GramJs.MessageActionChatEditPhoto) {
+        const photo = buildChatPhotoForLocalDb(action.photo);
+        const avatar = buildAvatar(photo);
+
+        const localDbChatId = Math.abs(resolveMessageApiChatId(update.message)!);
+        localDb.chats[localDbChatId].photo = photo;
+
+        if (avatar) {
+          onUpdate({
+            '@type': 'updateChat',
+            id: message.chatId,
+            chat: { avatar },
+          });
+        }
+      } else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
+        const localDbChatId = Math.abs(resolveMessageApiChatId(update.message)!);
+        localDb.chats[localDbChatId].photo = new GramJs.ChatPhotoEmpty();
+
+        onUpdate({
+          '@type': 'updateChat',
+          id: message.chatId,
+          chat: { avatar: undefined },
+        });
+      } else if (action instanceof GramJs.MessageActionChatDeleteUser) {
+        // eslint-disable-next-line no-underscore-dangle
+        if (update._entities && update._entities.some((e): e is GramJs.User => (
+          e instanceof GramJs.User && !!e.self && e.id === action.userId
+        ))) {
+          onUpdate({
+            '@type': 'updateChatLeave',
+            id: message.chatId,
+          });
+        }
+      } else if (action instanceof GramJs.MessageActionChatAddUser) {
+        // eslint-disable-next-line no-underscore-dangle
+        if (update._entities && update._entities.some((e): e is GramJs.User => (
+          e instanceof GramJs.User && !!e.self && action.users.includes(e.id)
+        ))) {
+          onUpdate({
+            '@type': 'updateChatJoin',
+            id: message.chatId,
+          });
+        }
+      }
+    }
+  } else if (
+    update instanceof GramJs.UpdateEditMessage
+    || update instanceof GramJs.UpdateEditChannelMessage
+  ) {
+    if (update.message instanceof GramJs.Message) {
+      const messageFullId = `${resolveMessageApiChatId(update.message)}-${update.message.id}`;
+      localDb.messages[messageFullId] = update.message;
+    }
+
+    const message = buildApiMessage(update.message)!;
+
+    onUpdate({
+      '@type': 'updateMessage',
+      id: message.id,
+      chatId: message.chatId,
+      message,
+    });
+  } else if (
+    update instanceof GramJs.UpdateDeleteMessages
+    || update instanceof GramJs.UpdateDeleteChannelMessages
+  ) {
+    onUpdate({
+      '@type': 'deleteMessages',
+      ids: update.messages,
+      ...((update instanceof GramJs.UpdateDeleteChannelMessages) && {
+        chatId: getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel),
+      }),
+    });
+  } else if ((
+    originRequest instanceof GramJs.messages.SendMessage
+    || originRequest instanceof GramJs.messages.SendMedia
+    || originRequest instanceof GramJs.messages.ForwardMessages
+  ) && (
+    update instanceof GramJs.UpdateMessageID
+    || update instanceof GramJs.UpdateShortSentMessage
+  )) {
+    const { randomId } = originRequest;
+    const localMessage = localDb.localMessages[randomId.toString()];
+    if (!localMessage) {
+      throw new Error('Local message not found');
+    }
+
+    let newContent: ApiMessage['content'] | undefined;
+    if (update instanceof GramJs.UpdateShortSentMessage) {
+      if (localMessage.content.text && update.entities) {
+        newContent = {
+          text: buildMessageTextContent(localMessage.content.text.text, update.entities),
+        };
+      }
+      if (update.media) {
+        newContent = {
+          ...newContent,
+          ...buildMessageMediaContent(update.media),
+        };
+      }
+
+      const mtpMessage = buildMessageFromUpdateShortSent(update.id, localMessage.chatId, update);
+      const messageFullId = `${localMessage.chatId}-${update.id}`;
+      localDb.messages[messageFullId] = mtpMessage;
+    }
+
+    onUpdate({
+      '@type': 'updateMessageSendSucceeded',
+      chatId: localMessage.chatId,
+      localId: localMessage.id,
+      message: {
+        ...localMessage,
+        ...(newContent && {
+          content: {
+            ...localMessage.content,
+            ...newContent,
+          },
+        }),
+        id: update.id,
+        sendingState: undefined,
+        ...('date' in update && { date: update.date }),
+      },
+    });
+  } else if (update instanceof GramJs.UpdateReadMessagesContents) {
+    onUpdate({
+      '@type': 'updateCommonBoxMessages',
+      ids: update.messages,
+      messageUpdate: {
+        hasUnreadMention: false,
+        isMediaUnread: false,
+      },
+    });
+  } else if (update instanceof GramJs.UpdateChannelReadMessagesContents) {
+    onUpdate({
+      '@type': 'updateChannelMessages',
+      channelId: update.channelId,
+      ids: update.messages,
+      messageUpdate: {
+        hasUnreadMention: false,
+        isMediaUnread: false,
+      },
+    });
+  } else if (update instanceof GramJs.UpdateMessagePoll) {
+    const { pollId, poll, results } = update;
+    if (poll) {
+      const apiPoll = buildPoll(poll, results);
+
+      onUpdate({
+        '@type': 'updateMessagePoll',
+        pollId: pollId.toString(),
+        pollUpdate: apiPoll,
+      });
+    } else {
+      const pollResults = buildPollResults(results);
+      onUpdate({
+        '@type': 'updateMessagePoll',
+        pollId: pollId.toString(),
+        pollUpdate: { results: pollResults },
+      });
+    }
+  } else if (update instanceof GramJs.UpdateMessagePollVote) {
+    onUpdate({
+      '@type': 'updateMessagePollVote',
+      pollId: update.pollId.toString(),
+      userId: update.userId,
+      options: update.options.map((option) => String.fromCharCode(...option)),
+    });
+  } else if (update instanceof GramJs.UpdateChannelMessageViews) {
+    onUpdate({
+      '@type': 'updateMessage',
+      chatId: getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel),
+      id: update.id,
+      message: { views: update.views },
+    });
+
+    // Chats
+  } else if (update instanceof GramJs.UpdateReadHistoryInbox) {
+    onUpdate({
+      '@type': 'updateChatInbox',
+      id: getApiChatIdFromMtpPeer(update.peer),
+      chat: {
+        lastReadInboxMessageId: update.maxId,
+        unreadCount: update.stillUnreadCount,
+      },
+    });
+  } else if (update instanceof GramJs.UpdateReadHistoryOutbox) {
+    onUpdate({
+      '@type': 'updateChat',
+      id: getApiChatIdFromMtpPeer(update.peer),
+      chat: {
+        lastReadOutboxMessageId: update.maxId,
+      },
+    });
+  } else if (update instanceof GramJs.UpdateReadChannelInbox) {
+    onUpdate({
+      '@type': 'updateChat',
+      id: getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel),
+      chat: {
+        lastReadInboxMessageId: update.maxId,
+        unreadCount: update.stillUnreadCount,
+      },
+    });
+  } else if (update instanceof GramJs.UpdateReadChannelOutbox) {
+    onUpdate({
+      '@type': 'updateChat',
+      id: getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel),
+      chat: {
+        lastReadOutboxMessageId: update.maxId,
+      },
+    });
+  } else if (
+    update instanceof GramJs.UpdateDialogPinned
+    && update.peer instanceof GramJs.DialogPeer
+  ) {
+    onUpdate({
+      '@type': 'updateChatPinned',
+      id: getApiChatIdFromMtpPeer(update.peer.peer),
+      isPinned: update.pinned || false,
+    });
+  } else if (update instanceof GramJs.UpdatePinnedDialogs) {
+    const ids = update.order
+      ? update.order
+        .filter((dp): dp is GramJs.DialogPeer => dp instanceof GramJs.DialogPeer)
+        .map((dp) => getApiChatIdFromMtpPeer(dp.peer))
+      : [];
+
+    onUpdate({
+      '@type': 'updatePinnedChatIds',
+      ids,
+    });
+  } else if (update instanceof GramJs.UpdateChatParticipants) {
+    const replacedMembers = buildChatMembers(update.participants);
+
+    onUpdate({
+      '@type': 'updateChatMembers',
+      id: getApiChatIdFromMtpPeer({ chatId: update.participants.chatId } as GramJs.TypePeer),
+      replacedMembers,
+    });
+  } else if (update instanceof GramJs.UpdateChatParticipantAdd) {
+    const addedMember = buildChatMember(
+      pick(update, ['userId', 'inviterId', 'date']) as GramJs.ChatParticipant,
+    );
+
+    onUpdate({
+      '@type': 'updateChatMembers',
+      id: getApiChatIdFromMtpPeer({ chatId: update.chatId } as GramJs.PeerChat),
+      addedMember,
+    });
+  } else if (update instanceof GramJs.UpdateChatParticipantDelete) {
+    const { userId: deletedMemberId } = update;
+
+    onUpdate({
+      '@type': 'updateChatMembers',
+      id: getApiChatIdFromMtpPeer({ chatId: update.chatId } as GramJs.PeerChat),
+      deletedMemberId,
+    });
+  } else if (
+    update instanceof GramJs.UpdateChatPinnedMessage
+    || update instanceof GramJs.UpdateChannelPinnedMessage
+  ) {
+    const id = update instanceof GramJs.UpdateChatPinnedMessage
+      ? getApiChatIdFromMtpPeer({ chatId: update.chatId } as GramJs.PeerChat)
+      : getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel);
+
+    onUpdate({
+      '@type': 'updateChatFullInfo',
+      id,
+      fullInfo: {
+        pinnedMessageId: update.id,
+      },
+    });
+  } else if (
+    update instanceof GramJs.UpdateNotifySettings
+    && update.peer instanceof GramJs.NotifyPeer
+  ) {
+    const { silent, muteUntil } = update.notifySettings;
+
+    onUpdate({
+      '@type': 'updateChat',
+      id: getApiChatIdFromMtpPeer(update.peer.peer),
+      chat: {
+        isMuted: silent || (typeof muteUntil === 'number' && Date.now() < muteUntil * 1000),
+      },
+    });
+  } else if (
+    update instanceof GramJs.UpdateUserTyping
+    || update instanceof GramJs.UpdateChatUserTyping
+  ) {
+    const id = update instanceof GramJs.UpdateUserTyping
+      ? update.userId
+      : getApiChatIdFromMtpPeer({ chatId: update.chatId } as GramJs.PeerChat);
+
+    onUpdate({
+      '@type': 'updateChatTypingStatus',
+      id,
+      typingStatus: buildChatTypingStatus(update),
+    });
+  } else if (update instanceof GramJs.UpdateChannel) {
+    const { _entities } = update;
+    if (_entities) {
+      const channel = _entities.find((e): e is GramJs.Channel => (e instanceof GramJs.Channel));
+      const isForbidden = _entities.some((e) => e instanceof GramJs.ChannelForbidden);
+      const isVisible = !(isForbidden || (channel && channel.left));
+      // When `chatUpdate` is built from a preview,
+      // it can have an incorrect type of `channel` for supergroup chats.
+      // Since chat entitiy can't change its type anyway, we can safely ignore it.
+      const chat = channel && buildApiChatFromPreview(channel, true);
+
+      if (chat) {
+        onUpdate({
+          '@type': 'updateChat',
+          id: chat.id,
+          chat,
+        });
+      }
+
+      onUpdate({
+        '@type': isVisible ? 'updateChatJoin' : 'updateChatLeave',
+        id: getApiChatIdFromMtpPeer({ channelId: update.channelId } as GramJs.PeerChannel),
+      });
+    }
+  } else if (update instanceof GramJs.UpdateChatDefaultBannedRights) {
+    onUpdate({
+      '@type': 'updateChat',
+      id: getApiChatIdFromMtpPeer(update.peer),
+      chat: {
+        defaultBannedRights: omitVirtualClassFields(update.defaultBannedRights),
+      },
+    });
+
+    // Users
+  } else if (update instanceof GramJs.UpdateUserStatus) {
+    onUpdate({
+      '@type': 'updateUser',
+      id: update.userId,
+      user: {
+        status: buildApiUserStatus(update.status),
+      },
+    });
+  } else if (update instanceof GramJs.UpdateUserName) {
+    const updatedUser = localDb.users[update.userId];
+    const user = updatedUser && updatedUser.mutualContact && !updatedUser.self
+      ? pick(update, ['username'])
+      : pick(update, ['firstName', 'lastName', 'username']);
+
+    onUpdate({
+      '@type': 'updateUser',
+      id: update.userId,
+      user,
+    });
+  } else if (update instanceof GramJs.UpdateUserPhoto) {
+    const { userId, photo } = update;
+    const avatar = buildAvatar(photo);
+
+    localDb.users[userId].photo = photo;
+
+    onUpdate({
+      '@type': 'updateUser',
+      id: userId,
+      user: { avatar },
+    });
+  } else if (update instanceof GramJs.UpdateUserPhone) {
+    const { userId, phone } = update;
+
+    onUpdate({
+      '@type': 'updateUser',
+      id: userId,
+      user: { phoneNumber: phone },
+    });
+  } else if (update instanceof GramJs.UpdateUserPinnedMessage) {
+    onUpdate({
+      '@type': 'updateUserFullInfo',
+      id: update.userId,
+      fullInfo: {
+        pinnedMessageId: update.id,
+      },
+    });
+
+    // Misc
+  } else if (update instanceof GramJs.UpdateContactsReset) {
+    onUpdate({ '@type': 'updateResetContactList' });
+  }
+}
+
+export function handleError(err: Error) {
+  onUpdate({
+    '@type': 'error',
+    error: pick(err, ['message']),
+  });
+}

+ 2 - 1
src/api/types/updates.ts

@@ -17,7 +17,8 @@ export type ApiUpdateAuthorizationStateType = (
 
 export type ApiUpdateConnectionStateType = (
   'connectionStateConnecting' |
-  'connectionStateReady'
+  'connectionStateReady' |
+  'connectionStateBroken'
 );
 
 export type ApiUpdateAuthorizationState = {

+ 9 - 5
src/lib/gramjs/client/TelegramClient.js

@@ -161,7 +161,7 @@ class TelegramClient {
             connectTimeout: this._timeout,
             authKeyCallback: this._authKeyCallback.bind(this),
             updateCallback: this._handleUpdate.bind(this),
-
+            isMainSender: true,
         })
 
         const connection = new this._connection(this.session.serverAddress
@@ -256,6 +256,7 @@ class TelegramClient {
     // endregion
     // export region
 
+<<<<<<< HEAD
     _onAuth() {
         this._setupAdditionalDcConnections()
     }
@@ -266,6 +267,10 @@ class TelegramClient {
                 this._borrowExportedSender(i)
             }
         }
+=======
+    removeSender(dcId) {
+        delete this._borrowedSenderPromises[dcId];
+>>>>>>> 6129de89... GramJS: Log out on `InvalidBufferError` (#481)
     }
 
     async _borrowExportedSender(dcId, retries = 5) {
@@ -288,6 +293,8 @@ class TelegramClient {
                 autoReconnect: this._autoReconnect,
                 connectTimeout: this._timeout,
                 authKeyCallback: this._authKeyCallback.bind(this),
+                isMainSender: dcId===this.session.dcId,
+                senderCallback: this.removeSender.bind(this),
             })
         for (let i = 0; i < retries; i++) {
             try {
@@ -1097,10 +1104,7 @@ class TelegramClient {
     }
 
     _handleUpdate(update) {
-        if (update === 1) {
-            this._dispatchUpdate({ update: new UpdateConnectionState(update) })
-            return
-        } else if (update === -1) {
+        if ([-1, 0, 1].includes(update)){
             this._dispatchUpdate({ update: new UpdateConnectionState(update) })
             return
         }

+ 89 - 0
src/lib/gramjs/client/uploadFile.ts

@@ -0,0 +1,89 @@
+import { default as Api } from '../tl/api';
+
+import TelegramClient from './TelegramClient';
+import { generateRandomBytes, readBigIntFromBuffer } from '../Helpers';
+import { getAppropriatedPartSize } from '../Utils';
+
+interface OnProgress {
+    // Float between 0 and 1.
+    (progress: number): void;
+
+    isCanceled?: boolean;
+}
+
+export interface UploadFileParams {
+    file: File;
+    onProgress?: OnProgress;
+}
+
+const KB_TO_BYTES = 1024;
+const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024;
+
+export async function uploadFile(
+    client: TelegramClient,
+    fileParams: UploadFileParams,
+): Promise<Api.InputFile | Api.InputFileBig> {
+    const { file, onProgress } = fileParams;
+    const { name, size } = file;
+
+    const fileId = readBigIntFromBuffer(generateRandomBytes(8), true, true);
+    const isLarge = size > LARGE_FILE_THRESHOLD;
+
+    const partSize = getAppropriatedPartSize(size) * KB_TO_BYTES;
+    const partCount = Math.floor((size + partSize - 1) / partSize);
+    const buffer = Buffer.from(await fileToBuffer(file));
+
+    // We always upload from the DC we are in.
+    const sender = await client._borrowExportedSender(client.session.dcId);
+
+    if (onProgress) {
+        onProgress(0);
+    }
+
+    for (let i = 0; i < partCount; i++) {
+        const bytes = buffer.slice(i * partSize, (i + 1) * partSize);
+        const result = await sender.send(
+            isLarge
+                ? new Api.upload.SaveBigFilePart({
+                    fileId,
+                    filePart: i,
+                    fileTotalParts: partCount,
+                    bytes,
+                })
+                : new Api.upload.SaveFilePart({
+                    fileId,
+                    filePart: i,
+                    bytes,
+                }),
+        );
+
+        if (result && onProgress) {
+            if (onProgress.isCanceled) {
+                throw new Error('USER_CANCELED')
+            }
+
+            onProgress((i + 1) / partCount);
+        }
+    }
+
+    return isLarge
+        ? new Api.InputFileBig({
+            id: fileId,
+            parts: partCount,
+            name,
+        })
+        : new Api.InputFile({
+            id: fileId,
+            parts: partCount,
+            name,
+            md5Checksum: '', // This is not a "flag", so not sure if we can make it optional.
+        });
+}
+
+function generateRandomBigInt() {
+    return readBigIntFromBuffer(generateRandomBytes(8), false);
+}
+
+function fileToBuffer(file: File) {
+    return new Response(file).arrayBuffer();
+}

+ 18 - 3
src/lib/gramjs/network/MTProtoSender.js

@@ -53,6 +53,8 @@ class MTProtoSender {
         authKeyCallback: null,
         updateCallback: null,
         autoReconnectCallback: null,
+        isMainSender: null,
+        senderCallback: null,
     }
 
     /**
@@ -71,6 +73,8 @@ class MTProtoSender {
         this._authKeyCallback = args.authKeyCallback
         this._updateCallback = args.updateCallback
         this._autoReconnectCallback = args.autoReconnectCallback
+        this._isMainSender = args.isMainSender;
+        this._senderCallback = args.senderCallback;
 
         /**
          * Whether the user has explicitly connected or disconnected.
@@ -381,12 +385,23 @@ class MTProtoSender {
                     continue
                 } else if (e instanceof InvalidBufferError) {
                     this._log.info('Broken authorization key; resetting')
-                    await this.authKey.setKey(null)
+                    if (this._updateCallback && this._isMainSender){
+                        // 0 == broken
+                        this._updateCallback(0)
+                    } else if (this._senderCallback && !this._isMainSender){
+                        // Deletes the current sender from the object
+                        this._senderCallback(this._dcId)
+                    }
+
+                    // We don't really need to do this if we're going to sign in again
+                    /*await this.authKey.setKey(null)
 
                     if (this._authKeyCallback) {
                         await this._authKeyCallback(null)
-                    }
-                    await this.disconnect()
+                    }*/
+                    // We can disconnect at sign in
+                    /* await this.disconnect()
+                    */
                     return
                 } else {
                     this._log.error('Unhandled error while receiving data')

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

@@ -5,7 +5,8 @@ const MTProtoSender = require('./MTProtoSender')
 class UpdateConnectionState {
     static states = {
         disconnected: -1,
-        connected: 1
+        connected: 1,
+        broken: 0,
     }
 
     constructor(state) {

+ 140 - 0
src/modules/actions/apiUpdaters/initial.ts

@@ -0,0 +1,140 @@
+import {
+  addReducer, getDispatch, getGlobal, setGlobal,
+} from '../../../lib/teact/teactn';
+
+import {
+  ApiUpdate,
+  ApiUpdateAuthorizationState,
+  ApiUpdateAuthorizationError,
+  ApiUpdateConnectionState,
+  ApiUpdateCurrentUser,
+} from '../../../api/types';
+import { DEBUG } from '../../../config';
+import { updateUser } from '../../reducers';
+
+addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
+  if (DEBUG) {
+    // eslint-disable-next-line no-console
+    console.log('[GramJs] UPDATE', update['@type'], { update });
+  }
+
+  switch (update['@type']) {
+    case 'updateAuthorizationState':
+      onUpdateAuthorizationState(update);
+      break;
+
+    case 'updateAuthorizationError':
+      onUpdateAuthorizationError(update);
+      break;
+
+    case 'updateConnectionState':
+      onUpdateConnectionState(update);
+      break;
+
+    case 'updateCurrentUser':
+      onUpdateCurrentUser(update);
+      break;
+
+    case 'error':
+      actions.showError({ error: update.error });
+      break;
+
+    // TODO Move to another module
+    case 'updateResetContactList':
+      setGlobal({
+        ...getGlobal(),
+        contactList: {
+          hash: 0,
+          userIds: [],
+        },
+      });
+      break;
+  }
+});
+
+function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) {
+  const global = getGlobal();
+  const authState = update.authorizationState;
+
+  const wasAuthReady = global.authState === 'authorizationStateReady';
+
+  setGlobal({
+    ...global,
+    authState,
+    authIsLoading: false,
+  });
+
+  switch (authState) {
+    case 'authorizationStateLoggingOut':
+      setGlobal({
+        ...getGlobal(),
+        isLoggingOut: true,
+      });
+      break;
+    case 'authorizationStateWaitPassword':
+      setGlobal({
+        ...getGlobal(),
+        authHint: update.hint,
+      });
+      break;
+    case 'authorizationStateWaitQrCode':
+      setGlobal({
+        ...getGlobal(),
+        authIsLoadingQrCode: false,
+        authQrCode: update.qrCode,
+      });
+      break;
+    case 'authorizationStateReady': {
+      if (wasAuthReady) {
+        break;
+      }
+
+      const newGlobal = getGlobal();
+
+      setGlobal({
+        ...newGlobal,
+        isLoggingOut: false,
+        lastSyncTime: Date.now(),
+      });
+
+      const { sessionId } = update;
+      if (sessionId && getGlobal().authRememberMe) {
+        getDispatch().saveSession({ sessionId });
+      }
+
+      break;
+    }
+  }
+}
+
+function onUpdateAuthorizationError(update: ApiUpdateAuthorizationError) {
+  setGlobal({
+    ...getGlobal(),
+    authError: update.message,
+  });
+}
+
+function onUpdateConnectionState(update: ApiUpdateConnectionState) {
+  const { connectionState } = update;
+  const global = getGlobal();
+
+  setGlobal({
+    ...global,
+    connectionState,
+  });
+
+  if (connectionState === 'connectionStateReady' && global.authState === 'authorizationStateReady') {
+    getDispatch().sync();
+  } else if (connectionState === 'connectionStateBroken') {
+    getDispatch().signOut();
+  }
+}
+
+function onUpdateCurrentUser(update: ApiUpdateCurrentUser) {
+  const { currentUser } = update;
+
+  setGlobal({
+    ...updateUser(getGlobal(), currentUser.id, currentUser),
+    currentUserId: currentUser.id,
+  });
+}