Browse Source

MTProto new RSA method

Improve invokeAfter
Something more
master
Eduard Kuzmenko 3 years ago
parent
commit
ede6056f64
  1. 12
      src/components/appSelectPeers.ts
  2. 2
      src/components/buttonMenuToggle.ts
  3. 4
      src/components/chat/audio.ts
  4. 2
      src/components/chat/autocompleteHelper.ts
  5. 82
      src/components/chat/bubbles.ts
  6. 4
      src/components/chat/chat.ts
  7. 43
      src/components/chat/input.ts
  8. 4
      src/components/chat/pinnedMessage.ts
  9. 12
      src/components/chat/replyKeyboard.ts
  10. 16
      src/components/chat/selection.ts
  11. 12
      src/components/chat/topbar.ts
  12. 4
      src/components/checkboxField.ts
  13. 4
      src/components/codeInputField.ts
  14. 2
      src/components/editPeer.ts
  15. 2
      src/components/languageChangeButton.ts
  16. 9
      src/components/misc.ts
  17. 6
      src/components/popups/deleteDialog.ts
  18. 11
      src/components/popups/peer.ts
  19. 2
      src/components/privacySection.ts
  20. 9
      src/components/sidebarLeft/tabs/addMembers.ts
  21. 2
      src/components/sidebarLeft/tabs/background.ts
  22. 2
      src/components/sidebarLeft/tabs/blockedUsers.ts
  23. 19
      src/components/sidebarLeft/tabs/chatFolders.ts
  24. 4
      src/components/sidebarLeft/tabs/generalSettings.ts
  25. 6
      src/components/sidebarLeft/tabs/notifications.ts
  26. 6
      src/components/sidebarLeft/tabs/privacyAndSecurity.ts
  27. 6
      src/components/sidebarRight/tabs/editChat.ts
  28. 2
      src/components/sidebarRight/tabs/editContact.ts
  29. 8
      src/components/sidebarRight/tabs/groupPermissions.ts
  30. 76
      src/components/sidebarRight/tabs/sharedMedia.ts
  31. 2
      src/components/sidebarRight/tabs/userPermissions.ts
  32. 7
      src/components/toast.ts
  33. 2
      src/components/usernameInputField.ts
  34. 2
      src/components/wrappers.ts
  35. 26
      src/global.d.ts
  36. 4
      src/helpers/array.ts
  37. 47
      src/helpers/bytes.ts
  38. 4
      src/helpers/dom/clickEvent.ts
  39. 8
      src/helpers/dom/handleScrollSideEvent.ts
  40. 4
      src/helpers/dropdownHover.ts
  41. 23
      src/helpers/eventListenerBase.ts
  42. 71
      src/helpers/listenerSetter.ts
  43. 2
      src/hooks/useHeavyAnimationCheck.ts
  44. 13
      src/lang.ts
  45. 3440
      src/layer.d.ts
  46. 64
      src/lib/appManagers/appDialogsManager.ts
  47. 51
      src/lib/appManagers/appMessagesManager.ts
  48. 2
      src/lib/appManagers/appNotificationsManager.ts
  49. 4
      src/lib/appManagers/appProfileManager.ts
  50. 2
      src/lib/appManagers/appStickersManager.ts
  51. 15
      src/lib/appManagers/appUsersManager.ts
  52. 72
      src/lib/crypto/crypto_methods.ts
  53. 104
      src/lib/crypto/crypto_utils.ts
  54. 73
      src/lib/crypto/cryptoworker.ts
  55. 76
      src/lib/crypto/srp.ts
  56. 10
      src/lib/lottieLoader.ts
  57. 32
      src/lib/mediaPlayer.ts
  58. 13
      src/lib/mtproto/apiFileManager.ts
  59. 78
      src/lib/mtproto/apiManager.ts
  60. 297
      src/lib/mtproto/authorizer.ts
  61. 34
      src/lib/mtproto/bin_utils.ts
  62. 25
      src/lib/mtproto/mtproto.worker.ts
  63. 11
      src/lib/mtproto/mtprotoworker.ts
  64. 57
      src/lib/mtproto/networker.ts
  65. 4
      src/lib/mtproto/networkerFactory.ts
  66. 14
      src/lib/mtproto/passwordManager.ts
  67. 33
      src/lib/mtproto/referenceDatabase.ts
  68. 118
      src/lib/mtproto/rsaKeysManager.ts
  69. 2
      src/lib/mtproto/schema.ts
  70. 4
      src/lib/mtproto/telegramMeWebManager.ts
  71. 10
      src/lib/mtproto/timeManager.ts
  72. 134
      src/lib/mtproto/tl_utils.ts
  73. 5
      src/lib/mtproto/transports/http.ts
  74. 2
      src/lib/mtproto/transports/tcpObfuscated.ts
  75. 15
      src/lib/storages/dialogs.ts
  76. 4
      src/mock/srp.ts
  77. 2
      src/pages/pageAuthCode.ts
  78. 7
      src/pages/pageSignIn.ts
  79. 2
      src/pages/pageSignQR.ts
  80. 13
      src/scripts/format_schema.js
  81. 6
      src/scripts/in/schema.json
  82. 2
      src/scripts/out/schema.json
  83. 1
      src/scss/partials/_selector.scss
  84. 75
      src/tests/crypto_methods.test.ts
  85. 7
      src/tests/srp.test.ts
  86. 17
      src/types.d.ts

12
src/components/appSelectPeers.ts

@ -188,10 +188,10 @@ export default class AppSelectPeers { @@ -188,10 +188,10 @@ export default class AppSelectPeers {
this.cachedContacts = null;
}
//if(this.peerType.includes('dialogs')) {
if(this.peerType.includes('dialogs')) {
this.folderId = 0;
this.offsetIndex = 0;
//}
}
for(let i in this.tempIds) {
// @ts-ignore
@ -335,9 +335,9 @@ export default class AppSelectPeers { @@ -335,9 +335,9 @@ export default class AppSelectPeers {
this.loadedWhat.contacts = true;
// need to load non-contacts
if(!this.peerType.includes('dialogs')) {
/* if(!this.peerType.includes('dialogs')) {
return this.getMoreDialogs();
}
} */
}
}
@ -376,7 +376,7 @@ export default class AppSelectPeers { @@ -376,7 +376,7 @@ export default class AppSelectPeers {
const get = () => {
const promises: Promise<any>[] = [];
if(!loadedAllDialogs && (this.peerType.includes('dialogs') || this.peerType.includes('contacts'))) {
if(!loadedAllDialogs && (this.peerType.includes('dialogs')/* || this.peerType.includes('contacts') */)) {
if(!loadAllDialogsPromise) {
loadAllDialogsPromise = appMessagesManager.getConversationsAll()
.then(() => {
@ -389,7 +389,7 @@ export default class AppSelectPeers { @@ -389,7 +389,7 @@ export default class AppSelectPeers {
promises.push(loadAllDialogsPromise);
}
if((this.peerType.includes('dialogs') || this.loadedWhat.contacts) && !this.loadedWhat.archived) { // to load non-contacts
if((this.peerType.includes('dialogs')/* || this.loadedWhat.contacts */) && !this.loadedWhat.archived) { // to load non-contacts
promises.push(this.getMoreDialogs());
if(!this.loadedWhat.archived) {

2
src/components/buttonMenuToggle.ts

@ -24,7 +24,7 @@ const ButtonMenuToggle = (options: Partial<{noRipple: true, onlyMobile: true, li @@ -24,7 +24,7 @@ const ButtonMenuToggle = (options: Partial<{noRipple: true, onlyMobile: true, li
// TODO: refactor for attachClickEvent, because if move finger after touchstart, it will start anyway
const ButtonMenuToggleHandler = (el: HTMLElement, onOpen?: (e: Event) => void, options?: AttachClickOptions) => {
const add = options?.listenerSetter ? options.listenerSetter.add.bind(options.listenerSetter, el) : el.addEventListener.bind(el);
const add = options?.listenerSetter ? options.listenerSetter.add(el) : el.addEventListener.bind(el);
//console.trace('ButtonMenuToggleHandler attach', el, onOpen, options);
add(CLICK_EVENT_NAME, (e: Event) => {

4
src/components/chat/audio.ts

@ -42,7 +42,7 @@ export default class ChatAudio extends PinnedContainer { @@ -42,7 +42,7 @@ export default class ChatAudio extends PinnedContainer {
this.wrapper.prepend(this.toggleEl);
this.topbar.listenerSetter.add(rootScope, 'audio_play', (e) => {
this.topbar.listenerSetter.add(rootScope)('audio_play', (e) => {
const {doc, mid, peerId} = e;
let title: string | HTMLElement, subtitle: string;
@ -65,7 +65,7 @@ export default class ChatAudio extends PinnedContainer { @@ -65,7 +65,7 @@ export default class ChatAudio extends PinnedContainer {
this.toggle(false);
});
this.topbar.listenerSetter.add(rootScope, 'audio_pause', () => {
this.topbar.listenerSetter.add(rootScope)('audio_pause', () => {
this.toggleEl.classList.remove('flip-icon');
});
}

2
src/components/chat/autocompleteHelper.ts

@ -88,7 +88,7 @@ export default class AutocompleteHelper extends EventListenerBase<{ @@ -88,7 +88,7 @@ export default class AutocompleteHelper extends EventListenerBase<{
if(this.navigationItem) {
appNavigationController.removeItem(this.navigationItem);
}
}, true);
}, {once: true});
};
protected attachNavigation() {

82
src/components/chat/bubbles.ts

@ -157,6 +157,7 @@ export default class ChatBubbles { @@ -157,6 +157,7 @@ export default class ChatBubbles {
private onAnimateLadder: () => Promise<any> | void;
// private ladderDeferred: CancellablePromise<void>;
private resolveLadderAnimation: () => Promise<any>;
private emptyPlaceholderMid: number;
constructor(private chat: Chat,
private appMessagesManager: AppMessagesManager,
@ -194,7 +195,7 @@ export default class ChatBubbles { @@ -194,7 +195,7 @@ export default class ChatBubbles {
// * events
// will call when sent for update pos
this.listenerSetter.add(rootScope, 'history_update', (e) => {
this.listenerSetter.add(rootScope)('history_update', (e) => {
const {storage, peerId, mid} = e;
if(mid && peerId === this.peerId && this.chat.getMessagesStorage() === storage) {
@ -219,9 +220,9 @@ export default class ChatBubbles { @@ -219,9 +220,9 @@ export default class ChatBubbles {
}
});
//this.listenerSetter.add(rootScope, '')
//this.listenerSetter.add(rootScope)('')
this.listenerSetter.add(rootScope, 'dialog_flush', (e) => {
this.listenerSetter.add(rootScope)('dialog_flush', (e) => {
let peerId: number = e.peerId;
if(this.peerId === peerId) {
this.deleteMessagesByIds(Object.keys(this.bubbles).map(m => +m));
@ -229,7 +230,7 @@ export default class ChatBubbles { @@ -229,7 +230,7 @@ export default class ChatBubbles {
});
// Calls when message successfully sent and we have an id
this.listenerSetter.add(rootScope, 'message_sent', (e) => {
this.listenerSetter.add(rootScope)('message_sent', (e) => {
const {storage, tempId, tempMessage, mid} = e;
// ! can't use peerId to validate here, because id can be the same in 'scheduled' and 'chat' types
@ -349,7 +350,7 @@ export default class ChatBubbles { @@ -349,7 +350,7 @@ export default class ChatBubbles {
}
});
this.listenerSetter.add(rootScope, 'message_edit', (e) => {
this.listenerSetter.add(rootScope)('message_edit', (e) => {
fastRaf(() => {
const {storage, peerId, mid} = e;
@ -369,7 +370,7 @@ export default class ChatBubbles { @@ -369,7 +370,7 @@ export default class ChatBubbles {
});
});
this.listenerSetter.add(rootScope, 'album_edit', (e) => {
this.listenerSetter.add(rootScope)('album_edit', (e) => {
//fastRaf(() => { // ! can't use delayed smth here, need original bubble to be edited
const {peerId, groupId, deletedMids} = e;
@ -384,7 +385,7 @@ export default class ChatBubbles { @@ -384,7 +385,7 @@ export default class ChatBubbles {
//});
});
this.listenerSetter.add(rootScope, 'messages_downloaded', (e) => {
this.listenerSetter.add(rootScope)('messages_downloaded', (e) => {
const {peerId, mids} = e;
const middleware = this.getMiddleware();
@ -422,10 +423,10 @@ export default class ChatBubbles { @@ -422,10 +423,10 @@ export default class ChatBubbles {
});
});
this.listenerSetter.add(this.bubblesContainer, 'click', this.onBubblesClick/* , {capture: true, passive: false} */);
this.listenerSetter.add(this.bubblesContainer)('click', this.onBubblesClick/* , {capture: true, passive: false} */);
if(DEBUG) {
this.listenerSetter.add(this.bubblesContainer, 'dblclick', (e) => {
this.listenerSetter.add(this.bubblesContainer)('dblclick', (e) => {
const bubble = findUpClassName(e.target, 'grouped-item') || findUpClassName(e.target, 'bubble');
if(bubble) {
const mid = +bubble.dataset.mid
@ -436,7 +437,7 @@ export default class ChatBubbles { @@ -436,7 +437,7 @@ export default class ChatBubbles {
}
if(!isMobile) {
this.listenerSetter.add(this.bubblesContainer, 'dblclick', (e) => {
this.listenerSetter.add(this.bubblesContainer)('dblclick', (e) => {
if(this.chat.selection.isSelecting || !this.appMessagesManager.canWriteToPeer(this.peerId, this.chat.threadId)) {
return;
}
@ -479,7 +480,7 @@ export default class ChatBubbles { @@ -479,7 +480,7 @@ export default class ChatBubbles {
public constructPeerHelpers() {
// will call when message is sent (only 1)
this.listenerSetter.add(rootScope, 'history_append', (e) => {
this.listenerSetter.add(rootScope)('history_append', (e) => {
const {peerId, storage, mid} = e;
if(peerId !== this.peerId || storage !== this.chat.getMessagesStorage()) return;
@ -491,15 +492,13 @@ export default class ChatBubbles { @@ -491,15 +492,13 @@ export default class ChatBubbles {
}
});
this.listenerSetter.add(rootScope, 'history_multiappend', (e) => {
const msgIdsByPeer = e;
this.listenerSetter.add(rootScope)('history_multiappend', (msgIdsByPeer) => {
if(!(this.peerId in msgIdsByPeer)) return;
const msgIds = Array.from(msgIdsByPeer[this.peerId] as number[]).slice().sort((a, b) => b - a);
const msgIds = Array.from(msgIdsByPeer[this.peerId]).slice().sort((a, b) => b - a);
this.renderNewMessagesByIds(msgIds);
});
this.listenerSetter.add(rootScope, 'history_delete', (e) => {
this.listenerSetter.add(rootScope)('history_delete', (e) => {
const {peerId, msgs} = e;
const mids = Object.keys(msgs).map(s => +s);
@ -509,7 +508,7 @@ export default class ChatBubbles { @@ -509,7 +508,7 @@ export default class ChatBubbles {
}
});
this.listenerSetter.add(rootScope, 'dialog_unread', (e) => {
this.listenerSetter.add(rootScope)('dialog_unread', (e) => {
const info = e;
if(info.peerId === this.peerId) {
@ -518,7 +517,7 @@ export default class ChatBubbles { @@ -518,7 +517,7 @@ export default class ChatBubbles {
}
});
this.listenerSetter.add(rootScope, 'dialogs_multiupdate', (e) => {
this.listenerSetter.add(rootScope)('dialogs_multiupdate', (e) => {
const dialogs = e;
if(dialogs[this.peerId]) {
@ -526,13 +525,13 @@ export default class ChatBubbles { @@ -526,13 +525,13 @@ export default class ChatBubbles {
}
});
this.listenerSetter.add(rootScope, 'dialog_notify_settings', (dialog) => {
this.listenerSetter.add(rootScope)('dialog_notify_settings', (dialog) => {
if(this.peerId === dialog.peerId) {
this.chat.input.setUnreadCount();
}
});
this.listenerSetter.add(rootScope, 'chat_update', (e) => {
this.listenerSetter.add(rootScope)('chat_update', (e) => {
const chatId: number = e;
if(this.peerId === -chatId) {
const hadRights = this.chatInner.classList.contains('has-rights');
@ -545,7 +544,7 @@ export default class ChatBubbles { @@ -545,7 +544,7 @@ export default class ChatBubbles {
}
});
this.listenerSetter.add(rootScope, 'settings_updated', (e: BroadcastEvents['settings_updated']) => {
this.listenerSetter.add(rootScope)('settings_updated', (e: BroadcastEvents['settings_updated']) => {
if(e.key === 'settings.emoji.big') {
const isScrolledDown = this.scrollable.isScrolledDown;
if(!isScrolledDown) {
@ -735,7 +734,7 @@ export default class ChatBubbles { @@ -735,7 +734,7 @@ export default class ChatBubbles {
}
public constructPinnedHelpers() {
this.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => {
this.listenerSetter.add(rootScope)('peer_pinned_messages', (e) => {
const {peerId, mids, pinned} = e;
if(peerId !== this.peerId) return;
@ -752,7 +751,7 @@ export default class ChatBubbles { @@ -752,7 +751,7 @@ export default class ChatBubbles {
this.chat.topbar.setTitle(Object.keys(this.appMessagesManager.getScheduledMessagesStorage(this.peerId)).length);
};
this.listenerSetter.add(rootScope, 'scheduled_new', (e) => {
this.listenerSetter.add(rootScope)('scheduled_new', (e) => {
const {peerId, mid} = e;
if(peerId !== this.peerId) return;
@ -760,7 +759,7 @@ export default class ChatBubbles { @@ -760,7 +759,7 @@ export default class ChatBubbles {
onUpdate();
});
this.listenerSetter.add(rootScope, 'scheduled_delete', (e) => {
this.listenerSetter.add(rootScope)('scheduled_delete', (e) => {
const {peerId, mids} = e;
if(peerId !== this.peerId) return;
@ -1277,6 +1276,10 @@ export default class ChatBubbles { @@ -1277,6 +1276,10 @@ export default class ChatBubbles {
//this.unreaded.findAndSplice(mid => mid === id);
bubble.remove();
//bubble.remove();
if(this.emptyPlaceholderMid === mid) {
this.emptyPlaceholderMid = undefined;
}
});
if(permanent && this.chat.selection.isSelecting) {
@ -1345,7 +1348,13 @@ export default class ChatBubbles { @@ -1345,7 +1348,13 @@ export default class ChatBubbles {
return this.scrollable.scrollIntoViewNew(element, position, 4, undefined, forceDirection, forceDuration);
}
public scrollToBubbleEnd(bubble = this.chatInner.lastElementChild.lastElementChild as HTMLElement) {
public scrollToBubbleEnd(bubble?: HTMLElement) {
if(!bubble) {
const lastDateGroup = this.getLastDateGroup();
if(lastDateGroup) {
bubble = lastDateGroup.lastElementChild as HTMLElement;
}
}
/* if(DEBUG) {
this.log('scrollToNewLastBubble: will scroll into view:', bubble);
} */
@ -1358,9 +1367,23 @@ export default class ChatBubbles { @@ -1358,9 +1367,23 @@ export default class ChatBubbles {
}
}
// ! can't get it by chatInner.lastElementChild because placeholder can be the last...
private getLastDateGroup() {
let lastTime = 0, lastElem: HTMLElement;
for(const i in this.dateMessages) {
const dateMessage = this.dateMessages[i];
if(dateMessage.firstTimestamp > lastTime) {
lastElem = dateMessage.container;
lastTime = dateMessage.firstTimestamp;
}
}
return lastElem;
}
public scrollToBubbleIfLast(bubble: HTMLElement) {
if(bubble.parentElement.lastElementChild === bubble &&
bubble.parentElement.parentElement.lastElementChild === bubble.parentElement) {
this.getLastDateGroup().lastElementChild === bubble.parentElement) {
this.scrollToBubbleEnd(bubble);
}
}
@ -1533,7 +1556,9 @@ export default class ChatBubbles { @@ -1533,7 +1556,9 @@ export default class ChatBubbles {
this.middleware.clean();
this.onAnimateLadder = undefined;
this.resolveLadderAnimation = undefined;
this.emptyPlaceholderMid = undefined;
////console.timeEnd('appImManager cleanup');
}
@ -3206,7 +3231,7 @@ export default class ChatBubbles { @@ -3206,7 +3231,7 @@ export default class ChatBubbles {
elements.forEach((element: any) => element.classList.add('empty-placeholder-line'));
}
private processLocalMessageRender(message: Message) {
private processLocalMessageRender(message: Message.message | Message.messageService) {
const bubble = this.renderMessage(message, undefined, undefined, undefined, false);
bubble.classList.add('bubble-first', 'is-group-last', 'is-group-first');
bubble.classList.remove('can-have-tail', 'is-in');
@ -3249,6 +3274,8 @@ export default class ChatBubbles { @@ -3249,6 +3274,8 @@ export default class ChatBubbles {
} else {
this.chatInner.prepend(bubble);
}
this.emptyPlaceholderMid = message.mid;
}
private generateLocalFirstMessage<T extends boolean>(service?: T, fill?: (message: GenerateLocalMessageType<T>) => void): GenerateLocalMessageType<T> {
@ -3314,6 +3341,7 @@ export default class ChatBubbles { @@ -3314,6 +3341,7 @@ export default class ChatBubbles {
public checkIfEmptyPlaceholderNeeded() {
if(this.scrollable.loadedAll.top &&
this.scrollable.loadedAll.bottom &&
this.emptyPlaceholderMid === undefined &&
(
!this.appMessagesManager.getHistoryStorage(this.peerId).count ||
(

4
src/components/chat/chat.ts

@ -205,13 +205,13 @@ export default class Chat extends EventListenerBase<{ @@ -205,13 +205,13 @@ export default class Chat extends EventListenerBase<{
this.container.classList.add('type-' + this.type);
this.container.append(this.topbar.container, this.bubbles.bubblesContainer, this.input.chatInput);
this.bubbles.listenerSetter.add(rootScope, 'dialog_migrate', ({migrateFrom, migrateTo}) => {
this.bubbles.listenerSetter.add(rootScope)('dialog_migrate', ({migrateFrom, migrateTo}) => {
if(this.peerId === migrateFrom) {
this.setPeer(migrateTo);
}
});
this.bubbles.listenerSetter.add(rootScope, 'dialog_drop', (e) => {
this.bubbles.listenerSetter.add(rootScope)('dialog_drop', (e) => {
if(e.peerId === this.peerId) {
this.appImManager.setPeer(0);
}

43
src/components/chat/input.ts

@ -74,7 +74,8 @@ const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this @@ -74,7 +74,8 @@ const POSTING_MEDIA_NOT_ALLOWED = 'Posting media content isn\'t allowed in this
type ChatInputHelperType = 'edit' | 'webpage' | 'forward' | 'reply';
export default class ChatInput {
private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/;
// private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?::|.)(?!.*[:@]).*|(?:[@\/]\S*))$/;
private static AUTO_COMPLETE_REG_EXP = /(\s|^)((?:(?:@|^\/)\S*)|(?::|[^:@\/])(?!.*[:@\/]).*)$/;
public messageInput: HTMLElement;
public messageInputField: InputField;
private fileInput: HTMLInputElement;
@ -210,7 +211,7 @@ export default class ChatInput { @@ -210,7 +211,7 @@ export default class ChatInput {
// @ts-ignore
let height = window.visualViewport.height; */
// @ts-ignore
// this.listenerSetter.add(window.visualViewport, 'resize', () => {
// this.listenerSetter.add(window.visualViewport)('resize', () => {
// const scrollable = this.chat.bubbles.scrollable;
// const wasScrolledDown = scrollable.isScrolledDown;
@ -248,7 +249,7 @@ export default class ChatInput { @@ -248,7 +249,7 @@ export default class ChatInput {
// });
// ! Can't use it with resizeObserver
/* this.listenerSetter.add(window.visualViewport, 'resize', () => {
/* this.listenerSetter.add(window.visualViewport)('resize', () => {
const scrollable = this.chat.bubbles.scrollable;
const wasScrolledDown = scrollable.isScrolledDown;
@ -304,7 +305,7 @@ export default class ChatInput { @@ -304,7 +305,7 @@ export default class ChatInput {
this.appImManager.openScheduled(this.chat.peerId);
}, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope, 'scheduled_new', (e) => {
this.listenerSetter.add(rootScope)('scheduled_new', (e) => {
const peerId = e.peerId;
if(this.chat.peerId !== peerId) {
@ -314,7 +315,7 @@ export default class ChatInput { @@ -314,7 +315,7 @@ export default class ChatInput {
this.btnScheduled.classList.remove('hide');
});
this.listenerSetter.add(rootScope, 'scheduled_delete', (e) => {
this.listenerSetter.add(rootScope)('scheduled_delete', (e) => {
const peerId = e.peerId;
if(this.chat.peerId !== peerId) {
@ -333,8 +334,8 @@ export default class ChatInput { @@ -333,8 +334,8 @@ export default class ChatInput {
appMessagesManager: this.appMessagesManager,
btnHover: this.btnToggleReplyMarkup
});
this.listenerSetter.add(this.replyKeyboard, 'open', () => this.btnToggleReplyMarkup.classList.add('active'));
this.listenerSetter.add(this.replyKeyboard, 'close', () => this.btnToggleReplyMarkup.classList.remove('active'));
this.listenerSetter.add(this.replyKeyboard)('open', () => this.btnToggleReplyMarkup.classList.add('active'));
this.listenerSetter.add(this.replyKeyboard)('close', () => this.btnToggleReplyMarkup.classList.remove('active'));
}
this.attachMenuButtons = [{
@ -431,8 +432,8 @@ export default class ChatInput { @@ -431,8 +432,8 @@ export default class ChatInput {
this.inputContainer.append(this.btnCancelRecord, this.btnSendContainer);
emoticonsDropdown.attachButtonListener(this.btnToggleEmoticons, this.listenerSetter);
this.listenerSetter.add(emoticonsDropdown, 'open', this.onEmoticonsOpen);
this.listenerSetter.add(emoticonsDropdown, 'close', this.onEmoticonsClose);
this.listenerSetter.add(emoticonsDropdown)('open', this.onEmoticonsOpen);
this.listenerSetter.add(emoticonsDropdown)('close', this.onEmoticonsClose);
this.attachMessageInputField();
@ -445,7 +446,7 @@ export default class ChatInput { @@ -445,7 +446,7 @@ export default class ChatInput {
}
}, {passive: false, capture: true}); */
this.listenerSetter.add(rootScope, 'settings_updated', () => {
this.listenerSetter.add(rootScope)('settings_updated', () => {
if(this.stickersHelper || this.emojiHelper) {
this.previousQuery = undefined;
this.checkAutocomplete();
@ -461,13 +462,13 @@ export default class ChatInput { @@ -461,13 +462,13 @@ export default class ChatInput {
}
});
this.listenerSetter.add(rootScope, 'draft_updated', (e) => {
this.listenerSetter.add(rootScope)('draft_updated', (e) => {
const {peerId, threadId, draft} = e;
if(this.chat.threadId !== threadId || this.chat.peerId !== peerId) return;
this.setDraft(draft);
});
this.listenerSetter.add(rootScope, 'peer_changing', (chat) => {
this.listenerSetter.add(rootScope)('peer_changing', (chat) => {
if(this.chat === chat) {
this.saveDraft();
}
@ -489,7 +490,7 @@ export default class ChatInput { @@ -489,7 +490,7 @@ export default class ChatInput {
this.updateSendBtn();
this.listenerSetter.add(this.fileInput, 'change', (e) => {
this.listenerSetter.add(this.fileInput)('change', (e) => {
let files = (e.target as HTMLInputElement & EventTarget).files;
if(!files.length) {
return;
@ -576,7 +577,7 @@ export default class ChatInput { @@ -576,7 +577,7 @@ export default class ChatInput {
this.fakePinnedControlBtn = fakeContainer.firstChild as HTMLElement;
this.fakeRowsWrapper.append(fakeContainer);
this.listenerSetter.add(this.pinnedControlBtn, 'click', () => {
this.listenerSetter.add(this.pinnedControlBtn)('click', () => {
const peerId = this.chat.peerId;
new PopupPinMessage(peerId, 0, true, () => {
@ -801,7 +802,7 @@ export default class ChatInput { @@ -801,7 +802,7 @@ export default class ChatInput {
}
private attachMessageInputListeners() {
this.listenerSetter.add(this.messageInput, 'keydown', (e: KeyboardEvent) => {
this.listenerSetter.add(this.messageInput)('keydown', (e: KeyboardEvent) => {
if(isSendShortcutPressed(e)) {
this.sendMessage();
} else if(e.ctrlKey || e.metaKey) {
@ -831,12 +832,12 @@ export default class ChatInput { @@ -831,12 +832,12 @@ export default class ChatInput {
emoticonsDropdown.toggle(false);
}, {listenerSetter: this.listenerSetter});
/* this.listenerSetter.add(window, 'resize', () => {
/* this.listenerSetter.add(window)('resize', () => {
this.restoreScroll();
}); */
/* if(isSafari) {
this.listenerSetter.add(this.messageInput, 'mousedown', () => {
this.listenerSetter.add(this.messageInput)('mousedown', () => {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
emoticonsDropdown.toggle(false);
@ -846,7 +847,7 @@ export default class ChatInput { @@ -846,7 +847,7 @@ export default class ChatInput {
} */
}
/* this.listenerSetter.add(this.messageInput, 'beforeinput', (e: Event) => {
/* this.listenerSetter.add(this.messageInput)('beforeinput', (e: Event) => {
// * validate due to manual formatting through browser's context menu
const inputType = (e as InputEvent).inputType;
//console.log('message beforeinput event', e);
@ -859,13 +860,13 @@ export default class ChatInput { @@ -859,13 +860,13 @@ export default class ChatInput {
}
}
}); */
this.listenerSetter.add(this.messageInput, 'input', this.onMessageInput);
this.listenerSetter.add(this.messageInput, 'keyup', () => {
this.listenerSetter.add(this.messageInput)('input', this.onMessageInput);
this.listenerSetter.add(this.messageInput)('keyup', () => {
this.checkAutocomplete();
});
if(this.chat.type === 'chat' || this.chat.type === 'discussion') {
this.listenerSetter.add(this.messageInput, 'focusin', () => {
this.listenerSetter.add(this.messageInput)('focusin', () => {
if(this.chat.bubbles.scrollable.loadedAll.bottom) {
this.appMessagesManager.readAllHistory(this.chat.peerId, this.chat.threadId);
}

4
src/components/chat/pinnedMessage.ts

@ -288,7 +288,7 @@ export default class ChatPinnedMessage { @@ -288,7 +288,7 @@ export default class ChatPinnedMessage {
this.topbar.openPinned(true);
}, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => {
this.listenerSetter.add(rootScope)('peer_pinned_messages', (e) => {
const peerId = e.peerId;
if(peerId === this.topbar.peerId) {
@ -310,7 +310,7 @@ export default class ChatPinnedMessage { @@ -310,7 +310,7 @@ export default class ChatPinnedMessage {
}
});
this.listenerSetter.add(rootScope, 'peer_pinned_hidden', (e) => {
this.listenerSetter.add(rootScope)('peer_pinned_hidden', (e) => {
const {peerId, maxId} = e;
if(peerId === this.topbar.peerId) {

12
src/components/chat/replyKeyboard.ts

@ -23,7 +23,7 @@ export default class ReplyKeyboard extends DropdownHover { @@ -23,7 +23,7 @@ export default class ReplyKeyboard extends DropdownHover {
private appMessagesManager: AppMessagesManager;
private btnHover: HTMLElement;
private peerId: number;
private touchListener: Listener<HTMLElement>;
private touchListener: Listener;
constructor(options: {
listenerSetter: ListenerSetter,
@ -41,7 +41,7 @@ export default class ReplyKeyboard extends DropdownHover { @@ -41,7 +41,7 @@ export default class ReplyKeyboard extends DropdownHover {
this.element.style.display = 'none';
this.attachButtonListener(this.btnHover, this.listenerSetter);
this.listenerSetter.add(rootScope, 'history_reply_markup', ({peerId}) => {
this.listenerSetter.add(rootScope)('history_reply_markup', ({peerId}) => {
if(this.peerId === peerId && this.checkAvailability() && this.isActive()) {
this.render();
}
@ -51,18 +51,18 @@ export default class ReplyKeyboard extends DropdownHover { @@ -51,18 +51,18 @@ export default class ReplyKeyboard extends DropdownHover {
protected init() {
this.appendTo.append(this.element);
this.listenerSetter.add(this, 'open', () => {
this.listenerSetter.add(this)('open', () => {
this.render();
if(isTouchSupported) {
this.touchListener = this.listenerSetter.add(document.body, 'touchstart', this.onBodyTouchStart, {passive: false, capture: true});
this.listenerSetter.add(this, 'close', () => {
this.touchListener = this.listenerSetter.add(document.body)('touchstart', this.onBodyTouchStart, {passive: false, capture: true}) as any as Listener;
this.listenerSetter.add(this)('close', () => {
this.listenerSetter.remove(this.touchListener);
}, {once: true});
}
});
this.listenerSetter.add(this.element, 'click', (e) => {
this.listenerSetter.add(this.element)('click', (e) => {
const target = findUpClassName(e.target, 'btn');
if(!target) {
return;

16
src/components/chat/selection.ts

@ -50,14 +50,14 @@ export default class ChatSelection { @@ -50,14 +50,14 @@ export default class ChatSelection {
this.listenerSetter = bubbles.listenerSetter;
if(isTouchSupported) {
this.listenerSetter.add(bubblesContainer, 'touchend', (e) => {
this.listenerSetter.add(bubblesContainer)('touchend', (e) => {
if(!this.isSelecting) return;
this.selectedText = getSelectedText();
});
return;
}
this.listenerSetter.add(bubblesContainer, 'mousedown', (e) => {
this.listenerSetter.add(bubblesContainer)('mousedown', (e) => {
//console.log('selection mousedown', e);
const bubble = findUpClassName(e.target, 'bubble');
// LEFT BUTTON
@ -162,8 +162,8 @@ export default class ChatSelection { @@ -162,8 +162,8 @@ export default class ChatSelection {
};
const documentListenerOptions = {once: true};
this.listenerSetter.add(bubblesContainer, 'mousemove', onMouseMove);
this.listenerSetter.add(document, 'mouseup', onMouseUp, documentListenerOptions);
this.listenerSetter.add(bubblesContainer)('mousemove', onMouseMove);
this.listenerSetter.add(document)('mouseup', onMouseUp, documentListenerOptions);
});
}
@ -355,7 +355,7 @@ export default class ChatSelection { @@ -355,7 +355,7 @@ export default class ChatSelection {
this.selectionContainer.classList.add('selection-container');
const btnCancel = ButtonIcon('close', {noRipple: true});
this.listenerSetter.add(btnCancel, 'click', this.cancelSelection, {once: true});
this.listenerSetter.add(btnCancel)('click', this.cancelSelection, {once: true});
this.selectionCountEl = document.createElement('div');
this.selectionCountEl.classList.add('selection-container-count');
@ -363,7 +363,7 @@ export default class ChatSelection { @@ -363,7 +363,7 @@ export default class ChatSelection {
if(this.chat.type === 'scheduled') {
this.selectionSendNowBtn = Button('btn-primary btn-transparent btn-short text-bold selection-container-send', {icon: 'send2'});
this.selectionSendNowBtn.append(i18n('MessageScheduleSend'));
this.listenerSetter.add(this.selectionSendNowBtn, 'click', () => {
this.listenerSetter.add(this.selectionSendNowBtn)('click', () => {
new PopupSendNow(this.bubbles.peerId, [...this.selectedMids], () => {
this.cancelSelection();
})
@ -371,7 +371,7 @@ export default class ChatSelection { @@ -371,7 +371,7 @@ export default class ChatSelection {
} else {
this.selectionForwardBtn = Button('btn-primary btn-transparent text-bold selection-container-forward', {icon: 'forward'});
this.selectionForwardBtn.append(i18n('Forward'));
this.listenerSetter.add(this.selectionForwardBtn, 'click', () => {
this.listenerSetter.add(this.selectionForwardBtn)('click', () => {
new PopupForward(this.bubbles.peerId, [...this.selectedMids], () => {
this.cancelSelection();
});
@ -380,7 +380,7 @@ export default class ChatSelection { @@ -380,7 +380,7 @@ export default class ChatSelection {
this.selectionDeleteBtn = Button('btn-primary btn-transparent danger text-bold selection-container-delete', {icon: 'delete'});
this.selectionDeleteBtn.append(i18n('Delete'));
this.listenerSetter.add(this.selectionDeleteBtn, 'click', () => {
this.listenerSetter.add(this.selectionDeleteBtn)('click', () => {
new PopupDeleteMessages(this.bubbles.peerId, [...this.selectedMids], this.chat.type, () => {
this.cancelSelection();
});

12
src/components/chat/topbar.ts

@ -132,7 +132,7 @@ export default class ChatTopbar { @@ -132,7 +132,7 @@ export default class ChatTopbar {
// * fix topbar overflow section
this.listenerSetter.add(window, 'resize', this.onResize);
this.listenerSetter.add(window)('resize', this.onResize);
mediaSizes.addEventListener('changeScreen', this.onChangeScreen);
attachClickEvent(this.container, (e) => {
@ -299,7 +299,7 @@ export default class ChatTopbar { @@ -299,7 +299,7 @@ export default class ChatTopbar {
//});
}, {listenerSetter: this.listenerSetter});
this.listenerSetter.add(rootScope, 'chat_update', (e) => {
this.listenerSetter.add(rootScope)('chat_update', (e) => {
const chatId: number = e;
if(this.peerId === -chatId) {
const chat = this.appChatsManager.getChat(chatId) as Channel/* | Chat */;
@ -309,13 +309,13 @@ export default class ChatTopbar { @@ -309,13 +309,13 @@ export default class ChatTopbar {
}
});
this.listenerSetter.add(rootScope, 'dialog_notify_settings', (dialog) => {
this.listenerSetter.add(rootScope)('dialog_notify_settings', (dialog) => {
if(dialog.peerId === this.peerId) {
this.setMutedState();
}
});
this.listenerSetter.add(rootScope, 'peer_typings', (e) => {
this.listenerSetter.add(rootScope)('peer_typings', (e) => {
const {peerId} = e;
if(this.peerId === peerId) {
@ -323,7 +323,7 @@ export default class ChatTopbar { @@ -323,7 +323,7 @@ export default class ChatTopbar {
}
});
this.listenerSetter.add(rootScope, 'user_update', (e) => {
this.listenerSetter.add(rootScope)('user_update', (e) => {
const userId = e;
if(this.peerId === userId) {
@ -356,7 +356,7 @@ export default class ChatTopbar { @@ -356,7 +356,7 @@ export default class ChatTopbar {
}
public constructPinnedHelpers() {
this.listenerSetter.add(rootScope, 'peer_pinned_messages', (e) => {
this.listenerSetter.add(rootScope)('peer_pinned_messages', (e) => {
const {peerId, mids, pinned} = e;
if(peerId !== this.peerId) return;

4
src/components/checkboxField.ts

@ -137,6 +137,10 @@ export default class CheckboxField { @@ -137,6 +137,10 @@ export default class CheckboxField {
}
set checked(checked: boolean) {
/* if(this.checked === checked) {
return;
} */
this.setValueSilently(checked);
const event = new Event('change', {bubbles: true, cancelable: true});

4
src/components/codeInputField.ts

@ -9,7 +9,7 @@ import InputField, { InputFieldOptions } from "./inputField"; @@ -9,7 +9,7 @@ import InputField, { InputFieldOptions } from "./inputField";
export default class CodeInputField extends InputField {
constructor(options: InputFieldOptions & {
length: number,
onFill: (code: number) => void
onFill: (code: string) => void
}) {
super({
plainText: true,
@ -31,7 +31,7 @@ export default class CodeInputField extends InputField { @@ -31,7 +31,7 @@ export default class CodeInputField extends InputField {
const length = this.value.length;
if(length === options.length) { // submit code
options.onFill(+this.value);
options.onFill(this.value);
} else if(length === lastLength) {
return;
}

2
src/components/editPeer.ts

@ -49,7 +49,7 @@ export default class EditPeer { @@ -49,7 +49,7 @@ export default class EditPeer {
}
this.inputFields.forEach(inputField => {
this.listenerSetter.add(inputField.input, 'input', this.handleChange);
this.listenerSetter.add(inputField.input)('input', this.handleChange);
});
}

2
src/components/languageChangeButton.ts

@ -70,7 +70,7 @@ export default function getLanguageChangeButton(appendTo: HTMLElement) { @@ -70,7 +70,7 @@ export default function getLanguageChangeButton(appendTo: HTMLElement) {
rootScope.addEventListener('language_change', () => {
btnChangeLanguage.remove();
}, true);
}, {once: true});
backup.forEach(string => {
I18n.strings.set(string.key as LangPackKey, string);

9
src/components/misc.ts

@ -11,7 +11,7 @@ import { CLICK_EVENT_NAME } from "../helpers/dom/clickEvent"; @@ -11,7 +11,7 @@ import { CLICK_EVENT_NAME } from "../helpers/dom/clickEvent";
import ListenerSetter from "../helpers/listenerSetter";
import mediaSizes from "../helpers/mediaSizes";
import { isTouchSupported } from "../helpers/touchSupport";
import { isApple, isMobileSafari, isSafari } from "../helpers/userAgent";
import { isApple, isMobileSafari } from "../helpers/userAgent";
import appNavigationController from "./appNavigationController";
export function putPreloader(elem: Element, returnDiv = false): HTMLElement {
@ -308,18 +308,21 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme @@ -308,18 +308,21 @@ export function positionMenu({pageX, pageY}: MouseEvent | Touch, elem: HTMLEleme
}
export function attachContextMenuListener(element: HTMLElement, callback: (e: Touch | MouseEvent) => void, listenerSetter?: ListenerSetter) {
const add = listenerSetter ? listenerSetter.add.bind(listenerSetter, element) : element.addEventListener.bind(element);
const add = listenerSetter ? listenerSetter.add(element) : element.addEventListener.bind(element);
const remove = listenerSetter ? listenerSetter.removeManual.bind(listenerSetter, element) : element.removeEventListener.bind(element);
if(isApple && isTouchSupported) {
let timeout: number;
const options: any = /* null */{capture: true};
const options: EventListenerOptions = {capture: true};
const onCancel = () => {
clearTimeout(timeout);
// @ts-ignore
remove('touchmove', onCancel, options);
// @ts-ignore
remove('touchend', onCancel, options);
// @ts-ignore
remove('touchcancel', onCancel, options);
};

6
src/components/popups/deleteDialog.ts

@ -26,7 +26,7 @@ export default class PopupDeleteDialog { @@ -26,7 +26,7 @@ export default class PopupDeleteDialog {
const callbackLeave = (checked: PopupPeerButtonCallbackCheckboxes) => {
let promise = appChatsManager.leave(-peerId);
if(checkboxes && checked[checkboxes[0].text]) {
if(checkboxes && checked.size) {
promise = promise.then(() => {
return appMessagesManager.flushHistory(peerId);
}) as any;
@ -39,9 +39,9 @@ export default class PopupDeleteDialog { @@ -39,9 +39,9 @@ export default class PopupDeleteDialog {
let promise: Promise<any>;
if(peerId > 0) {
promise = appMessagesManager.flushHistory(peerId, false, checkboxes ? checked[checkboxes[0].text] : undefined);
promise = appMessagesManager.flushHistory(peerId, false, checkboxes ? !!checked.size : undefined);
} else {
if(checked[checkboxes[0].text]) {
if(checked.size) {
promise = appChatsManager.delete(-peerId);
} else {
return callbackLeave(checked);

11
src/components/popups/peer.ts

@ -9,8 +9,9 @@ import PopupElement, { addCancelButton, PopupButton, PopupOptions } from "."; @@ -9,8 +9,9 @@ import PopupElement, { addCancelButton, PopupButton, PopupOptions } from ".";
import { i18n, LangPackKey } from "../../lib/langPack";
import CheckboxField, { CheckboxFieldOptions } from "../checkboxField";
export type PopupPeerButtonCallbackCheckboxes = Partial<{[text in LangPackKey]: boolean}>;
export type PopupPeerButtonCallbackCheckboxes = Set<LangPackKey>;
export type PopupPeerButtonCallback = (checkboxes?: PopupPeerButtonCallbackCheckboxes) => void;
export type PopupPeerCheckboxOptions = CheckboxFieldOptions & {checkboxField?: CheckboxField};
export type PopupPeerOptions = PopupOptions & Partial<{
peerId: number,
@ -21,7 +22,7 @@ export type PopupPeerOptions = PopupOptions & Partial<{ @@ -21,7 +22,7 @@ export type PopupPeerOptions = PopupOptions & Partial<{
descriptionLangKey?: LangPackKey,
descriptionLangArgs?: any[],
buttons: Array<Omit<PopupButton, 'callback'> & Partial<{callback: PopupPeerButtonCallback}>>,
checkboxes: Array<CheckboxFieldOptions & {checkboxField?: CheckboxField}>
checkboxes: Array<PopupPeerCheckboxOptions>
}>;
export default class PopupPeer extends PopupElement {
constructor(private className: string, options: PopupPeerOptions = {}) {
@ -60,9 +61,11 @@ export default class PopupPeer extends PopupElement { @@ -60,9 +61,11 @@ export default class PopupPeer extends PopupElement {
if(button.callback) {
const original = button.callback;
button.callback = () => {
const c: PopupPeerButtonCallbackCheckboxes = {};
const c: Set<LangPackKey> = new Set();
options.checkboxes.forEach(o => {
c[o.text] = o.checkboxField.checked;
if(o.checkboxField.checked) {
c.add(o.text);
}
});
original(c);
};

2
src/components/privacySection.ts

@ -205,7 +205,7 @@ export default class PrivacySection { @@ -205,7 +205,7 @@ export default class PrivacySection {
}
appPrivacyManager.setPrivacy(options.inputKey, rules);
}, true);
}, {once: true});
});
}

9
src/components/sidebarLeft/tabs/addMembers.ts

@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
import { SliderSuperTab } from "../../slider";
import AppSelectPeers from "../../appSelectPeers";
import { putPreloader } from "../../misc";
import { putPreloader, setButtonLoader } from "../../misc";
import { LangPackKey, _i18n } from "../../../lib/langPack";
import ButtonCorner from "../../buttonCorner";
@ -41,13 +41,12 @@ export default class AppAddMembersTab extends SliderSuperTab { @@ -41,13 +41,12 @@ export default class AppAddMembersTab extends SliderSuperTab {
}
public attachToPromise(promise: Promise<any>) {
this.nextBtn.classList.remove('tgico-arrow_next');
this.nextBtn.disabled = true;
putPreloader(this.nextBtn);
this.selector.freezed = true;
const removeLoader = setButtonLoader(this.nextBtn, 'arrow_next');
promise.then(() => {
this.close();
}, () => {
removeLoader();
});
}

2
src/components/sidebarLeft/tabs/background.ts

@ -66,7 +66,7 @@ export default class AppBackgroundTab extends SliderSuperTab { @@ -66,7 +66,7 @@ export default class AppBackgroundTab extends SliderSuperTab {
withRipple: true
});
this.listenerSetter.add(blurCheckboxField.input, 'change', () => {
this.listenerSetter.add(blurCheckboxField.input)('change', () => {
this.theme.background.blur = blurCheckboxField.input.checked;
appStateManager.pushToState('settings', rootScope.settings);

2
src/components/sidebarLeft/tabs/blockedUsers.ts

@ -107,7 +107,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab { @@ -107,7 +107,7 @@ export default class AppBlockedUsersTab extends SliderSuperTab {
openBtnMenu(element);
}, this.listenerSetter);
this.listenerSetter.add(rootScope, 'peer_block', (update) => {
this.listenerSetter.add(rootScope)('peer_block', (update) => {
const {peerId, blocked} = update;
const li = list.querySelector(`[data-peer-id="${peerId}"]`);
if(blocked) {

19
src/components/sidebarLeft/tabs/chatFolders.ts

@ -170,7 +170,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { @@ -170,7 +170,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
onFiltersContainerUpdate();
});
this.listenerSetter.add(rootScope, 'filter_update', (e) => {
this.listenerSetter.add(rootScope)('filter_update', (e) => {
const filter = e;
if(this.filtersRendered.hasOwnProperty(filter.id)) {
this.renderFolder(filter, null, this.filtersRendered[filter.id]);
@ -183,7 +183,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { @@ -183,7 +183,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
this.getSuggestedFilters();
});
this.listenerSetter.add(rootScope, 'filter_delete', (e) => {
this.listenerSetter.add(rootScope)('filter_delete', (e) => {
const filter = e;
if(this.filtersRendered.hasOwnProperty(filter.id)) {
/* for(const suggested of this.suggestedFilters) {
@ -200,7 +200,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { @@ -200,7 +200,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
onFiltersContainerUpdate();
});
this.listenerSetter.add(rootScope, 'filter_order', (e: BroadcastEvents['filter_order']) => {
this.listenerSetter.add(rootScope)('filter_order', (e: BroadcastEvents['filter_order']) => {
const order = e;
order.forEach((filterId, idx) => {
const container = this.filtersRendered[filterId].container;
@ -208,9 +208,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { @@ -208,9 +208,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
});
});
this.getSuggestedFilters();
return this.loadAnimationPromise = lottieLoader.loadAnimationFromURL({
this.loadAnimationPromise = lottieLoader.loadAnimationFromURL({
container: this.stickerContainer,
loop: false,
autoplay: false,
@ -221,6 +219,13 @@ export default class AppChatFoldersTab extends SliderSuperTab { @@ -221,6 +219,13 @@ export default class AppChatFoldersTab extends SliderSuperTab {
return lottieLoader.waitForFirstFrame(player);
});
this.getSuggestedFilters()
/* return Promise.all([
this.loadAnimationPromise
]); */
return this.loadAnimationPromise;
}
onOpenAfterTimeout() {
@ -231,7 +236,7 @@ export default class AppChatFoldersTab extends SliderSuperTab { @@ -231,7 +236,7 @@ export default class AppChatFoldersTab extends SliderSuperTab {
}
private getSuggestedFilters() {
apiManager.invokeApi('messages.getSuggestedDialogFilters').then(suggestedFilters => {
return apiManager.invokeApi('messages.getSuggestedDialogFilters').then(suggestedFilters => {
this.suggestedSection.container.style.display = suggestedFilters.length ? '' : 'none';
Array.from(this.suggestedSection.content.children).slice(1).forEach(el => el.remove());

4
src/components/sidebarLeft/tabs/generalSettings.ts

@ -254,7 +254,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { @@ -254,7 +254,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab {
}
});
this.listenerSetter.add(rootScope, 'stickers_installed', (e) => {
this.listenerSetter.add(rootScope)('stickers_installed', (e) => {
const set: StickerSet.stickerSet = e;
if(!stickerSets[set.id]) {
@ -262,7 +262,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab { @@ -262,7 +262,7 @@ export default class AppGeneralSettingsTab extends SliderSuperTab {
}
});
this.listenerSetter.add(rootScope, 'stickers_deleted', (e) => {
this.listenerSetter.add(rootScope)('stickers_deleted', (e) => {
const set: StickerSet.stickerSet = e;
if(stickerSets[set.id]) {

6
src/components/sidebarLeft/tabs/notifications.ts

@ -73,9 +73,9 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { @@ -73,9 +73,9 @@ export default class AppNotificationsTab extends SliderSuperTabEventable {
inputSettings.show_previews = showPreviews;
appNotificationsManager.updateNotifySettings(inputNotifyPeer, inputSettings);
}, true);
}, {once: true});
this.listenerSetter.add(rootScope, 'notify_settings', (update: Update.updateNotifySettings) => {
this.listenerSetter.add(rootScope)('notify_settings', (update: Update.updateNotifySettings) => {
const inputKey = convertKeyToInputKey(update.peer._) as any;
if(options.inputKey === inputKey) {
notifySettings = update.notify_settings;
@ -134,7 +134,7 @@ export default class AppNotificationsTab extends SliderSuperTabEventable { @@ -134,7 +134,7 @@ export default class AppNotificationsTab extends SliderSuperTabEventable {
if(enabled !== _enabled) {
appNotificationsManager.setContactSignUpNotification(!_enabled);
}
}, true);
}, {once: true});
});
}
}

6
src/components/sidebarLeft/tabs/privacyAndSecurity.ts

@ -110,7 +110,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { @@ -110,7 +110,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
}
};
this.listenerSetter.add(rootScope, 'peer_block', () => {
this.listenerSetter.add(rootScope)('peer_block', () => {
/* const {blocked, peerId} = update;
if(!blocked) blockedPeerIds.findAndSplice(p => p === peerId);
else blockedPeerIds.unshift(peerId);
@ -261,7 +261,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { @@ -261,7 +261,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
apiManager.invokeApi('account.setContentSettings', {
sensitive_enabled: _enabled
});
}, true);
}, {once: true});
}));
this.scrollable.append(section.container);
@ -290,7 +290,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable { @@ -290,7 +290,7 @@ export default class AppPrivacyAndSecurityTab extends SliderSuperTabEventable {
};
const deleteButton = Button('btn-primary btn-transparent', {icon: 'delete', text: 'PrivacyDeleteCloudDrafts'});
this.listenerSetter.add(deleteButton, 'click', onDeleteClick);
this.listenerSetter.add(deleteButton)('click', onDeleteClick);
section.content.append(deleteButton);
/* promises.push(apiManager.invokeApi('messages.getAllDrafts').then(drafts => {

6
src/components/sidebarRight/tabs/editChat.ts

@ -86,7 +86,7 @@ export default class AppEditChatTab extends SliderSuperTab { @@ -86,7 +86,7 @@ export default class AppEditChatTab extends SliderSuperTab {
tab.chatFull = chatFull;
tab.open();
this.listenerSetter.add(tab.eventListener, 'destroy', setChatTypeSubtitle);
this.listenerSetter.add(tab.eventListener)('destroy', setChatTypeSubtitle);
},
icon: 'lock'
});
@ -137,7 +137,7 @@ export default class AppEditChatTab extends SliderSuperTab { @@ -137,7 +137,7 @@ export default class AppEditChatTab extends SliderSuperTab {
setPermissionsLength();
section.content.append(permissionsRow.container);
this.listenerSetter.add(rootScope, 'chat_update', (chatId) => {
this.listenerSetter.add(rootScope)('chat_update', (chatId) => {
if(this.chatId === chatId) {
setPermissionsLength();
}
@ -294,7 +294,7 @@ export default class AppEditChatTab extends SliderSuperTab { @@ -294,7 +294,7 @@ export default class AppEditChatTab extends SliderSuperTab {
if(!isChannel) {
// ! this one will fire earlier than tab's closeAfterTimeout (destroy) event and listeners will be erased, so destroy won't fire
this.listenerSetter.add(rootScope, 'dialog_migrate', ({migrateFrom, migrateTo}) => {
this.listenerSetter.add(rootScope)('dialog_migrate', ({migrateFrom, migrateTo}) => {
if(-this.chatId === migrateFrom) {
this.chatId = -migrateTo;
this._init();

2
src/components/sidebarRight/tabs/editContact.ts

@ -84,7 +84,7 @@ export default class AppEditContactTab extends SliderSuperTab { @@ -84,7 +84,7 @@ export default class AppEditContactTab extends SliderSuperTab {
appMessagesManager.mutePeer(this.peerId);
});
this.listenerSetter.add(rootScope, 'notify_settings', (update) => {
this.listenerSetter.add(rootScope)('notify_settings', (update) => {
if(update.peer._ !== 'notifyPeer') return;
const peerId = appPeersManager.getPeerId(update.peer.peer);
if(this.peerId === peerId) {

8
src/components/sidebarRight/tabs/groupPermissions.ts

@ -71,7 +71,7 @@ export class ChatPermissions { @@ -71,7 +71,7 @@ export class ChatPermissions {
if(options.participant && defaultBannedRights.pFlags[mainFlag]) {
info.checkboxField.input.disabled = true;
/* options.listenerSetter.add(info.checkboxField.input, 'change', (e) => {
/* options.listenerSetter.add(info.checkboxField.input)('change', (e) => {
if(!e.isTrusted) {
return;
}
@ -87,7 +87,7 @@ export class ChatPermissions { @@ -87,7 +87,7 @@ export class ChatPermissions {
}
if(this.toggleWith[mainFlag]) {
options.listenerSetter.add(info.checkboxField.input, 'change', () => {
options.listenerSetter.add(info.checkboxField.input)('change', () => {
if(!info.checkboxField.checked) {
const other = this.v.filter(i => this.toggleWith[mainFlag].includes(i.flags[0]));
other.forEach(info => {
@ -143,7 +143,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable { @@ -143,7 +143,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
this.eventListener.addEventListener('destroy', () => {
appChatsManager.editChatDefaultBannedRights(this.chatId, chatPermissions.takeOut());
});
}, {once: true});
this.scrollable.append(section.container);
}
@ -257,7 +257,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable { @@ -257,7 +257,7 @@ export default class AppGroupPermissionsTab extends SliderSuperTabEventable {
//dom.lastMessageSpan.innerHTML = 'Can Add Users and Pin Messages';
};
this.listenerSetter.add(rootScope, 'updateChannelParticipant', (update: Update.updateChannelParticipant) => {
this.listenerSetter.add(rootScope)('updateChannelParticipant', (update: Update.updateChannelParticipant) => {
const needAdd = update.new_participant?._ === 'channelParticipantBanned' && !update.new_participant.banned_rights.pFlags.view_messages;
const li = list.querySelector(`[data-peer-id="${update.user_id}"]`);
if(needAdd) {

76
src/components/sidebarRight/tabs/sharedMedia.ts

@ -29,7 +29,7 @@ import I18n, { i18n, LangPackKey } from "../../../lib/langPack"; @@ -29,7 +29,7 @@ import I18n, { i18n, LangPackKey } from "../../../lib/langPack";
import { SettingSection } from "../../sidebarLeft";
import Row from "../../row";
import { copyTextToClipboard } from "../../../helpers/clipboard";
import { toast } from "../../toast";
import { toast, toastNew } from "../../toast";
import { fastRaf } from "../../../helpers/schedulers";
import { safeAssign } from "../../../helpers/object";
import { forEachReverse } from "../../../helpers/array";
@ -39,7 +39,7 @@ import SwipeHandler from "../../swipeHandler"; @@ -39,7 +39,7 @@ import SwipeHandler from "../../swipeHandler";
import { MOUNT_CLASS_TO } from "../../../config/debug";
import AppAddMembersTab from "../../sidebarLeft/tabs/addMembers";
import PopupPickUser from "../../popups/pickUser";
import PopupPeer from "../../popups/peer";
import PopupPeer, { PopupPeerButtonCallbackCheckboxes, PopupPeerCheckboxOptions } from "../../popups/peer";
import Scrollable from "../../scrollable";
import { isTouchSupported } from "../../../helpers/touchSupport";
import { isFirefox } from "../../../helpers/userAgent";
@ -949,20 +949,52 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -949,20 +949,52 @@ export default class AppSharedMediaTab extends SliderSuperTab {
const id = -this.peerId;
const isChannel = appChatsManager.isChannel(id);
const showConfirmation = (peerIds: number[], callback: () => void) => {
let titleLangKey: LangPackKey = 'GroupAddMembers', descriptionLangKey: LangPackKey, descriptionLangArgs: any[];
const showConfirmation = (peerIds: number[], callback: (checked: PopupPeerButtonCallbackCheckboxes) => void) => {
let titleLangKey: LangPackKey, titleLangArgs: any[],
descriptionLangKey: LangPackKey, descriptionLangArgs: any[],
checkboxes: PopupPeerCheckboxOptions[];
if(peerIds.length > 1) {
descriptionLangKey = 'PeerInfo.Confirm.AddMembers1';
descriptionLangArgs = [peerIds.length];
titleLangKey = 'AddMembersAlertTitle';
titleLangArgs = [i18n('Members', [peerIds.length])];
descriptionLangKey = 'AddMembersAlertCountText';
descriptionLangArgs = peerIds.map(peerId => {
const b = document.createElement('b');
b.append(new PeerTitle({peerId}).element);
return b;
});
if(!isChannel) {
checkboxes = [{
text: 'AddMembersForwardMessages',
checked: true
}];
}
} else {
descriptionLangKey = 'PeerInfo.Confirm.AddMember';
descriptionLangArgs = [new PeerTitle({
peerId: peerIds[0],
onlyFirstName: true
}).element];
titleLangKey = 'AddOneMemberAlertTitle';
descriptionLangKey = 'AddMembersAlertNamesText';
const b = document.createElement('b');
b.append(new PeerTitle({
peerId: peerIds[0]
}).element);
descriptionLangArgs = [b];
if(!isChannel) {
checkboxes = [{
text: 'AddOneMemberForwardMessages',
textArgs: [new PeerTitle({
peerId: peerIds[0],
onlyFirstName: true
}).element],
checked: true
}];
}
}
descriptionLangArgs.push(new PeerTitle({
peerId: -id
}).element);
new PopupPeer('popup-add-members', {
peerId: -id,
titleLangKey,
@ -970,12 +1002,17 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -970,12 +1002,17 @@ export default class AppSharedMediaTab extends SliderSuperTab {
descriptionLangArgs,
buttons: [{
langKey: 'Add',
callback: () => {
callback();
}
}]
callback
}],
checkboxes
}).show();
};
const onError = (err: any) => {
if(err.type === 'USER_PRIVACY_RESTRICTED') {
toastNew({langPackKey: 'InviteToGroupError'});
}
};
if(isChannel) {
const tab = new AppAddMembersTab(this.slider);
@ -985,7 +1022,9 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -985,7 +1022,9 @@ export default class AppSharedMediaTab extends SliderSuperTab {
skippable: false,
takeOut: (peerIds) => {
showConfirmation(peerIds, () => {
tab.attachToPromise(appChatsManager.inviteToChannel(id, peerIds));
const promise = appChatsManager.inviteToChannel(id, peerIds);
promise.catch(onError);
tab.attachToPromise(promise);
});
return false;
@ -999,8 +1038,9 @@ export default class AppSharedMediaTab extends SliderSuperTab { @@ -999,8 +1038,9 @@ export default class AppSharedMediaTab extends SliderSuperTab {
placeholder: 'Search',
onSelect: (peerId) => {
setTimeout(() => {
showConfirmation([peerId], () => {
appChatsManager.addChatUser(id, peerId);
showConfirmation([peerId], (checked) => {
appChatsManager.addChatUser(id, peerId, checked.size ? undefined : 0)
.catch(onError);
});
}, 0);
},

2
src/components/sidebarRight/tabs/userPermissions.ts

@ -66,7 +66,7 @@ export default class AppUserPermissionsTab extends SliderSuperTabEventable { @@ -66,7 +66,7 @@ export default class AppUserPermissionsTab extends SliderSuperTabEventable {
appChatsManager.editBanned(this.chatId, this.participant, rights);
};
this.eventListener.addEventListener('destroy', destroyListener);
this.eventListener.addEventListener('destroy', destroyListener, {once: true});
this.scrollable.append(section.container);
}

7
src/components/toast.ts

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
*/
import replaceContent from "../helpers/dom/replaceContent";
import { i18n, LangPackKey } from "../lib/langPack";
const toastEl = document.createElement('div');
toastEl.classList.add('toast');
@ -18,3 +19,9 @@ export function toast(content: string | Node) { @@ -18,3 +19,9 @@ export function toast(content: string | Node) {
delete toastEl.dataset.timeout;
}, 3000);
}
export function toastNew(options: Partial<{
langPackKey: LangPackKey
}>) {
toast(i18n(options.langPackKey));
}

2
src/components/usernameInputField.ts

@ -30,7 +30,7 @@ export class UsernameInputField extends InputField { @@ -30,7 +30,7 @@ export class UsernameInputField extends InputField {
this.checkUsernameDebounced = debounce(this.checkUsername.bind(this), 150, false, true);
options.listenerSetter.add(this.input, 'input', () => {
options.listenerSetter.add(this.input)('input', () => {
const value = this.getValue();
//console.log('userNameInput:', value);

2
src/components/wrappers.ts

@ -1095,7 +1095,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -1095,7 +1095,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
appDocsManager.saveLottiePreview(doc, animation.canvas, toneIndex);
//deferred.resolve();
}, true);
}, {once: true});
if(emoji) {
attachClickEvent(div, (e) => {

26
src/global.d.ts vendored

@ -1,11 +1,21 @@ @@ -1,11 +1,21 @@
declare module 'worker-loader!*' {
class WebpackWorker extends Worker {
constructor();
import type ListenerSetter from "./helpers/listenerSetter";
declare global {
interface AddEventListenerOptions extends EventListenerOptions {
once?: boolean;
passive?: boolean;
// ls?: ListenerSetter;
}
export default WebpackWorker;
}
declare module 'worker-loader!*' {
class WebpackWorker extends Worker {
constructor();
}
export default WebpackWorker;
}
declare const electronHelpers: {
openExternal(url): void;
} | undefined;
declare const electronHelpers: {
openExternal(url): void;
} | undefined;
}

4
src/helpers/array.ts

@ -64,3 +64,7 @@ export function insertInDescendSortedArray<T extends {[smth in K]?: number}, K e @@ -64,3 +64,7 @@ export function insertInDescendSortedArray<T extends {[smth in K]?: number}, K e
console.error('wtf', array, element);
return array.indexOf(element);
}
export function filterUnique<T extends Array<any>>(arr: T): T {
return [...new Set(arr)] as T;
}

47
src/helpers/bytes.ts

@ -10,26 +10,24 @@ @@ -10,26 +10,24 @@
*/
export function bytesToHex(bytes: ArrayLike<number>) {
bytes = bytes || [];
let arr: string[] = [];
const arr: string[] = new Array(bytes.length);
for(let i = 0; i < bytes.length; ++i) {
arr.push((bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16));
arr[i] = (bytes[i] < 16 ? '0' : '') + (bytes[i] || 0).toString(16);
}
return arr.join('');
}
export function bytesFromHex(hexString: string) {
const len = hexString.length;
const bytes = new Uint8Array(Math.ceil(len / 2));
let start = 0;
let bytes: number[] = [];
if(len % 2) { // read 0x581 as 0x0581
bytes.push(parseInt(hexString.charAt(0), 16));
++start;
bytes[start++] = parseInt(hexString.charAt(0), 16);
}
for(let i = start; i < len; i += 2) {
bytes.push(parseInt(hexString.substr(i, 2), 16));
bytes[start++] = parseInt(hexString.substr(i, 2), 16);
}
return bytes;
@ -67,7 +65,7 @@ export function uint6ToBase64(nUint6: number) { @@ -67,7 +65,7 @@ export function uint6ToBase64(nUint6: number) {
? 43
: nUint6 === 63
? 47
: 65
: 65;
}
export function bytesCmp(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8Array) {
@ -85,9 +83,9 @@ export function bytesCmp(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8 @@ -85,9 +83,9 @@ export function bytesCmp(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8
return true;
}
export function bytesXor(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8Array) {
export function bytesXor(bytes1: Uint8Array, bytes2: Uint8Array) {
const len = bytes1.length;
const bytes: number[] = [];
const bytes = new Uint8Array(len);
for(let i = 0; i < len; ++i) {
bytes[i] = bytes1[i] ^ bytes2[i];
@ -96,7 +94,7 @@ export function bytesXor(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8 @@ -96,7 +94,7 @@ export function bytesXor(bytes1: number[] | Uint8Array, bytes2: number[] | Uint8
return bytes;
}
export function bytesToArrayBuffer(b: number[]) {
/* export function bytesToArrayBuffer(b: number[]) {
return (new Uint8Array(b)).buffer;
}
@ -110,11 +108,11 @@ export function convertToArrayBuffer(bytes: any | ArrayBuffer | Uint8Array) { @@ -110,11 +108,11 @@ export function convertToArrayBuffer(bytes: any | ArrayBuffer | Uint8Array) {
return bytes.buffer;
}
return bytesToArrayBuffer(bytes);
}
} */
export function convertToUint8Array(bytes: Uint8Array | ArrayBuffer | number[] | string): Uint8Array {
if((bytes as Uint8Array).buffer !== undefined) {
return bytes as Uint8Array;
if(bytes instanceof Uint8Array) {
return bytes;
} else if(typeof(bytes) === 'string') {
return new TextEncoder().encode(bytes);
}
@ -122,7 +120,7 @@ export function convertToUint8Array(bytes: Uint8Array | ArrayBuffer | number[] | @@ -122,7 +120,7 @@ export function convertToUint8Array(bytes: Uint8Array | ArrayBuffer | number[] |
return new Uint8Array(bytes);
}
export function bytesFromArrayBuffer(buffer: ArrayBuffer) {
/* export function bytesFromArrayBuffer(buffer: ArrayBuffer) {
const len = buffer.byteLength;
const byteView = new Uint8Array(buffer);
const bytes: number[] = [];
@ -142,36 +140,33 @@ export function bufferConcat(buffer1: any, buffer2: any) { @@ -142,36 +140,33 @@ export function bufferConcat(buffer1: any, buffer2: any) {
tmp.set(buffer2 instanceof ArrayBuffer ? new Uint8Array(buffer2) : buffer2, l1);
return tmp.buffer;
}
} */
export function bufferConcats(...args: any[]) {
let length = 0;
args.forEach(b => length += b.byteLength || b.length);
export function bufferConcats(...args: (ArrayBuffer | Uint8Array | number[])[]) {
const length = args.reduce((acc, v) => acc + ((v as ArrayBuffer).byteLength || (v as Uint8Array).length), 0);
const tmp = new Uint8Array(length);
let lastLength = 0;
args.forEach(b => {
tmp.set(b instanceof ArrayBuffer ? new Uint8Array(b) : b, lastLength);
lastLength += b.byteLength || b.length;
lastLength += (b as ArrayBuffer).byteLength || (b as Uint8Array).length;
});
return tmp/* .buffer */;
}
export function bytesFromWordss(input: Uint32Array) {
const o: number[] = [];
const o = new Uint8Array(input.byteLength);
for(let i = 0, length = input.length * 4; i < length; ++i) {
o.push((input[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
o[i] = ((input[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
}
return o;
}
export function bytesToWordss(input: ArrayBuffer | Uint8Array) {
let bytes: Uint8Array;
if(input instanceof ArrayBuffer) bytes = new Uint8Array(input);
else bytes = input;
export function bytesToWordss(input: Parameters<typeof convertToUint8Array>[0]) {
const bytes = convertToUint8Array(input);
const words: number[] = [];
for(let i = 0, len = bytes.length; i < len; ++i) {

4
src/helpers/dom/clickEvent.ts

@ -10,8 +10,8 @@ import { isTouchSupported } from "../touchSupport"; @@ -10,8 +10,8 @@ import { isTouchSupported } from "../touchSupport";
export const CLICK_EVENT_NAME: 'mousedown' | 'touchend' | 'click' = (isTouchSupported ? 'mousedown' : 'click') as any;
export type AttachClickOptions = AddEventListenerOptions & Partial<{listenerSetter: ListenerSetter, touchMouseDown: true}>;
export function attachClickEvent(elem: HTMLElement, callback: (e: TouchEvent | MouseEvent) => void, options: AttachClickOptions = {}) {
const add = options.listenerSetter ? options.listenerSetter.add.bind(options.listenerSetter, elem) : elem.addEventListener.bind(elem);
const remove = options.listenerSetter ? options.listenerSetter.removeManual.bind(options.listenerSetter, elem) : elem.removeEventListener.bind(elem);
const add = options.listenerSetter ? options.listenerSetter.add(elem) : elem.addEventListener.bind(elem);
// const remove = options.listenerSetter ? options.listenerSetter.removeManual.bind(options.listenerSetter, elem) : elem.removeEventListener.bind(elem);
options.touchMouseDown = true;
/* if(options.touchMouseDown && CLICK_EVENT_NAME === 'touchend') {

8
src/helpers/dom/handleScrollSideEvent.ts

@ -11,7 +11,7 @@ export default function handleScrollSideEvent(elem: HTMLElement, side: 'top' | ' @@ -11,7 +11,7 @@ export default function handleScrollSideEvent(elem: HTMLElement, side: 'top' | '
if(isTouchSupported) {
let lastY: number;
const options = {passive: true};
listenerSetter.add(elem, 'touchstart', (e) => {
listenerSetter.add(elem)('touchstart', (e) => {
if(e.touches.length > 1) {
onTouchEnd();
return;
@ -19,8 +19,8 @@ export default function handleScrollSideEvent(elem: HTMLElement, side: 'top' | ' @@ -19,8 +19,8 @@ export default function handleScrollSideEvent(elem: HTMLElement, side: 'top' | '
lastY = e.touches[0].clientY;
listenerSetter.add(elem, 'touchmove', onTouchMove, options);
listenerSetter.add(elem, 'touchend', onTouchEnd, options);
listenerSetter.add(elem)('touchmove', onTouchMove, options);
listenerSetter.add(elem)('touchend', onTouchEnd, options);
}, options);
const onTouchMove = (e: TouchEvent) => {
@ -38,7 +38,7 @@ export default function handleScrollSideEvent(elem: HTMLElement, side: 'top' | ' @@ -38,7 +38,7 @@ export default function handleScrollSideEvent(elem: HTMLElement, side: 'top' | '
listenerSetter.removeManual(elem, 'touchend', onTouchEnd, options);
};
} else {
listenerSetter.add(elem, 'wheel', (e) => {
listenerSetter.add(elem)('wheel', (e) => {
const isDown = e.deltaY > 0;
//this.log('wheel', e, isDown);
if(side === 'bottom' && isDown) callback();

4
src/helpers/dropdownHover.ts

@ -45,10 +45,10 @@ export default class DropdownHover extends EventListenerBase<{ @@ -45,10 +45,10 @@ export default class DropdownHover extends EventListenerBase<{
}
}, {listenerSetter});
} else {
listenerSetter.add(button, 'mouseover', (e) => {
listenerSetter.add(button)('mouseover', (e) => {
//console.log('onmouseover button');
if(firstTime) {
listenerSetter.add(button, 'mouseout', this.onMouseOut);
listenerSetter.add(button)('mouseout', this.onMouseOut);
firstTime = false;
}

23
src/helpers/eventListenerBase.ts

@ -53,9 +53,12 @@ import type { ArgumentTypes, SuperReturnType } from "../types"; @@ -53,9 +53,12 @@ import type { ArgumentTypes, SuperReturnType } from "../types";
* Better not to remove listeners during setting
* Should add listener callback only once
*/
export default class EventListenerBase<Listeners extends {[name: string]: Function}> {
// type EventLitenerCallback<T> = (data: T) =>
// export default class EventListenerBase<Listeners extends {[name: string]: Function}> {
export default class EventListenerBase<Listeners extends Record<string, Function>> {
protected listeners: Partial<{
[k in keyof Listeners]: Array<{callback: Listeners[k], once?: boolean}>
[k in keyof Listeners]: Array<{callback: Listeners[k], options: boolean | AddEventListenerOptions}>
}>;
protected listenerResults: Partial<{
[k in keyof Listeners]: ArgumentTypes<Listeners[k]>
@ -73,16 +76,18 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi @@ -73,16 +76,18 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi
this.listenerResults = {};
}
public addEventListener<T extends keyof Listeners>(name: T, callback: Listeners[T], once?: boolean) {
public addEventListener<T extends keyof Listeners>(name: T, callback: Listeners[T], options?: boolean | AddEventListenerOptions) {
(this.listeners[name] ?? (this.listeners[name] = [])).push({callback, options}); // ! add before because if you don't, you won't be able to delete it from callback
if(this.listenerResults.hasOwnProperty(name)) {
callback(...this.listenerResults[name]);
if(once) {
if((options as AddEventListenerOptions)?.once) {
this.listeners[name].pop();
return;
}
}
(this.listeners[name] ?? (this.listeners[name] = [])).push({callback, once});
//e.add(this, name, {callback, once});
}
@ -94,7 +99,7 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi @@ -94,7 +99,7 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi
}
}
public removeEventListener<T extends keyof Listeners>(name: T, callback: Listeners[T]) {
public removeEventListener<T extends keyof Listeners>(name: T, callback: Listeners[T], options?: boolean | AddEventListenerOptions) {
if(this.listeners[name]) {
this.listeners[name].findAndSplice(l => l.callback === callback);
}
@ -116,15 +121,15 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi @@ -116,15 +121,15 @@ export default class EventListenerBase<Listeners extends {[name: string]: Functi
if(listeners) {
// ! this one will guarantee execution even if delete another listener during setting
const left = listeners.slice();
left.forEach((listener: any) => {
const index = listeners.findIndex((l: any) => l.callback === listener.callback);
left.forEach((listener) => {
const index = listeners.findIndex((l) => l.callback === listener.callback);
if(index === -1) {
return;
}
arr.push(listener.callback(...args));
if(listener.once) {
if((listener.options as AddEventListenerOptions)?.once) {
this.removeEventListener(name, listener.callback);
}
});

71
src/helpers/listenerSetter.ts

@ -4,23 +4,11 @@ @@ -4,23 +4,11 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { RootScope } from "../lib/rootScope";
import { ArgumentTypes } from "../types";
import type EventListenerBase from "./eventListenerBase";
/* export type Listener<T extends ListenerElement> = {
export type Listener = {
element: ListenerElement,
event: ListenerEvent<T>,
callback: ListenerCallback<T>,
options?: ListenerOptions
};
export type ListenerElement = HTMLElement | RootScope;
export type ListenerEvent<T extends ListenerElement> = ArgumentTypes<T['addEventListener']>[0];
export type ListenerCallback<T extends ListenerElement> = ArgumentTypes<T['addEventListener']>[1];
export type ListenerOptions = any; */
export type Listener<T extends ListenerElement> = {
element: ListenerElement,
event: ListenerEvent<T>,
event: ListenerEvent,
callback: ListenerCallback,
options?: ListenerOptions,
@ -28,22 +16,39 @@ export type Listener<T extends ListenerElement> = { @@ -28,22 +16,39 @@ export type Listener<T extends ListenerElement> = {
onceCallback?: () => void,
};
export type ListenerElement = Window | Document | HTMLElement | Element | RootScope | any;
//export type ListenerEvent<T extends ListenerElement> = ArgumentTypes<T['addEventListener']>[0];
export type ListenerEvent<T extends ListenerElement> = string;
export type ListenerCallback = (...args: any[]) => any;
export type ListenerOptions = any;
export type ListenerElement = Window | Document | HTMLElement | Element | EventListenerBase<any>;
export type ListenerEvent = string;
export type ListenerCallback = Function;
export type ListenerOptions = AddEventListenerOptions;
/* const originalAddEventListener = HTMLElement.prototype.addEventListener;
HTMLElement.prototype.addEventListener = function(this, name: string, callback: EventListenerOrEventListenerObject, options: AddEventListenerOptions) {
console.log('nu zdarova', name);
originalAddEventListener.call(this, name, callback, options);
if(options?.ls) {
return options.ls.addFromElement(this, name, callback as any, options);
}
}; */
export default class ListenerSetter {
private listeners: Set<Listener<any>> = new Set();
private listeners: Set<Listener> = new Set();
public add<T extends ListenerElement>(element: T): T['addEventListener'] {
return ((event: string, callback: Function, options: ListenerOptions) => {
const listener: Listener = {element, event, callback, options};
this.addManual(listener);
return listener;
}) as any;
}
public add<T extends ListenerElement>(element: T, event: ListenerEvent<T>, callback: ListenerCallback, options?: ListenerOptions) {
const listener: Listener<T> = {element, event, callback, options};
/* public addFromElement<T extends ListenerElement>(element: T, event: ListenerEvent, callback: ListenerCallback, options: ListenerOptions) {
const listener: Listener = {element, event, callback, options};
this.addManual(listener);
return listener;
}
} */
public addManual<T extends ListenerElement>(listener: Listener<T>) {
public addManual(listener: Listener) {
// @ts-ignore
listener.element.addEventListener(listener.event, listener.callback, listener.options);
@ -60,7 +65,7 @@ export default class ListenerSetter { @@ -60,7 +65,7 @@ export default class ListenerSetter {
this.listeners.add(listener);
}
public remove<T extends ListenerElement>(listener: Listener<T>) {
public remove(listener: Listener) {
if(!listener.onceFired) {
// @ts-ignore
listener.element.removeEventListener(listener.event, listener.callback, listener.options);
@ -74,10 +79,18 @@ export default class ListenerSetter { @@ -74,10 +79,18 @@ export default class ListenerSetter {
this.listeners.delete(listener);
}
public removeManual<T extends ListenerElement>(element: T, event: ListenerEvent<T>, callback: ListenerCallback, options?: ListenerOptions) {
let listener: Listener<T>;
public removeManual<T extends ListenerElement>(
element: T,
event: ListenerEvent,
callback: ListenerCallback,
options?: ListenerOptions
) {
let listener: Listener;
for(const _listener of this.listeners) {
if(_listener.element === element && _listener.event === event && _listener.callback === callback && _listener.options === options) {
if(_listener.element === element &&
_listener.event === event &&
_listener.callback === callback &&
_listener.options === options) {
listener = _listener;
break;
}

2
src/hooks/useHeavyAnimationCheck.ts

@ -88,7 +88,7 @@ export default function( @@ -88,7 +88,7 @@ export default function(
handleAnimationStart();
}
const add = listenerSetter ? listenerSetter.add.bind(listenerSetter, rootScope) : rootScope.addEventListener.bind(rootScope);
const add = listenerSetter ? listenerSetter.add(rootScope) : rootScope.addEventListener.bind(rootScope);
const remove = listenerSetter ? listenerSetter.removeManual.bind(listenerSetter, rootScope) : rootScope.removeEventListener.bind(rootScope);
add(ANIMATION_START_EVENT, handleAnimationStart);
add(ANIMATION_END_EVENT, handleAnimationEnd);

13
src/lang.ts

@ -510,6 +510,14 @@ const lang = { @@ -510,6 +510,14 @@ const lang = {
"NoMessages": "No messages here yet...",
"NoScheduledMessages": "No scheduled messages here yet...",
"NoMessagesGreetingsDescription": "Send a message or tap the greeting below.",
"InviteToGroupError": "Sorry, you can\'t add this user to groups because of user\'s privacy settings.",
"InviteToChannelError": "Sorry, you can\'t add this user to channels because of user\'s privacy settings.",
"AddMembersAlertTitle": "Add %1$s",
"AddOneMemberAlertTitle": "Add member",
"AddMembersAlertNamesText": "Are you sure you want to add %1$s to **%2$s**?",
"AddMembersAlertCountText": "Are you sure you want to add %1$s to **%2$s**?",
"AddMembersForwardMessages": "Show the last 100 messages to the new members",
"AddOneMemberForwardMessages": "Show the last 100 messages to **%1$s**",
// * macos
"AccountSettings.Filters": "Chat Folders",
@ -689,11 +697,6 @@ const lang = { @@ -689,11 +697,6 @@ const lang = {
"PeerInfo.SharedMedia": "Shared Media",
"PeerInfo.Subscribers": "Subscribers",
"PeerInfo.DeleteContact": "Delete Contact",
"PeerInfo.Confirm.AddMembers1": {
"one_value": "Add %d user to the group?",
"other_value": "Add %d users to the group?"
},
"PeerInfo.Confirm.AddMember": "Add \"%@\" to the group?",
//"PeerInfo.Confirm.RemovePeer": "Remove %@ from the group?",
"PeerMedia.Members": "Members",
"PollResults.Title.Poll": "Poll Results",

3440
src/layer.d.ts vendored

File diff suppressed because it is too large Load Diff

64
src/lib/appManagers/appDialogsManager.ts

@ -103,6 +103,8 @@ export class AppDialogsManager { @@ -103,6 +103,8 @@ export class AppDialogsManager {
private lastActiveElements: Set<HTMLElement> = new Set();
private offsets: {top: number, bottom: number} = {top: 0, bottom: 0};
constructor() {
this.chatsPreloader = putPreloader(null, true);
@ -435,7 +437,7 @@ export class AppDialogsManager { @@ -435,7 +437,7 @@ export class AppDialogsManager {
return this.loadDialogs();
}
private getOffset(side: 'top' | 'bottom'): {index: number, pos: number} {
/* private getOffset(side: 'top' | 'bottom'): {index: number, pos: number} {
if(!this.scroll.loadedAll[side]) {
const element = (side === 'top' ? this.chatList.firstElementChild : this.chatList.lastElementChild) as HTMLElement;
if(element) {
@ -446,13 +448,16 @@ export class AppDialogsManager { @@ -446,13 +448,16 @@ export class AppDialogsManager {
}
return {index: 0, pos: -1};
} */
private getOffsetIndex(side: 'top' | 'bottom') {
return {index: this.scroll.loadedAll[side] ? 0 : this.offsets[side]};
}
private isDialogMustBeInViewport(dialog: Dialog) {
if(dialog.migratedTo !== undefined) return false;
//return true;
const topOffset = this.getOffset('top');
const bottomOffset = this.getOffset('bottom');
const topOffset = this.getOffsetIndex('top');
const bottomOffset = this.getOffsetIndex('bottom');
if(!topOffset.index && !bottomOffset.index) {
return true;
@ -523,6 +528,7 @@ export class AppDialogsManager { @@ -523,6 +528,7 @@ export class AppDialogsManager {
this.scroll = this.scrollables[this.filterId];
this.scroll.loadedAll.top = true;
this.scroll.loadedAll.bottom = false;
this.offsets.top = this.offsets.bottom = 0;
this.loadDialogsPromise = undefined;
this.chatList = this.chatLists[this.filterId];
this.loadDialogs();
@ -665,22 +671,16 @@ export class AppDialogsManager { @@ -665,22 +671,16 @@ export class AppDialogsManager {
let loadCount = 30/*this.chatsLoadCount */;
let offsetIndex = 0;
if(side === 'top') {
const element = this.chatList.firstElementChild as HTMLElement;
if(element) {
const peerId = +element.dataset.peerId;
const {index: currentOffsetIndex} = this.getOffsetIndex(side);
if(currentOffsetIndex) {
if(side === 'top') {
const storage = appMessagesManager.dialogsStorage.getFolder(filterId);
const index = storage.findIndex(dialog => dialog.peerId === peerId);
const index = storage.findIndex(dialog => dialog.index <= currentOffsetIndex);
const needIndex = Math.max(0, index - loadCount);
loadCount = index - needIndex;
offsetIndex = storage[needIndex].index + 1;
}
} else {
const element = this.chatList.lastElementChild as HTMLElement;
if(element) {
const peerId = +element.dataset.peerId;
const dialog = appMessagesManager.getDialogOnly(peerId);
offsetIndex = dialog.index;
} else {
offsetIndex = currentOffsetIndex;
}
}
@ -727,6 +727,8 @@ export class AppDialogsManager { @@ -727,6 +727,8 @@ export class AppDialogsManager {
});
}
this.offsets[side] = result.dialogs[side === 'top' ? 0 : result.dialogs.length - 1].index;
this.onListLengthChange();
this.log.debug('getDialogs ' + loadCount + ' dialogs by offset:', offsetIndex, result, this.chatList.childElementCount);
@ -797,6 +799,11 @@ export class AppDialogsManager { @@ -797,6 +799,11 @@ export class AppDialogsManager {
if(this.sliceTimeout) clearTimeout(this.sliceTimeout);
this.sliceTimeout = window.setTimeout(() => {
this.sliceTimeout = undefined;
if(this.reorderDialogsTimeout) {
this.onChatsRegularScroll();
return;
}
if(!this.chatList.childElementCount) {
return;
@ -861,11 +868,11 @@ export class AppDialogsManager { @@ -861,11 +868,11 @@ export class AppDialogsManager {
} */
if(sliceFromStart.length) {
this.scroll.loadedAll['top'] = false;
this.scroll.loadedAll.top = false;
}
if(sliceFromEnd.length) {
this.scroll.loadedAll['bottom'] = false;
this.scroll.loadedAll.bottom = false;
}
sliced.push(...sliceFromStart);
@ -876,6 +883,8 @@ export class AppDialogsManager { @@ -876,6 +883,8 @@ export class AppDialogsManager {
this.deleteDialog(peerId);
});
this.setOffsets();
//this.log('[slicer] elements', firstElement, lastElement, rect, sliced, sliceFromStart.length, sliceFromEnd.length);
//this.log('[slicer] reset scrollTop', this.scroll.scrollTop, firstElement.offsetTop, firstElementRect.y, rect.y, elementOverflow);
@ -890,6 +899,18 @@ export class AppDialogsManager { @@ -890,6 +899,18 @@ export class AppDialogsManager {
}, 200);
};
private setOffsets() {
const firstDialog = this.getDialogFromElement(this.chatList.firstElementChild as HTMLElement);
const lastDialog = this.getDialogFromElement(this.chatList.lastElementChild as HTMLElement);
this.offsets.top = firstDialog.index;
this.offsets.bottom = lastDialog.index;
}
private getDialogFromElement(element: HTMLElement) {
return appMessagesManager.getDialogOnly(+element.dataset.peerId);
}
public onChatsScrollTop = () => {
this.onChatsScroll('top');
};
@ -982,11 +1003,14 @@ export class AppDialogsManager { @@ -982,11 +1003,14 @@ export class AppDialogsManager {
this.reorderDialogsTimeout = window.requestAnimationFrame(() => {
this.reorderDialogsTimeout = 0;
const offset = Math.max(0, this.getOffset('top').pos);
const dialogs = appMessagesManager.dialogsStorage.getFolder(this.filterId);
const currentOrder = (Array.from(this.chatList.children) as HTMLElement[]).map(el => +el.dataset.peerId);
const {index} = this.getOffsetIndex('top');
const pos = dialogs.findIndex(dialog => dialog.index <= index);
const offset = Math.max(0, pos);
dialogs.forEach((dialog, index) => {
const dom = this.getDialogDom(dialog.peerId);
if(!dom) {

51
src/lib/appManagers/appMessagesManager.ts

@ -113,6 +113,11 @@ export type MessagesStorage = { @@ -113,6 +113,11 @@ export type MessagesStorage = {
export type MyMessageActionType = Message.messageService['action']['_'];
type PendingAfterMsg = Partial<InvokeApiOptions & {
afterMessageId: string,
messageId: string
}>;
export class AppMessagesManager {
private static MESSAGE_ID_INCREMENT = 0x10000;
private static MESSAGE_ID_OFFSET = 0xFFFFFFFF;
@ -152,7 +157,7 @@ export class AppMessagesManager { @@ -152,7 +157,7 @@ export class AppMessagesManager {
}
} = {};
private pendingByMessageId: {[mid: string]: string} = {};
private pendingAfterMsgs: any = {};
private pendingAfterMsgs: {[peerId: string]: PendingAfterMsg} = {};
public pendingTopMsgs: {[peerId: string]: number} = {};
private tempNum = 0;
private tempFinalizeCallbacks: {
@ -487,7 +492,7 @@ export class AppMessagesManager { @@ -487,7 +492,7 @@ export class AppMessagesManager {
message.send = () => {
toggleError(false);
const sentRequestOptions: InvokeApiOptions = {};
const sentRequestOptions: PendingAfterMsg = {};
if(this.pendingAfterMsgs[peerId]) {
sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId;
}
@ -1259,7 +1264,7 @@ export class AppMessagesManager { @@ -1259,7 +1264,7 @@ export class AppMessagesManager {
};
message.send = () => {
const sentRequestOptions: any = {};
const sentRequestOptions: PendingAfterMsg = {};
if(this.pendingAfterMsgs[peerId]) {
sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId;
}
@ -1768,7 +1773,7 @@ export class AppMessagesManager { @@ -1768,7 +1773,7 @@ export class AppMessagesManager {
const count = (dialogsResult as MessagesDialogs.messagesDialogsSlice).count;
// exclude empty draft dialogs
const dialogs = this.dialogsStorage.getFolder(folderId);
const dialogs = this.dialogsStorage.getFolder(folderId, false);
let dialogsLength = 0;
for(let i = 0, length = dialogs.length; i < length; ++i) {
if(this.getServerMessageId(dialogs[i].top_message)) {
@ -1854,7 +1859,7 @@ export class AppMessagesManager { @@ -1854,7 +1859,7 @@ export class AppMessagesManager {
});
});
const sentRequestOptions: InvokeApiOptions = {};
const sentRequestOptions: PendingAfterMsg = {};
if(this.pendingAfterMsgs[peerId]) {
sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId;
}
@ -4582,6 +4587,8 @@ export class AppMessagesManager { @@ -4582,6 +4587,8 @@ export class AppMessagesManager {
const tempMessage = this.getMessageFromStorage(storage, tempId);
delete storage[tempId];
this.handleReleasingMessage(tempMessage);
rootScope.dispatchEvent('message_sent', {storage, tempId, tempMessage, mid});
}
@ -5078,6 +5085,24 @@ export class AppMessagesManager { @@ -5078,6 +5085,24 @@ export class AppMessagesManager {
});
}
private handleReleasingMessage(message: any) {
if((message as Message.message).media) {
// @ts-ignore
const c = message.media.webpage || message.media;
const smth = c.photo || c.document;
if(smth?.file_reference) {
referenceDatabase.deleteContext(smth.file_reference, {type: 'message', peerId: message.peerId, messageId: message.mid});
}
// @ts-ignore
if(message.media.webpage) {
// @ts-ignore
appWebPagesManager.deleteWebPageFromPending(message.media.webpage, mid);
}
}
}
private handleDeletedMessages(peerId: number, storage: MessagesStorage, messages: number[]) {
const history: {
count: number,
@ -5090,21 +5115,7 @@ export class AppMessagesManager { @@ -5090,21 +5115,7 @@ export class AppMessagesManager {
const message: MyMessage = this.getMessageFromStorage(storage, mid);
if(message.deleted) continue;
if((message as Message.message).media) {
// @ts-ignore
const c = message.media.webpage || message.media;
const smth = c.photo || c.document;
if(smth?.file_reference) {
referenceDatabase.deleteContext(smth.file_reference, {type: 'message', peerId, messageId: mid});
}
// @ts-ignore
if(message.media.webpage) {
// @ts-ignore
appWebPagesManager.deleteWebPageFromPending(message.media.webpage, mid);
}
}
this.handleReleasingMessage(message);
this.updateMessageRepliesIfNeeded(message);

2
src/lib/appManagers/appNotificationsManager.ts

@ -153,7 +153,7 @@ export class AppNotificationsManager { @@ -153,7 +153,7 @@ export class AppNotificationsManager {
rootScope.addEventListener('dialogs_multiupdate', () => {
//unregisterTopMsgs()
this.topMessagesDeferred.resolve();
}, true);
}, {once: true});
rootScope.addEventListener('push_notification_click', (notificationData) => {
if(notificationData.action === 'push_settings') {

4
src/lib/appManagers/appProfileManager.ts

@ -397,6 +397,10 @@ export class AppProfileManager { @@ -397,6 +397,10 @@ export class AppProfileManager {
public getMentions(chatId: number, query: string, threadId?: number): Promise<number[]> {
const processUserIds = (userIds: number[]) => {
/* const startsWithAt = query.charAt(0) === '@';
if(startsWithAt) query = query.slice(1);
const index = new SearchIndex<number>(!startsWithAt, !startsWithAt); */
const index = new SearchIndex<number>(true, true);
userIds.forEach(userId => {
index.indexObject(userId, appUsersManager.getUserSearchText(userId));

2
src/lib/appManagers/appStickersManager.ts

@ -54,7 +54,7 @@ export class AppStickersManager { @@ -54,7 +54,7 @@ export class AppStickersManager {
}
if(!this.getGreetingStickersPromise) {
this.getGreetingStickersPromise = this.getStickersByEmoticon('👋', false).then(docs => {
this.getGreetingStickersPromise = this.getStickersByEmoticon('👋', false).then(docs => {
this.greetingStickers = docs.slice() as Document.document[];
this.greetingStickers.sort((a, b) => Math.random() - Math.random());
});

15
src/lib/appManagers/appUsersManager.ts

@ -11,11 +11,12 @@ @@ -11,11 +11,12 @@
import { formatPhoneNumber } from "../../components/misc";
import { MOUNT_CLASS_TO } from "../../config/debug";
import { filterUnique } from "../../helpers/array";
import cleanSearchText from "../../helpers/cleanSearchText";
import cleanUsername from "../../helpers/cleanUsername";
import { tsNow } from "../../helpers/date";
import { safeReplaceObject, isObject } from "../../helpers/object";
import { InputUser, Update, User as MTUser, UserProfilePhoto, UserStatus } from "../../layer";
import { InputUser, User as MTUser, UserProfilePhoto, UserStatus } from "../../layer";
import I18n, { i18n, LangPackKey } from "../langPack";
//import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
@ -299,7 +300,7 @@ export class AppUsersManager { @@ -299,7 +300,7 @@ export class AppUsersManager {
}
public toggleBlock(peerId: number, block: boolean) {
return apiManager.invokeApi(block ? 'contacts.block' : 'contacts.unblock', {
return apiManager.invokeApiSingle(block ? 'contacts.block' : 'contacts.unblock', {
id: appPeersManager.getInputPeerById(peerId)
}).then(value => {
if(value) {
@ -553,7 +554,7 @@ export class AppUsersManager { @@ -553,7 +554,7 @@ export class AppUsersManager {
}
public isContact(id: number) {
return this.contactsList.has(id);
return this.contactsList.has(id) || (this.users[id] && this.users[id].pFlags.contact);
}
public isRegularUser(id: number) {
@ -566,7 +567,7 @@ export class AppUsersManager { @@ -566,7 +567,7 @@ export class AppUsersManager {
}
public hasUser(id: number, allowMin?: boolean) {
var user = this.users[id];
const user = this.users[id];
return isObject(user) && (allowMin || !user.pFlags.min);
}
@ -760,7 +761,7 @@ export class AppUsersManager { @@ -760,7 +761,7 @@ export class AppUsersManager {
}
public getBlocked(offset = 0, limit = 0) {
return apiManager.invokeApi('contacts.getBlocked', {offset, limit}).then(contactsBlocked => {
return apiManager.invokeApiSingle('contacts.getBlocked', {offset, limit}).then(contactsBlocked => {
this.saveApiUsers(contactsBlocked.users);
appChatsManager.saveApiChats(contactsBlocked.chats);
const count = contactsBlocked._ === 'contacts.blocked' ? contactsBlocked.users.length + contactsBlocked.chats.length : contactsBlocked.count;
@ -796,7 +797,7 @@ export class AppUsersManager { @@ -796,7 +797,7 @@ export class AppUsersManager {
});
} */
public searchContacts(query: string, limit = 20) {
return apiManager.invokeApi('contacts.search', {
return apiManager.invokeApiSingle('contacts.search', {
q: query,
limit
}).then(peers => {
@ -804,7 +805,7 @@ export class AppUsersManager { @@ -804,7 +805,7 @@ export class AppUsersManager {
appChatsManager.saveApiChats(peers.chats);
const out = {
my_results: [...new Set(peers.my_results.map(p => appPeersManager.getPeerId(p)))], // ! contacts.search returns duplicates in my_results
my_results: filterUnique(peers.my_results.map(p => appPeersManager.getPeerId(p))), // ! contacts.search returns duplicates in my_results
results: peers.results.map(p => appPeersManager.getPeerId(p))
};

72
src/lib/crypto/crypto_methods.ts

@ -1,54 +1,30 @@ @@ -1,54 +1,30 @@
import { convertToArrayBuffer } from "../../helpers/bytes";
import type { InputCheckPasswordSRP } from "../../layer";
import { aesEncryptSync, aesDecryptSync, sha256HashSync, sha1HashSync, bytesModPow } from "./crypto_utils";
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { Awaited } from "../../types";
import type { aesEncryptSync, aesDecryptSync, sha256HashSync, sha1HashSync, bytesModPow, hash_pbkdf2, rsaEncrypt, pqPrimeFactorization, gzipUncompress } from "./crypto_utils";
import type { computeSRP } from "./srp";
export type CryptoMethods = {
'sha1-hash': typeof sha1HashSync,
'sha256-hash': typeof sha256HashSync,
'pbkdf2': typeof hash_pbkdf2,
'aes-encrypt': typeof aesEncryptSync,
'aes-decrypt': typeof aesDecryptSync,
'rsa-encrypt': typeof rsaEncrypt,
'factorize': typeof pqPrimeFactorization,
'mod-pow': typeof bytesModPow,
'gzipUncompress': typeof gzipUncompress,
'computeSRP': typeof computeSRP
};
export default abstract class CryptoWorkerMethods {
abstract performTaskWorker<T>(task: string, ...args: any[]): Promise<T>;
public sha1Hash(bytes: Parameters<typeof sha1HashSync>[0]): Promise<Uint8Array> {
return this.performTaskWorker<Uint8Array>('sha1-hash', bytes);
}
public sha256Hash(bytes: Parameters<typeof sha256HashSync>[0]) {
return this.performTaskWorker<number[]>('sha256-hash', bytes);
}
public pbkdf2(buffer: Uint8Array, salt: Uint8Array, iterations: number) {
return this.performTaskWorker<ArrayBuffer>('pbkdf2', buffer, salt, iterations);
}
public aesEncrypt(bytes: Parameters<typeof aesEncryptSync>[0],
keyBytes: Parameters<typeof aesEncryptSync>[1],
ivBytes: Parameters<typeof aesEncryptSync>[2]) {
return this.performTaskWorker<ReturnType<typeof aesEncryptSync>>('aes-encrypt', convertToArrayBuffer(bytes),
convertToArrayBuffer(keyBytes), convertToArrayBuffer(ivBytes));
}
public aesDecrypt(encryptedBytes: Parameters<typeof aesDecryptSync>[0],
keyBytes: Parameters<typeof aesDecryptSync>[1],
ivBytes: Parameters<typeof aesDecryptSync>[2]): Promise<ArrayBuffer> {
return this.performTaskWorker<ArrayBuffer>('aes-decrypt',
encryptedBytes, keyBytes, ivBytes)
.then(bytes => convertToArrayBuffer(bytes));
}
public rsaEncrypt(publicKey: {modulus: string, exponent: string}, bytes: any): Promise<number[]> {
return this.performTaskWorker<number[]>('rsa-encrypt', publicKey, bytes);
}
public factorize(bytes: Uint8Array) {
return this.performTaskWorker<[number[], number[], number]>('factorize', [...bytes]);
}
public modPow(x: Parameters<typeof bytesModPow>[0], y: Parameters<typeof bytesModPow>[1], m: Parameters<typeof bytesModPow>[2]) {
return this.performTaskWorker<ReturnType<typeof bytesModPow>>('mod-pow', x, y, m);
}
public gzipUncompress<T>(bytes: ArrayBuffer, toString?: boolean) {
return this.performTaskWorker<T>('gzipUncompress', bytes, toString);
}
public computeSRP(password: string, state: any, isNew = false): Promise<InputCheckPasswordSRP> {
return this.performTaskWorker('computeSRP', password, state, isNew);
public invokeCrypto<Method extends keyof CryptoMethods>(method: Method, ...args: Parameters<CryptoMethods[typeof method]>): Promise<Awaited<ReturnType<CryptoMethods[typeof method]>>> {
return this.performTaskWorker<Awaited<ReturnType<CryptoMethods[typeof method]>>>(method, ...args as any[]);
}
}

104
src/lib/crypto/crypto_utils.ts

@ -23,10 +23,11 @@ import {str2bigInt, bpe, equalsInt, greater, @@ -23,10 +23,11 @@ import {str2bigInt, bpe, equalsInt, greater,
import { addPadding } from '../mtproto/bin_utils';
import { bytesToWordss, bytesFromWordss, bytesToHex, bytesFromHex, convertToUint8Array } from '../../helpers/bytes';
import { nextRandomInt } from '../../helpers/random';
import type { RSAPublicKeyHex } from '../mtproto/rsaKeysManager';
const subtle = typeof(window) !== 'undefined' && 'crypto' in window ? window.crypto.subtle : self.crypto.subtle;
export function longToBytes(sLong: string): Array<number> {
export function longToBytes(sLong: string) {
/* let perf = performance.now();
for(let i = 0; i < 1000000; ++i) {
bytesFromWords({words: longToInts(sLong), sigBytes: 8}).reverse();
@ -41,13 +42,14 @@ export function longToBytes(sLong: string): Array<number> { @@ -41,13 +42,14 @@ export function longToBytes(sLong: string): Array<number> {
}
console.log('longToBytes LEEMON', sLong, performance.now() - perf); */
const bytes = addPadding(bigInt2bytes(str2bigInt(sLong, 10), false), 8, true, false, false);
const bigIntBytes = new Uint8Array(bigInt2bytes(str2bigInt(sLong, 10), false));
const bytes = addPadding(bigIntBytes, 8, true, false, false);
//console.log('longToBytes', bytes, b);
return bytes;
}
export function sha1HashSync(bytes: Uint8Array | ArrayBuffer | string) {
export function sha1HashSync(bytes: Parameters<typeof convertToUint8Array>[0]) {
return subtle.digest('SHA-1', convertToUint8Array(bytes)).then(b => {
return new Uint8Array(b);
});
@ -66,7 +68,7 @@ export function sha1HashSync(bytes: Uint8Array | ArrayBuffer | string) { @@ -66,7 +68,7 @@ export function sha1HashSync(bytes: Uint8Array | ArrayBuffer | string) {
return new Uint8Array(hashBytes); */
}
export function sha256HashSync(bytes: Uint8Array | ArrayBuffer | string) {
export function sha256HashSync(bytes: Parameters<typeof convertToUint8Array>[0]) {
return subtle.digest('SHA-256', convertToUint8Array(bytes)).then(b => {
//console.log('legacy', performance.now() - perfS);
return new Uint8Array(b);
@ -86,7 +88,7 @@ export function sha256HashSync(bytes: Uint8Array | ArrayBuffer | string) { @@ -86,7 +88,7 @@ export function sha256HashSync(bytes: Uint8Array | ArrayBuffer | string) {
return bytesFromWordss(hash); */
}
export function aesEncryptSync(bytes: ArrayBuffer, keyBytes: ArrayBuffer, ivBytes: ArrayBuffer) {
export function aesEncryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: Uint8Array) {
//console.log(dT(), 'AES encrypt start', bytes, keyBytes, ivBytes);
// console.log('aes before padding bytes:', bytesToHex(bytes));
bytes = addPadding(bytes);
@ -110,66 +112,60 @@ export function aesDecryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes: @@ -110,66 +112,60 @@ export function aesDecryptSync(bytes: Uint8Array, keyBytes: Uint8Array, ivBytes:
return bytesFromWordss(decryptedBytes);
}
export function rsaEncrypt(publicKey: {modulus: string, exponent: string}, bytes: any): number[] {
export function rsaEncrypt(bytes: Uint8Array, publicKey: RSAPublicKeyHex) {
//console.log(dT(), 'RSA encrypt start', publicKey, bytes);
bytes = addPadding(bytes, 255);
const N = str2bigInt(publicKey.modulus, 16);
const E = str2bigInt(publicKey.exponent, 16);
const X = str2bigInt(bytesToHex(bytes), 16);
var N = str2bigInt(publicKey.modulus, 16);
var E = str2bigInt(publicKey.exponent, 16);
var X = str2bigInt(bytesToHex(bytes), 16);
var encryptedBigInt = powMod(X, E, N);
var encryptedBytes = bytesFromHex(bigInt2str(encryptedBigInt, 16));
const encryptedBigInt = powMod(X, E, N);
const encryptedBytes = bytesFromHex(bigInt2str(encryptedBigInt, 16));
//console.log(dT(), 'RSA encrypt finish');
return encryptedBytes;
}
export async function hash_pbkdf2(/* hasher: 'string', */buffer: any, salt: any, iterations: number) {
// @ts-ignore
let importKey = await subtle.importKey(
"raw", //only "raw" is allowed
buffer, //your password
{
name: "PBKDF2",
},
false, //whether the key is extractable (i.e. can be used in exportKey)
["deriveKey", "deriveBits"] //can be any combination of "deriveKey" and "deriveBits"
export async function hash_pbkdf2(buffer: Parameters<SubtleCrypto['importKey']>[1], salt: HkdfParams['salt'], iterations: number) {
const importKey = await subtle.importKey(
'raw',
buffer,
{name: 'PBKDF2'},
false,
[/* 'deriveKey', */'deriveBits']
);
/* let deriveKey = */await subtle.deriveKey(
/* await subtle.deriveKey(
{
"name": "PBKDF2",
salt: salt,
iterations: iterations,
hash: {name: "SHA-512"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
name: 'PBKDF2',
salt,
iterations,
hash: {name: 'SHA-512'}
},
importKey, //your key from generateKey or importKey
{ //the key type you want to create based on the derived bits
name: "AES-CTR", //can be any AES algorithm ("AES-CTR", "AES-CBC", "AES-CMAC", "AES-GCM", "AES-CFB", "AES-KW", "ECDH", "DH", or "HMAC")
//the generateKey parameters for that type of algorithm
length: 256, //can be 128, 192, or 256
importKey,
{
name: 'AES-CTR',
length: 256
},
false, //whether the derived key is extractable (i.e. can be used in exportKey)
["encrypt", "decrypt"] //limited to the options in that algorithm's importKey
);
false,
['encrypt', 'decrypt']
); */
let bits = subtle.deriveBits({
"name": "PBKDF2",
salt: salt,
iterations: iterations,
hash: {name: "SHA-512"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
name: 'PBKDF2',
salt,
iterations,
hash: {name: 'SHA-512'},
},
importKey, //your key from generateKey or importKey
512 //the number of bits you want to derive
importKey,
512
);
return bits;
return bits.then(buffer => new Uint8Array(buffer));
}
export function pqPrimeFactorization(pqBytes: number[]) {
export function pqPrimeFactorization(pqBytes: Uint8Array | number[]) {
let result: ReturnType<typeof pqPrimeLeemon>;
//console.log('PQ start', pqBytes, bytesToHex(pqBytes));
@ -178,7 +174,7 @@ export function pqPrimeFactorization(pqBytes: number[]) { @@ -178,7 +174,7 @@ export function pqPrimeFactorization(pqBytes: number[]) {
//console.time('PQ leemon');
result = pqPrimeLeemon(str2bigInt(bytesToHex(pqBytes), 16, Math.ceil(64 / bpe) + 1));
//console.timeEnd('PQ leemon');
} catch (e) {
} catch(e) {
console.error('Pq leemon Exception', e);
}
@ -187,7 +183,7 @@ export function pqPrimeFactorization(pqBytes: number[]) { @@ -187,7 +183,7 @@ export function pqPrimeFactorization(pqBytes: number[]) {
return result;
}
export function pqPrimeLeemon(what: any) {
export function pqPrimeLeemon(what: number[]): [Uint8Array, Uint8Array, number] {
var minBits = 64;
var minLen = Math.ceil(minBits / bpe) + 1;
var it = 0;
@ -262,15 +258,15 @@ export function pqPrimeLeemon(what: any) { @@ -262,15 +258,15 @@ export function pqPrimeLeemon(what: any) {
// console.log(dT(), 'done', bigInt2str(what, 10), bigInt2str(P, 10), bigInt2str(Q, 10))
return [bigInt2bytes(P), bigInt2bytes(Q), it];
return [new Uint8Array(bigInt2bytes(P)), new Uint8Array(bigInt2bytes(Q)), it];
}
export function bytesModPow(x: number[] | Uint8Array, y: number[] | Uint8Array, m: number[] | Uint8Array) {
try {
var xBigInt = str2bigInt(bytesToHex(x), 16);
var yBigInt = str2bigInt(bytesToHex(y), 16);
var mBigInt = str2bigInt(bytesToHex(m), 16);
var resBigInt = powMod(xBigInt, yBigInt, mBigInt);
const xBigInt = str2bigInt(bytesToHex(x), 16);
const yBigInt = str2bigInt(bytesToHex(y), 16);
const mBigInt = str2bigInt(bytesToHex(m), 16);
const resBigInt = powMod(xBigInt, yBigInt, mBigInt);
return bytesFromHex(bigInt2str(resBigInt, 16));
} catch(e) {
@ -280,11 +276,11 @@ export function bytesModPow(x: number[] | Uint8Array, y: number[] | Uint8Array, @@ -280,11 +276,11 @@ export function bytesModPow(x: number[] | Uint8Array, y: number[] | Uint8Array,
//return bytesFromBigInt(new BigInteger(x).modPow(new BigInteger(y), new BigInteger(m)), 256);
}
export function gzipUncompress(bytes: ArrayBuffer, toString: true): string;
export function gzipUncompress(bytes: ArrayBuffer, toString?: false): Uint8Array;
//export function gzipUncompress(bytes: ArrayBuffer, toString: true): string;
//export function gzipUncompress(bytes: ArrayBuffer, toString?: false): Uint8Array;
export function gzipUncompress(bytes: ArrayBuffer, toString?: boolean): string | Uint8Array {
//console.log(dT(), 'Gzip uncompress start');
var result = pako.inflate(bytes, toString ? {to: 'string'} : undefined);
const result = pako.inflate(bytes, toString ? {to: 'string'} : undefined);
//console.log(dT(), 'Gzip uncompress finish'/* , result */);
return result;
}

73
src/lib/crypto/cryptoworker.ts

@ -9,8 +9,13 @@ @@ -9,8 +9,13 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { MOUNT_CLASS_TO } from '../../config/debug';
import CryptoWorkerMethods from './crypto_methods';
// import { MOUNT_CLASS_TO } from '../../config/debug';
import CryptoWorkerMethods, { CryptoMethods } from './crypto_methods';
/// #if MTPROTO_WORKER
import { aesDecryptSync, aesEncryptSync, bytesModPow, gzipUncompress, hash_pbkdf2, pqPrimeFactorization, rsaEncrypt, sha1HashSync, sha256HashSync } from './crypto_utils';
import { computeSRP } from './srp';
/// #endif
type Task = {
taskId: number,
@ -31,36 +36,49 @@ class CryptoWorker extends CryptoWorkerMethods { @@ -31,36 +36,49 @@ class CryptoWorker extends CryptoWorkerMethods {
private pending: Array<Task> = [];
private debug = false;
private utils: {[name: string]: (...args: any[]) => any} = {};
private utils: CryptoMethods;
constructor() {
super();
console.log('CW constructor');
/// #if MTPROTO_WORKER
Promise.all([
import('./crypto_utils').then(utils => {
Object.assign(this.utils, {
'sha1-hash': utils.sha1HashSync,
'sha256-hash': utils.sha256HashSync,
'pbkdf2': utils.hash_pbkdf2,
'aes-encrypt': utils.aesEncryptSync,
'aes-decrypt': utils.aesDecryptSync,
'rsa-encrypt': utils.rsaEncrypt,
'factorize': utils.pqPrimeFactorization,
'mod-pow': utils.bytesModPow,
'gzipUncompress': utils.gzipUncompress,
});
}),
import('./srp').then(srp => {
this.utils.computeSRP = srp.computeSRP;
})/* ,
import('../bin_utils').then(utils => {
this.utils.unzip = utils.gzipUncompress;
}) */
]);
this.utils = {
'sha1-hash': sha1HashSync,
'sha256-hash': sha256HashSync,
'pbkdf2': hash_pbkdf2,
'aes-encrypt': aesEncryptSync,
'aes-decrypt': aesDecryptSync,
'rsa-encrypt': rsaEncrypt,
'factorize': pqPrimeFactorization,
'mod-pow': bytesModPow,
'gzipUncompress': gzipUncompress,
'computeSRP': computeSRP
};
// Promise.all([
// import('./crypto_utils').then(utils => {
// Object.assign(this.utils, {
// 'sha1-hash': utils.sha1HashSync,
// 'sha256-hash': utils.sha256HashSync,
// 'pbkdf2': utils.hash_pbkdf2,
// 'aes-encrypt': utils.aesEncryptSync,
// 'aes-decrypt': utils.aesDecryptSync,
// 'rsa-encrypt': utils.rsaEncrypt,
// 'factorize': utils.pqPrimeFactorization,
// 'mod-pow': utils.bytesModPow,
// 'gzipUncompress': utils.gzipUncompress,
// });
// }),
// import('./srp').then(srp => {
// this.utils.computeSRP = srp.computeSRP;
// })/* ,
// import('../bin_utils').then(utils => {
// this.utils.unzip = utils.gzipUncompress;
// }) */
// ]);
return;
/// #else
@ -102,6 +120,7 @@ class CryptoWorker extends CryptoWorkerMethods { @@ -102,6 +120,7 @@ class CryptoWorker extends CryptoWorkerMethods {
this.debug && console.log('CW start', task, args);
/// #if MTPROTO_WORKER
// @ts-ignore
return Promise.resolve<T>(this.utils[task](...args));
/// #else
return new Promise<T>((resolve, reject) => {
@ -136,5 +155,5 @@ class CryptoWorker extends CryptoWorkerMethods { @@ -136,5 +155,5 @@ class CryptoWorker extends CryptoWorkerMethods {
}
const cryptoWorker = new CryptoWorker();
MOUNT_CLASS_TO.CryptoWorker = cryptoWorker;
// MOUNT_CLASS_TO.CryptoWorker = cryptoWorker;
export default cryptoWorker;

76
src/lib/crypto/srp.ts

@ -1,32 +1,39 @@ @@ -1,32 +1,39 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import CryptoWorker from "../crypto/cryptoworker";
import {str2bigInt, isZero,
bigInt2str, powMod, int2bigInt, mult, mod, sub, bitSize, negative, add, greater} from '../../vendor/leemon';
import {logger, LogTypes} from '../logger';
import { AccountPassword, PasswordKdfAlgo } from "../../layer";
import { bufferConcats, bytesToHex, bytesFromHex, bufferConcat, bytesXor } from "../../helpers/bytes";
import { AccountPassword, InputCheckPasswordSRP, PasswordKdfAlgo } from "../../layer";
import { bufferConcats, bytesToHex, bytesFromHex, bytesXor, convertToUint8Array } from "../../helpers/bytes";
import { addPadding } from "../mtproto/bin_utils";
//import { MOUNT_CLASS_TO } from "../../config/debug";
const log = logger('SRP', LogTypes.Error);
//MOUNT_CLASS_TO && Object.assign(MOUNT_CLASS_TO, {str2bigInt, bigInt2str, int2bigInt});
export async function makePasswordHash(password: string, client_salt: Uint8Array, server_salt: Uint8Array): Promise<number[]> {
export async function makePasswordHash(password: string, client_salt: Uint8Array, server_salt: Uint8Array) {
// ! look into crypto_methods.test.ts
let buffer: any = await CryptoWorker.sha256Hash(bufferConcats(client_salt, new TextEncoder().encode(password), client_salt));
let buffer = await CryptoWorker.invokeCrypto('sha256-hash', bufferConcats(client_salt, new TextEncoder().encode(password), client_salt));
//log('encoded 1', bytesToHex(new Uint8Array(buffer)));
buffer = bufferConcats(server_salt, buffer, server_salt);
buffer = await CryptoWorker.sha256Hash(buffer);
buffer = await CryptoWorker.invokeCrypto('sha256-hash', buffer);
//log('encoded 2', buffer, bytesToHex(new Uint8Array(buffer)));
let hash = await CryptoWorker.pbkdf2(new Uint8Array(buffer), client_salt, 100000);
let hash = await CryptoWorker.invokeCrypto('pbkdf2', new Uint8Array(buffer), client_salt, 100000);
//log('encoded 3', hash, bytesToHex(new Uint8Array(hash)));
hash = bufferConcats(server_salt, hash, server_salt);
buffer = await CryptoWorker.sha256Hash(hash);
buffer = await CryptoWorker.invokeCrypto('sha256-hash', hash);
//log('got password hash:', buffer, bytesToHex(new Uint8Array(buffer)));
return buffer;
@ -63,13 +70,17 @@ export async function computeSRP(password: string, state: AccountPassword, isNew @@ -63,13 +70,17 @@ export async function computeSRP(password: string, state: AccountPassword, isNew
//check_prime_and_good(algo.p, g);
const pw_hash = await makePasswordHash(password, new Uint8Array(algo.salt1), new Uint8Array(algo.salt2));
const x = str2bigInt(bytesToHex(new Uint8Array(pw_hash)), 16);
const pw_hash = await makePasswordHash(password, algo.salt1, algo.salt2);
const x = str2bigInt(bytesToHex(pw_hash), 16);
//log('computed pw_hash:', pw_hash, x, bytesToHex(new Uint8Array(pw_hash)));
const padArray = function(arr: any[], len: number, fill = 0) {
return new Uint8Array(Array(len).fill(fill).concat(arr).slice(-len));
const padArray = function(arr: number[] | Uint8Array, len: number) {
if(!(arr instanceof Uint8Array)) {
arr = convertToUint8Array(arr);
}
return addPadding(arr, len, true, true, true);
};
const pForHash = padArray(bytesFromHex(bigInt2str(p, 16)), 256);
@ -102,8 +113,8 @@ export async function computeSRP(password: string, state: AccountPassword, isNew @@ -102,8 +113,8 @@ export async function computeSRP(password: string, state: AccountPassword, isNew
//log('g_x', bigInt2str(g_x, 16));
let k: any = await CryptoWorker.sha256Hash(bufferConcat(pForHash, gForHash));
k = str2bigInt(bytesToHex(new Uint8Array(k)), 16);
const kHash = await CryptoWorker.invokeCrypto('sha256-hash', bufferConcats(pForHash, gForHash));
const k = str2bigInt(bytesToHex(kHash), 16);
//log('k', bigInt2str(k, 16));
@ -140,9 +151,8 @@ export async function computeSRP(password: string, state: AccountPassword, isNew @@ -140,9 +151,8 @@ export async function computeSRP(password: string, state: AccountPassword, isNew
if(is_good_mod_exp_first(A, p)) {
const a_for_hash = bytesFromHex(bigInt2str(A, 16));
const s: any = await CryptoWorker.sha256Hash(
bufferConcat(new Uint8Array(a_for_hash), new Uint8Array(b_for_hash)));
const u = str2bigInt(bytesToHex(new Uint8Array(s)), 16);
const s = await CryptoWorker.invokeCrypto('sha256-hash', bufferConcats(a_for_hash, b_for_hash));
const u = str2bigInt(s.hex, 16);
if(!isZero(u) && !negative(u))
return {a, a_for_hash, u};
}
@ -150,7 +160,7 @@ export async function computeSRP(password: string, state: AccountPassword, isNew @@ -150,7 +160,7 @@ export async function computeSRP(password: string, state: AccountPassword, isNew
}
let {a, a_for_hash, u} = await generate_and_check_random();
const {a, a_for_hash, u} = await generate_and_check_random();
/* log('a', bigInt2str(a, 16));
log('a_for_hash', bytesToHex(a_for_hash));
@ -161,7 +171,7 @@ export async function computeSRP(password: string, state: AccountPassword, isNew @@ -161,7 +171,7 @@ export async function computeSRP(password: string, state: AccountPassword, isNew
log('subtract', bigInt2str(B, 16), bigInt2str(kg_x, 16));
log('B - kg_x', bigInt2str(sub(B, kg_x), 16)); */
let g_b;
let g_b: number[];
if(!greater(B, k_v)) {
//log('negative');
g_b = add(B, p);
@ -175,39 +185,37 @@ export async function computeSRP(password: string, state: AccountPassword, isNew @@ -175,39 +185,37 @@ export async function computeSRP(password: string, state: AccountPassword, isNew
/* if(!is_good_mod_exp_first(g_b, p))
throw new Error('bad g_b'); */
let ux = mult(u, x);
const ux = mult(u, x);
//log('u and x multiply', bigInt2str(u, 16), bigInt2str(x, 16), bigInt2str(ux, 16));
let a_ux = add(a, ux);
let S = powMod(g_b, a_ux, p);
const a_ux = add(a, ux);
const S = powMod(g_b, a_ux, p);
let K = await CryptoWorker.sha256Hash(padArray(bytesFromHex(bigInt2str(S, 16)), 256));
const K = await CryptoWorker.invokeCrypto('sha256-hash', padArray(bytesFromHex(bigInt2str(S, 16)), 256));
//log('K', bytesToHex(K), new Uint32Array(new Uint8Array(K).buffer));
let h1 = await CryptoWorker.sha256Hash(pForHash);
let h2 = await CryptoWorker.sha256Hash(gForHash);
h1 = bytesXor(new Uint8Array(h1), new Uint8Array(h2));
let h1 = await CryptoWorker.invokeCrypto('sha256-hash', pForHash);
const h2 = await CryptoWorker.invokeCrypto('sha256-hash', gForHash);
h1 = bytesXor(h1, h2);
let buff = bufferConcats(h1,
await CryptoWorker.sha256Hash(algo.salt1),
await CryptoWorker.sha256Hash(algo.salt2),
const buff = bufferConcats(h1,
await CryptoWorker.invokeCrypto('sha256-hash', algo.salt1),
await CryptoWorker.invokeCrypto('sha256-hash', algo.salt2),
a_for_hash,
b_for_hash,
K
);
let M1: any = await CryptoWorker.sha256Hash(buff);
const M1 = await CryptoWorker.invokeCrypto('sha256-hash', buff);
let out = {
const out = {
_: 'inputCheckPasswordSRP',
srp_id: state.srp_id,
A: new Uint8Array(a_for_hash),
M1: new Uint8Array(M1)
};
M1
} as InputCheckPasswordSRP.inputCheckPasswordSRP;
//log('out', bytesToHex(out.A), bytesToHex(out.M1));
return out;
/* console.log(gForHash, pForHash, bForHash); */
}

10
src/lib/lottieLoader.ts

@ -442,7 +442,7 @@ export class RLottiePlayer extends EventListenerBase<{ @@ -442,7 +442,7 @@ export class RLottiePlayer extends EventListenerBase<{
};
this.addEventListener('enterFrame', this.frameListener);
}, true);
}, {once: true});
}
}
@ -606,7 +606,7 @@ class LottieLoader { @@ -606,7 +606,7 @@ class LottieLoader {
resolve();
this.loaded = true;
}
});
}, {once: true});
}
});
}
@ -678,12 +678,12 @@ class LottieLoader { @@ -678,12 +678,12 @@ class LottieLoader {
return fetch(url)
.then(res => res.arrayBuffer())
.then(data => apiManager.gzipUncompress<string>(data, true))
.then(data => apiManager.invokeCrypto('gzipUncompress', data, true))
/* .then(str => {
return new Promise<string>((resolve) => setTimeout(() => resolve(str), 2e3));
}) */
.then(str => {
return this.loadAnimationWorker(Object.assign(params, {animationData: str/* JSON.parse(str) */, needUpscale: true}));
return this.loadAnimationWorker(Object.assign(params, {animationData: str as string/* JSON.parse(str) */, needUpscale: true}));
});
}
@ -695,7 +695,7 @@ class LottieLoader { @@ -695,7 +695,7 @@ class LottieLoader {
}, true);
}) */
new Promise<void>((resolve) => {
player.addEventListener('firstFrame', resolve, true);
player.addEventListener('firstFrame', resolve, {once: true});
}),
pause(2500)
]);

32
src/lib/mediaPlayer.ts

@ -238,7 +238,7 @@ export default class VideoPlayer { @@ -238,7 +238,7 @@ export default class VideoPlayer {
video.muted = !video.muted;
};
this.listenerSetter.add(volumeSvg, 'click', onMuteClick);
this.listenerSetter.add(volumeSvg)('click', onMuteClick);
const volumeProgress = new RangeSelector(0.01, 1, 0, 1);
volumeProgress.setListeners();
@ -277,7 +277,7 @@ export default class VideoPlayer { @@ -277,7 +277,7 @@ export default class VideoPlayer {
};
// не вызовется повторно если на 1 установить 1
this.listenerSetter.add(video, 'volumechange', () => {
this.listenerSetter.add(video)('volumechange', () => {
muted = video.muted;
lastVolume = video.volume;
setVolume();
@ -294,12 +294,12 @@ export default class VideoPlayer { @@ -294,12 +294,12 @@ export default class VideoPlayer {
leftControls.insertBefore(volumeDiv, timeElapsed.parentElement);
Array.from(toggle).forEach((button) => {
this.listenerSetter.add(button, 'click', () => {
this.listenerSetter.add(button)('click', () => {
this.togglePlay();
});
});
this.listenerSetter.add(video, 'click', () => {
this.listenerSetter.add(video)('click', () => {
if(!isTouchSupported) {
this.togglePlay();
}
@ -318,26 +318,26 @@ export default class VideoPlayer { @@ -318,26 +318,26 @@ export default class VideoPlayer {
};
if(isTouchSupported) {
this.listenerSetter.add(player, 'click', () => {
this.listenerSetter.add(player)('click', () => {
showControls();
});
this.listenerSetter.add(player, 'touchstart', () => {
this.listenerSetter.add(player)('touchstart', () => {
player.classList.add('show-controls');
clearTimeout(showControlsTimeout);
});
this.listenerSetter.add(player, 'touchend', () => {
this.listenerSetter.add(player)('touchend', () => {
if(player.classList.contains('is-playing')) {
showControls();
}
});
} else {
this.listenerSetter.add(this.wrapper, 'mousemove', () => {
this.listenerSetter.add(this.wrapper)('mousemove', () => {
showControls();
});
this.listenerSetter.add(document, 'keydown', (e: KeyboardEvent) => {
this.listenerSetter.add(document)('keydown', (e: KeyboardEvent) => {
if(e.code === 'KeyF') {
this.toggleFullScreen(fullScreenButton);
} else if(e.code === 'KeyM') {
@ -359,34 +359,34 @@ export default class VideoPlayer { @@ -359,34 +359,34 @@ export default class VideoPlayer {
/* video.addEventListener('play', () => {
}); */
this.listenerSetter.add(video, 'dblclick', () => {
this.listenerSetter.add(video)('dblclick', () => {
if(!isTouchSupported) {
this.toggleFullScreen(fullScreenButton);
}
});
this.listenerSetter.add(fullScreenButton, 'click', (e) => {
this.listenerSetter.add(fullScreenButton)('click', (e) => {
this.toggleFullScreen(fullScreenButton);
});
'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange'.split(' ').forEach(eventName => {
this.listenerSetter.add(player, eventName, this.onFullScreen, false);
this.listenerSetter.add(player)(eventName, this.onFullScreen, false);
});
this.listenerSetter.add(video, 'timeupdate', () => {
this.listenerSetter.add(video)('timeupdate', () => {
timeElapsed.innerHTML = String(video.currentTime | 0).toHHMMSS();
});
this.listenerSetter.add(video, 'play', () => {
this.listenerSetter.add(video)('play', () => {
this.wrapper.classList.add('played');
}, {once: true});
}
this.listenerSetter.add(video, 'play', () => {
this.listenerSetter.add(video)('play', () => {
this.wrapper.classList.add('is-playing');
});
this.listenerSetter.add(video, 'pause', () => {
this.listenerSetter.add(video)('pause', () => {
this.wrapper.classList.remove('is-playing');
});

13
src/lib/mtproto/apiFileManager.ts

@ -242,7 +242,7 @@ export class ApiFileManager { @@ -242,7 +242,7 @@ export class ApiFileManager {
private uncompressTGS = (bytes: Uint8Array, fileName: string) => {
//this.log('uncompressTGS', bytes, bytes.slice().buffer);
// slice нужен потому что в uint8array - 5053 length, в arraybuffer - 5084
return cryptoWorker.gzipUncompress<string>(bytes.slice().buffer, true);
return cryptoWorker.invokeCrypto('gzipUncompress', bytes.slice().buffer, true) as Promise<string>;
};
private convertWebp = (bytes: Uint8Array, fileName: string) => {
@ -261,10 +261,15 @@ export class ApiFileManager { @@ -261,10 +261,15 @@ export class ApiFileManager {
if(!havePromise) {
promise = deferredPromise<ReferenceBytes>();
this.refreshReferencePromises[hex] = promise;
}
promise.then(reference => {
(inputFileLocation as InputFileLocation.inputDocumentFileLocation).file_reference = reference;
promise = promise.then(reference => {
if(hex === bytesToHex(reference)) {
throw 'REFERENCE_IS_NOT_REFRESHED';
}
return (inputFileLocation as InputFileLocation.inputDocumentFileLocation).file_reference = reference;
});
if(havePromise) {

78
src/lib/mtproto/apiManager.ts

@ -18,7 +18,7 @@ import networkerFactory from './networkerFactory'; @@ -18,7 +18,7 @@ import networkerFactory from './networkerFactory';
import authorizer from './authorizer';
import dcConfigurator, { ConnectionType, DcConfigurator, TransportType } from './dcConfigurator';
import { logger } from '../logger';
import type { DcId, InvokeApiOptions, TrueDcId } from '../../types';
import type { DcAuthKey, DcId, DcServerSalt, InvokeApiOptions } from '../../types';
import type { MethodDeclMap } from '../../layer';
import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise';
import { bytesFromHex, bytesToHex } from '../../helpers/bytes';
@ -84,7 +84,12 @@ export class ApiManager { @@ -84,7 +84,12 @@ export class ApiManager {
private log: ReturnType<typeof logger> = logger('API');
private afterMessageTempIds: {[tempId: string]: string} = {};
private afterMessageTempIds: {
[tempId: string]: {
messageId: string,
promise: Promise<any>
}
} = {};
//private lol = false;
@ -122,7 +127,6 @@ export class ApiManager { @@ -122,7 +127,6 @@ export class ApiManager {
return this.baseDcId;
}
// mtpSetUserAuth
public async setUserAuth(userAuth: UserAuth) {
if(!userAuth.dcID) {
const baseDcId = await this.getBaseDcId();
@ -155,9 +159,8 @@ export class ApiManager { @@ -155,9 +159,8 @@ export class ApiManager {
});
}
// mtpLogOut
public async logOut() {
const storageKeys: Array<`dc${TrueDcId}_auth_key`> = [];
const storageKeys: Array<DcAuthKey> = [];
const prefix = 'dc';
for(let dcId = 1; dcId <= 5; dcId++) {
@ -187,15 +190,13 @@ export class ApiManager { @@ -187,15 +190,13 @@ export class ApiManager {
//return;
return Promise.all(logoutPromises).then(() => {
}, (error) => {
return Promise.all(logoutPromises).catch((error) => {
error.handled = true;
}).finally(clear)/* .then(() => {
location.pathname = '/';
}) */;
}
// mtpGetNetworker
public getNetworker(dcId: DcId, options: InvokeApiOptions = {}): Promise<MTPNetworker> {
const connectionType: ConnectionType = options.fileDownload ? 'download' : (options.fileUpload ? 'upload' : 'client');
//const connectionType: ConnectionType = 'client';
@ -244,8 +245,8 @@ export class ApiManager { @@ -244,8 +245,8 @@ export class ApiManager {
return this.gettingNetworkers[getKey];
}
const ak: `dc${TrueDcId}_auth_key` = `dc${dcId}_auth_key` as any;
const ss: `dc${TrueDcId}_server_salt` = `dc${dcId}_server_salt` as any;
const ak: DcAuthKey = `dc${dcId}_auth_key` as any;
const ss: DcServerSalt = `dc${dcId}_server_salt` as any;
return this.gettingNetworkers[getKey] = Promise.all([ak, ss].map(key => sessionStorage.get(key)))
.then(async([authKeyHex, serverSaltHex]) => {
@ -257,7 +258,7 @@ export class ApiManager { @@ -257,7 +258,7 @@ export class ApiManager {
}
const authKey = bytesFromHex(authKeyHex);
const authKeyId = (await CryptoWorker.sha1Hash(new Uint8Array(authKey))).slice(-8);
const authKeyId = (await CryptoWorker.invokeCrypto('sha1-hash', authKey)).slice(-8);
const serverSalt = bytesFromHex(serverSaltHex);
networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, transport, options);
@ -320,7 +321,6 @@ export class ApiManager { @@ -320,7 +321,6 @@ export class ApiManager {
});
}
// mtpInvokeApi
public invokeApi<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {}, options: InvokeApiOptions = {}): CancellablePromise<MethodDeclMap[T]["res"]> {
///////this.log('Invoke api', method, params, options);
@ -331,18 +331,14 @@ export class ApiManager { @@ -331,18 +331,14 @@ export class ApiManager {
const deferred = deferredPromise<MethodDeclMap[T]['res']>();
let afterMessageIdTemp = options.afterMessageId;
if(afterMessageIdTemp) {
deferred.finally(() => {
delete this.afterMessageTempIds[afterMessageIdTemp];
let {afterMessageId, prepareTempMessageId} = options;
if(prepareTempMessageId) {
deferred.then(() => {
delete this.afterMessageTempIds[prepareTempMessageId];
});
}
if(MOUNT_CLASS_TO) {
deferred.finally(() => {
clearInterval(interval);
});
const startTime = Date.now();
const interval = ctx.setInterval(() => {
if(!cachedNetworker || !cachedNetworker.isStopped()) {
@ -350,6 +346,10 @@ export class ApiManager { @@ -350,6 +346,10 @@ export class ApiManager {
}
//this.cachedUploadNetworkers[2].requestMessageStatus();
}, 5e3);
deferred.finally(() => {
clearInterval(interval);
});
}
const rejectPromise = (error: ApiError) => {
@ -396,17 +396,25 @@ export class ApiManager { @@ -396,17 +396,25 @@ export class ApiManager {
let cachedNetworker: MTPNetworker;
let stack = (new Error()).stack || 'empty stack';
const performRequest = (networker: MTPNetworker) => {
if(afterMessageIdTemp) {
options.afterMessageId = this.afterMessageTempIds[afterMessageIdTemp];
if(afterMessageId) {
const after = this.afterMessageTempIds[afterMessageId];
if(after) {
options.afterMessageId = after.messageId;
}
}
const promise = (cachedNetworker = networker).wrapApiCall(method, params, options);
if(options.prepareTempMessageId) {
this.afterMessageTempIds[options.prepareTempMessageId] = (options as MTMessage).messageId;
if(prepareTempMessageId) {
this.afterMessageTempIds[prepareTempMessageId] = {
messageId: (options as MTMessage).messageId,
promise: deferred
};
}
return promise.then(deferred.resolve, (error: ApiError) => {
//if(!options.ignoreErrors) {
if(error.type !== 'FILE_REFERENCE_EXPIRED' && error.type !== 'MSG_WAIT_FAILED') {
if(error.type !== 'FILE_REFERENCE_EXPIRED'/* && error.type !== 'MSG_WAIT_FAILED' */) {
this.log.error('Error', error.code, error.type, this.baseDcId, dcId, method, params);
}
@ -450,24 +458,24 @@ export class ApiManager { @@ -450,24 +458,24 @@ export class ApiManager {
}, rejectPromise);
}
} else if(!options.rawError && error.code === 420) {
const waitTime = +error.type.match(/^FLOOD_WAIT_(\d+)/)[1] || 10;
const waitTime = +error.type.match(/^FLOOD_WAIT_(\d+)/)[1] || 1;
if(waitTime > (options.floodMaxTimeout !== undefined ? options.floodMaxTimeout : 60)) {
if(waitTime > (options.floodMaxTimeout !== undefined ? options.floodMaxTimeout : 60) && !options.prepareTempMessageId) {
return rejectPromise(error);
}
setTimeout(() => {
performRequest(cachedNetworker);
}, waitTime/* (waitTime + 5) */ * 1000); // 03.02.2020
} else if(!options.rawError && error.code === 500) {
if(error.type === 'MSG_WAIT_FAILED') {
afterMessageIdTemp = undefined;
delete options.afterMessageId;
delete this.afterMessageTempIds[options.prepareTempMessageId];
performRequest(cachedNetworker);
return;
}
} else if(!options.rawError && ['MSG_WAIT_FAILED', 'MSG_WAIT_TIMEOUT'].includes(error.type)) {
const after = this.afterMessageTempIds[afterMessageId];
afterMessageId = undefined;
delete options.afterMessageId;
if(after) after.promise.then(() => performRequest(cachedNetworker));
else performRequest(cachedNetworker);
} else if(!options.rawError && error.code === 500) {
const now = Date.now();
if(options.stopTime) {
if(now >= options.stopTime) {

297
src/lib/mtproto/authorizer.ts

@ -20,7 +20,7 @@ import { logger, LogTypes } from "../logger"; @@ -20,7 +20,7 @@ import { logger, LogTypes } from "../logger";
import { bytesCmp, bytesToHex, bytesFromHex, bytesXor } from "../../helpers/bytes";
import DEBUG from "../../config/debug";
import { cmp, int2bigInt, one, pow, str2bigInt, sub } from "../../vendor/leemon";
import { Awaited } from "../../types";
import { addPadding } from "./bin_utils";
/* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse();
let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse();
@ -44,14 +44,14 @@ type AuthOptions = { @@ -44,14 +44,14 @@ type AuthOptions = {
},
// good
p?: number[],
q?: number[],
p?: Uint8Array,
q?: Uint8Array,
newNonce?: Uint8Array,
retry?: number,
b?: number[],
b?: Uint8Array,
g?: number,
gA?: Uint8Array,
dhPrime?: Uint8Array,
@ -60,13 +60,41 @@ type AuthOptions = { @@ -60,13 +60,41 @@ type AuthOptions = {
tmpAesIv?: Uint8Array,
authKeyId?: Uint8Array,
authKey?: number[],
serverSalt?: number[],
authKey?: Uint8Array,
serverSalt?: Uint8Array,
localTime?: number,
serverTime?: any
};
type ResPQ = {
_: 'resPQ';
nonce: Uint8Array;
pq: Uint8Array;
server_nonce: Uint8Array;
server_public_key_fingerprints: string[];
};
type P_Q_inner_data = {
_: 'p_q_inner_data_dc';
pq: Uint8Array;
p: Uint8Array;
q: Uint8Array;
nonce: Uint8Array;
server_nonce: Uint8Array;
new_nonce: Uint8Array;
dc: number;
};
type req_DH_params = {
nonce: Uint8Array;
server_nonce: Uint8Array;
p: Uint8Array;
q: Uint8Array;
public_key_fingerprint: string;
encrypted_data: Uint8Array;
};
export class Authorizer {
private cached: {
[dcId: number]: Promise<AuthOptions>
@ -78,36 +106,24 @@ export class Authorizer { @@ -78,36 +106,24 @@ export class Authorizer {
this.log = logger(`AUTHORIZER`, LogTypes.Error | LogTypes.Log);
}
public mtpSendPlainRequest(dcId: number, requestArray: Uint8Array) {
private sendPlainRequest(dcId: number, requestArray: Uint8Array) {
const requestLength = requestArray.byteLength;
//requestArray = new /* Int32Array */Uint8Array(requestBuffer);
const header = new TLSerialization();
header.storeLongP(0, 0, 'auth_key_id'); // Auth key
header.storeLong(timeManager.generateId(), 'msg_id'); // Msg_id
header.storeLongP(0, 0, 'auth_key_id');
header.storeLong(timeManager.generateId(), 'msg_id');
header.storeInt(requestLength, 'request_length');
const headerArray = header.getBytes(true) as Uint8Array;
const resultArray = new Uint8Array(headerArray.byteLength + requestLength);
resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length);
/* const headerBuffer = header.getBuffer(),
headerArray = new Int32Array(headerBuffer);
const headerLength = headerBuffer.byteLength;
const resultBuffer = new ArrayBuffer(headerLength + requestLength),
resultArray = new Int32Array(resultBuffer);
resultArray.set(headerArray);
resultArray.set(requestArray, headerArray.length);
const requestData = xhrSendBuffer ? resultBuffer : resultArray; */
const transport = dcConfigurator.chooseServer(dcId);
const baseError = {
code: 406,
type: 'NETWORK_BAD_RESPONSE',
transport: transport
transport
};
if(DEBUG) {
@ -121,7 +137,7 @@ export class Authorizer { @@ -121,7 +137,7 @@ export class Authorizer {
}
if(!result || !result.byteLength) {
return Promise.reject(baseError);
throw baseError;
}
try {
@ -129,6 +145,13 @@ export class Authorizer { @@ -129,6 +145,13 @@ export class Authorizer {
fResult = new Uint8Array(0); */
const deserializer = new TLDeserialization(result, {mtproto: true});
if(result.length === 4) {
const errorCode = deserializer.fetchInt();
this.log.error('mtpSendPlainRequest: wrong response, error code:', errorCode);
throw errorCode;
}
const auth_key_id = deserializer.fetchLong('auth_key_id');
if(auth_key_id !== '0') this.log.error('auth_key_id !== 0', auth_key_id);
@ -144,39 +167,38 @@ export class Authorizer { @@ -144,39 +167,38 @@ export class Authorizer {
const error = Object.assign(baseError, {originalError: e});
throw error;
}
}, error => {
}, (error) => {
if(!error.message && !error.type) {
error = Object.assign(baseError, {
originalError: error
});
}
return Promise.reject(error);
throw error;
});
}
public async mtpSendReqPQ(auth: AuthOptions) {
private async sendReqPQ(auth: AuthOptions) {
const request = new TLSerialization({mtproto: true});
request.storeMethod('req_pq_multi', {nonce: auth.nonce});
// need
rsaKeysManager.prepare().then(() => {});
if(DEBUG) {
this.log('Send req_pq', auth.nonce.hex);
}
let deserializer: TLDeserialization;
try {
deserializer = await this.mtpSendPlainRequest(auth.dcId, request.getBytes(true));
const promise = this.sendPlainRequest(auth.dcId, request.getBytes(true));
rsaKeysManager.prepare();
deserializer = await promise;
} catch(error) {
this.log.error('req_pq error', error.message);
throw error;
}
const response = deserializer.fetchObject('ResPQ');
const response: ResPQ = deserializer.fetchObject('ResPQ');
if(response._ !== 'resPQ') {
throw new Error('[MT] resPQ response invalid: ' + response._);
}
@ -186,8 +208,7 @@ export class Authorizer { @@ -186,8 +208,7 @@ export class Authorizer {
throw new Error('[MT] resPQ nonce mismatch');
}
//auth.serverNonce = response.server_nonce;
auth.serverNonce = new Uint8Array(response.server_nonce); // need
auth.serverNonce = response.server_nonce; // need
auth.pq = response.pq;
auth.fingerprints = response.server_public_key_fingerprints;
@ -206,9 +227,9 @@ export class Authorizer { @@ -206,9 +227,9 @@ export class Authorizer {
this.log('PQ factorization start', auth.pq);
}
let pAndQ: Awaited<ReturnType<typeof CryptoWorker['factorize']>>;
// let pAndQ: Awaited<ReturnType<typeof CryptoWorker['factorize']>>;
try {
pAndQ = await CryptoWorker.factorize(auth.pq);
var pAndQ = await CryptoWorker.invokeCrypto('factorize', auth.pq);
} catch(error) {
this.log.error('worker error factorize', error);
throw error;
@ -220,62 +241,66 @@ export class Authorizer { @@ -220,62 +241,66 @@ export class Authorizer {
if(DEBUG) {
this.log('PQ factorization done', pAndQ);
}
/* const p = new Uint32Array(new Uint8Array(auth.p).buffer)[0];
const q = new Uint32Array(new Uint8Array(auth.q).buffer)[0];
console.log(dT(), 'PQ factorization done', pAndQ, p.toString(16), q.toString(16)); */
return this.mtpSendReqDhParams(auth);
return this.sendReqDhParams(auth);
}
public async mtpSendReqDhParams(auth: AuthOptions) {
private async sendReqDhParams(auth: AuthOptions) {
auth.newNonce = new Uint8Array(32).randomize();
/* auth.newNonce = new Array(32); // need array, not uint8array!
MTProto.secureRandom.nextBytes(auth.newNonce); */
//console.log("TCL: Authorizer -> mtpSendReqDhParams -> auth.newNonce", auth.newNonce)
// remove
// auth.newNonce = fNewNonce ? fNewNonce : auth.newNonce;
// console.log("TCL: Authorizer -> mtpSendReqDhParams -> auth.newNonce", auth.newNonce);
const p_q_inner_data = {
_: 'p_q_inner_data',
const p_q_inner_data_dc: P_Q_inner_data = {
_: 'p_q_inner_data_dc',
pq: auth.pq,
p: auth.p,
q: auth.q,
nonce: auth.nonce,
server_nonce: auth.serverNonce,
new_nonce: auth.newNonce
new_nonce: auth.newNonce,
dc: 0
};
const data = new TLSerialization({mtproto: true});
data.storeObject(p_q_inner_data, 'P_Q_inner_data', 'DECRYPTED_DATA');
/* console.log('p_q_inner_data', p_q_inner_data,
bytesToHex(bytesFromArrayBuffer(data.getBuffer())),
sha1BytesSync(data.getBuffer()),
bytesFromArrayBuffer(await CryptoWorker.sha1Hash(data.getBuffer()))); */
const uint8Data = data.getBytes(true);
const sha1Hashed = await CryptoWorker.sha1Hash(uint8Data);
//const dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes() as number[]);
const dataWithHash = sha1Hashed.concat(uint8Data);
//dataWithHash = addPadding(dataWithHash, 255);
//dataWithHash = dataWithHash.concat(bytesFromHex('96228ea7790e71caaabc2ab67f4412e9aa224c664d232cc08617a32ce1796aa052da4a737083211689858f461e4473fd6394afd3aa0c8014840dc13f47beaf4fc3b9229aea9cfa83f9f6e676e50ee7676542fb75606879ee7e65cf3a2295b4ba0934ceec1011560c62395a6e9593bfb117cd0da75ba56723672d100ac17ec4d805aa59f7852e3a25a79ee4'));
//console.log('sha1Hashed', bytesToHex(sha1Hashed), 'dataWithHash', bytesToHex(dataWithHash), dataWithHash.length);
const rsaEncrypted = await CryptoWorker.rsaEncrypt(auth.publicKey, dataWithHash);
const pQInnerDataSerialization = new TLSerialization({mtproto: true});
pQInnerDataSerialization.storeObject(p_q_inner_data_dc, 'P_Q_inner_data', 'DECRYPTED_DATA');
const data = pQInnerDataSerialization.getBytes(true);
if(data.length > 144) {
throw 'DH_params: data is more than 144 bytes!';
}
const dataWithPadding = addPadding(data, 192, false, true, false);
const dataPadReversed = dataWithPadding.slice().reverse();
const getKeyAesEncrypted = async() => {
for(;;) {
const tempKey = new Uint8Array(32).randomize();
const dataWithHash = dataPadReversed.concat(await CryptoWorker.invokeCrypto('sha256-hash', tempKey.concat(dataWithPadding)));
if(dataWithHash.length !== 224) {
throw 'DH_params: dataWithHash !== 224 bytes!';
}
//console.log('rsaEncrypted', rsaEncrypted, new Uint8Array(rsaEncrypted).hex);
const aesEncrypted = await CryptoWorker.invokeCrypto('aes-encrypt', dataWithHash, tempKey, new Uint8Array([0]));
const tempKeyXor = bytesXor(tempKey, await CryptoWorker.invokeCrypto('sha256-hash', aesEncrypted));
const keyAesEncrypted = tempKeyXor.concat(aesEncrypted);
const keyAesEncryptedBigInt = str2bigInt(bytesToHex(keyAesEncrypted), 16);
const publicKeyModulusBigInt = str2bigInt(auth.publicKey.modulus, 16);
if(cmp(keyAesEncryptedBigInt, publicKeyModulusBigInt) === -1) {
return keyAesEncrypted;
}
}
};
const req_DH_params = {
const keyAesEncrypted = await getKeyAesEncrypted();
const encryptedData = await CryptoWorker.invokeCrypto('rsa-encrypt', keyAesEncrypted, auth.publicKey);
const req_DH_params: req_DH_params = {
nonce: auth.nonce,
server_nonce: auth.serverNonce,
p: auth.p,
q: auth.q,
public_key_fingerprint: auth.publicKey.fingerprint,
encrypted_data: rsaEncrypted
encrypted_data: encryptedData
};
const request = new TLSerialization({mtproto: true});
@ -289,7 +314,7 @@ export class Authorizer { @@ -289,7 +314,7 @@ export class Authorizer {
let deserializer: TLDeserialization;
try {
deserializer = await this.mtpSendPlainRequest(auth.dcId, requestBytes);
deserializer = await this.sendPlainRequest(auth.dcId, requestBytes);
} catch(error) {
this.log.error('Send req_DH_params FAIL!', error);
throw error;
@ -314,8 +339,7 @@ export class Authorizer { @@ -314,8 +339,7 @@ export class Authorizer {
}
if(response._ === 'server_DH_params_fail') {
//const newNonceHash = sha1BytesSync(auth.newNonce).slice(-16);
const newNonceHash = (await CryptoWorker.sha1Hash(auth.newNonce)).slice(-16);
const newNonceHash = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce)).slice(-16);
if(!bytesCmp(newNonceHash, response.new_nonce_hash)) {
throw new Error('[MT] server_DH_params_fail new_nonce_hash mismatch');
}
@ -325,7 +349,7 @@ export class Authorizer { @@ -325,7 +349,7 @@ export class Authorizer {
// fill auth object
try {
await this.mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer);
await this.decryptServerDhDataAnswer(auth, response.encrypted_answer);
} catch(e) {
this.log.error('mtpDecryptServerDhDataAnswer FAILED!', e);
throw e;
@ -333,35 +357,24 @@ export class Authorizer { @@ -333,35 +357,24 @@ export class Authorizer {
//console.log(dT(), 'mtpSendReqDhParams: executing mtpSendSetClientDhParams...');
return this.mtpSendSetClientDhParams(auth as any); // костыль
return this.sendSetClientDhParams(auth);
}
public async mtpDecryptServerDhDataAnswer(auth: AuthOptions, encryptedAnswer: any) {
private async decryptServerDhDataAnswer(auth: AuthOptions, encryptedAnswer: any) {
auth.localTime = Date.now();
// can't concat Array with Uint8Array!
//auth.tmpAesKey = sha1BytesSync(auth.newNonce.concat(auth.serverNonce)).concat(sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(0, 12));
//auth.tmpAesIv = sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(12).concat(sha1BytesSync([].concat(auth.newNonce, auth.newNonce)), auth.newNonce.slice(0, 4));
auth.tmpAesKey = (await CryptoWorker.sha1Hash(auth.newNonce.concat(auth.serverNonce)))
.concat((await CryptoWorker.sha1Hash(auth.serverNonce.concat(auth.newNonce))).slice(0, 12));
auth.tmpAesIv = (await CryptoWorker.sha1Hash(auth.serverNonce.concat(auth.newNonce))).slice(12)
.concat(await CryptoWorker.sha1Hash(auth.newNonce.concat(auth.newNonce)), auth.newNonce.slice(0, 4));
/* console.log(auth.serverNonce.concat(auth.newNonce));
console.log(auth.newNonce.concat(auth.serverNonce));
console.log(auth.newNonce.concat(auth.newNonce)); */
// ! can't concat Array with Uint8Array!
auth.tmpAesKey = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat(auth.serverNonce)))
.concat((await CryptoWorker.invokeCrypto('sha1-hash', auth.serverNonce.concat(auth.newNonce))).slice(0, 12));
auth.tmpAesIv = (await CryptoWorker.invokeCrypto('sha1-hash', auth.serverNonce.concat(auth.newNonce))).slice(12)
.concat(await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat(auth.newNonce)), auth.newNonce.slice(0, 4));
//const answerWithHash = aesDecryptSync(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv);
const answerWithHash = new Uint8Array(await CryptoWorker.aesDecrypt(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv));
const answerWithHash = new Uint8Array(await CryptoWorker.invokeCrypto('aes-decrypt', encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv));
const hash = answerWithHash.slice(0, 20);
const answerWithPadding = answerWithHash.slice(20);
// console.log('hash', hash);
const deserializer = new TLDeserialization(answerWithPadding, {mtproto: true});
const response = deserializer.fetchObject('Server_DH_inner_data');
@ -386,19 +399,18 @@ export class Authorizer { @@ -386,19 +399,18 @@ export class Authorizer {
auth.serverTime = response.server_time;
auth.retry = 0;
this.mtpVerifyDhParams(auth.g, auth.dhPrime, auth.gA);
this.verifyDhParams(auth.g, auth.dhPrime, auth.gA);
const offset = deserializer.getOffset();
//if(!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))) {
if(!bytesCmp(hash, await CryptoWorker.sha1Hash(answerWithPadding.slice(0, offset)))) {
if(!bytesCmp(hash, await CryptoWorker.invokeCrypto('sha1-hash', answerWithPadding.slice(0, offset)))) {
throw new Error('[MT] server_DH_inner_data SHA1-hash mismatch');
}
timeManager.applyServerTime(auth.serverTime, auth.localTime);
}
public mtpVerifyDhParams(g: number, dhPrime: Uint8Array, gA: Uint8Array) {
private verifyDhParams(g: number, dhPrime: Uint8Array, gA: Uint8Array) {
if(DEBUG) {
this.log('Verifying DH params', g, dhPrime, gA);
}
@ -413,20 +425,13 @@ export class Authorizer { @@ -413,20 +425,13 @@ export class Authorizer {
this.log('dhPrime cmp OK');
}
//const gABigInt = new BigInteger(bytesToHex(gA), 16);
const _gABigInt = str2bigInt(bytesToHex(gA), 16);
//const dhPrimeBigInt = new BigInteger(dhPrimeHex, 16);
const _dhPrimeBigInt = str2bigInt(dhPrimeHex, 16);
//this.log('gABigInt.compareTo(BigInteger.ONE) <= 0', gABigInt.compareTo(BigInteger.ONE), BigInteger.ONE.compareTo(BigInteger.ONE), greater(_gABigInt, one));
//if(gABigInt.compareTo(BigInteger.ONE) <= 0) {
if(cmp(_gABigInt, one) <= 0) {
throw new Error('[MT] DH params are not verified: gA <= 1');
}
/* this.log('gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0', gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)),
greater(gABigInt, sub(_dhPrimeBigInt, one))); */
//if(gABigInt.compareTo(dhPrimeBigInt.subtract(BigInteger.ONE)) >= 0) {
if(cmp(_gABigInt, sub(_dhPrimeBigInt, one)) >= 0) {
throw new Error('[MT] DH params are not verified: gA >= dhPrime - 1');
}
@ -435,25 +440,12 @@ export class Authorizer { @@ -435,25 +440,12 @@ export class Authorizer {
this.log('1 < gA < dhPrime-1 OK');
}
//const two = new BigInteger(/* null */'');
//two.fromInt(2);
const _two = int2bigInt(2, 32, 0);
//this.log('_two:', bigInt2str(_two, 16), two.toString(16));
// let perf = performance.now();
//const twoPow = two.pow(2048 - 64);
//console.log('jsbn pow', performance.now() - perf);
// perf = performance.now();
const _twoPow = pow(_two, 2048 - 64);
//console.log('leemon pow', performance.now() - perf);
//this.log('twoPow:', twoPow.toString(16), bigInt2str(_twoPow, 16));
// this.log('gABigInt.compareTo(twoPow) < 0');
//if(gABigInt.compareTo(twoPow) < 0) {
if(cmp(_gABigInt, _twoPow) < 0) {
throw new Error('[MT] DH params are not verified: gA < 2^{2048-64}');
}
//if(gABigInt.compareTo(dhPrimeBigInt.subtract(twoPow)) >= 0) {
if(cmp(_gABigInt, sub(_dhPrimeBigInt, _twoPow)) >= 0) {
throw new Error('[MT] DH params are not verified: gA > dhPrime - 2^{2048-64}');
}
@ -465,16 +457,15 @@ export class Authorizer { @@ -465,16 +457,15 @@ export class Authorizer {
return true;
}
public async mtpSendSetClientDhParams(auth: AuthOptions): Promise<AuthOptions> {
private async sendSetClientDhParams(auth: AuthOptions): Promise<AuthOptions> {
const gBytes = bytesFromHex(auth.g.toString(16));
auth.b = new Array(256);
auth.b = [...new Uint8Array(auth.b.length).randomize()];
auth.b = new Uint8Array(256).randomize();
//MTProto.secureRandom.nextBytes(auth.b);
let gB: number[];
// let gB: Awaited<ReturnType<typeof CryptoWorker['modPow']>>;
try {
gB = await CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime);
var gB = await CryptoWorker.invokeCrypto('mod-pow', gBytes, auth.b, auth.dhPrime);
} catch(error) {
throw error;
}
@ -488,11 +479,8 @@ export class Authorizer { @@ -488,11 +479,8 @@ export class Authorizer {
g_b: gB
}, 'Client_DH_Inner_Data');
//const dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());
const dataWithHash = (await CryptoWorker.sha1Hash(data.getBuffer())).concat(data.getBytes());
//const encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv);
const encryptedData = await CryptoWorker.aesEncrypt(dataWithHash, auth.tmpAesKey, auth.tmpAesIv);
const dataWithHash = (await CryptoWorker.invokeCrypto('sha1-hash', data.getBuffer())).concat(data.getBytes(true));
const encryptedData = await CryptoWorker.invokeCrypto('aes-encrypt', dataWithHash, auth.tmpAesKey, auth.tmpAesIv);
const request = new TLSerialization({mtproto: true});
request.storeMethod('set_client_DH_params', {
@ -507,7 +495,7 @@ export class Authorizer { @@ -507,7 +495,7 @@ export class Authorizer {
let deserializer: TLDeserialization;
try {
deserializer = await this.mtpSendPlainRequest(auth.dcId, request.getBytes(true));
deserializer = await this.sendPlainRequest(auth.dcId, request.getBytes(true));
} catch(err) {
throw err;
}
@ -526,15 +514,14 @@ export class Authorizer { @@ -526,15 +514,14 @@ export class Authorizer {
throw new Error('[MT] Set_client_DH_params_answer server_nonce mismatch');
}
let authKey: number[];
// let authKey: Uint8Array;
try {
authKey = await CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime);
var authKey = await CryptoWorker.invokeCrypto('mod-pow', auth.gA, auth.b, auth.dhPrime);
} catch(err) {
throw authKey;
}
//const authKeyHash = sha1BytesSync(authKey),
const authKeyHash = await CryptoWorker.sha1Hash(new Uint8Array(authKey)),
const authKeyHash = await CryptoWorker.invokeCrypto('sha1-hash', authKey),
authKeyAux = authKeyHash.slice(0, 8),
authKeyId = authKeyHash.slice(-8);
@ -542,9 +529,8 @@ export class Authorizer { @@ -542,9 +529,8 @@ export class Authorizer {
this.log('Got Set_client_DH_params_answer', response._, authKey);
}
switch(response._) {
case 'dh_gen_ok':
const newNonceHash1 = (await CryptoWorker.sha1Hash(auth.newNonce.concat([1], authKeyAux))).slice(-16);
//const newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16);
case 'dh_gen_ok': {
const newNonceHash1 = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat([1], authKeyAux))).slice(-16);
if(!bytesCmp(newNonceHash1, response.new_nonce_hash1)) {
throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash1 mismatch');
@ -560,29 +546,28 @@ export class Authorizer { @@ -560,29 +546,28 @@ export class Authorizer {
auth.serverSalt = serverSalt;
return auth;
break;
}
case 'dh_gen_retry':
//const newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16);
const newNonceHash2 = (await CryptoWorker.sha1Hash(auth.newNonce.concat([2], authKeyAux))).slice(-16);
case 'dh_gen_retry': {
const newNonceHash2 = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat([2], authKeyAux))).slice(-16);
if(!bytesCmp(newNonceHash2, response.new_nonce_hash2)) {
throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash2 mismatch');
}
return this.mtpSendSetClientDhParams(auth);
return this.sendSetClientDhParams(auth);
}
case 'dh_gen_fail':
//const newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16);
const newNonceHash3 = (await CryptoWorker.sha1Hash(auth.newNonce.concat([3], authKeyAux))).slice(-16);
case 'dh_gen_fail': {
const newNonceHash3 = (await CryptoWorker.invokeCrypto('sha1-hash', auth.newNonce.concat([3], authKeyAux))).slice(-16);
if(!bytesCmp(newNonceHash3, response.new_nonce_hash3)) {
throw new Error('[MT] Set_client_DH_params_answer new_nonce_hash3 mismatch');
}
throw new Error('[MT] Set_client_DH_params_answer fail');
}
}
}
// mtpAuth
public async auth(dcId: number): Promise<AuthOptions> {
if(dcId in this.cached) {
return this.cached[dcId];
@ -596,8 +581,10 @@ export class Authorizer { @@ -596,8 +581,10 @@ export class Authorizer {
return Promise.reject(new Error('[MT] No server found for dc ' + dcId));
}
// await new Promise((resolve) => setTimeout(resolve, 2e3));
try {
const promise = this.mtpSendReqPQ({dcId, nonce});
const promise = this.sendReqPQ({dcId, nonce});
this.cached[dcId] = promise;
return await promise;
} catch(err) {

34
src/lib/mtproto/bin_utils.ts

@ -71,6 +71,11 @@ export function dataUrlToBlob(url: string) { @@ -71,6 +71,11 @@ export function dataUrlToBlob(url: string) {
return blob;
} */
export function intToUint(val: number) {
// return val < 0 ? val + 4294967296 : val; // 0 <= val <= Infinity
return val >>> 0; // (4294967296 >>> 0) === 0; 0 <= val <= 4294967295
}
/* export function bytesFromBigInt(bigInt: BigInteger, len?: number) {
var bytes = bigInt.toByteArray();
@ -97,6 +102,8 @@ export function longFromInts(high: number, low: number): string { @@ -97,6 +102,8 @@ export function longFromInts(high: number, low: number): string {
//let perf = performance.now();
//let str = bigint(high).shiftLeft(32).add(bigint(low)).toString(10);
//console.log('longFromInts jsbn', performance.now() - perf);
high = intToUint(high);
low = intToUint(low);
//perf = performance.now();
const bigInt = str2bigInt(high.toString(16), 16, 32);//int2bigInt(high, 64, 64);
@ -123,28 +130,35 @@ export function sortLongsArray(arr: string[]) { @@ -123,28 +130,35 @@ export function sortLongsArray(arr: string[]) {
});
}
export function addPadding(bytes: any, blockSize: number = 16, zeroes?: boolean, full = false, prepend = false) {
let len = bytes.byteLength || bytes.length;
let needPadding = blockSize - (len % blockSize);
if(needPadding > 0 && (needPadding < blockSize || full)) {
export function addPadding<T extends number[] | ArrayBuffer | Uint8Array>(
bytes: T,
blockSize: number = 16,
zeroes?: boolean,
blockSizeAsTotalLength = false,
prepend = false
): T {
const len = (bytes as ArrayBuffer).byteLength || (bytes as Uint8Array).length;
const needPadding = blockSizeAsTotalLength ? blockSize - len : blockSize - (len % blockSize);
if(needPadding > 0 && needPadding < blockSize) {
////console.log('addPadding()', len, blockSize, needPadding);
let padding = new Array(needPadding);
const padding: number[] = new Array(needPadding);
if(zeroes) {
for(let i = 0; i < needPadding; i++) {
for(let i = 0; i < needPadding; ++i) {
padding[i] = 0;
}
} else {
for(let i = 0; i < padding.length; ++i) {
for(let i = 0; i < needPadding; ++i) {
padding[i] = nextRandomInt(255);
}
}
if(bytes instanceof ArrayBuffer) {
bytes = (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)).buffer;
return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)).buffer as T;
} else if(bytes instanceof Uint8Array) {
bytes = prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding);
return (prepend ? bufferConcats(padding, bytes) : bufferConcats(bytes, padding)) as T;
} else {
bytes = prepend ? padding.concat(bytes) : bytes.concat(padding);
// @ts-ignore
return (prepend ? padding.concat(bytes) : bytes.concat(padding)) as T;
}
}

25
src/lib/mtproto/mtproto.worker.ts

@ -7,20 +7,19 @@ @@ -7,20 +7,19 @@
// just to include
import '../polyfill';
import type { LocalStorageProxyTask } from '../localStorage';
import type { WebpConvertTask } from '../webp/webpWorkerController';
import type { ToggleStorageTask } from './mtprotoworker';
import type { RefreshReferenceTaskResponse } from './apiFileManager';
import apiManager from "./apiManager";
import cryptoWorker from "../crypto/cryptoworker";
import networkerFactory from "./networkerFactory";
import apiFileManager, { RefreshReferenceTaskResponse } from './apiFileManager';
import type { RequestFilePartTask, RequestFilePartTaskResponse } from '../serviceWorker/index.service';
import apiFileManager from './apiFileManager';
import { ctx } from '../../helpers/userAgent';
import { notifyAll } from '../../helpers/context';
// import AppStorage from '../storage';
import CacheStorageController from '../cacheStorage';
import sessionStorage from '../sessionStorage';
import { LocalStorageProxyTask } from '../localStorage';
import { WebpConvertTask } from '../webp/webpWorkerController';
import { socketsProxied } from './transports/socketProxied';
import { ToggleStorageTask } from './mtprotoworker';
import { bytesToHex } from '../../helpers/bytes';
let webpSupported = false;
@ -102,7 +101,12 @@ const taskListeners = { @@ -102,7 +101,12 @@ const taskListeners = {
const onMessage = async(e: any) => {
try {
const task = e.data;
const task: {
task: string,
taskId: number,
args: any[],
type?: string
} = e.data;
const taskId = task.taskId;
// @ts-ignore
@ -119,8 +123,7 @@ const onMessage = async(e: any) => { @@ -119,8 +123,7 @@ const onMessage = async(e: any) => {
switch(task.task) {
case 'computeSRP':
case 'gzipUncompress':
// @ts-ignore
return cryptoWorker[task.task].apply(cryptoWorker, task.args).then(result => {
return cryptoWorker.invokeCrypto(task.task, ...task.args as any).then(result => {
notifyAll({taskId, result});
});
@ -131,7 +134,7 @@ const onMessage = async(e: any) => { @@ -131,7 +134,7 @@ const onMessage = async(e: any) => {
case 'downloadFile': {
try {
// @ts-ignore
let result = apiFileManager[task.task].apply(apiFileManager, task.args);
let result: any = apiFileManager[task.task].apply(apiFileManager, task.args);
if(result instanceof Promise) {
/* (result as ReturnType<ApiFileManager['downloadFile']>).notify = (progress: {done: number, total: number, offset: number}) => {
@ -186,7 +189,7 @@ const onMessage = async(e: any) => { @@ -186,7 +189,7 @@ const onMessage = async(e: any) => {
}
}
} catch(err) {
console.error('worker task error:', err);
}
};

11
src/lib/mtproto/mtprotoworker.ts

@ -53,8 +53,6 @@ export interface ToggleStorageTask extends WorkerTaskVoidTemplate { @@ -53,8 +53,6 @@ export interface ToggleStorageTask extends WorkerTaskVoidTemplate {
export class ApiManagerProxy extends CryptoWorkerMethods {
public worker: /* Window */Worker;
public postMessage: (...args: any[]) => void;
//public postSWMessage: (...args: any[]) => void = () => {};
private afterMessageIdTemp = 0;
private taskId = 0;
@ -99,6 +97,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods { @@ -99,6 +97,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
public onServiceWorkerFail: () => void;
private postMessagesWaiting: any[][] = [];
constructor() {
super();
this.log('constructor');
@ -292,6 +292,10 @@ export class ApiManagerProxy extends CryptoWorkerMethods { @@ -292,6 +292,10 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
});
}
public postMessage(...args: any[]) {
this.postMessagesWaiting.push(args);
}
public postSWMessage(message: any) {
if(navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage(message);
@ -305,6 +309,9 @@ export class ApiManagerProxy extends CryptoWorkerMethods { @@ -305,6 +309,9 @@ export class ApiManagerProxy extends CryptoWorkerMethods {
this.postMessage = this.worker.postMessage.bind(this.worker);
this.postMessagesWaiting.forEach(args => this.postMessage(...args));
this.postMessagesWaiting.length = 0;
const isWebpSupported = webpWorkerController.isWebpSupported();
this.log('WebP supported:', isWebpSupported);
this.postMessage({type: 'webpSupport', payload: isWebpSupported});

57
src/lib/mtproto/networker.ts

@ -20,7 +20,7 @@ import { logger, LogTypes } from '../logger'; @@ -20,7 +20,7 @@ import { logger, LogTypes } from '../logger';
import { InvokeApiOptions } from '../../types';
import { longToBytes } from '../crypto/crypto_utils';
import MTTransport from './transports/transport';
import { convertToUint8Array, bufferConcat, bytesCmp, bytesToHex } from '../../helpers/bytes';
import { convertToUint8Array, bytesCmp, bytesToHex, bufferConcats } from '../../helpers/bytes';
import { nextRandomInt } from '../../helpers/random';
import App from '../../config/app';
import DEBUG from '../../config/debug';
@ -138,8 +138,8 @@ export default class MTPNetworker { @@ -138,8 +138,8 @@ export default class MTPNetworker {
//private debugRequests: Array<{before: Uint8Array, after: Uint8Array}> = [];
constructor(public dcId: number, private authKey: number[], private authKeyId: Uint8Array,
serverSalt: number[], public transport: MTTransport, options: InvokeApiOptions = {}) {
constructor(public dcId: number, private authKey: Uint8Array, private authKeyId: Uint8Array,
serverSalt: Uint8Array, public transport: MTTransport, options: InvokeApiOptions = {}) {
this.authKeyUint8 = convertToUint8Array(this.authKey);
this.serverSalt = convertToUint8Array(serverSalt);
@ -299,7 +299,7 @@ export default class MTPNetworker { @@ -299,7 +299,7 @@ export default class MTPNetworker {
const invokeWithLayer = Schema.API.methods.find(m => m.method === 'invokeWithLayer');
if(!invokeWithLayer) throw new Error('no invokeWithLayer!');
serializer.storeInt(+invokeWithLayer.id >>> 0, 'invokeWithLayer');
serializer.storeInt(+invokeWithLayer.id, 'invokeWithLayer');
// @ts-ignore
serializer.storeInt(Schema.layer, 'layer');
@ -307,7 +307,7 @@ export default class MTPNetworker { @@ -307,7 +307,7 @@ export default class MTPNetworker {
const initConnection = Schema.API.methods.find(m => m.method === 'initConnection');
if(!initConnection) throw new Error('no initConnection!');
serializer.storeInt(+initConnection.id >>> 0, 'initConnection');
serializer.storeInt(+initConnection.id, 'initConnection');
serializer.storeInt(0x0, 'flags');
serializer.storeInt(App.id, 'api_id');
serializer.storeString(networkerFactory.userAgent || 'Unknown UserAgent', 'device_model');
@ -332,13 +332,13 @@ export default class MTPNetworker { @@ -332,13 +332,13 @@ export default class MTPNetworker {
if(options.afterMessageId) {
if(invokeAfterMsgConstructor === undefined) {
const m = Schema.API.methods.find(m => m.method === 'invokeAfterMsg');
invokeAfterMsgConstructor = m ? +m.id >>> 0 : 0;
invokeAfterMsgConstructor = m ? +m.id : 0;
}
if(invokeAfterMsgConstructor) {
//if(this.debug) {
//this.log('Api call options.afterMessageId!');
//}
// if(this.debug) {
// this.log('invokeApi: store invokeAfterMsg');
// }
serializer.storeInt(invokeAfterMsgConstructor, 'invokeAfterMsg');
serializer.storeLong(options.afterMessageId, 'msg_id');
@ -558,7 +558,7 @@ export default class MTPNetworker { @@ -558,7 +558,7 @@ export default class MTPNetworker {
const pingMessage = {
msg_id: timeManager.generateId(),
seq_no: this.generateSeqNo(true),
body: serializer.getBytes()
body: serializer.getBytes(true)
};
this.sendEncryptedRequest(pingMessage).then((result) => {
@ -797,29 +797,29 @@ export default class MTPNetworker { @@ -797,29 +797,29 @@ export default class MTPNetworker {
}
// * correct, fully checked
private async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) {
private async getMsgKey(dataWithPadding: Uint8Array, isOut: boolean) {
const x = isOut ? 0 : 8;
const msgKeyLargePlain = bufferConcat(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding);
const msgKeyLargePlain = bufferConcats(this.authKeyUint8.subarray(88 + x, 88 + x + 32), dataWithPadding);
const msgKeyLarge = await CryptoWorker.sha256Hash(msgKeyLargePlain);
const msgKeyLarge = await CryptoWorker.invokeCrypto('sha256-hash', msgKeyLargePlain);
const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24);
return msgKey;
};
// * correct, fully checked
private getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
private getAesKeyIv(msgKey: Uint8Array, isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
const x = isOut ? 0 : 8;
const sha2aText = new Uint8Array(52);
const sha2bText = new Uint8Array(52);
const promises: Array<Promise<number[]>> = [];
const promises: Array<Promise<Uint8Array>> = [];
sha2aText.set(msgKey, 0);
sha2aText.set(this.authKeyUint8.subarray(x, x + 36), 16);
promises.push(CryptoWorker.sha256Hash(sha2aText));
promises.push(CryptoWorker.invokeCrypto('sha256-hash', sha2aText));
sha2bText.set(this.authKeyUint8.subarray(40 + x, 40 + x + 36), 0);
sha2bText.set(msgKey, 36);
promises.push(CryptoWorker.sha256Hash(sha2bText));
promises.push(CryptoWorker.invokeCrypto('sha256-hash', sha2bText));
return Promise.all(promises).then((results) => {
const aesKey = new Uint8Array(32);
@ -954,7 +954,7 @@ export default class MTPNetworker { @@ -954,7 +954,7 @@ export default class MTPNetworker {
messages.push({
msg_id: timeManager.generateId(),
seq_no: this.generateSeqNo(),
body: serializer.getBytes()
body: serializer.getBytes(true)
});
}
/// #endif
@ -1012,6 +1012,7 @@ export default class MTPNetworker { @@ -1012,6 +1012,7 @@ export default class MTPNetworker {
const innerMessages: string[] = [];
messages.forEach((message, i) => {
innerMessages.push(message.msg_id);
// this.log('Pushing to container:', message.msg_id);
container.storeLong(message.msg_id, 'CONTAINER[' + i + '][msg_id]');
container.storeInt(message.seq_no, 'CONTAINER[' + i + '][seq_no]');
container.storeInt(message.body.length, 'CONTAINER[' + i + '][bytes]');
@ -1035,12 +1036,12 @@ export default class MTPNetworker { @@ -1035,12 +1036,12 @@ export default class MTPNetworker {
};
}
private async getEncryptedMessage(dataWithPadding: ArrayBuffer) {
private async getEncryptedMessage(dataWithPadding: Uint8Array) {
const msgKey = await this.getMsgKey(dataWithPadding, true);
const keyIv = await this.getAesKeyIv(msgKey, true);
// this.log('after msg key iv')
const encryptedBytes = await CryptoWorker.aesEncrypt(dataWithPadding, keyIv[0], keyIv[1]);
const encryptedBytes = await CryptoWorker.invokeCrypto('aes-encrypt', dataWithPadding, keyIv[0], keyIv[1]);
// this.log('Finish encrypt')
return {
@ -1049,11 +1050,11 @@ export default class MTPNetworker { @@ -1049,11 +1050,11 @@ export default class MTPNetworker {
};
}
private getDecryptedMessage(msgKey: Uint8Array, encryptedData: Uint8Array): Promise<ArrayBuffer> {
private getDecryptedMessage(msgKey: Uint8Array, encryptedData: Uint8Array) {
// this.log('get decrypted start')
return this.getAesKeyIv(msgKey, false).then((keyIv) => {
// this.log('after msg key iv')
return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
return CryptoWorker.invokeCrypto('aes-decrypt', encryptedData, keyIv[0], keyIv[1]);
});
}
@ -1102,7 +1103,7 @@ export default class MTPNetworker { @@ -1102,7 +1103,7 @@ export default class MTPNetworker {
this.log.error('wrong length', dataBuffer, canBeLength, message.msg_id);
} */
const paddingLength = (16 - (data.offset % 16)) + 16 * (1 + nextRandomInt(5));
const paddingLength = (16 - (data.getOffset() % 16)) + 16 * (1 + nextRandomInt(5));
const padding = /* (message as any).padding || */new Uint8Array(paddingLength).randomize()/* .fill(0) */;
/* const padding = [167, 148, 207, 226, 86, 192, 193, 57, 124, 153, 174, 145, 159, 1, 5, 70, 127, 157,
51, 241, 46, 85, 141, 212, 139, 234, 213, 164, 197, 116, 245, 70, 184, 40, 40, 201, 233, 211, 150,
@ -1112,7 +1113,7 @@ export default class MTPNetworker { @@ -1112,7 +1113,7 @@ export default class MTPNetworker {
//(message as any).padding = padding;
const dataWithPadding = bufferConcat(dataBuffer, padding);
const dataWithPadding = bufferConcats(dataBuffer, padding);
// this.log('Adding padding', dataBuffer, padding, dataWithPadding)
// this.log('auth_key_id', bytesToHex(self.authKeyID))
@ -1214,8 +1215,8 @@ export default class MTPNetworker { @@ -1214,8 +1215,8 @@ export default class MTPNetworker {
let deserializer = new TLDeserialization(dataWithPadding, {mtproto: true});
/* const salt = */deserializer.fetchIntBytes(64, false, 'salt'); // need
const sessionId = deserializer.fetchIntBytes(64, false, 'session_id');
/* const salt = */deserializer.fetchIntBytes(64, true, 'salt'); // need
const sessionId = deserializer.fetchIntBytes(64, true, 'session_id');
const messageId = deserializer.fetchLong('message_id');
if(!bytesCmp(sessionId, this.sessionId) &&
@ -1268,10 +1269,10 @@ export default class MTPNetworker { @@ -1268,10 +1269,10 @@ export default class MTPNetworker {
};
}
if(deserializer.offset !== offset + result.bytes) {
if(deserializer.getOffset() !== offset + result.bytes) {
// console.warn(dT(), 'set offset', this.offset, offset, result.bytes)
// this.log(result)
deserializer.offset = offset + result.bytes;
deserializer.setOffset(offset + result.bytes);
}
// this.log('override message', result)
},

4
src/lib/mtproto/networkerFactory.ts

@ -35,9 +35,9 @@ export class NetworkerFactory { @@ -35,9 +35,9 @@ export class NetworkerFactory {
this.updatesProcessor = callback;
}
public getNetworker(dcId: number, authKey: number[], authKeyID: Uint8Array, serverSalt: number[], transport: MTTransport, options: InvokeApiOptions) {
public getNetworker(dcId: number, authKey: Uint8Array, authKeyId: Uint8Array, serverSalt: Uint8Array, transport: MTTransport, options: InvokeApiOptions) {
//console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options);
const networker = new MTPNetworker(dcId, authKey, authKeyID, serverSalt, transport, options);
const networker = new MTPNetworker(dcId, authKey, authKeyId, serverSalt, transport, options);
this.networkers.push(networker);
return networker;
}

14
src/lib/mtproto/passwordManager.ts

@ -9,12 +9,10 @@ @@ -9,12 +9,10 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import type { AccountPassword, AccountPasswordInputSettings, AccountUpdatePasswordSettings, InputCheckPasswordSRP, PasswordKdfAlgo } from '../../layer';
import type CryptoWorkerMethods from '../crypto/crypto_methods';
import type { AccountPassword, AccountUpdatePasswordSettings, InputCheckPasswordSRP, PasswordKdfAlgo } from '../../layer';
import { MOUNT_CLASS_TO } from '../../config/debug';
import appUsersManager from '../appManagers/appUsersManager';
import apiManager from './mtprotoworker';
//import { computeCheck } from "../crypto/srp";
export class PasswordManager {
public getState(): Promise<AccountPassword> {
@ -33,7 +31,7 @@ export class PasswordManager { @@ -33,7 +31,7 @@ export class PasswordManager {
//state.new_algo = Object.assign({}, state.new_algo);
return this.getState().then(state => {
let currentHashPromise: ReturnType<CryptoWorkerMethods['computeSRP']>;
let currentHashPromise: Promise<InputCheckPasswordSRP>;
let newHashPromise: Promise<Uint8Array>;
const params: AccountUpdatePasswordSettings = {
password: null,
@ -45,7 +43,7 @@ export class PasswordManager { @@ -45,7 +43,7 @@ export class PasswordManager {
};
if(settings.currentPassword) {
currentHashPromise = apiManager.computeSRP(settings.currentPassword, state);
currentHashPromise = apiManager.invokeCrypto('computeSRP', settings.currentPassword, state, false) as any;
} else {
currentHashPromise = Promise.resolve({
_: 'inputCheckPasswordEmpty'
@ -60,7 +58,7 @@ export class PasswordManager { @@ -60,7 +58,7 @@ export class PasswordManager {
newAlgo.salt1 = salt1;
if(settings.newPassword) {
newHashPromise = apiManager.computeSRP(settings.newPassword, state, true) as any;
newHashPromise = apiManager.invokeCrypto('computeSRP', settings.newPassword, state, true) as any;
} else {
newHashPromise = Promise.resolve(new Uint8Array());
}
@ -76,10 +74,10 @@ export class PasswordManager { @@ -76,10 +74,10 @@ export class PasswordManager {
}
public check(password: string, state: AccountPassword, options: any = {}) {
return apiManager.computeSRP(password, state).then((inputCheckPassword) => {
return apiManager.invokeCrypto('computeSRP', password, state, false).then((inputCheckPassword) => {
//console.log('SRP', inputCheckPassword);
return apiManager.invokeApi('auth.checkPassword', {
password: inputCheckPassword
password: inputCheckPassword as InputCheckPasswordSRP.inputCheckPasswordSRP
}, options).then(auth => {
if(auth._ === 'auth.authorization') {
appUsersManager.saveApiUser(auth.user);

33
src/lib/mtproto/referenceDatabase.ts

@ -82,7 +82,7 @@ class ReferenceDatabase { @@ -82,7 +82,7 @@ class ReferenceDatabase {
public getContext(reference: ReferenceBytes): [ReferenceContext, ReferenceBytes] {
const contexts = this.getContexts(reference);
return contexts ? [contexts[0].values().next().value, contexts[1]] : undefined;
return contexts[0] ? [contexts[0].values().next().value, contexts[1]] : undefined;
}
public deleteContext(reference: ReferenceBytes, context: ReferenceContext, contexts?: ReferenceContexts) {
@ -104,10 +104,20 @@ class ReferenceDatabase { @@ -104,10 +104,20 @@ class ReferenceDatabase {
}
public refreshReference(reference: ReferenceBytes, context?: ReferenceContext): Promise<void> {
[context, reference] = this.getContext(reference);
if(!context) {
const c = this.getContext(reference);
if(!c) {
return Promise.reject('NO_CONTEXT');
}
[context, reference] = c;
}
let promise: Promise<any>;
switch(context?.type) {
case 'message': {
return appMessagesManager.wrapSingleMessage(context.peerId, context.messageId, true);
promise = appMessagesManager.wrapSingleMessage(context.peerId, context.messageId, true);
break;
// .then(() => {
// console.log('FILE_REFERENCE_EXPIRED: got message', context, appMessagesManager.getMessage((context as ReferenceContext.referenceContextMessage).messageId).media, reference);
// });
@ -118,6 +128,23 @@ class ReferenceDatabase { @@ -118,6 +128,23 @@ class ReferenceDatabase {
return Promise.reject();
}
}
const hex = bytesToHex(reference);
return promise.then(() => {
const newHex = bytesToHex(reference);
if(hex !== newHex) {
return;
}
this.deleteContext(reference, context);
const newContext = this.getContext(reference);
if(newContext) {
return this.refreshReference(reference, newContext[0]);
}
throw 'NO_NEW_CONTEXT';
});
}
/* handleReferenceError = (reference: ReferenceBytes, error: ApiError) => {

118
src/lib/mtproto/rsaKeysManager.ts

@ -11,8 +11,14 @@ @@ -11,8 +11,14 @@
import { TLSerialization } from "./tl_utils";
import CryptoWorker from '../crypto/cryptoworker';
import { bytesFromArrayBuffer, bytesFromHex, bytesToHex } from "../../helpers/bytes";
import { bytesFromHex, bytesToHex } from "../../helpers/bytes";
import { bigInt2str, str2bigInt } from "../../vendor/leemon";
import Modes from "../../config/modes";
export type RSAPublicKeyHex = {
modulus: string,
exponent: string
};
export class RSAKeysManager {
@ -21,59 +27,29 @@ export class RSAKeysManager { @@ -21,59 +27,29 @@ export class RSAKeysManager {
*
*
* -----BEGIN RSA PUBLIC KEY-----
* MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
* lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
* an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
* Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
* 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
* Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
* MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
* 5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
* 62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
* +aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
* t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
* 5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
* -----END RSA PUBLIC KEY-----
*
* -----BEGIN RSA PUBLIC KEY-----
* MIIBCgKCAQEBadMIUYSKhyznMh+Pg+OxTLyDZrWEjQIPZC3oJCtuZX7qUxgcWqFX
* Q1952TSY8S8NYuz12sK9Fvp+lil1hIG0U/cuPsK08VB1hB4VA+p0S46fGwVsRovq
* 4qUiUIzQSjSHDASuXTOinlYEHwmg/GaLc5G7qhePWa0p9YmqYR5Ha3xHJywcXZrn
* yE3nC9igL96Aanqv+Prbu1N+r9vAgZeHh9cfbtbV8WWwruOANOTEv2ctQLR0dfr9
* MwQXNePTPQlYsO9HNIGS1LWe7hZFtGBAVJH92F7Kig68WqHM3PIZ6Sq7N0VSzfzL
* b11Z/YHz2UXYtXADwL/m5pTpKBUtJBXkOQIDAQAB
* -----END RSA PUBLIC KEY-----
*
* -----BEGIN PUBLIC KEY-----
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAruw2yP/BCcsJliRoW5eB
* VBVle9dtjJw+OYED160Wybum9SXtBBLXriwt4rROd9csv0t0OHCaTmRqBcQ0J8fx
* hN6/cpR1GWgOZRUAiQxoMnlt0R93LCX/j1dnVa/gVbCjdSxpbrfY2g2L4frzjJvd
* l84Kd9ORYjDEAyFnEA7dD556OptgLQQ2e2iVNq8NZLYTzLp5YpOdO1doK+ttrltg
* gTCy5SrKeLoCPPbOgGsdxJxyz5KKcZnSLj16yE5HvJQn0CNpRdENvRUXe6tBP78O
* 39oJ8BTHp9oIjd6XWXAsp2CvK45Ol8wFXGF710w9lwCGNbmNxNYhtIkdqfsEcwR5
* JwIDAQAB
* -----END PUBLIC KEY-----
*
* -----BEGIN PUBLIC KEY-----
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvfLHfYH2r9R70w8prHbl
* Wt/nDkh+XkgpflqQVcnAfSuTtO05lNPspQmL8Y2XjVT4t8cT6xAkdgfmmvnvRPOO
* KPi0OfJXoRVylFzAQG/j83u5K3kRLbae7fLccVhKZhY46lvsueI1hQdLgNV9n1cQ
* 3TDS2pQOCtovG4eDl9wacrXOJTG2990VjgnIKNA0UMoP+KF03qzryqIt3oTvZq03
* DyWdGK+AZjgBLaDKSnC6qD2cFY81UryRWOab8zKkWAnhw2kFpcqhI0jdV5QaSCEx
* vnsjVaX0Y1N0870931/5Jb9ICe4nweZ9kSDF/gip3kWLG0o8XQpChDfyvsqB9OLV
* /wIDAQAB
* -----END PUBLIC KEY-----
*
* -----BEGIN PUBLIC KEY-----
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs/ditzm+mPND6xkhzwFI
* z6J/968CtkcSE/7Z2qAJiXbmZ3UDJPGrzqTDHkO30R8VeRM/Kz2f4nR05GIFiITl
* 4bEjvpy7xqRDspJcCFIOcyXm8abVDhF+th6knSU0yLtNKuQVP6voMrnt9MV1X92L
* GZQLgdHZbPQz0Z5qIpaKhdyA8DEvWWvSUwwc+yi1/gGaybwlzZwqXYoPOhwMebzK
* Uk0xW14htcJrRrq+PXXQbRzTMynseCoPIoke0dtCodbA3qQxQovE16q9zz4Otv2k
* 4j63cz53J+mhkVWAeWxVGI0lltJmWtEYK6er8VqqWot3nqmWMXogrgRLggv/Nbbo
* oQIDAQAB
* -----END PUBLIC KEY-----
*
* -----BEGIN PUBLIC KEY-----
* MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvmpxVY7ld/8DAjz6F6q0
* 5shjg8/4p6047bn6/m8yPy1RBsvIyvuDuGnP/RzPEhzXQ9UJ5Ynmh2XJZgHoE9xb
* nfxL5BXHplJhMtADXKM9bWB11PU1Eioc3+AXBB8QiNFBn2XI5UkO5hPhbb9mJpjA
* 9Uhw8EdfqJP8QetVsI/xrCEbwEXe0xvifRLJbY08/Gp66KpQvy7g8w7VB8wlgePe
* xW3pT13Ap6vuC+mQuJPyiHvSxjEKHgqePji9NP3tJUFQjcECqcm0yV7/2d0t/pbC
* m+ZH1sadZspQCEPPrtbkQBlvHb4OLiIWPGHKSMeRFvp3IWcmdJqXahxLCUS1Eh6M
* AQIDAQAB
* -----END PUBLIC KEY-----
*
* Bytes can be got via
* $ openssl rsa -pubin -in key.pub -text -noout
* $ openssl rsa -in rsa.pem -RSAPublicKey_in -pubout > pub.pem
* $ openssl rsa -pubin -in pub.pem -text -noout
*/
private publisKeysHex = [{
/* private publisKeysHex = [{
modulus: 'c150023e2f70db7985ded064759cfecf0af328e69a41daf4d6f01b538135a6f91f8f8b2a0ec9ba9720ce352efcf6c5680ffc424bd634864902de0b4bd6d49f4e580230e3ae97d95c8b19442b3c0a10d8f5633fecedd6926a7f6dab0ddb7d457f9ea81b8465fcd6fffeed114011df91c059caedaf97625f6c96ecc74725556934ef781d866b34f011fce4d835a090196e9a5f0e4449af7eb697ddb9076494ca5f81104a305b6dd27665722c46b60e5df680fb16b210607ef217652e60236c255f6a28315f4083a96791d7214bf64c1df4fd0db1944fb26a2a57031b32eee64ad15a8ba68885cde74a5bfc920f6abf59ba5c75506373e7130f9042da922179251f',
exponent: '010001'
}, {
@ -88,18 +64,31 @@ export class RSAKeysManager { @@ -88,18 +64,31 @@ export class RSAKeysManager {
}, {
modulus: 'be6a71558ee577ff03023cfa17aab4e6c86383cff8a7ad38edb9fafe6f323f2d5106cbc8cafb83b869cffd1ccf121cd743d509e589e68765c96601e813dc5b9dfc4be415c7a6526132d0035ca33d6d6075d4f535122a1cdfe017041f1088d1419f65c8e5490ee613e16dbf662698c0f54870f0475fa893fc41eb55b08ff1ac211bc045ded31be27d12c96d8d3cfc6a7ae8aa50bf2ee0f30ed507cc2581e3dec56de94f5dc0a7abee0be990b893f2887bd2c6310a1e0a9e3e38bd34fded2541508dc102a9c9b4c95effd9dd2dfe96c29be647d6c69d66ca500843cfaed6e440196f1dbe0e2e22163c61ca48c79116fa77216726749a976a1c4b0944b5121e8c01',
exponent: '010001'
}]; */
private testPublicKeysHex: RSAPublicKeyHex[] = [{
modulus: 'c8c11d635691fac091dd9489aedced2932aa8a0bcefef05fa800892d9b52ed03200865c9e97211cb2ee6c7ae96d3fb0e15aeffd66019b44a08a240cfdd2868a85e1f54d6fa5deaa041f6941ddf302690d61dc476385c2fa655142353cb4e4b59f6e5b6584db76fe8b1370263246c010c93d011014113ebdf987d093f9d37c2be48352d69a1683f8f6e6c2167983c761e3ab169fde5daaa12123fa1beab621e4da5935e9c198f82f35eae583a99386d8110ea6bd1abb0f568759f62694419ea5f69847c43462abef858b4cb5edc84e7b9226cd7bd7e183aa974a712c079dde85b9dc063b8a5c08e8f859c0ee5dcd824c7807f20153361a7f63cfd2a433a1be7f5',
exponent: '010001'
}];
private publisKeysHex: RSAPublicKeyHex[] = [{
// modulus: '00e8bb3305c0b52c6cf2afdf7637313489e63e05268e5badb601af417786472e5f93b85438968e20e6729a301c0afc121bf7151f834436f7fda680847a66bf64accec78ee21c0b316f0edafe2f41908da7bd1f4a5107638eeb67040ace472a14f90d9f7c2b7def99688ba3073adb5750bb02964902a359fe745d8170e36876d4fd8a5d41b2a76cbff9a13267eb9580b2d06d10357448d20d9da2191cb5d8c93982961cdfdeda629e37f1fb09a0722027696032fe61ed663db7a37f6f263d370f69db53a0dc0a1748bdaaff6209d5645485e6e001d1953255757e4b8e42813347b11da6ab500fd0ace7e6dfa3736199ccaf9397ed0745a427dcfa6cd67bcb1acff3',
modulus: 'e8bb3305c0b52c6cf2afdf7637313489e63e05268e5badb601af417786472e5f93b85438968e20e6729a301c0afc121bf7151f834436f7fda680847a66bf64accec78ee21c0b316f0edafe2f41908da7bd1f4a5107638eeb67040ace472a14f90d9f7c2b7def99688ba3073adb5750bb02964902a359fe745d8170e36876d4fd8a5d41b2a76cbff9a13267eb9580b2d06d10357448d20d9da2191cb5d8c93982961cdfdeda629e37f1fb09a0722027696032fe61ed663db7a37f6f263d370f69db53a0dc0a1748bdaaff6209d5645485e6e001d1953255757e4b8e42813347b11da6ab500fd0ace7e6dfa3736199ccaf9397ed0745a427dcfa6cd67bcb1acff3',
exponent: '010001'
}];
private publicKeysParsed: {
[hex: string]: {
modulus: string,
exponent: string
}
[hex: string]: RSAPublicKeyHex
} = {};
private prepared = false;
private preparePromise: Promise<void> = null;
// prepareRsaKeys
constructor() {
if(Modes.test) {
this.publisKeysHex = this.testPublicKeysHex;
}
}
public prepare(): Promise<void> {
if(this.preparePromise) return this.preparePromise;
else if(this.prepared) {
@ -107,14 +96,14 @@ export class RSAKeysManager { @@ -107,14 +96,14 @@ export class RSAKeysManager {
}
return this.preparePromise = Promise.all(this.publisKeysHex.map(keyParsed => {
let RSAPublicKey = new TLSerialization();
const RSAPublicKey = new TLSerialization();
RSAPublicKey.storeBytes(bytesFromHex(keyParsed.modulus), 'n');
RSAPublicKey.storeBytes(bytesFromHex(keyParsed.exponent), 'e');
let buffer = RSAPublicKey.getBuffer();
const buffer = RSAPublicKey.getBuffer();
return CryptoWorker.sha1Hash(buffer).then(hash => {
let fingerprintBytes = bytesFromArrayBuffer(hash).slice(-8);
return CryptoWorker.invokeCrypto('sha1-hash', buffer).then(bytes => {
const fingerprintBytes = bytes.slice(-8);
fingerprintBytes.reverse();
this.publicKeysParsed[bytesToHex(fingerprintBytes).toLowerCase()] = {
@ -130,28 +119,25 @@ export class RSAKeysManager { @@ -130,28 +119,25 @@ export class RSAKeysManager {
});
}
// selectRsaKeyByFingerPrint
public async select(fingerprints: Array<any>) {
public async select(fingerprints: Array<string>) {
await this.prepare();
var fingerprintHex, foundKey, i;
for(i = 0; i < fingerprints.length; i++) {
for(let i = 0; i < fingerprints.length; ++i) {
//fingerprintHex = bigStringInt(fingerprints[i]).toString(16);
fingerprintHex = bigInt2str(str2bigInt(fingerprints[i], 10), 16).toLowerCase();
let fingerprintHex = bigInt2str(str2bigInt(fingerprints[i], 10), 16).toLowerCase();
if(fingerprintHex.length < 16) {
fingerprintHex = new Array(16 - fingerprintHex.length).fill('0').join('') + fingerprintHex;
}
//console.log(fingerprintHex, this.publicKeysParsed);
if(foundKey = this.publicKeysParsed[fingerprintHex]) {
const foundKey = this.publicKeysParsed[fingerprintHex];
if(foundKey) {
return Object.assign({
fingerprint: fingerprints[i]
}, foundKey);
}
}
return false;
}
}

2
src/lib/mtproto/schema.ts

File diff suppressed because one or more lines are too long

4
src/lib/mtproto/telegramMeWebManager.ts

@ -18,12 +18,12 @@ import sessionStorage from '../sessionStorage'; @@ -18,12 +18,12 @@ import sessionStorage from '../sessionStorage';
export class TelegramMeWebManager {
private disabled = /* false && */(Modes.test || App.domains.indexOf(location.hostname) === -1);
public async setAuthorized(canRedirect: boolean) {
public setAuthorized(canRedirect: boolean) {
if(this.disabled) {
return;
}
sessionStorage.get('tgme_sync').then((curValue) => {
return sessionStorage.get('tgme_sync').then((curValue) => {
const ts = tsNow(true);
if(canRedirect &&
curValue &&

10
src/lib/mtproto/timeManager.ts

@ -56,12 +56,12 @@ export class TimeManager { @@ -56,12 +56,12 @@ export class TimeManager {
const ret = longFromInts(messageId[0], messageId[1]);
/* if(lol[ret]) {
console.error('[TimeManager]: Generated SAME msg id', messageId, this.timeOffset, ret);
}
lol[ret] = true;
// if(lol[ret]) {
// console.error('[TimeManager]: Generated SAME msg id', messageId, this.timeOffset, ret);
// }
// lol[ret] = true;
console.log('[TimeManager]: Generated msg id', messageId, this.timeOffset, ret); */
// console.log('[TimeManager]: Generated msg id', messageId, this.timeOffset, ret);
return ret
}

134
src/lib/mtproto/tl_utils.ts

@ -20,22 +20,22 @@ import Schema, { MTProtoConstructor } from './schema'; @@ -20,22 +20,22 @@ import Schema, { MTProtoConstructor } from './schema';
import { gzipUncompress } from '../crypto/crypto_utils';
/// #endif
const boolFalse = +Schema.API.constructors.find(c => c.predicate === 'boolFalse').id >>> 0;
const boolTrue = +Schema.API.constructors.find(c => c.predicate === 'boolTrue').id >>> 0;
const vector = +Schema.API.constructors.find(c => c.predicate === 'vector').id >>> 0;
const gzipPacked = +Schema.MTProto.constructors.find(c => c.predicate === 'gzip_packed').id >>> 0;
const boolFalse = +Schema.API.constructors.find(c => c.predicate === 'boolFalse').id;
const boolTrue = +Schema.API.constructors.find(c => c.predicate === 'boolTrue').id;
const vector = +Schema.API.constructors.find(c => c.predicate === 'vector').id;
const gzipPacked = +Schema.MTProto.constructors.find(c => c.predicate === 'gzip_packed').id;
//console.log('boolFalse', boolFalse === 0xbc799737);
class TLSerialization {
public maxLength = 2048; // 2Kb
public offset = 0; // in bytes
public mtproto = false;
private maxLength = 2048; // 2Kb
private offset = 0; // in bytes
private mtproto = false;
private debug = false;//Modes.debug;
public buffer: ArrayBuffer;
public intView: Int32Array;
public byteView: Uint8Array;
private buffer: ArrayBuffer;
private intView: Int32Array;
private byteView: Uint8Array;
constructor(options: Partial<{startMaxLength: number, mtproto: true}> = {}) {
this.maxLength = options.startMaxLength || 2048; // 2Kb
@ -75,7 +75,7 @@ class TLSerialization { @@ -75,7 +75,7 @@ class TLSerialization {
public getBytes(typed: true): Uint8Array;
public getBytes(typed?: false): number[];
public getBytes(typed?: boolean): number[] | Uint8Array {
public getBytes(typed: boolean = true): number[] | Uint8Array {
if(typed) {
const resultBuffer = new ArrayBuffer(this.offset);
const resultArray = new Uint8Array(resultBuffer);
@ -85,13 +85,17 @@ class TLSerialization { @@ -85,13 +85,17 @@ class TLSerialization {
return resultArray;
}
const bytes: number[] = [];
const bytes: number[] = new Array(this.offset);
for(let i = 0; i < this.offset; i++) {
bytes.push(this.byteView[i]);
bytes[i] = this.byteView[i];
}
return bytes;
}
public getOffset() {
return this.offset;
}
public checkLength(needBytes: number) {
if(this.offset + needBytes < this.maxLength) {
return;
@ -221,10 +225,11 @@ class TLSerialization { @@ -221,10 +225,11 @@ class TLSerialization {
} else if(bytes === undefined) {
bytes = [];
}
this.debug && console.log('>>>', bytesToHex(bytes as number[]), (field || '') + ':bytes');
// if uint8array were json.stringified, then will be: {'0': 123, '1': 123}
const len = (bytes as ArrayBuffer).byteLength || (bytes as Uint8Array).length;
// if uint8array was json.stringified, then will be: {'0': 123, '1': 123}
const len = (bytes as Uint8Array).length;
this.checkLength(len + 8);
if(len <= 253) {
this.byteView[this.offset++] = len;
@ -244,22 +249,22 @@ class TLSerialization { @@ -244,22 +249,22 @@ class TLSerialization {
}
}
public storeIntBytes(bytes: any, bits: any, field?: string) {
public storeIntBytes(bytes: ArrayBuffer | Uint8Array | number[], bits: number, field?: string) {
if(bytes instanceof ArrayBuffer) {
bytes = new Uint8Array(bytes);
}
const len = bytes.length;
const len = (bytes as Uint8Array).length;
if((bits % 32) || (len * 8) !== bits) {
const error = new Error('Invalid bits: ' + bits + ', ' + bytes.length);
const error = new Error('Invalid bits: ' + bits + ', ' + len);
console.error(error, bytes, field);
throw error;
}
this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':int' + bits);
this.debug && console.log('>>>', bytesToHex(bytes as Uint8Array), (field || '') + ':int' + bits);
this.checkLength(len);
this.byteView.set(bytes, this.offset);
this.byteView.set(bytes as Uint8Array, this.offset);
this.offset += len;
}
@ -439,31 +444,28 @@ class TLSerialization { @@ -439,31 +444,28 @@ class TLSerialization {
}
class TLDeserialization {
public offset = 0; // in bytes
public override: {[key: string]: (result: any, field: string) => void};
private offset = 0; // in bytes
private override: {[key: string]: (result: any, field: string) => void};
public buffer: ArrayBuffer;
//public intView: Uint32Array;
public byteView: Uint8Array;
private buffer: ArrayBuffer;
private intView: Int32Array;
private byteView: Uint8Array;
// this.debug =
public mtproto: boolean = false;
private mtproto: boolean = false;
private debug: boolean;
constructor(buffer: ArrayBuffer | Uint8Array, options: Partial<{override: any, mtproto: true, debug: true}> = {}) {
//buffer = addPadding(buffer, 4, true); // fix 21.01.2020 for wss
if(buffer instanceof ArrayBuffer) {
this.buffer = buffer;
this.intView = new Int32Array(buffer);
this.byteView = new Uint8Array(this.buffer);
} else {
this.buffer = buffer.buffer;
this.intView = new Int32Array(buffer.buffer);
this.byteView = buffer;
}
//console.log("TCL: TLDeserialization -> constructor -> buffer", buffer, this.byteView, this.byteView.hex);
/* this.buffer = buffer;
//this.intView = new Uint32Array(this.buffer);
this.byteView = new Uint8Array(this.buffer); */
//console.log(this.intView);
@ -472,15 +474,15 @@ class TLDeserialization { @@ -472,15 +474,15 @@ class TLDeserialization {
this.debug = options.debug !== undefined ? options.debug : /* Modes.debug */false;
}
public readInt(field: string) {
private readInt(field: string) {
//if(this.offset >= this.intView.length * 4) {
if((this.byteView.length - this.offset) < 4) {
console.error(this.byteView, this.offset);
throw new Error('Nothing to fetch: ' + field);
}
//var i = this.intView[this.offset / 4];
const i = new Uint32Array(this.byteView.buffer.slice(this.offset, this.offset + 4))[0];
const i = this.intView[this.offset / 4];
// const i = new Uint32Array(this.byteView.buffer.slice(this.offset, this.offset + 4))[0];
this.debug/* || field.includes('[dialog][read_outbox_max_id]') */
&& console.log('<<<', i.toString(16), i, field,
@ -560,7 +562,7 @@ class TLDeserialization { @@ -560,7 +562,7 @@ class TLDeserialization {
return s;
}
public fetchBytes(field?: string): Uint8Array {
public fetchBytes(field?: string) {
let len = this.byteView[this.offset++];
if(len === 254) {
@ -584,7 +586,7 @@ class TLDeserialization { @@ -584,7 +586,7 @@ class TLDeserialization {
public fetchIntBytes(bits: number, typed: true, field?: string): Uint8Array;
public fetchIntBytes(bits: number, typed?: false, field?: string): number[];
public fetchIntBytes(bits: number, typed?: boolean, field?: string) {
public fetchIntBytes(bits: number, typed: boolean = true, field?: string) {
if(bits % 32) {
throw new Error('Invalid bits: ' + bits);
}
@ -596,9 +598,9 @@ class TLDeserialization { @@ -596,9 +598,9 @@ class TLDeserialization {
return result;
}
const bytes: number[] = [];
const bytes: number[] = new Array(len);
for(let i = 0; i < len; i++) {
bytes.push(this.byteView[this.offset++]);
bytes[i] = this.byteView[this.offset++];
}
this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':int' + bits);
@ -606,9 +608,9 @@ class TLDeserialization { @@ -606,9 +608,9 @@ class TLDeserialization {
return bytes;
}
public fetchRawBytes(len: any, typed: true, field: string): Uint8Array;
public fetchRawBytes(len: any, typed: false, field: string): number[];
public fetchRawBytes(len: any, typed: boolean, field: string) {
public fetchRawBytes(len: number | false, typed: true, field: string): Uint8Array;
public fetchRawBytes(len: number | false, typed: false, field: string): number[];
public fetchRawBytes(len: number | false, typed: boolean = true, field: string) {
if(len === false) {
len = this.readInt((field || '') + '_length');
if(len > this.byteView.byteLength) {
@ -623,9 +625,9 @@ class TLDeserialization { @@ -623,9 +625,9 @@ class TLDeserialization {
return bytes;
}
const bytes: number[] = [];
const bytes: number[] = new Array(len);
for(let i = 0; i < len; i++) {
bytes.push(this.byteView[this.offset++]);
bytes[i] = this.byteView[this.offset++];
}
this.debug && console.log('<<<', bytesToHex(bytes), (field || ''));
@ -633,7 +635,7 @@ class TLDeserialization { @@ -633,7 +635,7 @@ class TLDeserialization {
return bytes;
}
public fetchObject(type: any, field?: string): any {
public fetchObject(type: string, field?: string): any {
switch(type) {
case '#':
case 'int':
@ -641,11 +643,11 @@ class TLDeserialization { @@ -641,11 +643,11 @@ class TLDeserialization {
case 'long':
return this.fetchLong(field);
case 'int128':
return this.fetchIntBytes(128, false, field);
return this.fetchIntBytes(128, true, field);
case 'int256':
return this.fetchIntBytes(256, false, field);
return this.fetchIntBytes(256, true, field);
case 'int512':
return this.fetchIntBytes(512, false, field);
return this.fetchIntBytes(512, true, field);
case 'string':
return this.fetchString(field);
case 'bytes':
@ -660,30 +662,29 @@ class TLDeserialization { @@ -660,30 +662,29 @@ class TLDeserialization {
field = field || type || 'Object';
if(type.substr(0, 6) === 'Vector' || type.substr(0, 6) === 'vector') {
if(type.charAt(0).toLowerCase() === 'v' && type.substr(1, 5) === 'ector') {
if(type.charAt(0) === 'V') {
const constructor = this.readInt(field + '[id]');
const constructorCmp = constructor;
const constructorCmp = this.readInt(field + '[id]');
if(constructorCmp === gzipPacked) { // Gzip packed
const compressed = this.fetchBytes(field + '[packed_string]');
const uncompressed = gzipUncompress(compressed);
const uncompressed = gzipUncompress(compressed) as Uint8Array;
const newDeserializer = new TLDeserialization(uncompressed);
return newDeserializer.fetchObject(type, field);
}
if(constructorCmp !== vector) {
throw new Error('Invalid vector constructor ' + constructor);
throw new Error('Invalid vector constructor ' + constructorCmp);
}
}
const len = this.readInt(field + '[count]');
const result: any = [];
const result: any[] = new Array(len);
if(len > 0) {
const itemType = type.substr(7, type.length - 8); // for "Vector<itemType>"
for(let i = 0; i < len; i++) {
result.push(this.fetchObject(itemType, field + '[' + i + ']'));
for(let i = 0; i < len; ++i) {
result[i] = this.fetchObject(itemType, field + '[' + i + ']');
}
}
@ -700,18 +701,17 @@ class TLDeserialization { @@ -700,18 +701,17 @@ class TLDeserialization {
if(!constructorData) {
throw new Error('Constructor not found for type: ' + type);
}
} else if(type.charAt(0) >= 97 && type.charAt(0) <= 122) {
}/* else if(type.charAt(0) >= 97 && type.charAt(0) <= 122) {
constructorData = schema.constructors.find(c => c.predicate === type);
if(!constructorData) {
throw new Error('Constructor not found for predicate: ' + type);
}
} else {
const constructor = this.readInt(field + '[id]');
const constructorCmp = constructor;
} */ else {
const constructorCmp = this.readInt(field + '[id]');
if(constructorCmp === gzipPacked) { // Gzip packed
const compressed = this.fetchBytes(field + '[packed_string]');
const uncompressed = gzipUncompress(compressed);
const uncompressed = gzipUncompress(compressed) as Uint8Array;
const newDeserializer = new TLDeserialization(uncompressed);
return newDeserializer.fetchObject(type, field);
@ -726,7 +726,7 @@ class TLDeserialization { @@ -726,7 +726,7 @@ class TLDeserialization {
}
const i = index[constructorCmp];
if(i) {
if(i !== undefined) {
constructorData = schema.constructors[i];
}
@ -744,7 +744,7 @@ class TLDeserialization { @@ -744,7 +744,7 @@ class TLDeserialization {
}
if(!constructorData) {
console.error('Constructor not found:', constructor);
console.error('Constructor not found:', constructorCmp);
let int1: number, int2: number;
try {
@ -754,7 +754,7 @@ class TLDeserialization { @@ -754,7 +754,7 @@ class TLDeserialization {
}
throw new Error('Constructor not found: ' + constructor + ' ' + int1 + ' ' + int2 + ' ' + field);
throw new Error('Constructor not found: ' + constructorCmp + ' ' + int1 + ' ' + int2 + ' ' + field);
}
}
@ -812,14 +812,18 @@ class TLDeserialization { @@ -812,14 +812,18 @@ class TLDeserialization {
public getOffset() {
return this.offset;
}
public setOffset(offset: number) {
this.offset = offset;
}
public fetchEnd() {
/* public fetchEnd() {
if(this.offset !== this.byteView.length) {
throw new Error('Fetch end with non-empty buffer');
}
return true;
}
} */
}
MOUNT_CLASS_TO.TLDeserialization = TLDeserialization;

5
src/lib/mtproto/transports/http.ts

@ -4,7 +4,6 @@ @@ -4,7 +4,6 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { bytesFromArrayBuffer } from '../../../helpers/bytes';
import MTTransport from './transport';
export default class HTTP implements MTTransport {
@ -18,14 +17,14 @@ export default class HTTP implements MTTransport { @@ -18,14 +17,14 @@ export default class HTTP implements MTTransport {
if(response.status !== 200) {
response.arrayBuffer().then(buffer => {
console.log('not 200',
new TextDecoder("utf-8").decode(new Uint8Array(bytesFromArrayBuffer(buffer))));
new TextDecoder("utf-8").decode(new Uint8Array(buffer)));
})
throw response;
}
return response.arrayBuffer().then(buffer => {
return new Uint8Array(bytesFromArrayBuffer(buffer));
return new Uint8Array(buffer);
});
});
}

2
src/lib/mtproto/transports/tcpObfuscated.ts

@ -194,7 +194,7 @@ export default class TcpObfuscated implements MTTransport { @@ -194,7 +194,7 @@ export default class TcpObfuscated implements MTTransport {
connection.addEventListener('message', this.onMessage);
connection.addEventListener('close', () => {
connection.removeEventListener('message', this.onMessage);
}, true);
}, {once: true});
connection.close();
}
}

15
src/lib/storages/dialogs.ts

@ -150,9 +150,10 @@ export default class DialogsStorage { @@ -150,9 +150,10 @@ export default class DialogsStorage {
return this.dialogsOffsetDate[folderId] || 0;
}
public getFolder(id: number) {
public getFolder(id: number, skipMigrated = true) {
if(id <= 1) {
return this.byFolders[id] ?? (this.byFolders[id] = []);
const arr = this.byFolders[id] ?? (this.byFolders[id] = []);
return skipMigrated ? arr.filter(dialog => dialog.migratedTo === undefined) : arr;
}
const dialogs: {dialog: Dialog, index: number}[] = [];
@ -160,7 +161,7 @@ export default class DialogsStorage { @@ -160,7 +161,7 @@ export default class DialogsStorage {
for(const peerId in this.dialogs) {
const dialog = this.dialogs[peerId];
if(this.appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter)) {
if(this.appMessagesManager.filtersStorage.testDialogForFilter(dialog, filter) && (!skipMigrated || dialog.migratedTo === undefined)) {
let index: number;
const pinnedIndex = filter.pinned_peers.indexOf(dialog.peerId);
@ -189,7 +190,7 @@ export default class DialogsStorage { @@ -189,7 +190,7 @@ export default class DialogsStorage {
folders.push(dialogs[folderId]);
}
} else {
folders.push(this.getFolder(folderId));
folders.push(this.getFolder(folderId, skipMigrated));
}
for(let folder of folders) {
@ -329,7 +330,7 @@ export default class DialogsStorage { @@ -329,7 +330,7 @@ export default class DialogsStorage {
}
public pushDialog(dialog: Dialog, offsetDate?: number) {
const dialogs = this.getFolder(dialog.folder_id);
const dialogs = this.getFolder(dialog.folder_id, false);
const pos = dialogs.findIndex(d => d.peerId === dialog.peerId);
if(pos !== -1) {
dialogs.splice(pos, 1);
@ -554,7 +555,7 @@ export default class DialogsStorage { @@ -554,7 +555,7 @@ export default class DialogsStorage {
public getDialogs(query = '', offsetIndex?: number, limit = 20, folderId = 0) {
const realFolderId = folderId > 1 ? 0 : folderId;
let curDialogStorage = this.getFolder(folderId);
let curDialogStorage = this.getFolder(folderId, false);
if(query) {
if(!limit || this.cachedResults.query !== query || this.cachedResults.folderId !== folderId) {
@ -699,7 +700,7 @@ export default class DialogsStorage { @@ -699,7 +700,7 @@ export default class DialogsStorage {
this.generateIndexForDialog(dialog);
});
this.getFolder(folderId).forEach(dialog => {
this.getFolder(folderId, false).forEach(dialog => {
const peerId = dialog.peerId;
if(dialog.pFlags.pinned && !newPinned[peerId]) {
this.appMessagesManager.scheduleHandleNewDialogs(peerId);

4
src/mock/srp.ts

@ -26,6 +26,6 @@ export const M1 = new Uint8Array([ @@ -26,6 +26,6 @@ export const M1 = new Uint8Array([
72, 29, 26, 252, 69, 33, 152, 165, 104, 187, 154, 206, 10, 169, 23, 103, 35, 211, 240, 73, 60, 187, 50, 212, 42, 209, 241, 100, 91, 201, 77, 7
]);
export const passwordHashed = [
export const passwordHashed = new Uint8Array([
66, 92, 210, 197, 237, 255, 209, 109, 38, 17, 14, 200, 177, 152, 124, 167, 92, 166, 132, 205, 195, 184, 24, 240, 111, 118, 45, 43, 66, 66, 248, 49
];
]);

2
src/pages/pageAuthCode.ts

@ -30,7 +30,7 @@ let onFirstMount = (): Promise<any> => { @@ -30,7 +30,7 @@ let onFirstMount = (): Promise<any> => {
name: randomLong(),
length: CODELENGTH,
onFill: (code) => {
submitCode('' + code);
submitCode(code);
}
});

7
src/pages/pageSignIn.ts

@ -34,7 +34,7 @@ import { attachClickEvent } from "../helpers/dom/clickEvent"; @@ -34,7 +34,7 @@ import { attachClickEvent } from "../helpers/dom/clickEvent";
import replaceContent from "../helpers/dom/replaceContent";
import toggleDisability from "../helpers/dom/toggleDisability";
import sessionStorage from "../lib/sessionStorage";
import { TrueDcId } from "../types";
import { DcAuthKey } from "../types";
type Country = _Country & {
li?: HTMLLIElement[]
@ -324,7 +324,8 @@ let onFirstMount = () => { @@ -324,7 +324,8 @@ let onFirstMount = () => {
const signedCheckboxField = new CheckboxField({
text: 'Login.KeepSigned',
name: 'keepSession',
withRipple: true
withRipple: true,
checked: true
});
signedCheckboxField.input.addEventListener('change', () => {
@ -453,7 +454,7 @@ let onFirstMount = () => { @@ -453,7 +454,7 @@ let onFirstMount = () => {
const dcId = _dcs.shift();
if(!dcId) return;
const dbKey: `dc${TrueDcId}_auth_key` = `dc${dcId}_auth_key` as any;
const dbKey: DcAuthKey = `dc${dcId}_auth_key` as any;
const key = await sessionStorage.get(dbKey);
if(key) {
return g();

2
src/pages/pageSignQR.ts

@ -64,7 +64,7 @@ let onFirstMount = async() => { @@ -64,7 +64,7 @@ let onFirstMount = async() => {
rootScope.addEventListener('user_auth', () => {
stop = true;
cachedPromise = null;
}, true);
}, {once: true});
let options: {dcId?: DcId, ignoreErrors: true} = {ignoreErrors: true};
let prevToken: Uint8Array | number[];

13
src/scripts/format_schema.js

@ -30,14 +30,23 @@ let top = {}; @@ -30,14 +30,23 @@ let top = {};
//process.exit(0);
}); */
function uintToInt(val) {
if(val > 2147483647) {
val = val - 4294967296;
}
return val;
}
['MTProto', 'API'].forEach(key => {
let schema = json[key];
['constructors', 'methods'].forEach(key => {
schema[key].forEach(smth => {
if(+smth.id < 0) {
/* if(+smth.id < 0) {
smth.id = +smth.id + 4294967296;
}
} */
smth.id = uintToInt(+smth.id);
});
});

6
src/scripts/in/schema.json

File diff suppressed because one or more lines are too long

2
src/scripts/out/schema.json

File diff suppressed because one or more lines are too long

1
src/scss/partials/_selector.scss

@ -176,6 +176,7 @@ @@ -176,6 +176,7 @@
padding: 0;
transform: translateY(-50%);
top: 50%;
z-index: 1;
&:first-child {
margin-right: 1.6875rem;

75
src/tests/crypto_methods.test.ts

@ -1,33 +1,34 @@ @@ -1,33 +1,34 @@
import { bytesFromArrayBuffer, bytesFromHex, bytesToHex } from '../helpers/bytes';
import { bytesFromHex, bytesToHex } from '../helpers/bytes';
import CryptoWorker from '../lib/crypto/cryptoworker';
import type { RSAPublicKeyHex } from '../lib/mtproto/rsaKeysManager';
import '../lib/polyfill';
test('factorize', () => {
for(let i = 0; i < 10; ++i) {
CryptoWorker.factorize(new Uint8Array([20, 149, 30, 137, 202, 169, 105, 69])).then(pAndQ => {
CryptoWorker.invokeCrypto('factorize', new Uint8Array([20, 149, 30, 137, 202, 169, 105, 69])).then(pAndQ => {
pAndQ.pop();
expect(pAndQ).toEqual([[59, 165, 190, 67], [88, 86, 117, 215]]);
expect(pAndQ).toEqual([new Uint8Array([59, 165, 190, 67]), new Uint8Array([88, 86, 117, 215])]);
});
}
});
test('sha1', () => {
const bytes = new Uint8Array(bytesFromHex('ec5ac983081eeb1da706316227000000044af6cfb1000000046995dd57000000d55105998729349339eb322d86ec13bc0884f6ba0449d8ecbad0ef574837422579a11a88591796cdcc4c05690da0652462489286450179a635924bcc0ab83848'));
CryptoWorker.sha1Hash(bytes)
.then(buffer => {
CryptoWorker.invokeCrypto('sha1-hash', bytes)
.then(bytes => {
//console.log(bytesFromArrayBuffer(buffer));
let bytes = bytesFromArrayBuffer(buffer);
expect(bytes).toEqual([
expect(bytes).toEqual(new Uint8Array([
55, 160, 249, 190, 133, 135,
3, 45, 56, 157, 186, 81,
249, 0, 96, 235, 11, 10,
173, 197
]);
]));
});
});
test('sha256', () => {
CryptoWorker.sha256Hash(new Uint8Array([112, 20, 211, 20, 106, 249, 203, 252, 39, 107, 106, 194, 63, 60, 13, 130, 51, 78, 107, 6, 110, 156, 214, 65, 205, 10, 30, 150, 79, 10, 145, 194, 232, 240, 127, 55, 146, 103, 248, 227, 160, 172, 30, 153, 122, 189, 110, 162, 33, 86, 174, 117]))
CryptoWorker.invokeCrypto('sha256-hash', new Uint8Array([112, 20, 211, 20, 106, 249, 203, 252, 39, 107, 106, 194, 63, 60, 13, 130, 51, 78, 107, 6, 110, 156, 214, 65, 205, 10, 30, 150, 79, 10, 145, 194, 232, 240, 127, 55, 146, 103, 248, 227, 160, 172, 30, 153, 122, 189, 110, 162, 33, 86, 174, 117]))
.then(bytes => {
expect(bytes).toEqual(new Uint8Array([158, 59, 39, 247, 130, 244, 235, 160, 16, 249, 34, 114, 67, 171, 203, 208, 187, 72, 217, 106, 253, 62, 195, 242, 52, 118, 99, 72, 221, 29, 203, 95]));
});
@ -50,13 +51,67 @@ test('sha256', () => { @@ -50,13 +51,67 @@ test('sha256', () => {
payload.forEach(pair => {
//const uint8 = new TextEncoder().encode(pair[0]);
//CryptoWorker.sha256Hash(new Uint8Array(pair[0].split('').map(c => c.charCodeAt(0)))).then(bytes => {
CryptoWorker.sha256Hash(pair[0]).then(bytes => {
CryptoWorker.invokeCrypto('sha256-hash', pair[0]).then(bytes => {
const hex = bytesToHex(bytes);
expect(hex).toEqual(pair[1]);
});
});
});
test('rsa', () => {
const publicKey: RSAPublicKeyHex = {
// "fingerprint": "15032203592031600005",
"modulus": "e8bb3305c0b52c6cf2afdf7637313489e63e05268e5badb601af417786472e5f93b85438968e20e6729a301c0afc121bf7151f834436f7fda680847a66bf64accec78ee21c0b316f0edafe2f41908da7bd1f4a5107638eeb67040ace472a14f90d9f7c2b7def99688ba3073adb5750bb02964902a359fe745d8170e36876d4fd8a5d41b2a76cbff9a13267eb9580b2d06d10357448d20d9da2191cb5d8c93982961cdfdeda629e37f1fb09a0722027696032fe61ed663db7a37f6f263d370f69db53a0dc0a1748bdaaff6209d5645485e6e001d1953255757e4b8e42813347b11da6ab500fd0ace7e6dfa3736199ccaf9397ed0745a427dcfa6cd67bcb1acff3",
"exponent": "010001"
};
const bytes = new Uint8Array([
128, 44, 176, 17, 43, 185, 92, 222, 101, 45, 211, 184, 175, 154,
124, 57, 15, 214, 164, 165, 113, 127, 147, 133, 5, 140, 185, 174,
99, 182, 38, 56, 213, 169, 199, 173, 52, 240, 128, 225, 246, 190,
234, 221, 108, 175, 228, 25, 204, 154, 57, 235, 143, 95, 98, 15,
8, 100, 65, 117, 58, 91, 110, 200, 76, 207, 234, 44, 21, 138, 99,
134, 212, 188, 177, 72, 177, 203, 60, 145, 209, 63, 35, 230, 185,
73, 26, 103, 199, 71, 54, 53, 183, 182, 218, 163, 209, 26, 248,
231, 170, 70, 224, 204, 137, 177, 9, 228, 176, 212, 231, 137,
104, 205, 1, 68, 172, 59, 53, 246, 33, 95, 193, 158, 52, 203,
230, 57, 177, 7, 190, 97, 183, 79, 154, 242, 187, 170, 65, 30, 82,
102, 10, 1, 188, 191, 69, 156, 174, 208, 173, 141, 58, 190, 46,
243, 78, 200, 129, 210, 184, 100, 130, 83, 191, 107, 192, 143, 44,
232, 163, 150, 67, 62, 15, 91, 141, 115, 172, 183, 206, 133, 131,
239, 149, 133, 39, 15, 187, 200, 239, 75, 209, 102, 27, 185, 223,
186, 156, 34, 112, 120, 223, 37, 105, 130, 184, 232, 56, 173, 0,
165, 156, 83, 207, 134, 167, 32, 57, 60, 177, 219, 127, 102, 247,
76, 60, 248, 16, 0, 232, 215, 5, 235, 79, 237, 181, 229, 216, 97,
45, 52, 252, 109, 44, 94, 55, 113, 248, 125, 60, 216, 152, 79, 4, 7
]);
const good = new Uint8Array([
166, 252, 51, 235, 146, 3, 147, 182, 43, 71, 71, 180, 236, 84, 235,
122, 40, 36, 254, 75, 52, 194, 162, 6, 166, 44, 227, 83, 148, 215,
72, 75, 80, 32, 100, 106, 172, 59, 220, 231, 233, 39, 122, 167, 255,
209, 132, 170, 109, 31, 151, 227, 70, 39, 196, 240, 25, 77, 255,
178, 17, 156, 153, 18, 19, 157, 208, 116, 49, 236, 150, 249, 245,
149, 226, 176, 101, 20, 201, 198, 177, 75, 166, 62, 151, 119, 64,
67, 253, 12, 199, 62, 210, 162, 59, 143, 170, 189, 66, 158, 51, 168,
56, 173, 231, 214, 100, 85, 54, 183, 1, 177, 162, 75, 245, 87, 205,
199, 245, 109, 60, 144, 78, 114, 38, 38, 71, 36, 34, 240, 40, 119,
154, 244, 35, 22, 5, 110, 174, 153, 62, 114, 182, 2, 180, 92, 137,
224, 218, 147, 197, 211, 168, 245, 147, 171, 80, 123, 178, 112, 76,
24, 104, 236, 117, 191, 60, 219, 25, 205, 128, 19, 59, 46, 67, 30,
240, 117, 194, 44, 247, 50, 55, 87, 139, 224, 23, 152, 129, 182,
101, 202, 24, 190, 67, 253, 63, 172, 210, 21, 151, 1, 30, 164, 52,
77, 75, 128, 86, 80, 177, 202, 69, 67, 65, 120, 217, 164, 251, 29,
86, 185, 43, 175, 22, 124, 10, 175, 181, 223, 130, 232, 47, 134, 67,
54, 226, 253, 25, 230, 197, 109, 205, 240, 242, 65, 233, 17, 98,
120, 106, 17, 142, 143, 9, 233
]);
CryptoWorker.invokeCrypto('rsa-encrypt', bytes, publicKey).then(encrypted => {
expect(encrypted).toEqual(good);
});
});
test('pbkdf2', () => {
/* const crypto = require('crypto');

7
src/tests/srp.test.ts

@ -1,5 +1,8 @@ @@ -1,5 +1,8 @@
import { salt1, salt2, g, p, srp_id, secure_random, srp_B, password, A, M1, passwordHashed } from '../mock/srp';
import { computeSRP, makePasswordHash } from '../lib/crypto/srp';
import '../lib/polyfill';
import assumeType from '../helpers/assumeType';
import { InputCheckPasswordSRP } from '../layer';
test('2FA hash', async() => {
const bytes = await makePasswordHash(password, salt1, salt2);
@ -22,7 +25,9 @@ test('2FA whole (with negative)', async() => { @@ -22,7 +25,9 @@ test('2FA whole (with negative)', async() => {
new_algo: null,
new_secure_algo: null
}, false).then((res: any) => {
}, false).then((res) => {
assumeType<InputCheckPasswordSRP.inputCheckPasswordSRP>(res);
expect(res.srp_id).toEqual(srp_id);
expect(res.A).toEqual(A);
expect(res.M1).toEqual(M1);

17
src/types.d.ts vendored

@ -3,6 +3,8 @@ import type { ApiError } from "./lib/mtproto/apiManager"; @@ -3,6 +3,8 @@ import type { ApiError } from "./lib/mtproto/apiManager";
export type DcId = number;
export type TrueDcId = 1 | 2 | 3 | 4 | 5;
export type DcAuthKey = `dc${TrueDcId}_auth_key`;
export type DcServerSalt = `dc${TrueDcId}_server_salt`;
export type InvokeApiOptions = Partial<{
dcId: DcId,
@ -50,6 +52,21 @@ export type NoneToVoidFunction = () => void; @@ -50,6 +52,21 @@ export type NoneToVoidFunction = () => void;
export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
// https://stackoverflow.com/a/60762482/6758968
type Shift<A extends Array<any>> = ((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;
type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
0: GrowExpRev<[...A, ...P[0]], N, P>,
1: GrowExpRev<A, N, Shift<P>>
}[[...A, ...P[0]][N] extends undefined ? 0 : 1];
type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {
0: GrowExp<[...A, ...A], N, [A, ...P]>,
1: GrowExpRev<A, N, P>
}[[...A, ...A][N] extends undefined ? 0 : 1];
export type FixedSizeArray<T, N extends number> = N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T, T], N, [[T]]>;
export type AuthState = AuthState.signIn | AuthState.signQr | AuthState.authCode | AuthState.password | AuthState.signUp | AuthState.signedIn;
export namespace AuthState {
export type signIn = {

Loading…
Cancel
Save