├── .editorconfig ├── .github └── workflows │ ├── check.yml │ ├── madge.yml │ └── regenerate_types.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── deno.jsonc ├── examples └── basic.ts └── src ├── README.md ├── classes.ts ├── client ├── 2fa.ts ├── abstract_telegram_client.ts ├── auth.ts ├── base_client.ts ├── bots.ts ├── buttons.ts ├── chats.ts ├── dialogs.ts ├── downloads.ts ├── message_parse.ts ├── messages.ts ├── mod.ts ├── telegram_client.ts ├── types.ts ├── updates.ts ├── uploads.ts ├── users.ts └── utils.ts ├── crypto ├── authkey.ts ├── converters.ts ├── crypto.ts ├── ctr.ts ├── factorizator.ts ├── ige.ts ├── mod.ts └── rsa.ts ├── define.d.ts ├── deps.ts ├── entity_cache.ts ├── errors ├── common.ts ├── mod.ts ├── rpc_base_errors.ts └── rpc_error_list.ts ├── events ├── album.ts ├── callback_query.ts ├── common.ts ├── deleted_message.ts ├── edited_message.ts ├── mod.ts ├── new_message.ts └── raw.ts ├── extensions ├── async_queue.ts ├── binary_reader.ts ├── binary_writer.ts ├── html.ts ├── interfaces.ts ├── logger.ts ├── markdown.ts ├── message_packer.ts ├── promised_net_sockets.ts └── promised_web_sockets.ts ├── helpers.ts ├── mod.ts ├── network ├── authenticator.ts ├── connection │ ├── connection.ts │ ├── tcp_full.ts │ ├── tcp_obfuscated.ts │ ├── tcpa_bridged.ts │ ├── tcpmt_proxy.ts │ └── types.ts ├── mod.ts ├── mtproto_plain_sender.ts ├── mtproto_sender.ts ├── mtproto_state.ts └── request_state.ts ├── password.ts ├── request_iter.ts ├── sessions ├── abstract.ts ├── memory_session.ts ├── mod.ts ├── store_session.ts └── string_session.ts ├── tl ├── all_tl_objects.ts ├── api.d.ts ├── api.js ├── api_tl.ts ├── core │ ├── core_objects.ts │ ├── gzip_packed.ts │ ├── message_container.ts │ ├── mod.ts │ ├── rpc_result.ts │ └── tl_message.ts ├── custom │ ├── button.ts │ ├── chat_getter.ts │ ├── dialog.ts │ ├── draft.ts │ ├── file.ts │ ├── forward.ts │ ├── inline_result.ts │ ├── inline_results.ts │ ├── message.ts │ ├── message_button.ts │ ├── mod.ts │ └── sender_getter.ts ├── generate_module.ts ├── generation_helpers.ts ├── helpers.ts ├── mod.ts ├── patcher.ts ├── schema_tl.ts ├── static │ ├── api.tl │ └── schema.tl └── types_generator │ ├── generate.ts │ └── template.ts ├── utils.ts └── version.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | charset = utf-8 4 | indent_style = space 5 | indent_size = 2 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | max_line_length = 80 -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: denoland/setup-deno@v1 16 | with: 17 | deno-version: v1.x 18 | 19 | - name: Format 20 | run: deno fmt --check 21 | 22 | - name: Lint 23 | run: deno lint 24 | 25 | - name: Check 26 | run: deno check src/mod.ts 27 | -------------------------------------------------------------------------------- /.github/workflows/madge.yml: -------------------------------------------------------------------------------- 1 | name: Circular Dependencies 2 | 3 | on: push 4 | 5 | jobs: 6 | check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - run: npx madge --circular --extensions ts ./ 12 | -------------------------------------------------------------------------------- /.github/workflows/regenerate_types.yml: -------------------------------------------------------------------------------- 1 | name: Regenerate Types 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | regenerate-types: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | 13 | - uses: denoland/setup-deno@v1 14 | with: 15 | deno-version: v1.x 16 | 17 | - run: deno task generate-mod 18 | 19 | - run: | 20 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 21 | git config --local user.name "github-actions[bot]" 22 | git commit -am "Regenerate types" || true 23 | 24 | - uses: ad-m/github-push-action@master 25 | with: 26 | github_token: ${{ secrets.GITHUB_TOKEN }} 27 | branch: ${{ github.ref }} 28 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno", 4 | "editorconfig.editorconfig" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2023 Dunkan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > 3 | > Grm is no longer maintained. Check out [MTKruto](https://mtkru.to) or [mtcute](https://mtcute.dev). Have a great day, good bye! 4 | 5 | # Grm [![deno module](https://shield.deno.dev/x/grm)](https://deno.land/x/grm) 6 | 7 | > MTProto client for Deno ported from [GramJS](https://github.com/gram-js/gramjs). 8 | 9 | ## Documentation 10 | 11 | Currently, there is no documentation dedicated to Grm. 12 | You can use the GramJS documentation and API reference. 13 | 14 | - 15 | - 16 | - 17 | 18 | ## Quick Start 19 | 20 | Here you'll learn how to obtain necessary information to initialize the client, authorize your personal account and send yourself a message. 21 | 22 | First, you'll need to obtain an API ID and hash: 23 | 24 | 1. Visit [my.telegram.org](https://my.telegram.org) and sign in. 25 | 2. Click "API development tools" and fill your application details (only the app title and the short name are required). 26 | 3. Click "Create application". 27 | 28 | > Don't leak your API credentials. 29 | > They can't be revoked. 30 | 31 | Then use your API credentials with the following example code: 32 | 33 | ```ts 34 | import { StringSession, TelegramClient } from "https://deno.land/x/grm/mod.ts"; 35 | 36 | const apiId = 123456; 37 | const apiHash = "abcd1234"; 38 | 39 | // Fill in this later with the value from `client.session.save()`, 40 | // so that you don't have to login every time. 41 | const stringSession = new StringSession(""); 42 | 43 | console.log("Loading interactive example..."); 44 | const client = new TelegramClient(stringSession, apiId, apiHash); 45 | 46 | await client.start({ 47 | phoneNumber: () => prompt("Phone number:")!, 48 | password: () => prompt("Password:")!, 49 | phoneCode: () => prompt("Verification code:")!, 50 | onError: console.error, 51 | }); 52 | 53 | console.log("Connected."); 54 | console.log(client.session.save()); 55 | 56 | // Send yourself a message. 57 | await client.sendMessage("me", { message: "Hello, world!" }); 58 | ``` 59 | 60 | You'll be prompted to enter your phone number (in international format), the 61 | code you received from Telegram, and your 2FA password if you have one set. 62 | 63 | You can then save output of `client.session.save()` and use it in `new StringSession("here")` to not login again each time. 64 | 65 | After connecting successfully, you should have a text message saying "Hello, world!" in 66 | your Saved Messages. 67 | 68 | Check out [examples/](examples/) for more examples. 69 | 70 | ## Used by 71 | 72 | Here are some awesome projects powered by Grm: 73 | 74 | - [xorgram/xor](https://github.com/xorgram/xor) 75 | 76 | Add yours to this list by opening a pull request. 77 | 78 | ## Contributing 79 | 80 | Feel free to open pull requests related to improvements and fixes to the core library and documentation. 81 | We are currently following API changes in the original GramJS repository applying them here. 82 | 83 | We'd appreciate if you could help with migrating from Node.js modules such as 84 | [socks](https://github.com/JoshGlazebrook/socks) and 85 | [websocket](https://github.com/theturtle32/WebSocket-Node) to Deno APIs. 86 | 87 | ## Credits 88 | 89 | This port wouldn't exist without these wonderful people. Thanks to 90 | 91 | - the original 92 | [authors and contributors](https://github.com/gram-js/gramjs/graphs/contributors) 93 | of GramJS, 94 | - authors of the dependencies, 95 | - authors of the already ported dependencies, 96 | - [contributors](https://github.com/dcdunkan/grm/graphs/contributors) of this 97 | repository, 98 | - and everyone else who were a part of this. 99 | 100 | --- 101 | 102 | ## Notes 103 | 104 | This is a _direct_ port of GramJS for Deno. 105 | This was just an attempt, which turned out to be a successful one. 106 | Most of the commonly used features are working as expected. 107 | 108 | It took me like 4 days; a total of 20h6m for this repository alone. 109 | Including dependency porting and reading the original code, it is a total of almost 110 | 34.8h for the first release. 111 | I didn't just copy and paste stuff — I did, but I 112 | manually wrote lot of the files. 113 | It made me realize how much effort have been put into the development of [GramJS](https://github.com/gram-js/gramjs). 114 | You should definitely give it a star if you're using this library. 115 | 116 | I had to port the following Node.js modules to Deno: 117 | 118 | - [JoshGlazebrook/socks](https://github.com/JoshGlazebrook/socks) — 119 | [deno_socks](https://github.com/dcdunkan/deno_socks) 120 | - [indutny/node-ip](https://github.com/indutny/node-ip) — 121 | [deno_ip](https://github.com/dcdunkan/deno_ip) 122 | - [JoshGlazebrook/smart-buffer](https://github.com/JoshGlazebrook/smart-buffer) 123 | — [deno_smart_buffer](https://github.com/dcdunkan/deno_smart_buffer) 124 | - [spalt08/cryptography](https://github.com/spalt08/cryptography) — 125 | [deno_cryptography](https://github.com/dcdunkan/deno_cryptography) 126 | 127 | > I know that some of them should not have been ported, but I didn't realized that then. 128 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "tasks": { 4 | "generate-mod": "deno run --allow-read --allow-env --allow-net --allow-write src/tl/generate_module.ts && deno fmt src/tl/api.d.ts" 5 | }, 6 | "fmt": { 7 | "options": { 8 | "indentWidth": 2, 9 | "proseWrap": "preserve" 10 | } 11 | }, 12 | "compilerOptions": { 13 | "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic.ts: -------------------------------------------------------------------------------- 1 | import { StringSession, TelegramClient } from "https://deno.land/x/grm/mod.ts"; 2 | 3 | // Login and create an application on https://my.telegram.org 4 | // to get values for API ID and API Hash. 5 | const apiId = 123456; 6 | const apiHash = "abcd1234"; 7 | 8 | // Fill in this later with the value from `client.session.save()`, 9 | // so you don't have to login each time you run the file. 10 | const stringSession = new StringSession(""); 11 | 12 | console.log("Loading interactive example..."); 13 | const client = new TelegramClient(stringSession, apiId, apiHash); 14 | 15 | await client.start({ 16 | phoneNumber: () => prompt("Enter your phone number:")!, 17 | password: () => prompt("Enter your password:")!, 18 | phoneCode: () => prompt("Enter the code you received:")!, 19 | onError: (err) => console.log(err), 20 | }); 21 | 22 | console.log("You should now be connected."); 23 | 24 | // Send a message to yourself 25 | await client.sendMessage("me", { message: "Hello!" }); 26 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # grm 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/classes.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "./deps.ts"; 2 | 3 | export class CustomFile { 4 | name: string; 5 | size: number; 6 | path: string; 7 | buffer?: Buffer; 8 | 9 | constructor(name: string, size: number, path: string, buffer?: Buffer) { 10 | this.name = name; 11 | this.size = size; 12 | this.path = path; 13 | this.buffer = buffer; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/2fa.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { generateRandomBytes } from "../helpers.ts"; 3 | import { computeCheck, computeDigest } from "../password.ts"; 4 | import { EmailUnconfirmedError } from "../errors/mod.ts"; 5 | import { Buffer } from "../deps.ts"; 6 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 7 | import { TwoFaParams } from "./types.ts"; 8 | 9 | export async function updateTwoFaSettings( 10 | client: AbstractTelegramClient, 11 | { 12 | isCheckPassword, 13 | currentPassword, 14 | newPassword, 15 | hint = "", 16 | email, 17 | emailCodeCallback, 18 | onEmailCodeError, 19 | }: TwoFaParams, 20 | ) { 21 | if (!newPassword && !currentPassword) { 22 | throw new Error( 23 | "Neither `currentPassword` nor `newPassword` is present", 24 | ); 25 | } 26 | 27 | if (email && !(emailCodeCallback && onEmailCodeError)) { 28 | throw new Error( 29 | "`email` present without `emailCodeCallback` and `onEmailCodeError`", 30 | ); 31 | } 32 | 33 | const pwd = await client.invoke(new Api.account.GetPassword()); 34 | 35 | if (!(pwd.newAlgo instanceof Api.PasswordKdfAlgoUnknown)) { 36 | pwd.newAlgo.salt1 = Buffer.concat([ 37 | pwd.newAlgo.salt1, 38 | generateRandomBytes(32), 39 | ]); 40 | } 41 | if (!pwd.hasPassword && currentPassword) { 42 | currentPassword = undefined; 43 | } 44 | 45 | const password = currentPassword 46 | ? await computeCheck(pwd, currentPassword!) 47 | : new Api.InputCheckPasswordEmpty(); 48 | 49 | if (isCheckPassword) { 50 | await client.invoke(new Api.auth.CheckPassword({ password })); 51 | return; 52 | } 53 | if (pwd.newAlgo instanceof Api.PasswordKdfAlgoUnknown) { 54 | throw new Error("Unknown password encryption method"); 55 | } 56 | try { 57 | await client.invoke( 58 | new Api.account.UpdatePasswordSettings({ 59 | password, 60 | newSettings: new Api.account.PasswordInputSettings({ 61 | newAlgo: pwd.newAlgo, 62 | newPasswordHash: newPassword 63 | ? await computeDigest(pwd.newAlgo, newPassword) 64 | : Buffer.alloc(0), 65 | hint, 66 | email, 67 | // not explained what it does and it seems to always be set to empty in tdesktop 68 | newSecureSettings: undefined, 69 | }), 70 | }), 71 | ); 72 | } catch (e) { 73 | if (e instanceof EmailUnconfirmedError) { 74 | while (true) { 75 | try { 76 | const code = await emailCodeCallback!(e.codeLength); 77 | if (!code) throw new Error("Code is empty"); 78 | await client.invoke(new Api.account.ConfirmPasswordEmail({ code })); 79 | break; 80 | } catch (err) { 81 | onEmailCodeError!(err); 82 | } 83 | } 84 | } else { 85 | throw e; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/client/bots.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { InlineResults } from "../tl/custom/inline_results.ts"; 3 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 4 | 5 | import GetInlineBotResults = Api.messages.GetInlineBotResults; 6 | 7 | export async function inlineQuery( 8 | client: AbstractTelegramClient, 9 | bot: Api.TypeEntityLike, 10 | query: string, 11 | entity?: Api.InputPeerSelf, 12 | offset?: string, 13 | geoPoint?: Api.TypeInputGeoPoint, 14 | ): Promise { 15 | bot = await client.getInputEntity(bot); 16 | let peer: Api.TypeInputPeer = new Api.InputPeerSelf(); 17 | if (entity) peer = await client.getInputEntity(entity); 18 | 19 | const result = await client.invoke( 20 | new GetInlineBotResults({ 21 | bot: bot, 22 | peer: peer, 23 | query: query, 24 | offset: offset || "", 25 | geoPoint: geoPoint, 26 | }), 27 | ); 28 | return new InlineResults(client, result, entity ? peer : undefined); 29 | } 30 | -------------------------------------------------------------------------------- /src/client/buttons.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { Button } from "../tl/custom/button.ts"; 3 | import { MessageButton } from "../tl/custom/message_button.ts"; 4 | import { isArrayLike } from "../helpers.ts"; 5 | 6 | export function buildReplyMarkup( 7 | buttons: 8 | | Api.TypeReplyMarkup 9 | | undefined 10 | | Api.TypeButtonLike 11 | | Api.TypeButtonLike[] 12 | | Api.TypeButtonLike[][], 13 | inlineOnly = false, 14 | ): Api.TypeReplyMarkup | undefined { 15 | if (buttons == undefined) { 16 | return undefined; 17 | } 18 | if ("SUBCLASS_OF_ID" in buttons) { 19 | if (buttons.SUBCLASS_OF_ID == 0xe2e10ef2) { 20 | return buttons; 21 | } 22 | } 23 | if (!isArrayLike(buttons)) { 24 | buttons = [[buttons]]; 25 | } else if (!buttons || !isArrayLike(buttons[0])) { 26 | // @ts-ignore Blah Blah 27 | buttons = [buttons]; 28 | } 29 | let isInline = false; 30 | let isNormal = false; 31 | let resize = undefined; 32 | const singleUse = false; 33 | const selective = false; 34 | 35 | const rows = []; 36 | // @ts-ignore heh 37 | for (const row of buttons) { 38 | const current = []; 39 | for (let button of row) { 40 | if (button instanceof Button) { 41 | if (button.resize != undefined) { 42 | resize = button.resize; 43 | } 44 | if (button.singleUse != undefined) { 45 | resize = button.singleUse; 46 | } 47 | if (button.selective != undefined) { 48 | resize = button.selective; 49 | } 50 | button = button.button; 51 | } else if (button instanceof MessageButton) { 52 | button = button.button; 53 | } 54 | const inline = Button._isInline(button); 55 | if (!isInline && inline) { 56 | isInline = true; 57 | } 58 | if (!isNormal && inline) { 59 | isNormal = false; 60 | } 61 | if (button.SUBCLASS_OF_ID == 0xbad74a3) { 62 | // 0xbad74a3 == crc32(b'KeyboardButton') 63 | current.push(button); 64 | } 65 | } 66 | if (current) { 67 | rows.push( 68 | new Api.KeyboardButtonRow({ 69 | buttons: current, 70 | }), 71 | ); 72 | } 73 | } 74 | if (inlineOnly && isNormal) { 75 | throw new Error("You cannot use non-inline buttons here"); 76 | } else if (isInline === isNormal && isNormal) { 77 | throw new Error("You cannot mix inline with normal buttons"); 78 | } else if (isInline) { 79 | return new Api.ReplyInlineMarkup({ 80 | rows: rows, 81 | }); 82 | } 83 | return new Api.ReplyKeyboardMarkup({ 84 | rows: rows, 85 | resize: resize, 86 | singleUse: singleUse, 87 | selective: selective, 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/client/chats.ts: -------------------------------------------------------------------------------- 1 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 2 | import { TotalList } from "../helpers.ts"; 3 | import { EntityType_, entityType_ } from "../tl/helpers.ts"; 4 | import { getDisplayName } from "../utils.ts"; 5 | import { RequestIter } from "../request_iter.ts"; 6 | import { Api } from "../tl/api.js"; 7 | import { bigInt } from "../deps.ts"; 8 | import { IterParticipantsParams } from "./types.ts"; 9 | 10 | const MAX_PARTICIPANTS_CHUNK_SIZE = 200; 11 | 12 | interface ParticipantsIterInterface { 13 | entity: Api.TypeEntityLike; 14 | // deno-lint-ignore no-explicit-any 15 | filter: any; 16 | offset?: number; 17 | search?: string; 18 | showTotal?: boolean; 19 | } 20 | 21 | export class _ParticipantsIter extends RequestIter { 22 | private filterEntity: ((entity: Api.TypeEntity) => boolean) | undefined; 23 | private requests?: Api.channels.GetParticipants[]; 24 | 25 | async _init({ 26 | entity, 27 | filter, 28 | offset, 29 | search, 30 | showTotal, 31 | }: ParticipantsIterInterface): Promise { 32 | if (!offset) offset = 0; 33 | if (filter && filter.constructor === Function) { 34 | if ( 35 | [ 36 | Api.ChannelParticipantsBanned, 37 | Api.ChannelParticipantsKicked, 38 | Api.ChannelParticipantsSearch, 39 | Api.ChannelParticipantsContacts, 40 | ].includes(filter) 41 | ) { 42 | filter = new filter({ 43 | q: "", 44 | }); 45 | } else { 46 | filter = new filter(); 47 | } 48 | } 49 | entity = await this.client.getInputEntity(entity); 50 | const ty = entityType_(entity); 51 | if (search && (filter || ty != EntityType_.CHANNEL)) { 52 | // We need to 'search' ourselves unless we have a PeerChannel 53 | search = search.toLowerCase(); 54 | this.filterEntity = (entity: Api.TypeEntity) => { 55 | return ( 56 | getDisplayName(entity) 57 | .toLowerCase() 58 | .includes(search!) || 59 | ("username" in entity ? entity.username || "" : "") 60 | .toLowerCase() 61 | .includes(search!) 62 | ); 63 | }; 64 | } else { 65 | this.filterEntity = (_entity) => true; 66 | } 67 | // Only used for channels, but we should always set the attribute 68 | this.requests = []; 69 | if (ty == EntityType_.CHANNEL) { 70 | if (showTotal) { 71 | const channel = await this.client.invoke( 72 | new Api.channels.GetFullChannel({ 73 | channel: entity, 74 | }), 75 | ); 76 | if (!(channel.fullChat instanceof Api.ChatFull)) { 77 | this.total = channel.fullChat.participantsCount; 78 | } 79 | } 80 | if (this.total && this.total <= 0) { 81 | return false; 82 | } 83 | this.requests.push( 84 | new Api.channels.GetParticipants({ 85 | channel: entity, 86 | filter: filter || 87 | new Api.ChannelParticipantsSearch({ 88 | q: search || "", 89 | }), 90 | offset, 91 | limit: MAX_PARTICIPANTS_CHUNK_SIZE, 92 | hash: bigInt.zero, 93 | }), 94 | ); 95 | } else if (ty == EntityType_.CHAT) { 96 | if (!(typeof entity === "object" && "chatId" in entity)) { 97 | throw new Error( 98 | "Found chat without id " + JSON.stringify(entity), 99 | ); 100 | } 101 | 102 | const full = await this.client.invoke( 103 | new Api.messages.GetFullChat({ 104 | chatId: entity.chatId, 105 | }), 106 | ); 107 | 108 | if (full.fullChat instanceof Api.ChatFull) { 109 | if ( 110 | !( 111 | full.fullChat.participants instanceof 112 | Api.ChatParticipantsForbidden 113 | ) 114 | ) { 115 | this.total = full.fullChat.participants.participants.length; 116 | } else { 117 | this.total = 0; 118 | return false; 119 | } 120 | 121 | const users = new Map(); 122 | for (const user of full.users) { 123 | users.set(user.id.toString(), user); 124 | } 125 | for ( 126 | const participant of full.fullChat.participants 127 | .participants 128 | ) { 129 | const user = users.get(participant.userId.toString())!; 130 | if (!this.filterEntity(user)) { 131 | continue; 132 | } 133 | // deno-lint-ignore no-explicit-any 134 | (user as any).participant = participant; 135 | this.buffer?.push(user); 136 | } 137 | return true; 138 | } 139 | } else { 140 | this.total = 1; 141 | if (this.limit != 0) { 142 | const user = await this.client.getEntity(entity); 143 | if (this.filterEntity(user)) { 144 | // deno-lint-ignore no-explicit-any 145 | (user as any).participant = undefined; 146 | this.buffer?.push(user); 147 | } 148 | } 149 | return true; 150 | } 151 | } 152 | 153 | async _loadNextChunk(): Promise { 154 | if (!this.requests?.length) { 155 | return true; 156 | } 157 | this.requests[0].limit = Math.min( 158 | this.limit - this.requests[0].offset, 159 | MAX_PARTICIPANTS_CHUNK_SIZE, 160 | ); 161 | const results = []; 162 | for (const request of this.requests) { 163 | results.push(await this.client.invoke(request)); 164 | } 165 | 166 | for (let i = this.requests.length - 1; i >= 0; i--) { 167 | const participants = results[i]; 168 | if ( 169 | participants instanceof 170 | Api.channels.ChannelParticipantsNotModified || 171 | !participants.users.length 172 | ) { 173 | this.requests.splice(i, 1); 174 | continue; 175 | } 176 | 177 | this.requests[i].offset += participants.participants.length; 178 | const users = new Map(); 179 | for (const user of participants.users) { 180 | users.set(user.id.toString(), user); 181 | } 182 | for (const participant of participants.participants) { 183 | if (!("userId" in participant)) { 184 | continue; 185 | } 186 | const user = users.get(participant.userId.toString())!; 187 | if (this.filterEntity && !this.filterEntity(user)) { 188 | continue; 189 | } 190 | // deno-lint-ignore no-explicit-any 191 | (user as any).participant = participant; 192 | this.buffer?.push(user); 193 | } 194 | } 195 | return undefined; 196 | } 197 | 198 | // deno-lint-ignore no-explicit-any 199 | [Symbol.asyncIterator](): AsyncIterator { 200 | return super[Symbol.asyncIterator](); 201 | } 202 | } 203 | 204 | export function iterParticipants( 205 | client: AbstractTelegramClient, 206 | entity: Api.TypeEntityLike, 207 | { limit, filter, offset, search, showTotal = true }: IterParticipantsParams, 208 | ) { 209 | return new _ParticipantsIter( 210 | client, 211 | limit ?? Number.MAX_SAFE_INTEGER, 212 | {}, 213 | { 214 | entity: entity, 215 | filter: filter, 216 | offset: offset ?? 0, 217 | search: search, 218 | showTotal: showTotal, 219 | }, 220 | ); 221 | } 222 | 223 | export async function getParticipants( 224 | client: AbstractTelegramClient, 225 | entity: Api.TypeEntityLike, 226 | params: IterParticipantsParams, 227 | ) { 228 | const it = client.iterParticipants(entity, params); 229 | const users = new TotalList(); 230 | for await (const user of it) { 231 | users.push(user); 232 | } 233 | return users; 234 | } 235 | -------------------------------------------------------------------------------- /src/client/dialogs.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { RequestIter } from "../request_iter.ts"; 3 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 4 | import { Dialog } from "../tl/custom/dialog.ts"; 5 | import { TotalList } from "../helpers.ts"; 6 | import { LogLevel } from "../extensions/logger.ts"; 7 | import { bigInt } from "../deps.ts"; 8 | import { getPeerId } from "../utils.ts"; 9 | import { CustomMessage } from "../tl/custom/message.ts"; 10 | import { IterDialogsParams } from "./types.ts"; 11 | 12 | const MAX_CHUNK_SIZE = 100; 13 | 14 | function _dialogMessageKey(peer: Api.TypePeer, messageId: number): string { 15 | // can't use arrays as keys for map :( need to convert to string. 16 | return ( 17 | "" + 18 | [peer instanceof Api.PeerChannel ? peer.channelId : undefined, messageId] 19 | ); 20 | } 21 | 22 | export interface DialogsIterInterface { 23 | offsetDate: number; 24 | offsetId: number; 25 | offsetPeer: Api.TypePeer; 26 | ignorePinned: boolean; 27 | ignoreMigrated: boolean; 28 | folder: number; 29 | } 30 | 31 | export class _DialogsIter extends RequestIter { 32 | private request?: Api.messages.GetDialogs; 33 | // deno-lint-ignore no-explicit-any 34 | private seen?: Set; 35 | private offsetDate?: number; 36 | private ignoreMigrated?: boolean; 37 | 38 | async _init({ 39 | offsetDate, 40 | offsetId, 41 | offsetPeer, 42 | ignorePinned, 43 | ignoreMigrated, 44 | folder, 45 | }: DialogsIterInterface) { 46 | this.request = new Api.messages.GetDialogs({ 47 | offsetDate, 48 | offsetId, 49 | offsetPeer, 50 | limit: 1, 51 | hash: bigInt.zero, 52 | excludePinned: ignorePinned, 53 | folderId: folder, 54 | }); 55 | if (this.limit <= 0) { 56 | // Special case, get a single dialog and determine count 57 | const dialogs = await this.client.invoke(this.request); 58 | if ("count" in dialogs) { 59 | this.total = dialogs.count; 60 | } else { 61 | this.total = dialogs.dialogs.length; 62 | } 63 | 64 | return true; 65 | } 66 | 67 | this.seen = new Set(); 68 | this.offsetDate = offsetDate; 69 | this.ignoreMigrated = ignoreMigrated; 70 | } 71 | 72 | // deno-lint-ignore no-explicit-any 73 | [Symbol.asyncIterator](): AsyncIterator { 74 | return super[Symbol.asyncIterator](); 75 | } 76 | 77 | async _loadNextChunk(): Promise { 78 | if (!this.request || !this.seen || !this.buffer) { 79 | return; 80 | } 81 | this.request.limit = Math.min(this.left, MAX_CHUNK_SIZE); 82 | const r = await this.client.invoke(this.request); 83 | if (r instanceof Api.messages.DialogsNotModified) { 84 | return; 85 | } 86 | if ("count" in r) { 87 | this.total = r.count; 88 | } else { 89 | this.total = r.dialogs.length; 90 | } 91 | const entities = new Map(); 92 | const messages = new Map(); 93 | 94 | for (const entity of [...r.users, ...r.chats]) { 95 | if ( 96 | entity instanceof Api.UserEmpty || 97 | entity instanceof Api.ChatEmpty 98 | ) { 99 | continue; 100 | } 101 | entities.set(getPeerId(entity), entity); 102 | } 103 | for (const m of r.messages) { 104 | const message = new CustomMessage(m as unknown as Api.Message); 105 | try { 106 | // todo make sure this never fails 107 | message._finishInit(this.client, entities, undefined); 108 | } catch (e) { 109 | this.client._log.error( 110 | `Got error while trying to finish init message with id ${m.id}`, 111 | ); 112 | if (this.client._log.canSend(LogLevel.ERROR)) { 113 | console.error(e); 114 | } 115 | } 116 | messages.set( 117 | _dialogMessageKey(message.peerId!, message.id), 118 | message, 119 | ); 120 | } 121 | 122 | for (const d of r.dialogs) { 123 | if (d instanceof Api.DialogFolder) { 124 | continue; 125 | } 126 | const message = messages.get( 127 | _dialogMessageKey(d.peer, d.topMessage), 128 | ); 129 | if (this.offsetDate != undefined) { 130 | const date = message?.date!; 131 | if (date == undefined || date > this.offsetDate) { 132 | continue; 133 | } 134 | } 135 | const peerId = getPeerId(d.peer); 136 | if (!this.seen.has(peerId)) { 137 | this.seen.add(peerId); 138 | if (!entities.has(peerId)) continue; 139 | const cd = new Dialog( 140 | this.client, 141 | d, 142 | entities, 143 | message?.originalMessage, 144 | ); 145 | if ( 146 | !this.ignoreMigrated || 147 | (cd.entity != undefined && "migratedTo" in cd.entity) 148 | ) { 149 | this.buffer.push(cd); 150 | } 151 | } 152 | } 153 | if ( 154 | r.dialogs.length < this.request.limit || 155 | !(r instanceof Api.messages.DialogsSlice) 156 | ) { 157 | return true; 158 | } 159 | let lastMessage; 160 | for (const dialog of r.dialogs.reverse()) { 161 | lastMessage = messages.get( 162 | _dialogMessageKey(dialog.peer, dialog.topMessage), 163 | ); 164 | if (lastMessage) break; 165 | } 166 | this.request.excludePinned = true; 167 | this.request.offsetId = lastMessage ? lastMessage.id : 0; 168 | this.request.offsetDate = lastMessage ? lastMessage.date! : 0; 169 | this.request.offsetPeer = this.buffer[this.buffer.length - 1].inputEntity; 170 | } 171 | } 172 | 173 | export function iterDialogs( 174 | client: AbstractTelegramClient, 175 | { 176 | limit = undefined, 177 | offsetDate = undefined, 178 | offsetId = 0, 179 | offsetPeer = new Api.InputPeerEmpty(), 180 | ignorePinned = false, 181 | ignoreMigrated = false, 182 | folder = undefined, 183 | archived = undefined, 184 | }: IterDialogsParams, 185 | ): _DialogsIter { 186 | if (archived != undefined) { 187 | folder = archived ? 1 : 0; 188 | } 189 | 190 | return new _DialogsIter( 191 | client, 192 | limit, 193 | {}, 194 | { 195 | offsetDate, 196 | offsetId, 197 | offsetPeer, 198 | ignorePinned, 199 | ignoreMigrated, 200 | folder, 201 | }, 202 | ); 203 | } 204 | 205 | export async function getDialogs( 206 | client: AbstractTelegramClient, 207 | params: IterDialogsParams, 208 | ): Promise> { 209 | const dialogs = new TotalList(); 210 | for await (const dialog of client.iterDialogs(params)) { 211 | dialogs.push(dialog); 212 | } 213 | return dialogs; 214 | } 215 | -------------------------------------------------------------------------------- /src/client/message_parse.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { getPeerId, sanitizeParseMode } from "../utils.ts"; 3 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 4 | import { isArrayLike } from "../helpers.ts"; 5 | import { EntityType_, entityType_ } from "../tl/helpers.ts"; 6 | import { bigInt } from "../deps.ts"; 7 | import { CustomMessage } from "../tl/custom/message.ts"; 8 | import { ParseInterface } from "./types.ts"; 9 | 10 | export async function _replaceWithMention( 11 | client: AbstractTelegramClient, 12 | entities: Api.TypeMessageEntity[], 13 | i: number, 14 | user: Api.TypeEntityLike, 15 | ) { 16 | try { 17 | entities[i] = new Api.InputMessageEntityMentionName({ 18 | offset: entities[i].offset, 19 | length: entities[i].length, 20 | userId: 21 | (await client.getInputEntity(user)) as unknown as Api.TypeInputUser, 22 | }); 23 | return true; 24 | } catch (_e) { 25 | return false; 26 | } 27 | } 28 | 29 | export async function _parseMessageText( 30 | client: AbstractTelegramClient, 31 | message: string, 32 | parseMode: false | string | ParseInterface, 33 | ): Promise<[string, Api.TypeMessageEntity[]]> { 34 | if (parseMode === false) { 35 | return [message, []]; 36 | } 37 | if (parseMode === undefined) { 38 | if (client.parseMode === undefined) { 39 | return [message, []]; 40 | } 41 | parseMode = client.parseMode as ParseInterface; 42 | } else if (typeof parseMode === "string") { 43 | parseMode = sanitizeParseMode(parseMode) as ParseInterface; 44 | } 45 | const [rawMessage, msgEntities] = parseMode.parse(message); 46 | for (let i = msgEntities.length - 1; i >= 0; i--) { 47 | const e = msgEntities[i]; 48 | if (e instanceof Api.MessageEntityTextUrl) { 49 | const m = /^@|\+|tg:\/\/user\?id=(\d+)/.exec(e.url); 50 | if (m) { 51 | const userIdOrUsername = m[1] ? Number(m[1]) : e.url; 52 | const isMention = await _replaceWithMention( 53 | client, 54 | msgEntities, 55 | i, 56 | userIdOrUsername, 57 | ); 58 | if (!isMention) msgEntities.splice(i, 1); 59 | } 60 | } 61 | } 62 | return [rawMessage, msgEntities]; 63 | } 64 | 65 | export function _getResponseMessage( 66 | client: AbstractTelegramClient, 67 | // deno-lint-ignore no-explicit-any 68 | request: any, 69 | // deno-lint-ignore no-explicit-any 70 | result: any, 71 | // deno-lint-ignore no-explicit-any 72 | inputChat: any, 73 | ) { 74 | let updates = []; 75 | 76 | const entities = new Map(); 77 | if (result instanceof Api.UpdateShort) { 78 | updates = [result.update]; 79 | } else if ( 80 | result instanceof Api.Updates || 81 | result instanceof Api.UpdatesCombined 82 | ) { 83 | updates = result.updates; 84 | for (const x of [...result.users, ...result.chats]) { 85 | entities.set(getPeerId(x), x); 86 | } 87 | } else { 88 | return; 89 | } 90 | const randomToId = new Map(); 91 | const idToMessage = new Map(); 92 | let schedMessage; 93 | for (const update of updates) { 94 | if (update instanceof Api.UpdateMessageID) { 95 | randomToId.set(update.randomId!.toString(), update.id); 96 | } else if ( 97 | update instanceof Api.UpdateNewChannelMessage || 98 | update instanceof Api.UpdateNewMessage 99 | ) { 100 | const message = new CustomMessage( 101 | update.message as unknown as Api.Message, 102 | ); 103 | 104 | message._finishInit( 105 | client, 106 | entities, 107 | inputChat, 108 | ); 109 | if ("randomId" in request || isArrayLike(request)) { 110 | idToMessage.set( 111 | update.message.id, 112 | message, 113 | ); 114 | } else { 115 | return message; 116 | } 117 | } else if ( 118 | update instanceof Api.UpdateEditMessage && 119 | "peer" in request && 120 | entityType_(request.peer) !== EntityType_.CHANNEL 121 | ) { 122 | const message = new CustomMessage( 123 | update.message as unknown as Api.Message, 124 | ); 125 | message._finishInit( 126 | client, 127 | entities, 128 | inputChat, 129 | ); 130 | if ("randomId" in request) { 131 | idToMessage.set( 132 | update.message.id, 133 | message, 134 | ); 135 | } else if ("id" in request && request.id === update.message.id) { 136 | return message; 137 | } 138 | } else if ( 139 | update instanceof Api.UpdateEditChannelMessage && 140 | "peer" in request && 141 | getPeerId(request.peer) === 142 | getPeerId((update.message as unknown as Api.Message).peerId!) 143 | ) { 144 | if (request.id === update.message.id) { 145 | const message = new CustomMessage( 146 | update.message as unknown as Api.Message, 147 | ); 148 | message._finishInit( 149 | client, 150 | entities, 151 | inputChat, 152 | ); 153 | return message; 154 | } 155 | } else if (update instanceof Api.UpdateNewScheduledMessage) { 156 | const message = new CustomMessage( 157 | update.message as unknown as Api.Message, 158 | ); 159 | message._finishInit( 160 | client, 161 | entities, 162 | inputChat, 163 | ); 164 | schedMessage = message; 165 | idToMessage.set( 166 | update.message.id, 167 | message, 168 | ); 169 | } else if (update instanceof Api.UpdateMessagePoll) { 170 | if (request.media.poll.id === update.pollId) { 171 | const m = new CustomMessage({ 172 | id: request.id, 173 | peerId: getPeerId(request.peer), 174 | media: new Api.MessageMediaPoll({ 175 | poll: update.poll!, 176 | results: update.results, 177 | }), 178 | message: "", 179 | date: 0, 180 | }); 181 | m._finishInit(client, entities, inputChat); 182 | return m; 183 | } 184 | } 185 | } 186 | if (request === undefined) { 187 | return idToMessage; 188 | } 189 | let randomId; 190 | if ( 191 | isArrayLike(request) || 192 | typeof request === "number" || 193 | bigInt.isInstance(request) 194 | ) { 195 | randomId = request; 196 | } else { 197 | randomId = request.randomId; 198 | } 199 | if (!randomId) { 200 | if (schedMessage) return schedMessage; 201 | client._log.warn( 202 | `No randomId in ${request} to map to. returning undefined for ${result}`, 203 | ); 204 | return undefined; 205 | } 206 | if (!isArrayLike(randomId)) { 207 | const msg = idToMessage.get(randomToId.get(randomId.toString())!); 208 | if (!msg) { 209 | client._log.warn( 210 | `Request ${request.className} had missing message mapping ${result.className}`, 211 | ); 212 | } 213 | return msg; 214 | } 215 | const final = []; 216 | let warned = false; 217 | for (const rnd of randomId) { 218 | // deno-lint-ignore no-explicit-any 219 | const tmp = randomToId.get((rnd as any).toString()); 220 | if (!tmp) { 221 | warned = true; 222 | break; 223 | } 224 | const tmp2 = idToMessage.get(tmp); 225 | if (!tmp2) { 226 | warned = true; 227 | break; 228 | } 229 | final.push(tmp2); 230 | } 231 | if (warned) { 232 | client._log.warn( 233 | `Request ${request.className} had missing message mapping ${result.className}`, 234 | ); 235 | } 236 | const finalToReturn = []; 237 | for (const rnd of randomId) { 238 | finalToReturn.push( 239 | // deno-lint-ignore no-explicit-any 240 | idToMessage.get(randomToId.get((rnd as any).toString())!), 241 | ); 242 | } 243 | 244 | return finalToReturn; 245 | } 246 | -------------------------------------------------------------------------------- /src/client/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./2fa.ts"; 2 | export * from "./auth.ts"; 3 | export * from "./base_client.ts"; 4 | export * from "./bots.ts"; 5 | export * from "./buttons.ts"; 6 | export * from "./chats.ts"; 7 | export * from "./dialogs.ts"; 8 | export * from "./downloads.ts"; 9 | export * from "./message_parse.ts"; 10 | export * from "./messages.ts"; 11 | export * from "./telegram_client.ts"; 12 | export * from "./updates.ts"; 13 | export * from "./uploads.ts"; 14 | export * from "./users.ts"; 15 | 16 | // aliases for compat with node-gramjs 17 | export * as twoFa from "./2fa.ts"; 18 | export * as auth from "./auth.ts"; 19 | export * as telegramBaseClient from "./base_client.ts"; 20 | export * as bots from "./bots.ts"; 21 | export * as buttons from "./buttons.ts"; 22 | export * as chats from "./chats.ts"; 23 | export * as dialogs from "./dialogs.ts"; 24 | export * as downloads from "./downloads.ts"; 25 | export * as messageParse from "./message_parse.ts"; 26 | export * as message from "./messages.ts"; 27 | export * as tgClient from "./abstract_telegram_client.ts"; 28 | export * as updates from "./updates.ts"; 29 | export * as uploads from "./uploads.ts"; 30 | export * as users from "./users.ts"; 31 | export * from "./types.ts"; 32 | -------------------------------------------------------------------------------- /src/client/updates.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { bigInt } from "../deps.ts"; 3 | import { UpdateConnectionState } from "../network/mod.ts"; 4 | import { getPeerId } from "../utils.ts"; 5 | import { getRandomInt, returnBigInt, sleep } from "../helpers.ts"; 6 | import type { Raw } from "../events/raw.ts"; 7 | import type { EventBuilder } from "../events/common.ts"; 8 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 9 | 10 | export class StopPropagation extends Error {} 11 | 12 | export function on(client: AbstractTelegramClient, event?: EventBuilder) { 13 | // deno-lint-ignore no-explicit-any 14 | return (f: { (event: any): void }) => { 15 | client.addEventHandler(f, event); 16 | return f; 17 | }; 18 | } 19 | 20 | export function addEventHandler( 21 | client: AbstractTelegramClient, 22 | callback: CallableFunction, 23 | event?: EventBuilder, 24 | ) { 25 | if (event === undefined) { 26 | // recursive imports :( 27 | import("../events/raw.ts").then((r) => { 28 | event = new r.Raw({}) as Raw; 29 | event.client = client; 30 | client._eventBuilders.push([event, callback]); 31 | }); 32 | } else { 33 | event.client = client; 34 | client._eventBuilders.push([event, callback]); 35 | } 36 | } 37 | 38 | export function removeEventHandler( 39 | client: AbstractTelegramClient, 40 | callback: CallableFunction, 41 | event: EventBuilder, 42 | ) { 43 | client._eventBuilders = client._eventBuilders.filter(function (item) { 44 | return item[0] !== event && item[1] !== callback; 45 | }); 46 | } 47 | 48 | export function listEventHandlers(client: AbstractTelegramClient) { 49 | return client._eventBuilders; 50 | } 51 | 52 | export function catchUp() { 53 | // TODO 54 | } 55 | 56 | export function _handleUpdate( 57 | client: AbstractTelegramClient, 58 | update: Api.TypeUpdate | number, 59 | ): void { 60 | if (typeof update === "number") { 61 | if ([-1, 0, 1].includes(update)) { 62 | _dispatchUpdate(client, { 63 | update: new UpdateConnectionState(update), 64 | }); 65 | return; 66 | } 67 | } 68 | 69 | // this.session.processEntities(update) 70 | client._entityCache.add(update); 71 | client.session.processEntities(update); 72 | 73 | if ( 74 | update instanceof Api.Updates || 75 | update instanceof Api.UpdatesCombined 76 | ) { 77 | // TODO deal with entities 78 | const entities = new Map(); 79 | for (const x of [...update.users, ...update.chats]) { 80 | entities.set(getPeerId(x), x); 81 | } 82 | for (const u of update.updates) { 83 | _processUpdate(client, u, update.updates, entities); 84 | } 85 | } else if (update instanceof Api.UpdateShort) { 86 | _processUpdate(client, update.update, null); 87 | } else { 88 | _processUpdate(client, update, null); 89 | } 90 | } 91 | 92 | export function _processUpdate( 93 | client: AbstractTelegramClient, 94 | // deno-lint-ignore no-explicit-any 95 | update: any, 96 | // deno-lint-ignore no-explicit-any 97 | others: any, 98 | // deno-lint-ignore no-explicit-any 99 | entities?: any, 100 | ) { 101 | update._entities = entities || new Map(); 102 | const args = { 103 | update: update, 104 | others: others, 105 | }; 106 | 107 | _dispatchUpdate(client, args); 108 | } 109 | 110 | export async function _dispatchUpdate( 111 | client: AbstractTelegramClient, 112 | // deno-lint-ignore no-explicit-any 113 | args: { update: UpdateConnectionState | any }, 114 | ): Promise { 115 | for (const [builder, callback] of client._eventBuilders) { 116 | if (!builder || !callback) { 117 | continue; 118 | } 119 | if (!builder.resolved) { 120 | await builder.resolve(client); 121 | } 122 | let event = args.update; 123 | if (event) { 124 | if (!client._selfInputPeer) { 125 | try { 126 | await client.getMe(true); 127 | } catch (_e) { 128 | // do nothing 129 | } 130 | } 131 | if (!(event instanceof UpdateConnectionState)) { 132 | // TODO fix me 133 | } 134 | // TODO fix others not being passed 135 | event = builder.build( 136 | event, 137 | callback, 138 | client._selfInputPeer 139 | ? returnBigInt(client._selfInputPeer.userId) 140 | : undefined, 141 | ); 142 | if (event) { 143 | event._client = client; 144 | 145 | if ("_eventName" in event) { 146 | event._setClient(client); 147 | event.originalUpdate = args.update; 148 | event._entities = args.update._entities; 149 | } 150 | const filter = builder.filter(event); 151 | if (!filter) { 152 | continue; 153 | } 154 | try { 155 | await callback(event); 156 | } catch (e) { 157 | if (e instanceof StopPropagation) { 158 | break; 159 | } 160 | console.error(e); 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | export async function _updateLoop( 168 | client: AbstractTelegramClient, 169 | ): Promise { 170 | while (client.connected) { 171 | try { 172 | await sleep(60 * 1000); 173 | if (!client._sender?._transportConnected()) { 174 | continue; 175 | } 176 | await client.invoke( 177 | new Api.Ping({ 178 | pingId: bigInt( 179 | getRandomInt(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER), 180 | ), 181 | }), 182 | ); 183 | } catch (_err) { 184 | return; 185 | } 186 | 187 | client.session.save(); 188 | 189 | if ( 190 | new Date().getTime() - (client._lastRequest || 0) > 191 | 30 * 60 * 1000 192 | ) { 193 | try { 194 | await client.invoke(new Api.updates.GetState()); 195 | } catch (_e) { 196 | // we don't care about errors here 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/client/utils.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { getAttributes, getInputMedia, isImage } from "../utils.ts"; 3 | import { AbstractTelegramClient } from "./abstract_telegram_client.ts"; 4 | import { _parseMessageText } from "./message_parse.ts"; 5 | import { basename, Buffer } from "../deps.ts"; 6 | import { CustomFile } from "../classes.ts"; 7 | import { FileToMediaInterface } from "./types.ts"; 8 | 9 | export async function _fileToMedia( 10 | client: AbstractTelegramClient, 11 | { 12 | file, 13 | forceDocument, 14 | progressCallback, 15 | attributes, 16 | thumb, 17 | voiceNote = false, 18 | videoNote = false, 19 | supportsStreaming = false, 20 | mimeType, 21 | asImage, 22 | workers = 1, 23 | }: FileToMediaInterface, 24 | ): Promise<{ 25 | // deno-lint-ignore no-explicit-any 26 | fileHandle?: any; 27 | media?: Api.TypeInputMedia; 28 | image?: boolean; 29 | }> { 30 | if (!file) { 31 | return { fileHandle: undefined, media: undefined, image: undefined }; 32 | } 33 | const isImage_ = isImage(file); 34 | 35 | if (asImage === undefined) { 36 | asImage = isImage_ && !forceDocument; 37 | } 38 | if ( 39 | typeof file === "object" && 40 | !Buffer.isBuffer(file) && 41 | !(file instanceof Api.InputFile) && 42 | !(file instanceof Api.InputFileBig) && 43 | !(file instanceof CustomFile) && 44 | !("read" in file) 45 | ) { 46 | try { 47 | return { 48 | fileHandle: undefined, 49 | media: getInputMedia(file, { 50 | isPhoto: asImage, 51 | attributes: attributes, 52 | forceDocument: forceDocument, 53 | voiceNote: voiceNote, 54 | videoNote: videoNote, 55 | supportsStreaming: supportsStreaming, 56 | }), 57 | image: asImage, 58 | }; 59 | } catch (_e) { 60 | return { 61 | fileHandle: undefined, 62 | media: undefined, 63 | image: isImage_, 64 | }; 65 | } 66 | } 67 | let media; 68 | let fileHandle; 69 | let createdFile; 70 | 71 | if (file instanceof Api.InputFile || file instanceof Api.InputFileBig) { 72 | fileHandle = file; 73 | } else if ( 74 | typeof file === "string" && 75 | (file.startsWith("https://") || file.startsWith("http://")) 76 | ) { 77 | if (asImage) { 78 | media = new Api.InputMediaPhotoExternal({ url: file }); 79 | } else { 80 | media = new Api.InputMediaDocumentExternal({ url: file }); 81 | } 82 | } else if (!(typeof file === "string") || (await Deno.lstat(file)).isFile) { 83 | if (typeof file === "string") { 84 | createdFile = new CustomFile( 85 | basename(file), 86 | (await Deno.stat(file)).size, 87 | file, 88 | ); 89 | } else if ( 90 | (typeof File !== "undefined" && file instanceof File) || 91 | file instanceof CustomFile 92 | ) { 93 | createdFile = file; 94 | } else { 95 | let name; 96 | if ("name" in file) { 97 | // @ts-ignore wut 98 | name = file.name; 99 | } else { 100 | name = "unnamed"; 101 | } 102 | if (Buffer.isBuffer(file)) { 103 | createdFile = new CustomFile(name, file.length, "", file); 104 | } 105 | } 106 | if (!createdFile) { 107 | throw new Error( 108 | `Could not create file from ${JSON.stringify(file)}`, 109 | ); 110 | } 111 | fileHandle = await client.uploadFile({ 112 | file: createdFile, 113 | onProgress: progressCallback, 114 | workers: workers, 115 | }); 116 | } else { 117 | throw new Error(`"Not a valid path nor a url ${file}`); 118 | } 119 | if (media !== undefined) { // 120 | } else if (fileHandle === undefined) { 121 | throw new Error( 122 | `Failed to convert ${file} to media. Not an existing file or an HTTP URL`, 123 | ); 124 | } else if (asImage) { 125 | media = new Api.InputMediaUploadedPhoto({ 126 | file: fileHandle, 127 | }); 128 | } else { 129 | // @ts-ignore x 130 | const res = getAttributes(file, { 131 | mimeType: mimeType, 132 | attributes: attributes, 133 | forceDocument: forceDocument && !isImage_, 134 | voiceNote: voiceNote, 135 | videoNote: videoNote, 136 | supportsStreaming: supportsStreaming, 137 | thumb: thumb, 138 | }); 139 | attributes = res.attrs; 140 | mimeType = res.mimeType; 141 | 142 | let uploadedThumb; 143 | if (!thumb) { 144 | uploadedThumb = undefined; 145 | } else { 146 | // todo refactor 147 | if (typeof thumb === "string") { 148 | uploadedThumb = new CustomFile( 149 | basename(thumb), 150 | (await Deno.stat(thumb)).size, 151 | thumb, 152 | ); 153 | } else if (typeof File !== "undefined" && thumb instanceof File) { 154 | uploadedThumb = thumb; 155 | } else { 156 | let name; 157 | if ("name" in thumb) { 158 | name = thumb.name; 159 | } else { 160 | name = "unnamed"; 161 | } 162 | if (Buffer.isBuffer(thumb)) { 163 | uploadedThumb = new CustomFile( 164 | name, 165 | thumb.length, 166 | "", 167 | thumb, 168 | ); 169 | } 170 | } 171 | if (!uploadedThumb) { 172 | throw new Error(`Could not create file from ${file}`); 173 | } 174 | uploadedThumb = await client.uploadFile({ 175 | file: uploadedThumb, 176 | workers: 1, 177 | }); 178 | } 179 | media = new Api.InputMediaUploadedDocument({ 180 | file: fileHandle, 181 | mimeType: mimeType!, 182 | attributes: attributes!, 183 | thumb: uploadedThumb, 184 | forceFile: forceDocument && !isImage_, 185 | }); 186 | } 187 | return { 188 | fileHandle: fileHandle, 189 | media: media, 190 | image: asImage, 191 | }; 192 | } 193 | -------------------------------------------------------------------------------- /src/crypto/authkey.ts: -------------------------------------------------------------------------------- 1 | import { 2 | readBigIntFromBuffer, 3 | readBufferFromBigInt, 4 | sha1, 5 | sleep, 6 | toSignedLittleBuffer, 7 | } from "../helpers.ts"; 8 | import { BinaryReader } from "../extensions/binary_reader.ts"; 9 | import { bigInt, Buffer } from "../deps.ts"; 10 | 11 | export class AuthKey { 12 | private _key?: Buffer; 13 | private _hash?: Buffer; 14 | private auxHash?: bigInt.BigInteger; 15 | keyId?: bigInt.BigInteger; 16 | 17 | constructor(value?: Buffer, hash?: Buffer) { 18 | if (!hash || !value) return; 19 | this._key = value; 20 | this._hash = hash; 21 | const reader = new BinaryReader(hash); 22 | this.auxHash = reader.readLong(false); 23 | reader.read(4); 24 | this.keyId = reader.readLong(false); 25 | } 26 | 27 | async setKey(value?: Buffer | AuthKey) { 28 | if (!value) { 29 | this._key = 30 | this.auxHash = 31 | this.keyId = 32 | this._hash = 33 | undefined; 34 | return; 35 | } 36 | if (value instanceof AuthKey) { 37 | this._key = value._key; 38 | this.auxHash = value.auxHash; 39 | this.keyId = value.keyId; 40 | this._hash = value._hash; 41 | return; 42 | } 43 | this._key = value; 44 | this._hash = await sha1(this._key); 45 | const reader = new BinaryReader(this._hash); 46 | this.auxHash = reader.readLong(false); 47 | reader.read(4); 48 | this.keyId = reader.readLong(false); 49 | } 50 | 51 | async waitForKey() { 52 | while (!this.keyId) { 53 | await sleep(20); 54 | } 55 | } 56 | 57 | getKey() { 58 | return this._key; 59 | } 60 | 61 | async calcNewNonceHash( 62 | newNonce: bigInt.BigInteger, 63 | number: number, 64 | ): Promise { 65 | if (this.auxHash) { 66 | const nonce = toSignedLittleBuffer(newNonce, 32); 67 | const n = Buffer.alloc(1); 68 | n.writeUInt8(number, 0); 69 | const data = Buffer.concat([ 70 | nonce, 71 | Buffer.concat([n, readBufferFromBigInt(this.auxHash, 8, true)]), 72 | ]); 73 | const shaData = (await sha1(data)).slice(4, 20); 74 | return readBigIntFromBuffer(shaData, true, true); 75 | } 76 | throw new Error("Auth key not set"); 77 | } 78 | 79 | equals(other: AuthKey) { 80 | return ( 81 | other instanceof this.constructor && 82 | this._key && 83 | Buffer.isBuffer(other.getKey()) && 84 | other.getKey()?.equals(this._key) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/crypto/converters.ts: -------------------------------------------------------------------------------- 1 | export function i2abLow(buf: Uint32Array): ArrayBuffer { 2 | const uint8 = new Uint8Array(buf.length * 4); 3 | let i = 0; 4 | 5 | for (let j = 0; j < buf.length; j++) { 6 | const int = buf[j]; 7 | uint8[i++] = int >>> 24; 8 | uint8[i++] = (int >> 16) & 0xff; 9 | uint8[i++] = (int >> 8) & 0xff; 10 | uint8[i++] = int & 0xff; 11 | } 12 | 13 | return uint8.buffer; 14 | } 15 | 16 | export function i2abBig(buf: Uint32Array): ArrayBuffer { 17 | return buf.buffer; 18 | } 19 | 20 | export function ab2iLow( 21 | ab: ArrayBuffer | SharedArrayBuffer | Uint8Array, 22 | ): Uint32Array { 23 | const uint8 = new Uint8Array(ab); 24 | const buf = new Uint32Array(uint8.length / 4); 25 | 26 | for (let i = 0; i < uint8.length; i += 4) { 27 | buf[i / 4] = (uint8[i] << 24) ^ 28 | (uint8[i + 1] << 16) ^ 29 | (uint8[i + 2] << 8) ^ 30 | uint8[i + 3]; 31 | } 32 | 33 | return buf; 34 | } 35 | 36 | export function ab2iBig( 37 | ab: ArrayBuffer | SharedArrayBuffer | Uint8Array, 38 | ): Uint32Array { 39 | return new Uint32Array(ab); 40 | } 41 | 42 | export const isBigEndian = 43 | new Uint8Array(new Uint32Array([0x01020304]))[0] === 0x01; 44 | export const i2ab = isBigEndian ? i2abBig : i2abLow; 45 | export const ab2i = isBigEndian ? ab2iBig : ab2iLow; 46 | -------------------------------------------------------------------------------- /src/crypto/crypto.ts: -------------------------------------------------------------------------------- 1 | import { ab2i, i2ab } from "./converters.ts"; 2 | import { AES, Buffer, getWords } from "../deps.ts"; 3 | 4 | export class Counter { 5 | _counter: Buffer; 6 | 7 | // deno-lint-ignore no-explicit-any 8 | constructor(initialValue: any) { 9 | this._counter = Buffer.from(initialValue); 10 | } 11 | 12 | increment() { 13 | for (let i = 15; i >= 0; i--) { 14 | if (this._counter[i] === 255) { 15 | this._counter[i] = 0; 16 | } else { 17 | this._counter[i]++; 18 | break; 19 | } 20 | } 21 | } 22 | } 23 | 24 | export class CTR { 25 | private _counter: Counter; 26 | private _remainingCounter?: Buffer; 27 | private _remainingCounterIndex: number; 28 | private _aes: AES; 29 | 30 | // deno-lint-ignore no-explicit-any 31 | constructor(key: Buffer, counter: any) { 32 | if (!(counter instanceof Counter)) { 33 | counter = new Counter(counter); 34 | } 35 | 36 | this._counter = counter; 37 | this._remainingCounter = undefined; 38 | this._remainingCounterIndex = 16; 39 | this._aes = new AES(getWords(key)); 40 | } 41 | 42 | // deno-lint-ignore no-explicit-any 43 | update(plainText: any) { 44 | return this.encrypt(plainText); 45 | } 46 | 47 | // deno-lint-ignore no-explicit-any 48 | encrypt(plainText: any) { 49 | const encrypted = Buffer.from(plainText); 50 | 51 | for (let i = 0; i < encrypted.length; i++) { 52 | if (this._remainingCounterIndex === 16) { 53 | this._remainingCounter = Buffer.from( 54 | i2ab(this._aes.encrypt(ab2i(this._counter._counter))), 55 | ); 56 | this._remainingCounterIndex = 0; 57 | this._counter.increment(); 58 | } 59 | if (this._remainingCounter) { 60 | encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++]; 61 | } 62 | } 63 | 64 | return encrypted; 65 | } 66 | } 67 | 68 | export function createDecipheriv(algorithm: string, key: Buffer, iv: Buffer) { 69 | if (algorithm.includes("ECB")) { 70 | throw new Error("Not supported"); 71 | } else { 72 | return new CTR(key, iv); 73 | } 74 | } 75 | 76 | export function createCipheriv(algorithm: string, key: Buffer, iv: Buffer) { 77 | if (algorithm.includes("ECB")) { 78 | throw new Error("Not supported"); 79 | } else { 80 | return new CTR(key, iv); 81 | } 82 | } 83 | 84 | export function randomBytes(count: number) { 85 | const bytes = new Uint8Array(count); 86 | crypto.getRandomValues(bytes); 87 | return bytes; 88 | } 89 | export class Hash { 90 | private readonly algorithm: string; 91 | private data?: Uint8Array; 92 | 93 | constructor(algorithm: string) { 94 | this.algorithm = algorithm; 95 | } 96 | 97 | update(data: Buffer) { 98 | // We shouldn't be needing new Uint8Array but it doesn't 99 | // work without it 100 | this.data = new Uint8Array(data); 101 | } 102 | 103 | async digest() { 104 | if (this.data) { 105 | if (this.algorithm === "sha1") { 106 | return Buffer.from( 107 | await self.crypto.subtle.digest("SHA-1", this.data), 108 | ); 109 | } else if (this.algorithm === "sha256") { 110 | return Buffer.from( 111 | await self.crypto.subtle.digest("SHA-256", this.data), 112 | ); 113 | } 114 | } 115 | return Buffer.alloc(0); 116 | } 117 | } 118 | 119 | export async function pbkdf2Sync( 120 | // deno-lint-ignore no-explicit-any 121 | password: any, 122 | // deno-lint-ignore no-explicit-any 123 | salt: any, 124 | // deno-lint-ignore no-explicit-any 125 | iterations: any, 126 | // deno-lint-ignore no-explicit-any 127 | ..._args: any[] 128 | ) { 129 | const passwordKey = await crypto.subtle.importKey( 130 | "raw", 131 | password, 132 | { name: "PBKDF2" }, 133 | false, 134 | ["deriveBits"], 135 | ); 136 | return Buffer.from( 137 | await crypto.subtle.deriveBits( 138 | { 139 | name: "PBKDF2", 140 | hash: "SHA-512", 141 | salt, 142 | iterations, 143 | }, 144 | passwordKey, 145 | 512, 146 | ), 147 | ); 148 | } 149 | 150 | export function createHash(algorithm: string) { 151 | return new Hash(algorithm); 152 | } 153 | -------------------------------------------------------------------------------- /src/crypto/ctr.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { Buffer } from "../deps.ts"; 3 | import { createCipheriv } from "../crypto/mod.ts"; 4 | 5 | export class CTR { 6 | private cipher: any; 7 | 8 | constructor(key: Buffer, iv: Buffer) { 9 | if (!Buffer.isBuffer(key) || !Buffer.isBuffer(iv) || iv.length !== 16) { 10 | throw new Error("Key and iv need to be a buffer"); 11 | } 12 | this.cipher = createCipheriv("AES-256-CTR", key, iv); 13 | } 14 | 15 | encrypt(data: any) { 16 | return Buffer.from(this.cipher.update(data)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/crypto/factorizator.ts: -------------------------------------------------------------------------------- 1 | import { modExp } from "../helpers.ts"; 2 | import { bigInt } from "../deps.ts"; 3 | 4 | export class Factorizator { 5 | static gcd(a: bigInt.BigInteger, b: bigInt.BigInteger) { 6 | while (b.neq(bigInt.zero)) { 7 | const temp = b; 8 | b = a.remainder(b); 9 | a = temp; 10 | } 11 | return a; 12 | } 13 | 14 | static factorize(pq: bigInt.BigInteger) { 15 | if (pq.remainder(2).equals(bigInt.zero)) { 16 | return { p: bigInt(2), q: pq.divide(bigInt(2)) }; 17 | } 18 | let y = bigInt.randBetween(bigInt(1), pq.minus(1)); 19 | const c = bigInt.randBetween(bigInt(1), pq.minus(1)); 20 | const m = bigInt.randBetween(bigInt(1), pq.minus(1)); 21 | 22 | let g = bigInt.one; 23 | let r = bigInt.one; 24 | let q = bigInt.one; 25 | let x = bigInt.zero; 26 | let ys = bigInt.zero; 27 | let k; 28 | 29 | while (g.eq(bigInt.one)) { 30 | x = y; 31 | for (let i = 0; bigInt(i).lesser(r); i++) { 32 | y = modExp(y, bigInt(2), pq).add(c).remainder(pq); 33 | } 34 | k = bigInt.zero; 35 | 36 | while (k.lesser(r) && g.eq(bigInt.one)) { 37 | ys = y; 38 | const condition = bigInt.min(m, r.minus(k)); 39 | for (let i = 0; bigInt(i).lesser(condition); i++) { 40 | y = modExp(y, bigInt(2), pq).add(c).remainder(pq); 41 | q = q.multiply(x.minus(y).abs()).remainder(pq); 42 | } 43 | g = Factorizator.gcd(q, pq); 44 | k = k.add(m); 45 | } 46 | 47 | r = r.multiply(2); 48 | } 49 | 50 | if (g.eq(pq)) { 51 | while (true) { 52 | ys = modExp(ys, bigInt(2), pq).add(c).remainder(pq); 53 | g = Factorizator.gcd(x.minus(ys).abs(), pq); 54 | 55 | if (g.greater(1)) { 56 | break; 57 | } 58 | } 59 | } 60 | const p = g; 61 | q = pq.divide(g); 62 | return p < q ? { p: p, q: q } : { p: q, q: p }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/crypto/ige.ts: -------------------------------------------------------------------------------- 1 | import { convertToLittle, generateRandomBytes } from "../helpers.ts"; 2 | import { Buffer, IGE as IGE_ } from "../deps.ts"; 3 | 4 | export class IGE { 5 | // deno-lint-ignore no-explicit-any 6 | private ige: any; 7 | 8 | constructor(key: Buffer, iv: Buffer) { 9 | this.ige = new IGE_(key, iv); 10 | } 11 | 12 | decryptIge(cipherText: Buffer): Buffer { 13 | return convertToLittle(this.ige.decrypt(cipherText)); 14 | } 15 | 16 | encryptIge(plainText: Buffer): Buffer { 17 | const padding = plainText.length % 16; 18 | if (padding) { 19 | plainText = Buffer.concat([plainText, generateRandomBytes(16 - padding)]); 20 | } 21 | return convertToLittle(this.ige.encrypt(plainText)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/crypto/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./authkey.ts"; 2 | export * from "./converters.ts"; 3 | export * from "./crypto.ts"; 4 | export * from "./factorizator.ts"; 5 | export * from "./ige.ts"; 6 | export * from "./rsa.ts"; 7 | -------------------------------------------------------------------------------- /src/crypto/rsa.ts: -------------------------------------------------------------------------------- 1 | import { 2 | generateRandomBytes, 3 | modExp, 4 | readBigIntFromBuffer, 5 | readBufferFromBigInt, 6 | sha1, 7 | } from "../helpers.ts"; 8 | import { bigInt, Buffer } from "../deps.ts"; 9 | 10 | const PUBLIC_KEYS = [ 11 | { 12 | fingerprint: bigInt("-3414540481677951611"), 13 | n: bigInt( 14 | "2937959817066933702298617714945612856538843112005886376816255642404751219133084745514657634448776440866" + 15 | "1701890505066208632169112269581063774293102577308490531282748465986139880977280302242772832972539403531" + 16 | "3160108704012876427630091361567343395380424193887227773571344877461690935390938502512438971889287359033" + 17 | "8945177273024525306296338410881284207988753897636046529094613963869149149606209957083647645485599631919" + 18 | "2747663615955633778034897140982517446405334423701359108810182097749467210509584293428076654573384828809" + 19 | "574217079944388301239431309115013843331317877374435868468779972014486325557807783825502498215169806323", 20 | ), 21 | e: 65537, 22 | }, 23 | { 24 | fingerprint: bigInt("-5595554452916591101"), 25 | n: bigInt( 26 | "2534288944884041556497168959071347320689884775908477905258202659454602246385394058588521595116849196570822" + 27 | "26493991806038180742006204637761354248846321625124031637930839216416315647409595294193595958529411668489405859523" + 28 | "37613333022396096584117954892216031229237302943701877588456738335398602461675225081791820393153757504952636234951" + 29 | "32323782003654358104782690612092797248736680529211579223142368426126233039432475078545094258975175539015664775146" + 30 | "07193514399690599495696153028090507215003302390050778898553239175099482557220816446894421272976054225797071426466" + 31 | "60768825302832201908302295573257427896031830742328565032949", 32 | ), 33 | e: 65537, 34 | }, 35 | ]; 36 | 37 | export const serverKeys = new Map< 38 | string, 39 | { n: bigInt.BigInteger; e: number } 40 | >(); 41 | 42 | PUBLIC_KEYS.forEach(({ fingerprint, ...keyInfo }) => { 43 | serverKeys.set(fingerprint.toString(), keyInfo); 44 | }); 45 | 46 | export async function encrypt(fingerprint: bigInt.BigInteger, data: Buffer) { 47 | const key = serverKeys.get(fingerprint.toString()); 48 | if (!key) { 49 | return undefined; 50 | } 51 | 52 | // len(sha1.digest) is always 20, so we're left with 255 - 20 - x padding 53 | const rand = generateRandomBytes(235 - data.length); 54 | 55 | const toEncrypt = Buffer.concat([await sha1(data), data, rand]); 56 | 57 | // rsa module rsa.encrypt adds 11 bits for padding which we don't want 58 | // rsa module uses rsa.transform.bytes2int(to_encrypt), easier way: 59 | const payload = readBigIntFromBuffer(toEncrypt, false); 60 | const encrypted = modExp(payload, bigInt(key.e), key.n); 61 | // rsa module uses transform.int2bytes(encrypted, keylength), easier: 62 | return readBufferFromBigInt(encrypted, 256, false); 63 | } 64 | -------------------------------------------------------------------------------- /src/define.d.ts: -------------------------------------------------------------------------------- 1 | import { bigInt, Buffer, WriteStream } from "./deps.ts"; 2 | 3 | type ValueOf = T[keyof T]; 4 | type Phone = string; 5 | type Username = string; 6 | type PeerID = number; 7 | 8 | type LocalPath = string; 9 | type ExternalUrl = string; 10 | type BotFileID = string; 11 | 12 | type OutFile = 13 | | string 14 | | Buffer 15 | | WriteStream; 16 | type ProgressCallback = ( 17 | total: bigInt.BigInteger, 18 | downloaded: bigInt.BigInteger, 19 | ) => void; 20 | 21 | type DateLike = number; 22 | -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | // std/ 2 | export { 3 | basename, 4 | dirname, 5 | fromFileUrl, 6 | join, 7 | resolve, 8 | } from "https://deno.land/std@0.180.0/path/mod.ts"; 9 | 10 | // node: 11 | export { Buffer } from "node:buffer"; 12 | 13 | // x/ 14 | export { getWords } from "https://deno.land/x/dryptography@v0.1.4/aes/utils/words.ts"; 15 | 16 | // cdn.skypack.dev/ 17 | export { 18 | Mutex, 19 | Semaphore, 20 | } from "https://cdn.skypack.dev/async-mutex@v0.4.0?dts"; 21 | export { 22 | default as AES, 23 | IGE, 24 | } from "https://cdn.skypack.dev/@cryptography/aes@0.1.1?dts"; 25 | export { inflate } from "https://cdn.skypack.dev/pako@v2.1.0?dts"; 26 | export { getExtension, getType } from "https://cdn.skypack.dev/mime@v3.0.0?dts"; 27 | export { default as bigInt } from "https://cdn.skypack.dev/big-integer@v1.6.51?dts"; 28 | export { 29 | CancellablePromise, 30 | Cancellation, 31 | pseudoCancellable, 32 | } from "https://cdn.skypack.dev/real-cancellable-promise@v1.1.2?dts"; 33 | 34 | // ghc.deno.dev/ 35 | export { 36 | type Handler, 37 | Parser, 38 | } from "https://ghc.deno.dev/tbjgolden/deno-htmlparser2@1f76cdf/htmlparser2/Parser.ts"; 39 | 40 | import { type Socket as Socket_ } from "node:net"; 41 | 42 | export let Socket = null as unknown as typeof Socket_; 43 | 44 | if (typeof document === "undefined") { 45 | Socket = (await import("node:net")).Socket; 46 | } 47 | 48 | import { type SocksClient as SocksClient_ } from "https://deno.land/x/deno_socks@v2.6.1/mod.ts"; 49 | 50 | export let SocksClient = null as unknown as typeof SocksClient_; 51 | 52 | if (typeof document === "undefined") { 53 | SocksClient = 54 | (await import("https://deno.land/x/deno_socks@v2.6.1/mod.ts")).SocksClient; 55 | } 56 | 57 | export class WriteStream { 58 | constructor(public path: string, public file: Deno.FsFile) { 59 | } 60 | 61 | write(p: Uint8Array) { 62 | return this.file.write(p); 63 | } 64 | 65 | close() { 66 | this.file.close(); 67 | } 68 | } 69 | 70 | export function createWriteStream(path: string) { 71 | return new WriteStream( 72 | path, 73 | Deno.openSync(path, { write: true, create: true }), 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/entity_cache.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { Api } from "./tl/api.js"; 3 | import { getInputPeer, getPeerId } from "./utils.ts"; 4 | import { isArrayLike, returnBigInt } from "./helpers.ts"; 5 | import { bigInt } from "./deps.ts"; 6 | 7 | export function getEntityPair_( 8 | entityId: string, 9 | entities: Map, 10 | cache: EntityCache, 11 | getInputPeerFunction: any = getInputPeer, 12 | ): [Api.TypeEntity?, Api.TypeInputPeer?] { 13 | const entity = entities.get(entityId); 14 | let inputEntity; 15 | try { 16 | inputEntity = cache.get(entityId); 17 | } catch (_e) { 18 | try { 19 | inputEntity = getInputPeerFunction(inputEntity); 20 | } catch (_e) { 21 | // 22 | } 23 | } 24 | return [entity, inputEntity]; 25 | } 26 | 27 | export class EntityCache { 28 | private cacheMap: Map; 29 | 30 | constructor() { 31 | this.cacheMap = new Map(); 32 | } 33 | 34 | add(entities: any) { 35 | const temp = []; 36 | 37 | if (!isArrayLike(entities)) { 38 | if (entities !== undefined) { 39 | if (typeof entities === "object") { 40 | if ("chats" in entities) { 41 | temp.push(...entities.chats); 42 | } 43 | if ("users" in entities) { 44 | temp.push(...entities.users); 45 | } 46 | if ("user" in entities) { 47 | temp.push(entities.user); 48 | } 49 | } 50 | } 51 | 52 | if (temp.length) { 53 | entities = temp; 54 | } else { 55 | return; 56 | } 57 | } 58 | 59 | for (const entity of entities) { 60 | try { 61 | const pid = getPeerId(entity); 62 | if (!this.cacheMap.has(pid.toString())) { 63 | this.cacheMap.set(pid.toString(), getInputPeer(entity)); 64 | } 65 | } catch (_e) { 66 | // 67 | } 68 | } 69 | } 70 | 71 | get(item: bigInt.BigInteger | string | undefined) { 72 | if (item === undefined) { 73 | throw new Error("No cached entity for the given key"); 74 | } 75 | item = returnBigInt(item); 76 | if (item.lesser(bigInt.zero)) { 77 | let res; 78 | try { 79 | res = this.cacheMap.get(getPeerId(item).toString()); 80 | if (res) return res; 81 | } catch (_e) { 82 | throw new Error("Invalid key will not have entity"); 83 | } 84 | } 85 | for (const cls of [Api.PeerUser, Api.PeerChat, Api.PeerChannel]) { 86 | const result = this.cacheMap.get( 87 | getPeerId( 88 | new cls({ 89 | userId: item, 90 | chatId: item, 91 | channelId: item, 92 | }), 93 | ).toString(), 94 | ); 95 | if (result) return result; 96 | } 97 | throw new Error("No cached entity for the given key"); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/errors/common.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "../deps.ts"; 2 | 3 | export class ReadCancelledError extends Error { 4 | constructor() { 5 | super("The read operation was cancelled."); 6 | } 7 | } 8 | 9 | export class TypeNotFoundError extends Error { 10 | constructor(public invalidConstructorId: number, public remaining: Buffer) { 11 | super( 12 | `Could not find a matching Constructor ID for the TLObject that was \ 13 | supposed to be read with ID ${invalidConstructorId}. Most likely, a TLObject \ 14 | was trying to be read when it should not be read. Remaining bytes: ${remaining.length}`, 15 | ); 16 | console.warn( 17 | `Missing MTProto Entity: Please, make sure to add TL definition for ID ${invalidConstructorId}`, 18 | ); 19 | } 20 | } 21 | 22 | export class InvalidChecksumError extends Error { 23 | constructor(private checksum: number, private validChecksum: number) { 24 | super( 25 | `Invalid checksum (${checksum} when ${validChecksum} was expected). This packet should be skipped.`, 26 | ); 27 | } 28 | } 29 | 30 | export class InvalidBufferError extends Error { 31 | code?: number; 32 | 33 | // deno-lint-ignore constructor-super 34 | constructor(public payload: Buffer) { 35 | let code = undefined; 36 | if (payload.length === 4) { 37 | code = -payload.readInt32LE(0); 38 | super(`Invalid response buffer (HTTP code ${code})`); 39 | } else { 40 | super(`Invalid response buffer (too short ${payload})`); 41 | } 42 | this.code = code; 43 | } 44 | } 45 | 46 | export class SecurityError extends Error { 47 | // deno-lint-ignore no-explicit-any 48 | constructor(...args: any[]) { 49 | if (!args.length) args = ["A security check failed."]; 50 | super(...args); 51 | } 52 | } 53 | 54 | export class CdnFileTamperedError extends SecurityError { 55 | constructor() { 56 | super("The CDN file has been altered and its download cancelled."); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/errors/mod.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { RPCError } from "./rpc_base_errors.ts"; 3 | import { rpcErrorRe } from "./rpc_error_list.ts"; 4 | 5 | export class BadMessageError extends Error { 6 | static ErrorMessages = { 7 | 16: 8 | "msg_id too low (most likely, client time is wrong it would be worthwhile to \ 9 | synchronize it using msg_id notifications and re-send the original message \ 10 | with the “correct” msg_id or wrap it in a container with a new msg_id if the \ 11 | original message had waited too long on the client to be transmitted).", 12 | 17: 13 | "msg_id too high (similar to the previous case, the client time has to be \ 14 | synchronized, and the message re-sent with the correct msg_id).", 15 | 18: 16 | "Incorrect two lower order msg_id bits (the server expects client message \ 17 | msg_id to be divisible by 4).", 18 | 19: 19 | "Container msg_id is the same as msg_id of a previously received message \ 20 | (this must never happen).", 21 | 20: 22 | "Message too old, and it cannot be verified whether the server has received \ 23 | a message with this msg_id or not.", 24 | 32: 25 | "msg_seqno too low (the server has already received a message with a lower \ 26 | msg_id but with either a higher or an equal and odd seqno).", 27 | 33: 28 | "msg_seqno too high (similarly, there is a message with a higher msg_id but with \ 29 | either a lower or an equal and odd seqno).", 30 | 34: "An even msg_seqno expected (irrelevant message), but odd received.", 31 | 35: "Odd msg_seqno expected (relevant message), but even received.", 32 | 48: 33 | "Incorrect server salt (in this case, the bad_server_salt response is received with \ 34 | the correct salt, and the message is to be re-sent with it).", 35 | 64: "Invalid container.", 36 | }; 37 | 38 | private errorMessage: string; 39 | 40 | constructor(request: Api.AnyRequest, private code: number) { 41 | // deno-lint-ignore no-explicit-any 42 | let errorMessage = (BadMessageError.ErrorMessages as any)[code] || 43 | `Unknown error code (this should not happen): ${code}.`; 44 | errorMessage += ` Caused by ${request.className}`; 45 | super(errorMessage); 46 | this.errorMessage = errorMessage; 47 | } 48 | } 49 | 50 | export function RPCMessageToError( 51 | rpcError: Api.RpcError, 52 | request: Api.AnyRequest, 53 | ) { 54 | for (const [msgRegex, Cls] of rpcErrorRe) { 55 | const m = rpcError.errorMessage.match(msgRegex); 56 | if (m) { 57 | const capture = m.length === 2 ? parseInt(m[1]) : null; 58 | return new Cls({ request: request, capture: capture }); 59 | } 60 | } 61 | return new RPCError(rpcError.errorMessage, request, rpcError.errorCode); 62 | } 63 | 64 | export * from "./common.ts"; 65 | export * from "./rpc_base_errors.ts"; 66 | export * from "./rpc_error_list.ts"; 67 | -------------------------------------------------------------------------------- /src/errors/rpc_base_errors.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | 3 | class CustomError extends Error { 4 | constructor(message?: string) { 5 | super(message); 6 | } 7 | } 8 | 9 | export class RPCError extends CustomError { 10 | constructor( 11 | public errorMessage: string, 12 | request: Api.AnyRequest, 13 | public code?: number, 14 | ) { 15 | super( 16 | "{0}: {1}{2}" 17 | .replace("{0}", code?.toString() || "") 18 | .replace("{1}", errorMessage || "") 19 | .replace("{2}", RPCError._fmtRequest(request)), 20 | ); 21 | } 22 | 23 | static _fmtRequest(request: Api.AnyRequest) { 24 | // TODO fix this 25 | if (request) { 26 | return ` (caused by ${request.className})`; 27 | } else { 28 | return ""; 29 | } 30 | } 31 | } 32 | 33 | export class InvalidDCError extends RPCError { 34 | constructor(message: string, request: Api.AnyRequest, code?: number) { 35 | super(message, request, code); 36 | this.code = code || 303; 37 | this.errorMessage = message || "ERROR_SEE_OTHER"; 38 | } 39 | } 40 | export class BadRequestError extends RPCError { 41 | code = 400; 42 | errorMessage = "BAD_REQUEST"; 43 | } 44 | 45 | export class UnauthorizedError extends RPCError { 46 | code = 401; 47 | errorMessage = "UNAUTHORIZED"; 48 | } 49 | 50 | export class ForbiddenError extends RPCError { 51 | code = 403; 52 | errorMessage = "FORBIDDEN"; 53 | } 54 | 55 | export class NotFoundError extends RPCError { 56 | code = 404; 57 | errorMessage = "NOT_FOUND"; 58 | } 59 | 60 | export class AuthKeyError extends RPCError { 61 | code = 406; 62 | errorMessage = "AUTH_KEY"; 63 | } 64 | 65 | export class FloodError extends RPCError { 66 | code = 420; 67 | errorMessage = "FLOOD"; 68 | } 69 | 70 | export class ServerError extends RPCError { 71 | code = 500; // Also witnessed as -500 72 | errorMessage = "INTERNAL"; 73 | } 74 | 75 | export class TimedOutError extends RPCError { 76 | code = 503; // Only witnessed as -503 77 | errorMessage = "Timeout"; 78 | } 79 | -------------------------------------------------------------------------------- /src/errors/rpc_error_list.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { 3 | BadRequestError, 4 | FloodError, 5 | InvalidDCError, 6 | RPCError, 7 | } from "./rpc_base_errors.ts"; 8 | 9 | export class UserMigrateError extends InvalidDCError { 10 | public newDc: number; 11 | 12 | constructor(args: any) { 13 | const newDc = Number(args.capture || 0); 14 | super( 15 | `The user whose identity is being used to execute queries is associated with DC ${newDc}` + 16 | RPCError._fmtRequest(args.request), 17 | args.request, 18 | ); 19 | this.message = 20 | `The user whose identity is being used to execute queries is associated with DC ${newDc}` + 21 | RPCError._fmtRequest(args.request); 22 | this.newDc = newDc; 23 | } 24 | } 25 | 26 | export class PhoneMigrateError extends InvalidDCError { 27 | public newDc: number; 28 | 29 | constructor(args: any) { 30 | const newDc = Number(args.capture || 0); 31 | super( 32 | `The phone number a user is trying to use for authorization is associated with DC ${newDc}` + 33 | RPCError._fmtRequest(args.request), 34 | args.request, 35 | ); 36 | this.message = 37 | `The phone number a user is trying to use for authorization is associated with DC ${newDc}` + 38 | RPCError._fmtRequest(args.request); 39 | this.newDc = newDc; 40 | } 41 | } 42 | 43 | export class SlowModeWaitError extends FloodError { 44 | public seconds: number; 45 | 46 | constructor(args: any) { 47 | const seconds = Number(args.capture || 0); 48 | super( 49 | `A wait of ${seconds} seconds is required before sending another message in this chat` + 50 | RPCError._fmtRequest(args.request), 51 | args.request, 52 | ); 53 | this.message = 54 | `A wait of ${seconds} seconds is required before sending another message in this chat` + 55 | RPCError._fmtRequest(args.request); 56 | this.seconds = seconds; 57 | } 58 | } 59 | 60 | export class FloodWaitError extends FloodError { 61 | public seconds: number; 62 | 63 | constructor(args: any) { 64 | const seconds = Number(args.capture || 0); 65 | super( 66 | `A wait of ${seconds} seconds is required` + 67 | RPCError._fmtRequest(args.request), 68 | args.request, 69 | ); 70 | this.message = `A wait of ${seconds} seconds is required` + 71 | RPCError._fmtRequest(args.request); 72 | this.seconds = seconds; 73 | } 74 | } 75 | 76 | export class FloodTestPhoneWaitError extends FloodError { 77 | public seconds: number; 78 | 79 | constructor(args: any) { 80 | const seconds = Number(args.capture || 0); 81 | super( 82 | `A wait of ${seconds} seconds is required in the test servers` + 83 | RPCError._fmtRequest(args.request), 84 | args.request, 85 | ); 86 | this.message = 87 | `A wait of ${seconds} seconds is required in the test servers` + 88 | RPCError._fmtRequest(args.request); 89 | this.seconds = seconds; 90 | } 91 | } 92 | 93 | export class FileMigrateError extends InvalidDCError { 94 | public newDc: number; 95 | 96 | constructor(args: any) { 97 | const newDc = Number(args.capture || 0); 98 | super( 99 | `The file to be accessed is currently stored in DC ${newDc}` + 100 | RPCError._fmtRequest(args.request), 101 | args.request, 102 | ); 103 | this.message = 104 | `The file to be accessed is currently stored in DC ${newDc}` + 105 | RPCError._fmtRequest(args.request); 106 | this.newDc = newDc; 107 | } 108 | } 109 | 110 | export class NetworkMigrateError extends InvalidDCError { 111 | public newDc: number; 112 | 113 | constructor(args: any) { 114 | const newDc = Number(args.capture || 0); 115 | super( 116 | `The source IP address is associated with DC ${newDc}` + 117 | RPCError._fmtRequest(args.request), 118 | args.request, 119 | ); 120 | this.message = `The source IP address is associated with DC ${newDc}` + 121 | RPCError._fmtRequest(args.request); 122 | this.newDc = newDc; 123 | } 124 | } 125 | 126 | export class EmailUnconfirmedError extends BadRequestError { 127 | codeLength: number; 128 | 129 | constructor(args: any) { 130 | const codeLength = Number(args.capture || 0); 131 | super( 132 | `Email unconfirmed, the length of the code must be ${codeLength}${ 133 | RPCError._fmtRequest( 134 | args.request, 135 | ) 136 | }`, 137 | args.request, 138 | 400, 139 | ); 140 | // eslint-disable-next-line max-len 141 | this.message = 142 | `Email unconfirmed, the length of the code must be ${codeLength}${ 143 | RPCError._fmtRequest( 144 | args.request, 145 | ) 146 | }`; 147 | this.codeLength = codeLength; 148 | } 149 | } 150 | 151 | export class MsgWaitError extends FloodError { 152 | constructor(args: any) { 153 | super( 154 | `Message failed to be sent.${RPCError._fmtRequest(args.request)}`, 155 | args.request, 156 | ); 157 | this.message = `Message failed to be sent.${ 158 | RPCError._fmtRequest( 159 | args.request, 160 | ) 161 | }`; 162 | } 163 | } 164 | 165 | export const rpcErrorRe = new Map([ 166 | [/FILE_MIGRATE_(\d+)/, FileMigrateError], 167 | [/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError], 168 | [/FLOOD_WAIT_(\d+)/, FloodWaitError], 169 | [/MSG_WAIT_(.*)/, MsgWaitError], 170 | [/PHONE_MIGRATE_(\d+)/, PhoneMigrateError], 171 | [/SLOWMODE_WAIT_(\d+)/, SlowModeWaitError], 172 | [/USER_MIGRATE_(\d+)/, UserMigrateError], 173 | [/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError], 174 | [/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError], 175 | ]); 176 | -------------------------------------------------------------------------------- /src/events/album.ts: -------------------------------------------------------------------------------- 1 | import { DefaultEventInterface, EventBuilder, EventCommon } from "./common.ts"; 2 | import { Api } from "../tl/api.js"; 3 | import { AbstractTelegramClient } from "../client/abstract_telegram_client.ts"; 4 | import { LogLevel } from "../extensions/logger.ts"; 5 | import { CustomMessage } from "../tl/custom/message.ts"; 6 | 7 | const ALBUM_DELAY = 500; 8 | 9 | export class Album extends EventBuilder { 10 | declare func?: { (event: Album): boolean }; 11 | 12 | constructor(albumParams: DefaultEventInterface) { 13 | const { chats, func, blacklistChats = false } = albumParams; 14 | super({ chats, blacklistChats, func }); 15 | } 16 | 17 | build(update: Api.TypeUpdate, dispatch?: CallableFunction) { 18 | if (!("message" in update && update.message instanceof Api.Message)) { 19 | return; 20 | } 21 | 22 | const groupedId = update.message.groupedId; 23 | if (!groupedId) { 24 | return; 25 | } 26 | const albums = this.client!._ALBUMS; 27 | const oldTimeout = albums.get(groupedId.toString()); 28 | const oldValues: Api.TypeUpdate[] = []; 29 | if (oldTimeout) { 30 | clearTimeout(oldTimeout[0]); 31 | oldValues.push(...oldTimeout[1]); 32 | } 33 | albums.set(groupedId.toString(), [ 34 | setTimeout(() => { 35 | const values = albums.get(groupedId.toString()); 36 | albums.delete(groupedId.toString()); 37 | if (!values) { 38 | return; 39 | } 40 | const updates = values[1]; 41 | 42 | if (!updates) { 43 | return; 44 | } 45 | const messages = new Array(); 46 | for (const update of updates) { 47 | // there is probably an easier way 48 | if ( 49 | "message" in update && 50 | update.message instanceof Api.Message 51 | ) { 52 | messages.push(new CustomMessage(update.message)); 53 | } 54 | } 55 | const event = new AlbumEvent( 56 | messages.map((v) => v.originalMessage!), 57 | values[1], 58 | ); 59 | event._setClient(this.client!); 60 | event._entities = messages[0]._entities!; 61 | dispatch!(event); 62 | }, ALBUM_DELAY), 63 | [...oldValues, update], 64 | ]); 65 | } 66 | } 67 | 68 | export class AlbumEvent extends EventCommon { 69 | messages: CustomMessage[]; 70 | originalUpdates: 71 | (Api.TypeUpdate & { _entities?: Map })[]; 72 | 73 | constructor(messages: Api.Message[], originalUpdates: Api.TypeUpdate[]) { 74 | super({ 75 | msgId: messages[0].id, 76 | chatPeer: messages[0].peerId, 77 | broadcast: messages[0].post, 78 | }); 79 | this.originalUpdates = originalUpdates; 80 | this.messages = messages.map((v) => new CustomMessage(v)); 81 | } 82 | 83 | _setClient(client: AbstractTelegramClient) { 84 | super._setClient(client); 85 | for (let i = 0; i < this.messages.length; i++) { 86 | try { 87 | // todo make sure this never fails 88 | this.messages[i]._finishInit( 89 | client, 90 | this.originalUpdates[i]._entities || new Map(), 91 | undefined, 92 | ); 93 | } catch (e) { 94 | client._log.error( 95 | "Got error while trying to finish init message with id " + 96 | this.messages[i].id, 97 | ); 98 | if (client._log.canSend(LogLevel.ERROR)) { 99 | console.error(e); 100 | } 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/events/common.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { ChatGetter } from "../tl/custom/chat_getter.ts"; 3 | import { AbstractTelegramClient } from "../client/abstract_telegram_client.ts"; 4 | import { isArrayLike, returnBigInt } from "../helpers.ts"; 5 | import { getPeerId, parseID } from "../utils.ts"; 6 | import { SenderGetter } from "../tl/custom/sender_getter.ts"; 7 | import { bigInt } from "../deps.ts"; 8 | 9 | export async function _intoIdSet( 10 | client: AbstractTelegramClient, 11 | chats: Api.TypeEntityLike[] | Api.TypeEntityLike | undefined, 12 | ): Promise { 13 | if (chats == undefined) { 14 | return undefined; 15 | } 16 | if (!isArrayLike(chats)) { 17 | chats = [chats]; 18 | } 19 | const result: Set = new Set(); 20 | for (let chat of chats) { 21 | if ( 22 | typeof chat == "number" || 23 | typeof chat == "bigint" || 24 | (typeof chat == "string" && parseID(chat)) || 25 | bigInt.isInstance(chat) 26 | ) { 27 | chat = returnBigInt(chat); 28 | if (chat.lesser(0)) { 29 | result.add(chat.toString()); 30 | } else { 31 | result.add( 32 | getPeerId( 33 | new Api.PeerUser({ 34 | userId: chat, 35 | }), 36 | ), 37 | ); 38 | result.add( 39 | getPeerId( 40 | new Api.PeerChat({ 41 | chatId: chat, 42 | }), 43 | ), 44 | ); 45 | result.add( 46 | getPeerId( 47 | new Api.PeerChannel({ 48 | channelId: chat, 49 | }), 50 | ), 51 | ); 52 | } 53 | } else if ( 54 | typeof chat == "object" && 55 | chat.SUBCLASS_OF_ID == 0x2d45687 56 | ) { 57 | result.add(getPeerId(chat)); 58 | } else { 59 | chat = await client.getInputEntity(chat); 60 | if (chat instanceof Api.InputPeerSelf) { 61 | chat = await client.getMe(true); 62 | } 63 | result.add(getPeerId(chat)); 64 | } 65 | } 66 | return Array.from(result); 67 | } 68 | 69 | export interface DefaultEventInterface { 70 | chats?: Api.TypeEntityLike[]; 71 | blacklistChats?: boolean; 72 | func?: CallableFunction; 73 | } 74 | 75 | export class EventBuilder { 76 | chats?: string[]; 77 | blacklistChats: boolean; 78 | resolved: boolean; 79 | func?: CallableFunction; 80 | client?: AbstractTelegramClient; 81 | 82 | constructor(eventParams: DefaultEventInterface) { 83 | this.chats = eventParams.chats?.map((x) => x.toString()); 84 | this.blacklistChats = eventParams.blacklistChats || false; 85 | this.resolved = false; 86 | this.func = eventParams.func; 87 | } 88 | 89 | build( 90 | update: Api.TypeUpdate, 91 | _callback?: CallableFunction, 92 | _selfId?: bigInt.BigInteger, 93 | // deno-lint-ignore no-explicit-any 94 | ): any { 95 | if (update) return update; 96 | } 97 | 98 | async resolve(client: AbstractTelegramClient) { 99 | if (this.resolved) { 100 | return; 101 | } 102 | await this._resolve(client); 103 | this.resolved = true; 104 | } 105 | 106 | async _resolve(client: AbstractTelegramClient) { 107 | this.chats = await _intoIdSet(client, this.chats); 108 | } 109 | 110 | filter( 111 | event: EventCommon | EventCommonSender, 112 | ): undefined | EventCommon | EventCommonSender { 113 | if (!this.resolved) { 114 | return; 115 | } 116 | if (this.chats != undefined) { 117 | if (event.chatId == undefined) { 118 | return; 119 | } 120 | const inside = this.chats.includes(event.chatId.toString()); 121 | if (inside == this.blacklistChats) { 122 | // If this chat matches but it's a blacklist ignore. 123 | // If it doesn't match but it's a whitelist ignore. 124 | return; 125 | } 126 | } 127 | if (this.func && !this.func(event)) { 128 | return; 129 | } 130 | return event; 131 | } 132 | } 133 | 134 | export interface EventCommonInterface { 135 | chatPeer?: Api.TypeEntityLike; 136 | msgId?: number; 137 | broadcast?: boolean; 138 | } 139 | 140 | export class EventCommon extends ChatGetter { 141 | _eventName = "Event"; 142 | _entities: Map; 143 | _messageId?: number; 144 | 145 | constructor({ 146 | chatPeer = undefined, 147 | msgId = undefined, 148 | broadcast = undefined, 149 | }: EventCommonInterface) { 150 | super(); 151 | ChatGetter.initChatClass(this, { chatPeer, broadcast }); 152 | this._entities = new Map(); 153 | this._client = undefined; 154 | this._messageId = msgId; 155 | } 156 | 157 | _setClient(client: AbstractTelegramClient) { 158 | this._client = client; 159 | } 160 | 161 | get client() { 162 | return this._client; 163 | } 164 | } 165 | 166 | export class EventCommonSender extends SenderGetter { 167 | _eventName = "Event"; 168 | _entities: Map; 169 | _messageId?: number; 170 | 171 | constructor({ 172 | chatPeer = undefined, 173 | msgId = undefined, 174 | broadcast = undefined, 175 | }: EventCommonInterface) { 176 | super(); 177 | ChatGetter.initChatClass(this, { chatPeer, broadcast }); 178 | SenderGetter.initChatClass(this, { chatPeer, broadcast }); 179 | this._entities = new Map(); 180 | this._client = undefined; 181 | this._messageId = msgId; 182 | } 183 | 184 | _setClient(client: AbstractTelegramClient) { 185 | this._client = client; 186 | } 187 | 188 | get client() { 189 | return this._client; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/events/deleted_message.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { DefaultEventInterface, EventBuilder, EventCommon } from "./common.ts"; 3 | import { bigInt } from "../deps.ts"; 4 | 5 | export class DeletedMessage extends EventBuilder { 6 | constructor(eventParams: DefaultEventInterface) { 7 | super(eventParams); 8 | } 9 | 10 | build( 11 | update: Api.TypeUpdate | Api.TypeUpdates, 12 | _callback: undefined, 13 | _selfId: bigInt.BigInteger, 14 | ) { 15 | if (update instanceof Api.UpdateDeleteChannelMessages) { 16 | return new DeletedMessageEvent( 17 | update.messages, 18 | new Api.PeerChannel({ channelId: update.channelId }), 19 | ); 20 | } else if (update instanceof Api.UpdateDeleteMessages) { 21 | return new DeletedMessageEvent(update.messages); 22 | } 23 | } 24 | } 25 | 26 | export class DeletedMessageEvent extends EventCommon { 27 | deletedIds: number[]; 28 | peer?: Api.TypeEntityLike; 29 | 30 | constructor(deletedIds: number[], peer?: Api.TypeEntityLike) { 31 | super({ 32 | chatPeer: peer, 33 | msgId: Array.isArray(deletedIds) ? deletedIds[0] : 0, 34 | }); 35 | this.deletedIds = deletedIds; 36 | this.peer = peer; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/events/edited_message.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { 3 | NewMessage, 4 | NewMessageEvent, 5 | NewMessageInterface, 6 | } from "./new_message.ts"; 7 | import { bigInt } from "../deps.ts"; 8 | 9 | export interface EditedMessageInterface extends NewMessageInterface { 10 | func?: { (event: EditedMessageEvent): boolean }; 11 | } 12 | 13 | export class EditedMessage extends NewMessage { 14 | declare func?: { (event: EditedMessageEvent): boolean }; 15 | 16 | constructor(editedMessageParams: EditedMessageInterface) { 17 | super(editedMessageParams); 18 | } 19 | 20 | build( 21 | update: Api.TypeUpdate | Api.TypeUpdates, 22 | _callback: undefined, 23 | _selfId: bigInt.BigInteger, 24 | ) { 25 | if ( 26 | update instanceof Api.UpdateEditChannelMessage || 27 | update instanceof Api.UpdateEditMessage 28 | ) { 29 | if (!(update.message instanceof Api.Message)) { 30 | return undefined; 31 | } 32 | const event = new EditedMessageEvent(update.message, update); 33 | this.addAttributes(event); 34 | return event; 35 | } 36 | } 37 | } 38 | 39 | export class EditedMessageEvent extends NewMessageEvent { 40 | constructor( 41 | message: Api.Message, 42 | originalUpdate: Api.TypeUpdate | Api.TypeUpdates, 43 | ) { 44 | super(message, originalUpdate); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/events/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./album.ts"; 2 | export * from "./callback_query.ts"; 3 | export * from "./common.ts"; 4 | export * from "./deleted_message.ts"; 5 | export * from "./edited_message.ts"; 6 | export * from "./new_message.ts"; 7 | export * from "./raw.ts"; 8 | -------------------------------------------------------------------------------- /src/events/new_message.ts: -------------------------------------------------------------------------------- 1 | import { 2 | _intoIdSet, 3 | DefaultEventInterface, 4 | EventBuilder, 5 | EventCommon, 6 | } from "./common.ts"; 7 | import { AbstractTelegramClient } from "../client/abstract_telegram_client.ts"; 8 | import { Api } from "../tl/api.js"; 9 | import { LogLevel } from "../extensions/logger.ts"; 10 | import { bigInt } from "../deps.ts"; 11 | import { CustomMessage } from "../tl/custom/message.ts"; 12 | 13 | export interface NewMessageInterface extends DefaultEventInterface { 14 | func?: { (event: NewMessageEvent): boolean }; 15 | incoming?: boolean; 16 | outgoing?: boolean; 17 | fromUsers?: Api.TypeEntityLike[]; 18 | forwards?: boolean; 19 | pattern?: RegExp; 20 | } 21 | 22 | export class NewMessage extends EventBuilder { 23 | declare func?: { (event: NewMessageEvent): boolean }; 24 | incoming?: boolean; 25 | outgoing?: boolean; 26 | fromUsers?: Api.TypeEntityLike[]; 27 | forwards?: boolean; 28 | pattern?: RegExp; 29 | 30 | /** @hidden */ 31 | private readonly _noCheck: boolean; 32 | 33 | constructor(newMessageParams: NewMessageInterface = {}) { 34 | let { 35 | chats, 36 | func, 37 | incoming, 38 | outgoing, 39 | fromUsers, 40 | forwards, 41 | pattern, 42 | blacklistChats = false, 43 | } = newMessageParams; 44 | if (incoming && outgoing) { 45 | incoming = outgoing = undefined; 46 | } else if (incoming != undefined && outgoing == undefined) { 47 | outgoing = !incoming; 48 | } else if (outgoing != undefined && incoming == undefined) { 49 | incoming = !outgoing; 50 | } else if (outgoing == false && incoming == false) { 51 | throw new Error( 52 | "Don't create an event handler if you don't want neither incoming nor outgoing!", 53 | ); 54 | } 55 | super({ chats, blacklistChats, func }); 56 | this.incoming = incoming; 57 | this.outgoing = outgoing; 58 | this.fromUsers = fromUsers; 59 | this.forwards = forwards; 60 | this.pattern = pattern; 61 | this._noCheck = [ 62 | incoming, 63 | outgoing, 64 | chats, 65 | pattern, 66 | fromUsers, 67 | forwards, 68 | func, 69 | ].every((v) => v == undefined); 70 | } 71 | 72 | async _resolve(client: AbstractTelegramClient) { 73 | await super._resolve(client); 74 | this.fromUsers = await _intoIdSet(client, this.fromUsers); 75 | } 76 | 77 | build( 78 | update: Api.TypeUpdate | Api.TypeUpdates, 79 | _callback: undefined, 80 | selfId: bigInt.BigInteger, 81 | ) { 82 | if ( 83 | update instanceof Api.UpdateNewMessage || 84 | update instanceof Api.UpdateNewChannelMessage 85 | ) { 86 | if (!(update.message instanceof Api.Message)) { 87 | return undefined; 88 | } 89 | const event = new NewMessageEvent(update.message, update); 90 | this.addAttributes(event); 91 | return event; 92 | } else if (update instanceof Api.UpdateShortMessage) { 93 | return new NewMessageEvent( 94 | new Api.Message({ 95 | out: update.out, 96 | mentioned: update.mentioned, 97 | mediaUnread: update.mediaUnread, 98 | silent: update.silent, 99 | id: update.id, 100 | peerId: new Api.PeerUser({ userId: update.userId }), 101 | fromId: new Api.PeerUser({ 102 | userId: update.out ? selfId : update.userId, 103 | }), 104 | message: update.message, 105 | date: update.date, 106 | fwdFrom: update.fwdFrom, 107 | viaBotId: update.viaBotId, 108 | replyTo: update.replyTo, 109 | entities: update.entities, 110 | ttlPeriod: update.ttlPeriod, 111 | }), 112 | update, 113 | ); 114 | } else if (update instanceof Api.UpdateShortChatMessage) { 115 | return new NewMessageEvent( 116 | new Api.Message({ 117 | out: update.out, 118 | mentioned: update.mentioned, 119 | mediaUnread: update.mediaUnread, 120 | silent: update.silent, 121 | id: update.id, 122 | peerId: new Api.PeerChat({ chatId: update.chatId }), 123 | fromId: new Api.PeerUser({ 124 | userId: update.out ? selfId : update.fromId, 125 | }), 126 | message: update.message, 127 | date: update.date, 128 | fwdFrom: update.fwdFrom, 129 | viaBotId: update.viaBotId, 130 | replyTo: update.replyTo, 131 | entities: update.entities, 132 | ttlPeriod: update.ttlPeriod, 133 | }), 134 | update, 135 | ); 136 | } 137 | } 138 | 139 | filter(event: NewMessageEvent) { 140 | if (this._noCheck) { 141 | return event; 142 | } 143 | if (this.incoming && event.message.out) { 144 | return; 145 | } 146 | if (this.outgoing && !event.message.out) { 147 | return; 148 | } 149 | if (this.forwards != undefined) { 150 | if (this.forwards != !!event.message.fwdFrom) { 151 | return; 152 | } 153 | } 154 | 155 | if (this.fromUsers != undefined) { 156 | if ( 157 | !event.message.senderId || 158 | !this.fromUsers.includes(event.message.senderId.toString()) 159 | ) { 160 | return; 161 | } 162 | } 163 | 164 | if (this.pattern) { 165 | const match = event.message.message?.match(this.pattern); 166 | if (!match) { 167 | return; 168 | } 169 | event.message.patternMatch = match; 170 | } 171 | return super.filter(event); 172 | } 173 | 174 | // deno-lint-ignore no-explicit-any 175 | addAttributes(_update: any) { 176 | //update.patternMatch = 177 | } 178 | } 179 | 180 | export class NewMessageEvent extends EventCommon { 181 | message: CustomMessage; 182 | originalUpdate: (Api.TypeUpdate | Api.TypeUpdates) & { 183 | _entities?: Map; 184 | }; 185 | 186 | constructor( 187 | message: Api.Message, 188 | originalUpdate: Api.TypeUpdate | Api.TypeUpdates, 189 | ) { 190 | super({ 191 | msgId: message.id, 192 | chatPeer: message.peerId, 193 | broadcast: message.post, 194 | }); 195 | this.originalUpdate = originalUpdate; 196 | this.message = new CustomMessage(message); 197 | } 198 | 199 | _setClient(client: AbstractTelegramClient) { 200 | super._setClient(client); 201 | const m = this.message; 202 | try { 203 | // todo make sure this never fails 204 | m._finishInit( 205 | client, 206 | this.originalUpdate._entities || new Map(), 207 | undefined, 208 | ); 209 | } catch (e) { 210 | client._log.error( 211 | "Got error while trying to finish init message with id " + m.id, 212 | ); 213 | if (client._log.canSend(LogLevel.ERROR)) { 214 | console.error(e); 215 | } 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/events/raw.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import { AbstractTelegramClient } from "../client/abstract_telegram_client.ts"; 3 | import { EventBuilder, EventCommon } from "./common.ts"; 4 | 5 | export interface RawInterface { 6 | // deno-lint-ignore ban-types 7 | types?: Function[]; 8 | func?: CallableFunction; 9 | } 10 | 11 | export class Raw extends EventBuilder { 12 | // deno-lint-ignore ban-types 13 | private readonly types?: Function[]; 14 | 15 | constructor(params: RawInterface) { 16 | super({ func: params.func }); 17 | this.types = params.types; 18 | } 19 | 20 | // deno-lint-ignore require-await 21 | async resolve(_client: AbstractTelegramClient) { 22 | this.resolved = true; 23 | } 24 | 25 | build(update: Api.TypeUpdate): Api.TypeUpdate { 26 | return update; 27 | } 28 | 29 | filter(event: EventCommon) { 30 | if (this.types) { 31 | let correct = false; 32 | for (const type of this.types) { 33 | if (event instanceof type) { 34 | correct = true; 35 | break; 36 | } 37 | } 38 | if (!correct) { 39 | return; 40 | } 41 | } 42 | return super.filter(event); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/extensions/async_queue.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | export class AsyncQueue { 3 | public _queue: any[]; 4 | private canGet: Promise; 5 | private resolveGet: (value?: any) => void; 6 | private canPush: boolean | Promise; 7 | private resolvePush: (value?: any) => void; 8 | 9 | constructor() { 10 | this._queue = []; 11 | this.canPush = true; 12 | this.resolvePush = (_value) => {}; 13 | this.resolveGet = (_value) => {}; 14 | this.canGet = new Promise((resolve) => { 15 | this.resolveGet = resolve; 16 | }); 17 | } 18 | 19 | async push(value: any) { 20 | await this.canPush; 21 | this._queue.push(value); 22 | this.resolveGet(true); 23 | this.canPush = new Promise((resolve) => { 24 | this.resolvePush = resolve; 25 | }); 26 | } 27 | 28 | async pop() { 29 | await this.canGet; 30 | const returned = this._queue.pop(); 31 | this.resolvePush(true); 32 | this.canGet = new Promise((resolve) => { 33 | this.resolveGet = resolve; 34 | }); 35 | return returned; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/extensions/binary_reader.ts: -------------------------------------------------------------------------------- 1 | import { TypeNotFoundError } from "../errors/common.ts"; 2 | import { coreObjects } from "../tl/core/core_objects.ts"; 3 | import { tlObjects } from "../tl/all_tl_objects.ts"; 4 | import { readBigIntFromBuffer } from "../helpers.ts"; 5 | import { Buffer } from "../deps.ts"; 6 | import { BinaryReader as IBinaryReader } from "./interfaces.ts"; 7 | 8 | export class BinaryReader implements IBinaryReader { 9 | private readonly stream: Buffer; 10 | private _last?: Buffer; 11 | offset: number; 12 | 13 | constructor(data: Buffer) { 14 | this.stream = data; 15 | this._last = undefined; 16 | this.offset = 0; 17 | } 18 | 19 | read(length = -1, checkLength = true) { 20 | if (length === -1) { 21 | length = this.stream.length - this.offset; 22 | } 23 | const result = this.stream.slice(this.offset, this.offset + length); 24 | this.offset += length; 25 | if (checkLength && result.length !== length) { 26 | throw Error( 27 | `No more data left to read (need ${length}, got ${result.length}: ${result}); last read ${this._last}`, 28 | ); 29 | } 30 | this._last = result; 31 | return result; 32 | } 33 | 34 | readByte() { 35 | return this.read(1)[0]; 36 | } 37 | 38 | readInt(signed = true) { 39 | let res; 40 | if (signed) { 41 | res = this.stream.readInt32LE(this.offset); 42 | } else { 43 | res = this.stream.readUInt32LE(this.offset); 44 | } 45 | this.offset += 4; 46 | return res; 47 | } 48 | 49 | readLargeInt(bits: number, signed = true) { 50 | const buffer = this.read(Math.floor(bits / 8)); 51 | return readBigIntFromBuffer(buffer, true, signed); 52 | } 53 | 54 | readLong(signed = true) { 55 | return this.readLargeInt(64, signed); 56 | } 57 | 58 | readFloat() { 59 | return this.read(4).readFloatLE(0); 60 | } 61 | 62 | readDouble() { 63 | // was this a bug ? it should have been 0) { 87 | padding = 4 - padding; 88 | this.read(padding); 89 | } 90 | 91 | return data; 92 | } 93 | 94 | tgReadString() { 95 | return this.tgReadBytes().toString("utf-8"); 96 | } 97 | 98 | tgReadBool() { 99 | const value = this.readInt(false); 100 | if (value === 0x997275b5) { 101 | // boolTrue 102 | return true; 103 | } else if (value === 0xbc799737) { 104 | // boolFalse 105 | return false; 106 | } else { 107 | throw new Error(`Invalid boolean code ${value.toString(16)}`); 108 | } 109 | } 110 | tgReadDate() { 111 | const value = this.readInt(); 112 | return new Date(value * 1000); 113 | } 114 | 115 | seek(offset: number) { 116 | this.offset += offset; 117 | } 118 | 119 | setPosition(position: number) { 120 | this.offset = position; 121 | } 122 | 123 | tellPosition() { 124 | return this.offset; 125 | } 126 | 127 | // deno-lint-ignore no-explicit-any 128 | tgReadObject(): any { 129 | const constructorId = this.readInt(false); 130 | 131 | let clazz = tlObjects[constructorId]; 132 | if (clazz === undefined) { 133 | /** 134 | * The class was undefined, but there's still a 135 | * chance of it being a manually parsed value like bool! 136 | */ 137 | const value = constructorId; 138 | if (value === 0x997275b5) { 139 | // boolTrue 140 | return true; 141 | } else if (value === 0xbc799737) { 142 | // boolFalse 143 | return false; 144 | } else if (value === 0x1cb5c415) { 145 | // Vector 146 | const temp = []; 147 | const length = this.readInt(); 148 | for (let i = 0; i < length; i++) { 149 | temp.push(this.tgReadObject()); 150 | } 151 | return temp; 152 | } 153 | 154 | clazz = coreObjects.get(constructorId); 155 | 156 | if (clazz === undefined) { 157 | // If there was still no luck, give up 158 | this.seek(-4); // Go back 159 | const pos = this.tellPosition(); 160 | const error = new TypeNotFoundError(constructorId, this.read()); 161 | this.setPosition(pos); 162 | throw error; 163 | } 164 | } 165 | return clazz.fromReader(this); 166 | } 167 | 168 | tgReadVector() { 169 | if (this.readInt(false) !== 0x1cb5c415) { 170 | throw new Error("Invalid constructor code, vector was expected"); 171 | } 172 | const count = this.readInt(); 173 | const temp = []; 174 | for (let i = 0; i < count; i++) { 175 | temp.push(this.tgReadObject()); 176 | } 177 | return temp; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/extensions/binary_writer.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from "../deps.ts"; 2 | 3 | export class BinaryWriter { 4 | private _stream: Buffer; 5 | 6 | constructor(stream: Buffer) { 7 | this._stream = stream; 8 | } 9 | 10 | write(buffer: Buffer) { 11 | this._stream = Buffer.concat([this._stream, buffer]); 12 | } 13 | 14 | getValue() { 15 | return this._stream; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/extensions/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { bigInt, Buffer } from "../deps.ts"; 2 | 3 | export interface BinaryReader { 4 | offset: number; 5 | read(length?: number, checkLength?: boolean): Buffer; 6 | readByte(): number; 7 | readInt(signed?: boolean): number; 8 | readLargeInt(bits: number, signed?: boolean): bigInt.BigInteger; 9 | readLong(signed?: boolean): bigInt.BigInteger; 10 | readFloat(): number; 11 | readDouble(): number; 12 | getBuffer(): Buffer; 13 | tgReadBytes(): Buffer; 14 | tgReadString(): string; 15 | tgReadBool(): boolean; 16 | tgReadDate(): Date; 17 | seek(offset: number): void; 18 | setPosition(position: number): void; 19 | tellPosition(): number; 20 | // deno-lint-ignore no-explicit-any 21 | tgReadObject(): any; 22 | // deno-lint-ignore no-explicit-any 23 | tgReadVector(): any[]; 24 | } 25 | -------------------------------------------------------------------------------- /src/extensions/logger.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | NONE = "none", 3 | ERROR = "error", 4 | WARN = "warn", 5 | INFO = "info", 6 | DEBUG = "debug", 7 | } 8 | 9 | export class Logger { 10 | private levels = ["error", "warn", "info", "debug"]; 11 | private colors: { 12 | warn: string; 13 | debug: string; 14 | start: string; 15 | end: string; 16 | error: string; 17 | info: string; 18 | }; 19 | public messageFormat: string; 20 | private _logLevel: LogLevel | `${LogLevel}`; 21 | public tzOffset: number; 22 | 23 | constructor(level?: LogLevel) { 24 | this._logLevel = level || LogLevel.INFO; 25 | this.colors = { 26 | start: "\x1b[2m", 27 | warn: "\x1b[35m", 28 | info: "\x1b[33m", 29 | debug: "\x1b[36m", 30 | error: "\x1b[31m", 31 | end: "\x1b[0m", 32 | }; 33 | this.messageFormat = "[%t] [%l] - [%m]"; 34 | this.tzOffset = new Date().getTimezoneOffset() * 60000; 35 | } 36 | 37 | canSend(level: LogLevel) { 38 | return this._logLevel 39 | ? this.levels.indexOf(this._logLevel) >= this.levels.indexOf(level) 40 | : false; 41 | } 42 | 43 | warn(message: string) { 44 | this._log(LogLevel.WARN, message, this.colors.warn); 45 | } 46 | 47 | info(message: string) { 48 | this._log(LogLevel.INFO, message, this.colors.info); 49 | } 50 | 51 | debug(message: string) { 52 | this._log(LogLevel.DEBUG, message, this.colors.debug); 53 | } 54 | 55 | error(message: string) { 56 | this._log(LogLevel.ERROR, message, this.colors.error); 57 | } 58 | 59 | format(message: string, level: string) { 60 | return this.messageFormat 61 | .replace("%t", this.getDateTime()) 62 | .replace("%l", level.toUpperCase()) 63 | .replace("%m", message); 64 | } 65 | 66 | get logLevel() { 67 | return this._logLevel; 68 | } 69 | 70 | setLevel(level: LogLevel | `${LogLevel}`) { 71 | this._logLevel = level; 72 | } 73 | 74 | static setLevel(_level: string) { 75 | console.log( 76 | "Logger.setLevel is deprecated, it will has no effect. Please, use client.setLogLevel instead.", 77 | ); 78 | } 79 | 80 | _log(level: LogLevel, message: string, color: string) { 81 | if (this.canSend(level)) { 82 | this.log(level, message, color); 83 | } else { 84 | return; 85 | } 86 | } 87 | 88 | log(level: LogLevel, message: string, color: string) { 89 | console[level == "none" ? "log" : level]( 90 | (typeof document === "undefined" ? this.colors.start : "") + 91 | this.format(message, level), 92 | typeof document === "undefined" ? color : "", 93 | ); 94 | } 95 | 96 | getDateTime() { 97 | return new Date(Date.now() - this.tzOffset).toISOString().slice(0, -1); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/extensions/markdown.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | 3 | type messageEntities = 4 | | typeof Api.MessageEntityBold 5 | | typeof Api.MessageEntityItalic 6 | | typeof Api.MessageEntityStrike 7 | | typeof Api.MessageEntityCode 8 | | typeof Api.MessageEntityPre; 9 | 10 | const DEFAULT_DELIMITERS: { [key: string]: messageEntities } = { 11 | "**": Api.MessageEntityBold, 12 | __: Api.MessageEntityItalic, 13 | "~~": Api.MessageEntityStrike, 14 | "`": Api.MessageEntityCode, 15 | "```": Api.MessageEntityPre, 16 | }; 17 | 18 | export class MarkdownParser { 19 | static parse(message: string): [string, Api.TypeMessageEntity[]] { 20 | let i = 0; 21 | const keys: { [key: string]: boolean } = {}; 22 | for (const k in DEFAULT_DELIMITERS) { 23 | keys[k] = false; 24 | } 25 | const entities = []; 26 | // deno-lint-ignore no-explicit-any 27 | const tempEntities: { [key: string]: any } = {}; 28 | while (i < message.length) { 29 | let foundIndex = -1; 30 | let foundDelim = undefined; 31 | for (const key of Object.keys(DEFAULT_DELIMITERS)) { 32 | const index = message.indexOf(key, i); 33 | if (index > -1 && (foundIndex === -1 || index < foundIndex)) { 34 | foundIndex = index; 35 | foundDelim = key; 36 | } 37 | } 38 | 39 | if (foundIndex === -1 || foundDelim === undefined) { 40 | break; 41 | } 42 | if (!keys[foundDelim]) { 43 | tempEntities[foundDelim] = new DEFAULT_DELIMITERS[foundDelim]({ 44 | offset: foundIndex, 45 | length: -1, 46 | language: "", 47 | }); 48 | keys[foundDelim] = true; 49 | } else { 50 | keys[foundDelim] = false; 51 | tempEntities[foundDelim].length = foundIndex - 52 | tempEntities[foundDelim].offset; 53 | entities.push(tempEntities[foundDelim]); 54 | } 55 | message = message.replace(foundDelim, ""); 56 | i = foundIndex; 57 | } 58 | return [message, entities]; 59 | } 60 | 61 | static unparse( 62 | text: string, 63 | entities: Api.TypeMessageEntity[] | undefined, 64 | ) { 65 | const delimiters = DEFAULT_DELIMITERS; 66 | if (!text || !entities) { 67 | return text; 68 | } 69 | let insertAt: [number, string][] = []; 70 | 71 | const tempDelimiters: Map = new Map(); 72 | Object.keys(delimiters).forEach((key) => { 73 | tempDelimiters.set(delimiters[key].className, key); 74 | }); 75 | for (const entity of entities) { 76 | const s = entity.offset; 77 | const e = entity.offset + entity.length; 78 | const delimiter = tempDelimiters.get(entity.className); 79 | if (delimiter) { 80 | insertAt.push([s, delimiter]); 81 | insertAt.push([e, delimiter]); 82 | } 83 | } 84 | insertAt = insertAt.sort((a: [number, string], b: [number, string]) => { 85 | return a[0] - b[0]; 86 | }); 87 | while (insertAt.length) { 88 | const [at, what] = insertAt.pop() as [number, string]; 89 | text = text.slice(0, at) + what + text.slice(at); 90 | } 91 | return text; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/extensions/message_packer.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { Buffer } from "../deps.ts"; 3 | import { MessageContainer, TLMessage } from "../tl/core/mod.ts"; 4 | import { BinaryWriter } from "./binary_writer.ts"; 5 | import type { MTProtoState } from "../network/mtproto_state.ts"; 6 | import type { RequestState } from "../network/request_state.ts"; 7 | 8 | export class MessagePacker { 9 | private _state: MTProtoState; 10 | private _pendingStates: RequestState[]; 11 | private _queue: any[]; 12 | private _ready: Promise; 13 | private setReady: ((value?: any) => void) | undefined; 14 | private _log: any; 15 | 16 | constructor(state: MTProtoState, logger: any) { 17 | this._state = state; 18 | this._queue = []; 19 | this._pendingStates = []; 20 | 21 | this._ready = new Promise((resolve) => { 22 | this.setReady = resolve; 23 | }); 24 | this._log = logger; 25 | } 26 | 27 | values() { 28 | return this._queue; 29 | } 30 | 31 | append(state: RequestState) { 32 | this._queue.push(state); 33 | 34 | if (this.setReady) { 35 | this.setReady(true); 36 | } 37 | } 38 | 39 | extend(states: RequestState[]) { 40 | for (const state of states) { 41 | this.append(state); 42 | } 43 | } 44 | 45 | async get() { 46 | if (!this._queue.length) { 47 | this._ready = new Promise((resolve) => { 48 | this.setReady = resolve; 49 | }); 50 | await this._ready; 51 | } 52 | let data; 53 | let buffer = new BinaryWriter(Buffer.alloc(0)); 54 | 55 | const batch = []; 56 | let size = 0; 57 | 58 | while ( 59 | this._queue.length && 60 | batch.length <= MessageContainer.MAXIMUM_LENGTH 61 | ) { 62 | const state = this._queue.shift(); 63 | size += state.data.length + TLMessage.SIZE_OVERHEAD; 64 | if (size <= MessageContainer.MAXIMUM_SIZE) { 65 | let afterId; 66 | if (state.after) { 67 | afterId = state.after.msgId; 68 | } 69 | state.msgId = await this._state.writeDataAsMessage( 70 | buffer, 71 | state.data, 72 | state.request.classType === "request", 73 | afterId, 74 | ); 75 | this._log.debug( 76 | `Assigned msgId = ${state.msgId} to ${ 77 | state.request.className || 78 | state.request.constructor.name 79 | }`, 80 | ); 81 | batch.push(state); 82 | continue; 83 | } 84 | if (batch.length) { 85 | this._queue.unshift(state); 86 | break; 87 | } 88 | this._log.warn( 89 | `Message payload for ${ 90 | state.request.className || state.request.constructor.name 91 | } is too long ${state.data.length} and cannot be sent`, 92 | ); 93 | state.promise.reject("Request Payload is too big"); 94 | size = 0; 95 | } 96 | if (!batch.length) { 97 | return null; 98 | } 99 | if (batch.length > 1) { 100 | const b = Buffer.alloc(8); 101 | b.writeUInt32LE(MessageContainer.CONSTRUCTOR_ID, 0); 102 | b.writeInt32LE(batch.length, 4); 103 | data = Buffer.concat([b, buffer.getValue()]); 104 | buffer = new BinaryWriter(Buffer.alloc(0)); 105 | const containerId = await this._state.writeDataAsMessage( 106 | buffer, 107 | data, 108 | false, 109 | ); 110 | for (const s of batch) { 111 | s.containerId = containerId; 112 | } 113 | } 114 | 115 | data = buffer.getValue(); 116 | return { batch, data }; 117 | } 118 | rejectAll() { 119 | this._pendingStates.forEach((requestState) => { 120 | requestState.reject( 121 | new Error( 122 | "Disconnect (caused from " + 123 | requestState?.request?.className + 124 | ")", 125 | ), 126 | ); 127 | }); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/extensions/promised_net_sockets.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { Buffer, Mutex, Socket, SocksClient } from "../deps.ts"; 3 | import { ProxyInterface } from "../network/connection/types.ts"; 4 | 5 | const mutex = new Mutex(); 6 | const closeError = new Error("NetSocket was closed"); 7 | 8 | export class PromisedNetSockets { 9 | private client?: InstanceType; 10 | private closed: boolean; 11 | private stream: Buffer; 12 | private canRead?: boolean | Promise; 13 | private resolveRead: ((value?: any) => void) | undefined; 14 | private proxy?: ProxyInterface; 15 | 16 | constructor(proxy?: ProxyInterface) { 17 | this.client = undefined; 18 | this.closed = true; 19 | this.stream = Buffer.alloc(0); 20 | 21 | if (!proxy?.MTProxy) { 22 | if (proxy) { 23 | if (!proxy.ip || !proxy.port || !proxy.socksType) { 24 | throw new Error( 25 | `Invalid sockets params. ${proxy.ip}, ${proxy.port}, ${proxy.socksType}`, 26 | ); 27 | } 28 | } 29 | 30 | this.proxy = proxy; 31 | } 32 | } 33 | 34 | async readExactly(number: number) { 35 | let readData = Buffer.alloc(0); 36 | while (true) { 37 | const thisTime = await this.read(number); 38 | readData = Buffer.concat([readData, thisTime]); 39 | number = number - thisTime.length; 40 | if (!number) { 41 | return readData; 42 | } 43 | } 44 | } 45 | 46 | async read(number: number) { 47 | if (this.closed) { 48 | throw closeError; 49 | } 50 | await this.canRead; 51 | if (this.closed) { 52 | throw closeError; 53 | } 54 | const toReturn = this.stream.slice(0, number); 55 | this.stream = this.stream.slice(number); 56 | if (this.stream.length === 0) { 57 | this.canRead = new Promise((resolve) => { 58 | this.resolveRead = resolve; 59 | }); 60 | } 61 | 62 | return toReturn; 63 | } 64 | 65 | async readAll() { 66 | if (this.closed || !(await this.canRead)) { 67 | throw closeError; 68 | } 69 | const toReturn = this.stream; 70 | this.stream = Buffer.alloc(0); 71 | this.canRead = new Promise((resolve) => { 72 | this.resolveRead = resolve; 73 | }); 74 | return toReturn; 75 | } 76 | 77 | async connect(port: number, ip: string) { 78 | this.stream = Buffer.alloc(0); 79 | let connected = false; 80 | if (this.proxy) { 81 | const info = await SocksClient.createConnection({ 82 | proxy: { 83 | host: this.proxy.ip, 84 | port: this.proxy.port, 85 | type: this.proxy.socksType != undefined ? this.proxy.socksType : 5, // Proxy version (4 or 5) 86 | userId: this.proxy.username, 87 | password: this.proxy.password, 88 | }, 89 | 90 | command: "connect", 91 | timeout: (this.proxy.timeout || 5) * 1000, 92 | destination: { 93 | host: ip, 94 | port: port, 95 | }, 96 | }); 97 | // @ts-ignore heh 98 | this.client = info.socket; 99 | connected = true; 100 | } else { 101 | // @ts-ignore missing args 102 | this.client = new Socket(); 103 | } 104 | 105 | this.canRead = new Promise((resolve) => { 106 | this.resolveRead = resolve; 107 | }); 108 | this.closed = false; 109 | return new Promise((resolve, reject) => { 110 | if (this.client) { 111 | if (connected) { 112 | this.receive(); 113 | resolve(this); 114 | } else { 115 | this.client.connect(port, ip, () => { 116 | this.receive(); 117 | resolve(this); 118 | }); 119 | } 120 | this.client.on("error", reject); 121 | this.client.on("close", () => { 122 | if (this.client && this.client.destroyed) { 123 | if (this.resolveRead) { 124 | this.resolveRead(false); 125 | } 126 | this.closed = true; 127 | } 128 | }); 129 | } 130 | }); 131 | } 132 | 133 | write(data: Buffer) { 134 | if (this.closed) { 135 | throw closeError; 136 | } 137 | if (this.client) { 138 | this.client.write(data); 139 | } 140 | } 141 | 142 | close() { 143 | if (this.client) { 144 | this.client.destroy(); 145 | this.client.unref(); 146 | } 147 | this.closed = true; 148 | } 149 | 150 | receive() { 151 | if (this.client) { 152 | this.client.on("data", async (message: Buffer) => { 153 | const release = await mutex.acquire(); 154 | try { 155 | let _data; 156 | //CONTEST BROWSER 157 | this.stream = Buffer.concat([this.stream, message]); 158 | if (this.resolveRead) { 159 | this.resolveRead(true); 160 | } 161 | } finally { 162 | release(); 163 | } 164 | }); 165 | } 166 | } 167 | 168 | toString() { 169 | return "PromisedNetSocket"; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/extensions/promised_web_sockets.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { Buffer, Mutex } from "../deps.ts"; 3 | 4 | const mutex = new Mutex(); 5 | const closeError = new Error("WebSocket was closed"); 6 | 7 | export class PromisedWebSockets { 8 | private closed: boolean; 9 | private stream: Buffer; 10 | private canRead?: boolean | Promise; 11 | private resolveRead: ((value?: any) => void) | undefined; 12 | private client: WebSocket | undefined; 13 | private website?: string; 14 | 15 | constructor() { 16 | this.client = undefined; 17 | this.stream = Buffer.alloc(0); 18 | this.closed = true; 19 | } 20 | 21 | async readExactly(number: number) { 22 | let readData = Buffer.alloc(0); 23 | while (true) { 24 | const thisTime = await this.read(number); 25 | readData = Buffer.concat([readData, thisTime]); 26 | number = number - thisTime.length; 27 | 28 | if (!number) return readData; 29 | } 30 | } 31 | 32 | async read(number: number) { 33 | if (this.closed) throw closeError; 34 | await this.canRead; 35 | if (this.closed) throw closeError; 36 | 37 | const toReturn = this.stream.slice(0, number); 38 | this.stream = this.stream.slice(number); 39 | 40 | if (this.stream.length === 0) { 41 | this.canRead = new Promise((resolve) => { 42 | this.resolveRead = resolve; 43 | }); 44 | } 45 | 46 | return toReturn; 47 | } 48 | 49 | async readAll() { 50 | if (this.closed || !(await this.canRead)) throw closeError; 51 | 52 | const toReturn = this.stream; 53 | this.stream = Buffer.alloc(0); 54 | this.canRead = new Promise((resolve) => { 55 | this.resolveRead = resolve; 56 | }); 57 | 58 | return toReturn; 59 | } 60 | 61 | getWebSocketLink(ip: string, port: number, testServers: boolean) { 62 | if (port === 443) { 63 | return `wss://${ip}:${port}/apiws${testServers ? "_test" : ""}`; 64 | } else { 65 | return `ws://${ip}:${port}/apiws${testServers ? "_test" : ""}`; 66 | } 67 | } 68 | 69 | connect(port: number, ip: string, testServers = false) { 70 | this.stream = Buffer.alloc(0); 71 | this.canRead = new Promise((resolve) => { 72 | this.resolveRead = resolve; 73 | }); 74 | this.closed = false; 75 | this.website = this.getWebSocketLink(ip, port, testServers); 76 | this.client = new WebSocket(this.website, "binary"); 77 | 78 | return new Promise((resolve, reject) => { 79 | if (this.client) { 80 | this.client.onopen = () => { 81 | this.receive(); 82 | resolve(this); 83 | }; 84 | this.client.onerror = (error: any) => { 85 | reject(error); 86 | }; 87 | this.client.onclose = () => { 88 | if (this.resolveRead) { 89 | this.resolveRead(false); 90 | } 91 | this.closed = true; 92 | }; 93 | } 94 | }); 95 | } 96 | 97 | write(data: Buffer) { 98 | if (this.closed) { 99 | throw closeError; 100 | } 101 | if (this.client) { 102 | this.client.send(data); 103 | } 104 | } 105 | 106 | close() { 107 | if (this.client) { 108 | this.client.close(); 109 | } 110 | this.closed = true; 111 | } 112 | 113 | receive() { 114 | if (this.client) { 115 | this.client.onmessage = async (message: any) => { 116 | const release = await mutex.acquire(); 117 | try { 118 | const data = Buffer.from( 119 | await new Response(message.data).arrayBuffer(), 120 | ); 121 | this.stream = Buffer.concat([this.stream, data]); 122 | if (this.resolveRead) { 123 | this.resolveRead(true); 124 | } 125 | } finally { 126 | release(); 127 | } 128 | }; 129 | } 130 | } 131 | 132 | toString() { 133 | return "PromisedWebSocket"; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./client/mod.ts"; 2 | export * from "./crypto/mod.ts"; 3 | export * from "./errors/mod.ts"; 4 | export * from "./events/mod.ts"; 5 | export * from "./network/mod.ts"; 6 | export * from "./sessions/mod.ts"; 7 | export * from "./tl/mod.ts"; 8 | 9 | // compat 10 | export * as client from "./client/mod.ts"; 11 | export * as crypto from "./crypto/mod.ts"; 12 | export * as errors from "./errors/mod.ts"; 13 | export * as events from "./events/mod.ts"; 14 | export * as network from "./network/mod.ts"; 15 | export * as sessions from "./sessions/mod.ts"; 16 | export * as tl from "./tl/mod.ts"; 17 | export * as helpers from "./helpers.ts"; 18 | export * as utils from "./utils.ts"; 19 | export * as password from "./password.ts"; 20 | 21 | export * from "./define.d.ts"; 22 | export * from "./request_iter.ts"; 23 | export * from "./entity_cache.ts"; 24 | export * from "./entity_cache.ts"; 25 | export * from "./version.ts"; 26 | export * from "./classes.ts"; 27 | -------------------------------------------------------------------------------- /src/network/connection/connection.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { PromisedNetSockets } from "../../extensions/promised_net_sockets.ts"; 3 | import { PromisedWebSockets } from "../../extensions/promised_web_sockets.ts"; 4 | import { Logger } from "../../extensions/logger.ts"; 5 | import { AsyncQueue } from "../../extensions/async_queue.ts"; 6 | import { 7 | Buffer, 8 | CancellablePromise, 9 | Cancellation, 10 | pseudoCancellable, 11 | } from "../../deps.ts"; 12 | import { ProxyInterface } from "./types.ts"; 13 | 14 | export interface ConnectionInterfaceParams { 15 | ip: string; 16 | port: number; 17 | dcId: number; 18 | loggers: Logger; 19 | proxy?: ProxyInterface; 20 | socket: typeof PromisedNetSockets | typeof PromisedWebSockets; 21 | testServers: boolean; 22 | } 23 | 24 | export class Connection { 25 | PacketCodecClass?: typeof PacketCodec; 26 | readonly _ip: string; 27 | readonly _port: number; 28 | _dcId: number; 29 | _log: Logger; 30 | _proxy?: ProxyInterface; 31 | _connected: boolean; 32 | private _sendTask?: Promise; 33 | private _recvTask?: Promise; 34 | protected _codec: any; 35 | protected _obfuscation: any; 36 | _sendArray: AsyncQueue; 37 | _recvArray: AsyncQueue; 38 | recvCancel?: CancellablePromise; 39 | sendCancel?: CancellablePromise; 40 | 41 | socket: PromisedNetSockets | PromisedWebSockets; 42 | public _testServers: boolean; 43 | 44 | constructor({ 45 | ip, 46 | port, 47 | dcId, 48 | loggers, 49 | proxy, 50 | socket, 51 | testServers, 52 | }: ConnectionInterfaceParams) { 53 | this._ip = ip; 54 | this._port = port; 55 | this._dcId = dcId; 56 | this._log = loggers; 57 | this._proxy = proxy; 58 | this._connected = false; 59 | this._sendTask = undefined; 60 | this._recvTask = undefined; 61 | this._codec = undefined; 62 | this._obfuscation = undefined; // TcpObfuscated and MTProxy 63 | this._sendArray = new AsyncQueue(); 64 | this._recvArray = new AsyncQueue(); 65 | this.socket = new socket(proxy); 66 | this._testServers = testServers; 67 | } 68 | 69 | async _connect() { 70 | this._log.debug("Connecting"); 71 | this._codec = new this.PacketCodecClass!(this); 72 | await this.socket.connect(this._port, this._ip, this._testServers); 73 | this._log.debug("Finished connecting"); 74 | this._initConn(); 75 | } 76 | 77 | async connect() { 78 | await this._connect(); 79 | this._connected = true; 80 | 81 | this._sendTask = this._sendLoop(); 82 | this._recvTask = this._recvLoop(); 83 | } 84 | 85 | _cancelLoops() { 86 | this.recvCancel!.cancel(); 87 | this.sendCancel!.cancel(); 88 | } 89 | 90 | async disconnect() { 91 | this._connected = false; 92 | this._cancelLoops(); 93 | try { 94 | await this.socket.close(); 95 | } catch (_e) { 96 | this._log.error("error while closing socket connection"); 97 | } 98 | } 99 | 100 | async send(data: Buffer) { 101 | if (!this._connected) { 102 | throw new Error("Not connected"); 103 | } 104 | await this._sendArray.push(data); 105 | } 106 | 107 | async recv() { 108 | while (this._connected) { 109 | const result = await this._recvArray.pop(); 110 | if (result && result.length) return result; 111 | } 112 | throw new Error("Not connected"); 113 | } 114 | 115 | async _sendLoop() { 116 | try { 117 | while (this._connected) { 118 | this.sendCancel = pseudoCancellable(this._sendArray.pop()); 119 | const data = await this.sendCancel; 120 | if (!data) continue; 121 | await this._send(data); 122 | } 123 | } catch (e) { 124 | if (e instanceof Cancellation) return; 125 | this._log.info("The server closed the connection while sending"); 126 | await this.disconnect(); 127 | } 128 | } 129 | 130 | async _recvLoop() { 131 | let data; 132 | while (this._connected) { 133 | try { 134 | this.recvCancel = pseudoCancellable(this._recv()); 135 | data = await this.recvCancel; 136 | } catch (e) { 137 | if (e instanceof Cancellation) return; 138 | this._log.info("The server closed the connection"); 139 | await this.disconnect(); 140 | if (!this._recvArray._queue.length) { 141 | await this._recvArray.push(undefined); 142 | } 143 | break; 144 | } 145 | try { 146 | await this._recvArray.push(data); 147 | } catch (_e) { 148 | break; 149 | } 150 | } 151 | } 152 | 153 | _initConn() { 154 | if (this._codec.tag) { 155 | this.socket.write(this._codec.tag); 156 | } 157 | } 158 | 159 | _send(data: Buffer) { 160 | const encodedPacket = this._codec.encodePacket(data); 161 | this.socket.write(encodedPacket); 162 | } 163 | 164 | async _recv() { 165 | return await this._codec.readPacket(this.socket); 166 | } 167 | 168 | toString() { 169 | return `${this._ip}:${this._port}/${ 170 | this.constructor.name.replace("Connection", "") 171 | }`; 172 | } 173 | } 174 | 175 | export class ObfuscatedConnection extends Connection { 176 | ObfuscatedIO: any = undefined; 177 | 178 | async _initConn() { 179 | this._obfuscation = new this.ObfuscatedIO(this); 180 | await this._obfuscation.initHeader(); 181 | this.socket.write(this._obfuscation.header); 182 | } 183 | 184 | _send(data: Buffer) { 185 | this._obfuscation.write(this._codec.encodePacket(data)); 186 | } 187 | 188 | async _recv() { 189 | return await this._codec.readPacket(this._obfuscation); 190 | } 191 | } 192 | 193 | export class PacketCodec { 194 | private _conn: Connection; 195 | 196 | constructor(connection: Connection) { 197 | this._conn = connection; 198 | } 199 | 200 | encodePacket(_data: Buffer): Buffer { 201 | throw new Error("Not Implemented"); 202 | } 203 | 204 | readPacket( 205 | _reader: PromisedNetSockets | PromisedWebSockets, 206 | ): Promise { 207 | throw new Error("Not Implemented"); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/network/connection/tcp_full.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PacketCodec } from "./connection.ts"; 2 | import { crc32 } from "../../helpers.ts"; 3 | import { InvalidChecksumError } from "../../errors/mod.ts"; 4 | import type { PromisedNetSockets } from "../../extensions/promised_net_sockets.ts"; 5 | import type { PromisedWebSockets } from "../../extensions/promised_web_sockets.ts"; 6 | import { Buffer } from "../../deps.ts"; 7 | 8 | export class FullPacketCodec extends PacketCodec { 9 | private _sendCounter: number; 10 | 11 | // deno-lint-ignore no-explicit-any 12 | constructor(connection: any) { 13 | super(connection); 14 | this._sendCounter = 0; 15 | } 16 | 17 | encodePacket(data: Buffer) { 18 | // https://core.telegram.org/mtproto#tcp-transport 19 | // total length, sequence number, packet and checksum (CRC32) 20 | const length = data.length + 12; 21 | const e = Buffer.alloc(8); 22 | e.writeInt32LE(length, 0); 23 | e.writeInt32LE(this._sendCounter, 4); 24 | data = Buffer.concat([e, data]); 25 | const crc = Buffer.alloc(4); 26 | crc.writeUInt32LE(crc32(data), 0); 27 | this._sendCounter += 1; 28 | return Buffer.concat([data, crc]); 29 | } 30 | 31 | async readPacket( 32 | reader: PromisedNetSockets | PromisedWebSockets, 33 | ): Promise { 34 | const packetLenSeq = await reader.readExactly(8); // 4 and 4 35 | 36 | if (packetLenSeq === undefined) { 37 | return Buffer.alloc(0); 38 | } 39 | const packetLen = packetLenSeq.readInt32LE(0); 40 | let body = await reader.readExactly(packetLen - 8); 41 | const checksum = body.slice(-4).readUInt32LE(0); 42 | body = body.slice(0, -4); 43 | 44 | const validChecksum = crc32(Buffer.concat([packetLenSeq, body])); 45 | if (!(validChecksum === checksum)) { 46 | throw new InvalidChecksumError(checksum, validChecksum); 47 | } 48 | return body; 49 | } 50 | } 51 | 52 | export class ConnectionTCPFull extends Connection { 53 | PacketCodecClass = FullPacketCodec; 54 | } 55 | -------------------------------------------------------------------------------- /src/network/connection/tcp_obfuscated.ts: -------------------------------------------------------------------------------- 1 | import { generateRandomBytes } from "../../helpers.ts"; 2 | import { ObfuscatedConnection } from "./connection.ts"; 3 | import { AbridgedPacketCodec } from "./tcpa_bridged.ts"; 4 | import { CTR } from "../../crypto/ctr.ts"; 5 | import type { PromisedNetSockets } from "../../extensions/promised_net_sockets.ts"; 6 | import type { PromisedWebSockets } from "../../extensions/promised_web_sockets.ts"; 7 | import { Buffer } from "../../deps.ts"; 8 | 9 | class ObfuscatedIO { 10 | header?: Buffer = undefined; 11 | private connection: PromisedNetSockets | PromisedWebSockets; 12 | private _encrypt?: CTR; 13 | private _decrypt?: CTR; 14 | private _packetClass; 15 | constructor(connection: ConnectionTCPObfuscated) { 16 | this.connection = connection.socket; 17 | this._packetClass = connection.PacketCodecClass; 18 | } 19 | 20 | initHeader() { 21 | const keywords = [ 22 | Buffer.from("50567247", "hex"), 23 | Buffer.from("474554", "hex"), 24 | Buffer.from("504f5354", "hex"), 25 | Buffer.from("eeeeeeee", "hex"), 26 | ]; 27 | 28 | let random; 29 | 30 | while (true) { 31 | random = generateRandomBytes(64); 32 | if ( 33 | random[0] !== 0xef && 34 | !random.slice(4, 8).equals(Buffer.alloc(4)) 35 | ) { 36 | let ok = true; 37 | for (const key of keywords) { 38 | if (key.equals(random.slice(0, 4))) { 39 | ok = false; 40 | break; 41 | } 42 | } 43 | if (ok) { 44 | break; 45 | } 46 | } 47 | } 48 | 49 | random = random.toJSON().data; 50 | 51 | const randomReversed = Buffer.from(random.slice(8, 56)).reverse(); 52 | // Encryption has "continuous buffer" enabled 53 | const encryptKey = Buffer.from(random.slice(8, 40)); 54 | const encryptIv = Buffer.from(random.slice(40, 56)); 55 | const decryptKey = Buffer.from(randomReversed.slice(0, 32)); 56 | const decryptIv = Buffer.from(randomReversed.slice(32, 48)); 57 | const encryptor = new CTR(encryptKey, encryptIv); 58 | const decryptor = new CTR(decryptKey, decryptIv); 59 | 60 | random = Buffer.concat([ 61 | Buffer.from(random.slice(0, 56)), 62 | this._packetClass.obfuscateTag, 63 | Buffer.from(random.slice(60)), 64 | ]); 65 | random = Buffer.concat([ 66 | Buffer.from(random.slice(0, 56)), 67 | Buffer.from(encryptor.encrypt(random).slice(56, 64)), 68 | Buffer.from(random.slice(64)), 69 | ]); 70 | this.header = random; 71 | 72 | this._encrypt = encryptor; 73 | this._decrypt = decryptor; 74 | } 75 | 76 | async read(n: number) { 77 | const data = await this.connection.readExactly(n); 78 | return this._decrypt!.encrypt(data); 79 | } 80 | 81 | write(data: Buffer) { 82 | this.connection.write(this._encrypt!.encrypt(data)); 83 | } 84 | } 85 | 86 | export class ConnectionTCPObfuscated extends ObfuscatedConnection { 87 | ObfuscatedIO = ObfuscatedIO; 88 | PacketCodecClass = AbridgedPacketCodec; 89 | } 90 | -------------------------------------------------------------------------------- /src/network/connection/tcpa_bridged.ts: -------------------------------------------------------------------------------- 1 | import { Connection, PacketCodec } from "./connection.ts"; 2 | import { readBufferFromBigInt } from "../../helpers.ts"; 3 | import type { PromisedNetSockets } from "../../extensions/promised_net_sockets.ts"; 4 | import type { PromisedWebSockets } from "../../extensions/promised_web_sockets.ts"; 5 | import { bigInt, Buffer } from "../../deps.ts"; 6 | 7 | export class AbridgedPacketCodec extends PacketCodec implements PacketCodec { 8 | static tag = Buffer.from("ef", "hex"); 9 | static obfuscateTag = Buffer.from("efefefef", "hex"); 10 | private tag: Buffer; 11 | obfuscateTag: Buffer; 12 | 13 | // deno-lint-ignore no-explicit-any 14 | constructor(props: any) { 15 | super(props); 16 | this.tag = AbridgedPacketCodec.tag; 17 | this.obfuscateTag = AbridgedPacketCodec.obfuscateTag; 18 | } 19 | 20 | encodePacket(data: Buffer) { 21 | const length = data.length >> 2; 22 | let temp; 23 | if (length < 127) { 24 | const b = Buffer.alloc(1); 25 | b.writeUInt8(length, 0); 26 | temp = b; 27 | } else { 28 | temp = Buffer.concat([ 29 | Buffer.from("7f", "hex"), 30 | readBufferFromBigInt(bigInt(length), 3), 31 | ]); 32 | } 33 | return Buffer.concat([temp, data]); 34 | } 35 | 36 | async readPacket( 37 | reader: PromisedNetSockets | PromisedWebSockets, 38 | ): Promise { 39 | const readData = await reader.read(1); 40 | let length = readData[0]; 41 | if (length >= 127) { 42 | length = Buffer.concat([ 43 | await reader.read(3), 44 | Buffer.alloc(1), 45 | ]).readInt32LE(0); 46 | } 47 | 48 | return reader.read(length << 2); 49 | } 50 | } 51 | 52 | export class ConnectionTCPAbridged extends Connection { 53 | PacketCodecClass = AbridgedPacketCodec; 54 | } 55 | -------------------------------------------------------------------------------- /src/network/connection/tcpmt_proxy.ts: -------------------------------------------------------------------------------- 1 | import { ObfuscatedConnection } from "./connection.ts"; 2 | import { AbridgedPacketCodec } from "./tcpa_bridged.ts"; 3 | import { generateRandomBytes, sha256 } from "../../helpers.ts"; 4 | import { CTR } from "../../crypto/ctr.ts"; 5 | import type { Logger } from "../../extensions/logger.ts"; 6 | import type { PromisedNetSockets } from "../../extensions/promised_net_sockets.ts"; 7 | import type { PromisedWebSockets } from "../../extensions/promised_web_sockets.ts"; 8 | import { Buffer } from "../../deps.ts"; 9 | import { ProxyInterface } from "./types.ts"; 10 | 11 | export class MTProxyIO { 12 | header?: Buffer = undefined; 13 | private connection: PromisedNetSockets | PromisedWebSockets; 14 | private _encrypt?: CTR; 15 | private _decrypt?: CTR; 16 | private _packetClass: AbridgedPacketCodec; 17 | private _secret: Buffer; 18 | private _dcId: number; 19 | 20 | constructor(connection: TCPMTProxy) { 21 | this.connection = connection.socket; 22 | this._packetClass = connection 23 | .PacketCodecClass as unknown as AbridgedPacketCodec; 24 | 25 | this._secret = connection._secret; 26 | this._dcId = connection._dcId; 27 | } 28 | 29 | async initHeader() { 30 | let secret = this._secret; 31 | const isDD = secret.length == 17 && secret[0] == 0xdd; 32 | secret = isDD ? secret.slice(1) : secret; 33 | if (secret.length != 16) { 34 | throw new Error( 35 | "MTProxy secret must be a hex-string representing 16 bytes", 36 | ); 37 | } 38 | const keywords = [ 39 | Buffer.from("50567247", "hex"), 40 | Buffer.from("474554", "hex"), 41 | Buffer.from("504f5354", "hex"), 42 | Buffer.from("eeeeeeee", "hex"), 43 | ]; 44 | let random; 45 | 46 | while (true) { 47 | random = generateRandomBytes(64); 48 | if ( 49 | random[0] !== 0xef && 50 | !random.slice(4, 8).equals(Buffer.alloc(4)) 51 | ) { 52 | let ok = true; 53 | for (const key of keywords) { 54 | if (key.equals(random.slice(0, 4))) { 55 | ok = false; 56 | break; 57 | } 58 | } 59 | if (ok) { 60 | break; 61 | } 62 | } 63 | } 64 | random = random.toJSON().data; 65 | const randomReversed = Buffer.from(random.slice(8, 56)).reverse(); 66 | // Encryption has "continuous buffer" enabled 67 | const encryptKey = await sha256( 68 | Buffer.concat([Buffer.from(random.slice(8, 40)), secret]), 69 | ); 70 | const encryptIv = Buffer.from(random.slice(40, 56)); 71 | 72 | const decryptKey = await sha256( 73 | Buffer.concat([Buffer.from(randomReversed.slice(0, 32)), secret]), 74 | ); 75 | const decryptIv = Buffer.from(randomReversed.slice(32, 48)); 76 | 77 | const encryptor = new CTR(encryptKey, encryptIv); 78 | const decryptor = new CTR(decryptKey, decryptIv); 79 | random = Buffer.concat([ 80 | Buffer.from(random.slice(0, 56)), 81 | this._packetClass.obfuscateTag, 82 | Buffer.from(random.slice(60)), 83 | ]); 84 | const dcIdBytes = Buffer.alloc(2); 85 | dcIdBytes.writeInt8(this._dcId, 0); 86 | random = Buffer.concat([ 87 | Buffer.from(random.slice(0, 60)), 88 | dcIdBytes, 89 | Buffer.from(random.slice(62)), 90 | ]); 91 | random = Buffer.concat([ 92 | Buffer.from(random.slice(0, 56)), 93 | Buffer.from(encryptor.encrypt(random).slice(56, 64)), 94 | Buffer.from(random.slice(64)), 95 | ]); 96 | this.header = random; 97 | 98 | this._encrypt = encryptor; 99 | this._decrypt = decryptor; 100 | } 101 | 102 | async read(n: number) { 103 | const data = await this.connection.readExactly(n); 104 | return this._decrypt!.encrypt(data); 105 | } 106 | 107 | write(data: Buffer) { 108 | this.connection.write(this._encrypt!.encrypt(data)); 109 | } 110 | } 111 | 112 | export interface TCPMTProxyInterfaceParams { 113 | ip: string; 114 | port: number; 115 | dcId: number; 116 | loggers: Logger; 117 | proxy: ProxyInterface; 118 | socket: typeof PromisedNetSockets | typeof PromisedWebSockets; 119 | testServers: boolean; 120 | } 121 | 122 | export class TCPMTProxy extends ObfuscatedConnection { 123 | ObfuscatedIO = MTProxyIO; 124 | _secret: Buffer; 125 | 126 | constructor({ 127 | dcId, 128 | loggers, 129 | proxy, 130 | socket, 131 | testServers, 132 | }: TCPMTProxyInterfaceParams) { 133 | super({ 134 | ip: proxy.ip, 135 | port: proxy.port, 136 | dcId: dcId, 137 | loggers: loggers, 138 | socket: socket, 139 | proxy: proxy, 140 | testServers: testServers, 141 | }); 142 | if (!proxy.MTProxy) { 143 | throw new Error("This connection only supports MPTProxies"); 144 | } 145 | if (!proxy.secret) { 146 | throw new Error("You need to provide the secret for the MTProxy"); 147 | } 148 | if (proxy.secret && proxy.secret.match(/^[0-9a-f]+$/i)) { 149 | // probably hex 150 | this._secret = Buffer.from(proxy.secret, "hex"); 151 | } else { 152 | // probably b64 153 | this._secret = Buffer.from(proxy.secret, "base64"); 154 | } 155 | } 156 | } 157 | 158 | export class ConnectionTCPMTProxyAbridged extends TCPMTProxy { 159 | PacketCodecClass = AbridgedPacketCodec; 160 | } 161 | -------------------------------------------------------------------------------- /src/network/connection/types.ts: -------------------------------------------------------------------------------- 1 | export interface ProxyInterface { 2 | socksType?: 4 | 5; 3 | ip: string; 4 | port: number; 5 | secret?: string; 6 | MTProxy?: boolean; 7 | timeout?: number; 8 | username?: string; 9 | password?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/network/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./connection/connection.ts"; 2 | export * from "./connection/tcp_full.ts"; 3 | export * from "./connection/tcp_obfuscated.ts"; 4 | export * from "./connection/tcpa_bridged.ts"; 5 | export * from "./connection/tcpmt_proxy.ts"; 6 | export * from "./authenticator.ts"; 7 | export * from "./mtproto_plain_sender.ts"; 8 | export * from "./mtproto_sender.ts"; 9 | export * from "./mtproto_state.ts"; 10 | export * from "./request_state.ts"; 11 | -------------------------------------------------------------------------------- /src/network/mtproto_plain_sender.ts: -------------------------------------------------------------------------------- 1 | import { MTProtoState } from "./mtproto_state.ts"; 2 | import { Api } from "../tl/api.js"; 3 | import { toSignedLittleBuffer } from "../helpers.ts"; 4 | import { InvalidBufferError } from "../errors/mod.ts"; 5 | import { BinaryReader } from "../extensions/binary_reader.ts"; 6 | import type { Connection } from "./connection/connection.ts"; 7 | import { bigInt, Buffer } from "../deps.ts"; 8 | 9 | export class MTProtoPlainSender { 10 | private _state: MTProtoState; 11 | private _connection: Connection; 12 | 13 | // deno-lint-ignore no-explicit-any 14 | constructor(connection: any, loggers: any) { 15 | this._state = new MTProtoState(undefined, loggers); 16 | this._connection = connection; 17 | } 18 | 19 | async send(request: Api.AnyRequest) { 20 | let body = request.getBytes(); 21 | let msgId = this._state._getNewMsgId(); 22 | const m = toSignedLittleBuffer(msgId, 8); 23 | const b = Buffer.alloc(4); 24 | b.writeInt32LE(body.length, 0); 25 | 26 | const res = Buffer.concat([ 27 | Buffer.concat([Buffer.alloc(8), m, b]), 28 | body, 29 | ]); 30 | await this._connection.send(res); 31 | body = await this._connection.recv(); 32 | if (body.length < 8) throw new InvalidBufferError(body); 33 | 34 | const reader = new BinaryReader(body); 35 | const authKeyId = reader.readLong(); 36 | if (authKeyId.neq(bigInt(0))) throw new Error("Bad authKeyId"); 37 | 38 | msgId = reader.readLong(); 39 | if (msgId.eq(bigInt(0))) throw new Error("Bad msgId"); 40 | 41 | const length = reader.readInt(); 42 | if (length <= 0) throw new Error("Bad length"); 43 | return reader.tgReadObject(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/network/request_state.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { bigInt, Buffer } from "../deps.ts"; 3 | 4 | export class RequestState { 5 | public containerId?: bigInt.BigInteger; 6 | public msgId?: bigInt.BigInteger; 7 | public request: any; 8 | public data: Buffer; 9 | public after: any; 10 | public result: undefined; 11 | promise: Promise; 12 | // @ts-ignore mm 13 | public resolve: (value?: any) => void; 14 | // @ts-ignore mm2 15 | public reject: (reason?: any) => void; 16 | 17 | constructor(request: any, after = undefined) { 18 | this.containerId = undefined; 19 | this.msgId = undefined; 20 | this.request = request; 21 | this.data = request.getBytes(); 22 | this.after = after; 23 | this.result = undefined; 24 | this.promise = new Promise((resolve, reject) => { 25 | this.resolve = resolve; 26 | this.reject = reject; 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/password.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "./tl/api.js"; 2 | import { 3 | bigIntMod, 4 | generateRandomBytes, 5 | modExp, 6 | readBigIntFromBuffer, 7 | readBufferFromBigInt, 8 | sha256, 9 | } from "./helpers.ts"; 10 | import { bigInt, Buffer } from "./deps.ts"; 11 | import { pbkdf2Sync } from "./crypto/crypto.ts"; 12 | 13 | const SIZE_FOR_HASH = 256; 14 | 15 | function checkPrimeAndGood(primeBytes: Buffer, g: number) { 16 | // deno-fmt-ignore 17 | const goodPrime = Buffer.from([ 18 | 0xc7, 0x1c, 0xae, 0xb9, 0xc6, 0xb1, 0xc9, 0x04, 0x8e, 0x6c, 0x52, 0x2f, 19 | 0x70, 0xf1, 0x3f, 0x73, 0x98, 0x0d, 0x40, 0x23, 0x8e, 0x3e, 0x21, 0xc1, 20 | 0x49, 0x34, 0xd0, 0x37, 0x56, 0x3d, 0x93, 0x0f, 0x48, 0x19, 0x8a, 0x0a, 21 | 0xa7, 0xc1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xd2, 0x25, 0x30, 0xf4, 0xdb, 22 | 0xfa, 0x33, 0x6f, 0x6e, 0x0a, 0xc9, 0x25, 0x13, 0x95, 0x43, 0xae, 0xd4, 23 | 0x4c, 0xce, 0x7c, 0x37, 0x20, 0xfd, 0x51, 0xf6, 0x94, 0x58, 0x70, 0x5a, 24 | 0xc6, 0x8c, 0xd4, 0xfe, 0x6b, 0x6b, 0x13, 0xab, 0xdc, 0x97, 0x46, 0x51, 25 | 0x29, 0x69, 0x32, 0x84, 0x54, 0xf1, 0x8f, 0xaf, 0x8c, 0x59, 0x5f, 0x64, 26 | 0x24, 0x77, 0xfe, 0x96, 0xbb, 0x2a, 0x94, 0x1d, 0x5b, 0xcd, 0x1d, 0x4a, 27 | 0xc8, 0xcc, 0x49, 0x88, 0x07, 0x08, 0xfa, 0x9b, 0x37, 0x8e, 0x3c, 0x4f, 28 | 0x3a, 0x90, 0x60, 0xbe, 0xe6, 0x7c, 0xf9, 0xa4, 0xa4, 0xa6, 0x95, 0x81, 29 | 0x10, 0x51, 0x90, 0x7e, 0x16, 0x27, 0x53, 0xb5, 0x6b, 0x0f, 0x6b, 0x41, 30 | 0x0d, 0xba, 0x74, 0xd8, 0xa8, 0x4b, 0x2a, 0x14, 0xb3, 0x14, 0x4e, 0x0e, 31 | 0xf1, 0x28, 0x47, 0x54, 0xfd, 0x17, 0xed, 0x95, 0x0d, 0x59, 0x65, 0xb4, 32 | 0xb9, 0xdd, 0x46, 0x58, 0x2d, 0xb1, 0x17, 0x8d, 0x16, 0x9c, 0x6b, 0xc4, 33 | 0x65, 0xb0, 0xd6, 0xff, 0x9c, 0xa3, 0x92, 0x8f, 0xef, 0x5b, 0x9a, 0xe4, 34 | 0xe4, 0x18, 0xfc, 0x15, 0xe8, 0x3e, 0xbe, 0xa0, 0xf8, 0x7f, 0xa9, 0xff, 35 | 0x5e, 0xed, 0x70, 0x05, 0x0d, 0xed, 0x28, 0x49, 0xf4, 0x7b, 0xf9, 0x59, 36 | 0xd9, 0x56, 0x85, 0x0c, 0xe9, 0x29, 0x85, 0x1f, 0x0d, 0x81, 0x15, 0xf6, 37 | 0x35, 0xb1, 0x05, 0xee, 0x2e, 0x4e, 0x15, 0xd0, 0x4b, 0x24, 0x54, 0xbf, 38 | 0x6f, 0x4f, 0xad, 0xf0, 0x34, 0xb1, 0x04, 0x03, 0x11, 0x9c, 0xd8, 0xe3, 39 | 0xb9, 0x2f, 0xcc, 0x5b, 40 | ]); 41 | if (goodPrime.equals(primeBytes)) { 42 | if ([3, 4, 5, 7].includes(g)) return; // It's good 43 | } 44 | throw new Error("Changing passwords unsupported"); 45 | } 46 | 47 | function isGoodLarge(number: bigInt.BigInteger, p: bigInt.BigInteger) { 48 | return number.greater(bigInt(0)) && p.subtract(number).greater(bigInt(0)); 49 | } 50 | 51 | function numBytesForHash(number: Buffer) { 52 | return Buffer.concat([Buffer.alloc(SIZE_FOR_HASH - number.length), number]); 53 | } 54 | 55 | function bigNumForHash(g: bigInt.BigInteger) { 56 | return readBufferFromBigInt(g, SIZE_FOR_HASH, false); 57 | } 58 | 59 | function isGoodModExpFirst( 60 | modexp: bigInt.BigInteger, 61 | prime: bigInt.BigInteger, 62 | ) { 63 | const diff = prime.subtract(modexp); 64 | const minDiffBitsCount = 2048 - 64; 65 | const maxModExpSize = 256; 66 | return !( 67 | diff.lesser(bigInt(0)) || 68 | diff.bitLength().toJSNumber() < minDiffBitsCount || 69 | modexp.bitLength().toJSNumber() < minDiffBitsCount || 70 | Math.floor((modexp.bitLength().toJSNumber() + 7) / 8) > maxModExpSize 71 | ); 72 | } 73 | 74 | function xor(a: Buffer, b: Buffer) { 75 | const length = Math.min(a.length, b.length); 76 | for (let i = 0; i < length; i++) a[i] = a[i] ^ b[i]; 77 | return a; 78 | } 79 | 80 | function pbkdf2sha512(password: Buffer, salt: Buffer, iterations: number) { 81 | return pbkdf2Sync(password, salt, iterations, 64, "sha512"); 82 | } 83 | 84 | async function computeHash( 85 | algo: Api.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, 86 | password: string, 87 | ) { 88 | const hash1 = await sha256( 89 | Buffer.concat([algo.salt1, Buffer.from(password, "utf-8"), algo.salt1]), 90 | ); 91 | const hash2 = await sha256(Buffer.concat([algo.salt2, hash1, algo.salt2])); 92 | const hash3 = await pbkdf2sha512(hash2, algo.salt1, 100000); 93 | return sha256(Buffer.concat([algo.salt2, hash3, algo.salt2])); 94 | } 95 | 96 | export async function computeDigest( 97 | algo: Api.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, 98 | password: string, 99 | ) { 100 | try { 101 | checkPrimeAndGood(algo.p, algo.g); 102 | } catch (_e) { 103 | throw new Error("bad p/g in password"); 104 | } 105 | const value = modExp( 106 | bigInt(algo.g), 107 | readBigIntFromBuffer(await computeHash(algo, password), false), 108 | readBigIntFromBuffer(algo.p, false), 109 | ); 110 | return bigNumForHash(value); 111 | } 112 | 113 | export async function computeCheck( 114 | request: Api.account.Password, 115 | password: string, 116 | ) { 117 | const algo = request.currentAlgo; 118 | if ( 119 | !(algo instanceof 120 | Api.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow) 121 | ) { 122 | throw new Error(`Unsupported password algorithm ${algo?.className}`); 123 | } 124 | const srp_B = request.srp_B; 125 | const srpId = request.srpId; 126 | if (!srp_B || !srpId) { 127 | throw new Error(`Undefined srp_b ${request}`); 128 | } 129 | const pwHash = await computeHash(algo, password); 130 | const p = readBigIntFromBuffer(algo.p, false); 131 | const g = algo.g; 132 | const B = readBigIntFromBuffer(srp_B, false); 133 | try { 134 | checkPrimeAndGood(algo.p, g); 135 | } catch (_e) { 136 | throw new Error("bad /g in password"); 137 | } 138 | if (!isGoodLarge(B, p)) { 139 | throw new Error("bad b in check"); 140 | } 141 | const x = readBigIntFromBuffer(pwHash, false); 142 | const pForHash = numBytesForHash(algo.p); 143 | const gForHash = bigNumForHash(bigInt(g)); 144 | const bForHash = numBytesForHash(srp_B); 145 | const gX = modExp(bigInt(g), x, p); 146 | const k = readBigIntFromBuffer( 147 | await sha256(Buffer.concat([pForHash, gForHash])), 148 | false, 149 | ); 150 | const kgX = bigIntMod(k.multiply(gX), p); 151 | const generateAndCheckRandom = async () => { 152 | const randomSize = 256; 153 | while (true) { 154 | const random = generateRandomBytes(randomSize); 155 | const a = readBigIntFromBuffer(random, false); 156 | const A = modExp(bigInt(g), a, p); 157 | if (isGoodModExpFirst(A, p)) { 158 | const aForHash = bigNumForHash(A); 159 | const u = readBigIntFromBuffer( 160 | await sha256(Buffer.concat([aForHash, bForHash])), 161 | false, 162 | ); 163 | if (u.greater(bigInt(0))) { 164 | return { a: a, aForHash: aForHash, u: u }; 165 | } 166 | } 167 | } 168 | }; 169 | const { a, aForHash, u } = await generateAndCheckRandom(); 170 | const gB = bigIntMod(B.subtract(kgX), p); 171 | if (!isGoodModExpFirst(gB, p)) { 172 | throw new Error("bad gB"); 173 | } 174 | 175 | const ux = u.multiply(x); 176 | const aUx = a.add(ux); 177 | const S = modExp(gB, aUx, p); 178 | const K = await sha256(bigNumForHash(S)); 179 | const pSha = await sha256(pForHash); 180 | const gSha = await sha256(gForHash); 181 | const salt1Sha = await sha256(algo.salt1); 182 | const salt2Sha = await sha256(algo.salt2); 183 | 184 | const M1 = await sha256( 185 | Buffer.concat([ 186 | xor(pSha, gSha), 187 | salt1Sha, 188 | salt2Sha, 189 | aForHash, 190 | bForHash, 191 | K, 192 | ]), 193 | ); 194 | 195 | return new Api.InputCheckPasswordSRP({ 196 | srpId: srpId, 197 | A: Buffer.from(aForHash), 198 | M1: M1, 199 | }); 200 | } 201 | -------------------------------------------------------------------------------- /src/request_iter.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { sleep, TotalList } from "./helpers.ts"; 3 | import { AbstractTelegramClient } from "./client/abstract_telegram_client.ts"; 4 | 5 | interface BaseRequestIterInterface { 6 | reverse?: boolean; 7 | waitTime?: number; 8 | } 9 | 10 | export class RequestIter implements AsyncIterable { 11 | public client: AbstractTelegramClient; 12 | public reverse: boolean | undefined; 13 | public waitTime: number | undefined; 14 | protected readonly limit: number; 15 | protected left: number; 16 | protected buffer: Array | undefined; 17 | private index: number; 18 | protected total: number | undefined; 19 | private lastLoad: number; 20 | kwargs: Record; 21 | 22 | constructor( 23 | client: AbstractTelegramClient, 24 | limit?: number, 25 | params: BaseRequestIterInterface = {}, 26 | args = {}, 27 | ) { 28 | this.client = client; 29 | this.reverse = params.reverse; 30 | this.waitTime = params.waitTime; 31 | this.limit = Math.max(!limit ? Number.MAX_SAFE_INTEGER : limit, 0); 32 | this.left = this.limit; 33 | this.buffer = undefined; 34 | this.kwargs = args; 35 | this.index = 0; 36 | this.total = undefined; 37 | this.lastLoad = 0; 38 | } 39 | 40 | async _init(_kwargs: any): Promise { 41 | // for overload 42 | } 43 | 44 | [Symbol.asyncIterator](): AsyncIterator { 45 | this.buffer = undefined; 46 | this.index = 0; 47 | this.lastLoad = 0; 48 | this.left = this.limit; 49 | return { 50 | next: async () => { 51 | if (this.buffer === undefined) { 52 | this.buffer = []; 53 | if (await this._init(this.kwargs)) { 54 | this.left = this.buffer.length; 55 | } 56 | } 57 | if (this.left <= 0) { 58 | return { value: undefined, done: true }; 59 | } 60 | if (this.index === this.buffer.length) { 61 | if (this.waitTime) { 62 | await sleep( 63 | this.waitTime - (new Date().getTime() / 1000 - this.lastLoad), 64 | ); 65 | } 66 | this.lastLoad = new Date().getTime() / 1000; 67 | this.index = 0; 68 | this.buffer = []; 69 | const nextChunk = await this._loadNextChunk(); 70 | if (nextChunk === false) { 71 | // we exit; 72 | return { 73 | value: undefined, 74 | done: true, 75 | }; 76 | } 77 | if (nextChunk) this.left = this.buffer.length; 78 | } 79 | 80 | if (!this.buffer || !this.buffer.length) { 81 | return { value: undefined, done: true }; 82 | } 83 | const result = this.buffer[this.index]; 84 | this.left -= 1; 85 | this.index += 1; 86 | return { value: result, done: false }; 87 | }, 88 | }; 89 | } 90 | 91 | async collect() { 92 | const result = new TotalList(); 93 | for await (const message of this) { 94 | result.push(message); 95 | } 96 | result.total = this.total; 97 | return result; 98 | } 99 | 100 | _loadNextChunk(): Promise { 101 | throw new Error("Not Implemented"); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/sessions/abstract.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../tl/api.js"; 2 | import type { AuthKey } from "../crypto/authkey.ts"; 3 | 4 | export abstract class Session { 5 | abstract setDC(dcId: number, serverAddress: string, port: number): void; 6 | abstract get dcId(): number; 7 | abstract get serverAddress(): string; 8 | abstract get port(): number; 9 | abstract get authKey(): AuthKey | undefined; 10 | abstract set authKey(value: AuthKey | undefined); 11 | abstract load(): Promise; 12 | abstract setAuthKey(authKey?: AuthKey, dcId?: number): void; 13 | abstract getAuthKey(dcId?: number): AuthKey | undefined; 14 | abstract getInputEntity(key: Api.TypeEntityLike): Promise; 15 | abstract close(): void; 16 | abstract save(): void; 17 | abstract delete(): void; 18 | // deno-lint-ignore no-explicit-any 19 | abstract processEntities(tlo: any): void; 20 | } 21 | -------------------------------------------------------------------------------- /src/sessions/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./abstract.ts"; 2 | export * from "./memory_session.ts"; 3 | export * from "./store_session.ts"; 4 | export * from "./string_session.ts"; 5 | -------------------------------------------------------------------------------- /src/sessions/store_session.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { MemorySession } from "./memory_session.ts"; 3 | import { AuthKey } from "../crypto/authkey.ts"; 4 | import { bigInt, Buffer } from "../deps.ts"; 5 | 6 | export class StoreSession extends MemorySession { 7 | private readonly sessionName: string; 8 | private store: Storage; 9 | 10 | constructor(sessionName: string, divider = ":") { 11 | super(); 12 | this.store = localStorage; 13 | if (divider === undefined) divider = ":"; 14 | this.sessionName = sessionName + divider; 15 | } 16 | 17 | async load() { 18 | const authKey_ = this.store.getItem(this.sessionName + "authKey"); 19 | if (authKey_) { 20 | let authKey = JSON.parse(authKey_); 21 | if (authKey && typeof authKey === "object") { 22 | this._authKey = new AuthKey(); 23 | if ("data" in authKey) { 24 | authKey = Buffer.from(authKey.data); 25 | } 26 | await this._authKey.setKey(authKey); 27 | } 28 | } 29 | 30 | const dcId = this.store.getItem(this.sessionName + "dcId"); 31 | if (dcId) this._dcId = parseInt(dcId); 32 | 33 | const port = this.store.getItem(this.sessionName + "port"); 34 | if (port) this._port = parseInt(port); 35 | 36 | const serverAddress = this.store.getItem( 37 | this.sessionName + "serverAddress", 38 | ); 39 | if (serverAddress) this._serverAddress = serverAddress; 40 | } 41 | 42 | setDC(dcId: number, serverAddress: string, port: number) { 43 | this.store.setItem(this.sessionName + "dcId", dcId.toString()); 44 | this.store.setItem(this.sessionName + "port", port.toString()); 45 | this.store.setItem(this.sessionName + "serverAddress", serverAddress); 46 | super.setDC(dcId, serverAddress, port); 47 | } 48 | 49 | set authKey(value: AuthKey | undefined) { 50 | if (value) { 51 | this._authKey = value; 52 | this.store.setItem( 53 | this.sessionName + "authKey", 54 | JSON.stringify(value.getKey()), 55 | ); 56 | } 57 | } 58 | 59 | get authKey() { 60 | return this._authKey; 61 | } 62 | 63 | processEntities(tlo: any) { 64 | const rows = this._entitiesToRows(tlo); 65 | if (!rows) return; 66 | for (const row of rows) { 67 | row.push(new Date().getTime().toString()); 68 | this.store.setItem(this.sessionName + row[0], JSON.stringify(row)); 69 | } 70 | } 71 | 72 | getEntityRowsById( 73 | id: string | bigInt.BigInteger, 74 | _exact = true, 75 | ): any { 76 | return JSON.parse(this.store.getItem(this.sessionName + id.toString())!); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/sessions/string_session.ts: -------------------------------------------------------------------------------- 1 | import { MemorySession } from "./memory_session.ts"; 2 | import { BinaryReader } from "../extensions/binary_reader.ts"; 3 | import { AuthKey } from "../crypto/authkey.ts"; 4 | import { Buffer } from "../deps.ts"; 5 | 6 | const CURRENT_VERSION = "1"; 7 | 8 | export class StringSession extends MemorySession { 9 | _key?: Buffer; 10 | 11 | constructor(session?: string) { 12 | super(); 13 | 14 | if (session) { 15 | if (session[0] !== CURRENT_VERSION) { 16 | throw new Error("Not a valid string"); 17 | } 18 | 19 | session = session.slice(1); 20 | const r = StringSession.decode(session); 21 | const reader = new BinaryReader(r); 22 | this._dcId = reader.read(1).readUInt8(0); 23 | 24 | if (session.length === 352) { 25 | // Telethon session 26 | const ipv4 = reader.read(4); 27 | this._serverAddress = `${ipv4[0].toString()}.${ipv4[1].toString()}.\ 28 | ${ipv4[2].toString()}.${ipv4[3].toString()}`; 29 | } else { 30 | const serverAddressLen = reader.read(2).readInt16BE(0); 31 | if (serverAddressLen > 100) { 32 | reader.offset -= 2; 33 | this._serverAddress = reader 34 | .read(16) 35 | .toString("hex") 36 | .match(/.{1,4}/g)! 37 | .map((val) => val.replace(/^0+/, "")) 38 | .join(":") 39 | .replace(/0000\:/g, ":") 40 | .replace(/:{2,}/g, "::"); 41 | } else { 42 | this._serverAddress = reader.read(serverAddressLen).toString(); 43 | } 44 | } 45 | this._port = reader.read(2).readInt16BE(0); 46 | this._key = reader.read(-1); 47 | } 48 | } 49 | 50 | static encode(x: Buffer) { 51 | return x.toString("base64"); 52 | } 53 | 54 | static decode(x: string) { 55 | return Buffer.from(x, "base64"); 56 | } 57 | 58 | async load() { 59 | if (this._key) { 60 | this._authKey = new AuthKey(); 61 | await this._authKey.setKey(this._key); 62 | } 63 | } 64 | 65 | save() { 66 | if (!this.authKey || !this.serverAddress || !this.port) { 67 | return ""; 68 | } 69 | 70 | const key = this.authKey.getKey(); 71 | if (!key) return ""; 72 | const dcBuffer = Buffer.from([this.dcId]); 73 | const addressBuffer = Buffer.from(this.serverAddress); 74 | const addressLengthBuffer = Buffer.alloc(2); 75 | addressLengthBuffer.writeInt16BE(addressBuffer.length, 0); 76 | const portBuffer = Buffer.alloc(2); 77 | portBuffer.writeInt16BE(this.port, 0); 78 | 79 | return ( 80 | CURRENT_VERSION + 81 | StringSession.encode( 82 | Buffer.concat([ 83 | dcBuffer, 84 | addressLengthBuffer, 85 | addressBuffer, 86 | portBuffer, 87 | key, 88 | ]), 89 | ) 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/tl/all_tl_objects.ts: -------------------------------------------------------------------------------- 1 | export const LAYER = 150; 2 | 3 | import { Api } from "./api.js"; 4 | 5 | // deno-lint-ignore no-explicit-any 6 | const tlObjects: any = {}; 7 | 8 | for (const tl of Object.values(Api)) { 9 | if ("CONSTRUCTOR_ID" in tl) { 10 | tlObjects[tl.CONSTRUCTOR_ID] = tl; 11 | } else { 12 | for (const sub of Object.values(tl)) { 13 | tlObjects[sub.CONSTRUCTOR_ID] = sub; 14 | } 15 | } 16 | } 17 | 18 | export { tlObjects }; 19 | -------------------------------------------------------------------------------- /src/tl/core/core_objects.ts: -------------------------------------------------------------------------------- 1 | import { GZIPPacked } from "./gzip_packed.ts"; 2 | import { RPCResult } from "./rpc_result.ts"; 3 | import { MessageContainer } from "./message_container.ts"; 4 | 5 | // deno-lint-ignore ban-types 6 | export const coreObjects = new Map([ 7 | [RPCResult.CONSTRUCTOR_ID, RPCResult], 8 | [GZIPPacked.CONSTRUCTOR_ID, GZIPPacked], 9 | [MessageContainer.CONSTRUCTOR_ID, MessageContainer], 10 | ]); 11 | -------------------------------------------------------------------------------- /src/tl/core/gzip_packed.ts: -------------------------------------------------------------------------------- 1 | import { serializeBytes } from "../generation_helpers.ts"; 2 | import { BinaryReader } from "../../extensions/interfaces.ts"; 3 | import { Buffer, inflate } from "../../deps.ts"; 4 | 5 | export class GZIPPacked { 6 | static CONSTRUCTOR_ID = 0x3072cfa1; 7 | static classType = "constructor"; 8 | data: Buffer; 9 | private CONSTRUCTOR_ID: number; 10 | private classType: string; 11 | 12 | constructor(data: Buffer) { 13 | this.data = data; 14 | this.CONSTRUCTOR_ID = 0x3072cfa1; 15 | this.classType = "constructor"; 16 | } 17 | 18 | static async gzipIfSmaller(contentRelated: boolean, data: Buffer) { 19 | if (contentRelated && data.length > 512) { 20 | const gzipped = await new GZIPPacked(data).toBytes(); 21 | if (gzipped.length < data.length) { 22 | return gzipped; 23 | } 24 | } 25 | return data; 26 | } 27 | 28 | static gzip(input: Buffer) { 29 | return Buffer.from(input); 30 | // TODO this usually makes it faster for large requests 31 | // return Buffer.from(deflate(input, { level: 9, gzip: true })) 32 | } 33 | 34 | static ungzip(input: Buffer) { 35 | return Buffer.from(inflate(input)); 36 | } 37 | 38 | toBytes() { 39 | const g = Buffer.alloc(4); 40 | g.writeUInt32LE(GZIPPacked.CONSTRUCTOR_ID, 0); 41 | return Buffer.concat([ 42 | g, 43 | serializeBytes(GZIPPacked.gzip(this.data)), 44 | ]); 45 | } 46 | 47 | // deno-lint-ignore no-explicit-any 48 | static read(reader: any) { 49 | const constructor = reader.readInt(false); 50 | if (constructor !== GZIPPacked.CONSTRUCTOR_ID) { 51 | throw new Error("not equal"); 52 | } 53 | return GZIPPacked.gzip(reader.tgReadBytes()); 54 | } 55 | 56 | static fromReader(reader: BinaryReader) { 57 | const data = reader.tgReadBytes(); 58 | return new GZIPPacked(GZIPPacked.ungzip(data)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/tl/core/message_container.ts: -------------------------------------------------------------------------------- 1 | import { TLMessage } from "./tl_message.ts"; 2 | import { BinaryReader } from "../../extensions/interfaces.ts"; 3 | 4 | export class MessageContainer { 5 | static CONSTRUCTOR_ID = 0x73f1f8dc; 6 | static classType = "constructor"; 7 | static MAXIMUM_SIZE = 1044456 - 8; 8 | static MAXIMUM_LENGTH = 100; 9 | private CONSTRUCTOR_ID: number; 10 | private classType: string; 11 | 12 | // deno-lint-ignore no-explicit-any 13 | constructor(private messages: any[]) { 14 | this.CONSTRUCTOR_ID = 0x73f1f8dc; 15 | this.messages = messages; 16 | this.classType = "constructor"; 17 | } 18 | 19 | static fromReader(reader: BinaryReader) { 20 | const messages = []; 21 | const length = reader.readInt(); 22 | for (let x = 0; x < length; x++) { 23 | const msgId = reader.readLong(); 24 | const seqNo = reader.readInt(); 25 | const length = reader.readInt(); 26 | const before = reader.tellPosition(); 27 | const obj = reader.tgReadObject(); 28 | reader.setPosition(before + length); 29 | const tlMessage = new TLMessage(msgId, seqNo, obj); 30 | messages.push(tlMessage); 31 | } 32 | return new MessageContainer(messages); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/tl/core/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./gzip_packed.ts"; 2 | export * from "./message_container.ts"; 3 | export * from "./rpc_result.ts"; 4 | export * from "./tl_message.ts"; 5 | -------------------------------------------------------------------------------- /src/tl/core/rpc_result.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { GZIPPacked } from "./gzip_packed.ts"; 3 | import { BinaryReader } from "../../extensions/interfaces.ts"; 4 | import { bigInt, Buffer } from "../../deps.ts"; 5 | 6 | export class RPCResult { 7 | static CONSTRUCTOR_ID = 0xf35c6d01; 8 | static classType = "constructor"; 9 | private CONSTRUCTOR_ID: number; 10 | private reqMsgId: bigInt.BigInteger; 11 | private body?: Buffer; 12 | private error?: Api.RpcError; 13 | private classType: string; 14 | 15 | constructor( 16 | reqMsgId: bigInt.BigInteger, 17 | body?: Buffer, 18 | error?: Api.RpcError, 19 | ) { 20 | this.CONSTRUCTOR_ID = 0xf35c6d01; 21 | this.reqMsgId = reqMsgId; 22 | this.body = body; 23 | this.error = error; 24 | this.classType = "constructor"; 25 | } 26 | 27 | static fromReader(reader: BinaryReader) { 28 | const msgId = reader.readLong(); 29 | const innerCode = reader.readInt(false); 30 | 31 | if (innerCode === Api.RpcError.CONSTRUCTOR_ID) { 32 | return new RPCResult(msgId, undefined, Api.RpcError.fromReader(reader)); 33 | } 34 | 35 | if (innerCode === GZIPPacked.CONSTRUCTOR_ID) { 36 | return new RPCResult(msgId, (GZIPPacked.fromReader(reader)).data); 37 | } 38 | 39 | reader.seek(-4); 40 | return new RPCResult(msgId, reader.read(), undefined); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/tl/core/tl_message.ts: -------------------------------------------------------------------------------- 1 | import { bigInt } from "../../deps.ts"; 2 | 3 | export class TLMessage { 4 | static SIZE_OVERHEAD = 12; 5 | static classType = "constructor"; 6 | private classType = "constructor"; 7 | 8 | constructor( 9 | public msgId: bigInt.BigInteger, 10 | private seqNo: number, 11 | // deno-lint-ignore no-explicit-any 12 | public obj: any, 13 | ) { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tl/custom/button.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { getInputUser } from "../../utils.ts"; 3 | import { Buffer } from "../../deps.ts"; 4 | 5 | export class Button implements Api.Button { 6 | public button: Api.TypeKeyboardButton; 7 | public resize: boolean | undefined; 8 | public selective: boolean | undefined; 9 | public singleUse: boolean | undefined; 10 | 11 | constructor( 12 | button: Api.TypeKeyboardButton, 13 | resize?: boolean, 14 | singleUse?: boolean, 15 | selective?: boolean, 16 | ) { 17 | this.button = button; 18 | this.resize = resize; 19 | this.singleUse = singleUse; 20 | this.selective = selective; 21 | } 22 | 23 | static _isInline(button: Api.TypeKeyboardButton) { 24 | return ( 25 | button instanceof Api.KeyboardButtonCallback || 26 | button instanceof Api.KeyboardButtonSwitchInline || 27 | button instanceof Api.KeyboardButtonUrl || 28 | button instanceof Api.KeyboardButtonUrlAuth || 29 | button instanceof Api.InputKeyboardButtonUrlAuth 30 | ); 31 | } 32 | 33 | static inline(text: string, data?: Buffer) { 34 | if (!data) data = Buffer.from(text, "utf-8"); 35 | if (data.length > 64) throw new Error("Too many bytes for the data"); 36 | return new Api.KeyboardButtonCallback({ text: text, data: data }); 37 | } 38 | 39 | static switchInline(text: string, query = "", samePeer = false) { 40 | return new Api.KeyboardButtonSwitchInline({ text, query, samePeer }); 41 | } 42 | 43 | static url(text: string, url?: string) { 44 | return new Api.KeyboardButtonUrl({ text: text, url: url || text }); 45 | } 46 | 47 | static auth( 48 | text: string, 49 | url?: string, 50 | bot?: Api.TypeEntityLike, 51 | writeAccess?: boolean, 52 | fwdText?: string, 53 | ) { 54 | return new Api.InputKeyboardButtonUrlAuth({ 55 | text, 56 | url: url || text, 57 | bot: getInputUser(bot || new Api.InputUserSelf()), 58 | requestWriteAccess: writeAccess, 59 | fwdText: fwdText, 60 | }); 61 | } 62 | 63 | static text( 64 | text: string, 65 | resize?: boolean, 66 | singleUse?: boolean, 67 | selective?: boolean, 68 | ) { 69 | return new this( 70 | new Api.KeyboardButton({ text }), 71 | resize, 72 | singleUse, 73 | selective, 74 | ); 75 | } 76 | 77 | static requestLocation( 78 | text: string, 79 | resize?: boolean, 80 | singleUse?: boolean, 81 | selective?: boolean, 82 | ) { 83 | return new this( 84 | new Api.KeyboardButtonRequestGeoLocation({ text }), 85 | resize, 86 | singleUse, 87 | selective, 88 | ); 89 | } 90 | 91 | static requestPhone( 92 | text: string, 93 | resize?: boolean, 94 | singleUse?: boolean, 95 | selective?: boolean, 96 | ) { 97 | return new this( 98 | new Api.KeyboardButtonRequestPhone({ text }), 99 | resize, 100 | singleUse, 101 | selective, 102 | ); 103 | } 104 | 105 | static requestPoll( 106 | text: string, 107 | resize?: boolean, 108 | singleUse?: boolean, 109 | selective?: boolean, 110 | ) { 111 | return new this( 112 | new Api.KeyboardButtonRequestPoll({ text }), 113 | resize, 114 | singleUse, 115 | selective, 116 | ); 117 | } 118 | 119 | static clear() { 120 | return new Api.ReplyKeyboardHide({}); 121 | } 122 | 123 | static forceReply() { 124 | return new Api.ReplyKeyboardForceReply({}); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/tl/custom/chat_getter.ts: -------------------------------------------------------------------------------- 1 | import { getPeerId } from "../../utils.ts"; 2 | import { Api } from "../api.js"; 3 | import { returnBigInt } from "../../helpers.ts"; 4 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 5 | 6 | export interface ChatGetterConstructorParams { 7 | chatPeer?: Api.TypeEntityLike; 8 | inputChat?: Api.TypeEntityLike; 9 | chat?: Api.TypeEntityLike; 10 | broadcast?: boolean; 11 | } 12 | 13 | export class ChatGetter { 14 | _chatPeer?: Api.TypeEntityLike; 15 | _inputChat?: Api.TypeEntityLike; 16 | _chat?: Api.TypeEntity; 17 | _broadcast?: boolean; 18 | public _client?: AbstractTelegramClient; 19 | 20 | static initChatClass( 21 | // deno-lint-ignore no-explicit-any 22 | c: any, 23 | { chatPeer, inputChat, chat, broadcast }: ChatGetterConstructorParams, 24 | ) { 25 | c._chatPeer = chatPeer; 26 | c._inputChat = inputChat; 27 | c._chat = chat; 28 | c._broadcast = broadcast; 29 | c._client = undefined; 30 | } 31 | 32 | get chat() { 33 | return this._chat; 34 | } 35 | 36 | async getChat() { 37 | if (!this._chat || ("min" in this._chat && (await this.getInputChat()))) { 38 | try { 39 | if (this._inputChat) { 40 | this._chat = await this._client?.getEntity(this._inputChat); 41 | } 42 | } catch (_e) { 43 | await this._refetchChat(); 44 | } 45 | } 46 | return this._chat; 47 | } 48 | 49 | get inputChat() { 50 | if (!this._inputChat && this._chatPeer && this._client) { 51 | try { 52 | this._inputChat = this._client._entityCache.get( 53 | getPeerId(this._chatPeer), 54 | ); 55 | } catch (_e) { 56 | // 57 | } 58 | } 59 | return this._inputChat; 60 | } 61 | 62 | async getInputChat() { 63 | if (!this.inputChat && this.chatId && this._client) { 64 | try { 65 | const target = this.chatId; 66 | for await ( 67 | const dialog of this._client.iterDialogs({ limit: 100 }) 68 | ) { 69 | if (dialog.id!.eq(target!)) { 70 | this._chat = dialog.entity; 71 | this._inputChat = dialog.inputEntity; 72 | break; 73 | } 74 | } 75 | } catch (_e) { 76 | // do nothing 77 | } 78 | return this._inputChat; 79 | } 80 | return this._inputChat; 81 | } 82 | 83 | get chatId() { 84 | return this._chatPeer ? returnBigInt(getPeerId(this._chatPeer)) : undefined; 85 | } 86 | 87 | get isPrivate() { 88 | return this._chatPeer ? this._chatPeer instanceof Api.PeerUser : undefined; 89 | } 90 | 91 | get isGroup() { 92 | if (!this._broadcast && this.chat && "broadcast" in this.chat) { 93 | this._broadcast = Boolean(this.chat.broadcast); 94 | } 95 | if (this._chatPeer instanceof Api.PeerChannel) { 96 | if (this._broadcast === undefined) { 97 | // deno-lint-ignore getter-return 98 | return; 99 | } else { 100 | return !this._broadcast; 101 | } 102 | } 103 | return this._chatPeer instanceof Api.PeerChat; 104 | } 105 | 106 | get isChannel() { 107 | return this._chatPeer instanceof Api.PeerChannel; 108 | } 109 | 110 | async _refetchChat() {} 111 | } 112 | -------------------------------------------------------------------------------- /src/tl/custom/dialog.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { Draft } from "./draft.ts"; 3 | import { getDisplayName, getInputPeer, getPeerId } from "../../utils.ts"; 4 | import { returnBigInt } from "../../helpers.ts"; 5 | import { bigInt } from "../../deps.ts"; 6 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 7 | 8 | export class Dialog { 9 | _client: AbstractTelegramClient; 10 | dialog: Api.Dialog; 11 | pinned: boolean; 12 | folderId?: number; 13 | archived: boolean; 14 | message?: Api.Message; 15 | date: number; 16 | entity?: Api.TypeEntity; 17 | inputEntity: Api.TypeInputPeer; 18 | id?: bigInt.BigInteger; 19 | name?: string; 20 | title?: string; 21 | unreadCount: number; 22 | unreadMentionsCount: number; 23 | draft: Draft; 24 | isUser: boolean; 25 | isGroup: boolean; 26 | isChannel: boolean; 27 | 28 | constructor( 29 | client: AbstractTelegramClient, 30 | dialog: Api.Dialog, 31 | entities: Map, 32 | message?: Api.Message, 33 | ) { 34 | this._client = client; 35 | this.dialog = dialog; 36 | this.pinned = !!dialog.pinned; 37 | this.folderId = dialog.folderId; 38 | this.archived = dialog.folderId != undefined; 39 | this.message = message; 40 | this.date = this.message!.date!; 41 | 42 | this.entity = entities.get(getPeerId(dialog.peer)); 43 | this.inputEntity = getInputPeer(this.entity); 44 | if (this.entity) { 45 | this.id = returnBigInt(getPeerId(this.entity)); // ^ May be InputPeerSelf(); 46 | this.name = this.title = getDisplayName(this.entity); 47 | } 48 | 49 | this.unreadCount = dialog.unreadCount; 50 | this.unreadMentionsCount = dialog.unreadMentionsCount; 51 | if (!this.entity) { 52 | throw new Error("Entity not found for dialog"); 53 | } 54 | this.draft = new Draft(client, this.entity, this.dialog.draft); 55 | 56 | this.isUser = this.entity instanceof Api.User; 57 | this.isGroup = !!( 58 | this.entity instanceof Api.Chat || 59 | this.entity instanceof Api.ChatForbidden || 60 | (this.entity instanceof Api.Channel && this.entity.megagroup) 61 | ); 62 | this.isChannel = this.entity instanceof Api.Channel; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tl/custom/draft.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { getInputPeer, getPeer } from "../../utils.ts"; 3 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 4 | 5 | export class Draft { 6 | private _client: AbstractTelegramClient; 7 | private readonly _entity?: Api.TypeEntity; 8 | private readonly _peer: ReturnType; 9 | private _inputEntity: Api.TypeInputPeer | undefined; 10 | private _text?: string; 11 | private _rawText?: string; 12 | private date?: Api.int; 13 | private linkPreview?: boolean; 14 | private replyToMsgId?: Api.int; 15 | 16 | constructor( 17 | client: AbstractTelegramClient, 18 | entity: Api.TypeEntity, 19 | draft: Api.TypeDraftMessage | undefined, 20 | ) { 21 | this._client = client; 22 | this._peer = getPeer(entity); 23 | this._entity = entity; 24 | this._inputEntity = entity ? getInputPeer(entity) : undefined; 25 | if (!draft || !(draft instanceof Api.DraftMessage)) { 26 | draft = new Api.DraftMessage({ 27 | message: "", 28 | date: -1, 29 | }); 30 | } 31 | if (!(draft instanceof Api.DraftMessageEmpty)) { 32 | this.linkPreview = !draft.noWebpage; 33 | this._text = client.parseMode 34 | ? client.parseMode.unparse(draft.message, draft.entities || []) 35 | : draft.message; 36 | this._rawText = draft.message; 37 | this.date = draft.date; 38 | this.replyToMsgId = draft.replyToMsgId; 39 | } 40 | } 41 | 42 | get entity() { 43 | return this._entity; 44 | } 45 | 46 | get inputEntity() { 47 | if (!this._inputEntity) { 48 | this._inputEntity = this._client._entityCache.get(this._peer); 49 | } 50 | return this._inputEntity; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/tl/custom/file.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { _photoSizeByteCount } from "../../utils.ts"; 3 | 4 | export class File { 5 | private readonly media: Api.TypeFileLike; 6 | 7 | constructor(media: Api.TypeFileLike) { 8 | this.media = media; 9 | } 10 | 11 | get id() { 12 | throw new Error("Unsupported"); 13 | } 14 | 15 | get name() { 16 | return this._fromAttr(Api.DocumentAttributeFilename, "fileName"); 17 | } 18 | 19 | // deno-lint-ignore getter-return 20 | get mimeType() { 21 | if (this.media instanceof Api.Photo) { 22 | return "image/jpeg"; 23 | } else if (this.media instanceof Api.Document) { 24 | return this.media.mimeType; 25 | } 26 | } 27 | 28 | get width() { 29 | return this._fromAttr( 30 | [Api.DocumentAttributeImageSize, Api.DocumentAttributeVideo], 31 | "w", 32 | ); 33 | } 34 | 35 | get height() { 36 | return this._fromAttr( 37 | [Api.DocumentAttributeImageSize, Api.DocumentAttributeVideo], 38 | "h", 39 | ); 40 | } 41 | 42 | get duration() { 43 | return this._fromAttr( 44 | [Api.DocumentAttributeAudio, Api.DocumentAttributeVideo], 45 | "duration", 46 | ); 47 | } 48 | 49 | get title() { 50 | return this._fromAttr(Api.DocumentAttributeAudio, "title"); 51 | } 52 | 53 | get performer() { 54 | return this._fromAttr(Api.DocumentAttributeAudio, "performer"); 55 | } 56 | 57 | get emoji() { 58 | return this._fromAttr(Api.DocumentAttributeSticker, "alt"); 59 | } 60 | 61 | get stickerSet() { 62 | return this._fromAttr(Api.DocumentAttributeSticker, "stickerset"); 63 | } 64 | 65 | // deno-lint-ignore getter-return 66 | get size() { 67 | if (this.media instanceof Api.Photo) { 68 | return _photoSizeByteCount(this.media.sizes[-1]); 69 | } else if (this.media instanceof Api.Document) { 70 | return this.media.size; 71 | } 72 | } 73 | 74 | // deno-lint-ignore no-explicit-any 75 | _fromAttr(cls: any, field: string) { 76 | if (this.media instanceof Api.Document) { 77 | for (const attr of this.media.attributes) { 78 | if (attr instanceof cls) { 79 | return (attr as typeof cls)[field]; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/tl/custom/forward.ts: -------------------------------------------------------------------------------- 1 | import { ChatGetter } from "./chat_getter.ts"; 2 | import { SenderGetter } from "./sender_getter.ts"; 3 | import { Api } from "../api.js"; 4 | import { returnBigInt } from "../../helpers.ts"; 5 | import { EntityType_, entityType_ } from "../../tl/helpers.ts"; 6 | import { getPeerId } from "../../utils.ts"; 7 | import { getEntityPair_ } from "../../entity_cache.ts"; 8 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 9 | 10 | export class Forward extends SenderGetter { 11 | private originalFwd: Api.MessageFwdHeader; 12 | 13 | constructor( 14 | client: AbstractTelegramClient, 15 | original: Api.MessageFwdHeader, 16 | entities: Map, 17 | ) { 18 | super(); 19 | // contains info for the original header sent by telegram. 20 | this.originalFwd = original; 21 | 22 | let senderId = undefined; 23 | let sender = undefined; 24 | let inputSender = undefined; 25 | let peer = undefined; 26 | // deno-lint-ignore no-unused-vars 27 | let chat = undefined; 28 | let inputChat = undefined; 29 | if (original.fromId) { 30 | const ty = entityType_(original.fromId); 31 | if (ty === EntityType_.USER) { 32 | senderId = getPeerId(original.fromId); 33 | [sender, inputSender] = getEntityPair_( 34 | senderId, 35 | entities, 36 | client._entityCache, 37 | ); 38 | } else if (ty === EntityType_.CHANNEL || ty === EntityType_.CHAT) { 39 | peer = original.fromId; 40 | [chat, inputChat] = getEntityPair_( 41 | getPeerId(peer), 42 | entities, 43 | client._entityCache, 44 | ); 45 | } 46 | } 47 | ChatGetter.initChatClass(this, { 48 | chatPeer: peer, 49 | inputChat: inputChat, 50 | }); 51 | SenderGetter.initSenderClass(this, { 52 | senderId: senderId ? returnBigInt(senderId) : undefined, 53 | sender: sender, 54 | inputSender: inputSender, 55 | }); 56 | this._client = client; 57 | } 58 | } 59 | 60 | export interface Forward extends ChatGetter, SenderGetter {} 61 | -------------------------------------------------------------------------------- /src/tl/custom/inline_result.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { getMessageId } from "../../utils.ts"; 3 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 4 | 5 | export class InlineResult { 6 | private _ARTICLE = "article"; 7 | private _PHOTO = "photo"; 8 | private _GIF = "gif"; 9 | private _VIDEO = "video"; 10 | private _VIDEO_GIF = "mpeg4_gif"; 11 | private _AUDIO = "audio"; 12 | private _DOCUMENT = "document"; 13 | private _LOCATION = "location"; 14 | private _VENUE = "venue"; 15 | private _CONTACT = "contact"; 16 | private _GAME = "game"; 17 | private readonly _entity: Api.TypeEntityLike | undefined; 18 | private readonly _queryId: Api.long | undefined; 19 | private readonly result: Api.TypeBotInlineResult; 20 | private _client: AbstractTelegramClient; 21 | 22 | constructor( 23 | client: AbstractTelegramClient, 24 | original: Api.TypeBotInlineResult, 25 | queryId?: Api.long, 26 | entity?: Api.TypeEntityLike, 27 | ) { 28 | this._client = client; 29 | this.result = original; 30 | this._queryId = queryId; 31 | this._entity = entity; 32 | } 33 | 34 | get type() { 35 | return this.result.type; 36 | } 37 | 38 | get message() { 39 | return this.result.sendMessage; 40 | } 41 | 42 | get description() { 43 | return this.result.description; 44 | } 45 | 46 | // deno-lint-ignore getter-return 47 | get url() { 48 | if (this.result instanceof Api.BotInlineResult) { 49 | return this.result.url; 50 | } 51 | } 52 | 53 | get photo() { 54 | if (this.result instanceof Api.BotInlineResult) { 55 | return this.result.thumb; 56 | } else { 57 | return this.result.photo; 58 | } 59 | } 60 | 61 | get document() { 62 | if (this.result instanceof Api.BotInlineResult) { 63 | return this.result.content; 64 | } else { 65 | return this.result.document; 66 | } 67 | } 68 | 69 | async click( 70 | entity?: Api.TypeEntityLike, 71 | replyTo?: Api.TypeMessageIDLike, 72 | silent = false, 73 | clearDraft = false, 74 | hideVia = false, 75 | ) { 76 | if (entity) { 77 | entity = await this._client.getInputEntity(entity); 78 | } else if (this._entity) { 79 | entity = this._entity; 80 | } else { 81 | throw new Error( 82 | "You must provide the entity where the result should be sent to", 83 | ); 84 | } 85 | const replyId = replyTo ? getMessageId(replyTo) : undefined; 86 | const request = new Api.messages.SendInlineBotResult({ 87 | peer: entity, 88 | queryId: this._queryId, 89 | id: this.result.id, 90 | silent: silent, 91 | clearDraft: clearDraft, 92 | hideVia: hideVia, 93 | replyToMsgId: replyId, 94 | }); 95 | return await this._client.invoke(request); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/tl/custom/inline_results.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { InlineResult } from "./inline_result.ts"; 3 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 4 | 5 | export class InlineResults extends Array { 6 | private result: Api.messages.TypeBotResults; 7 | private queryId: Api.long; 8 | private readonly cacheTime: Api.int; 9 | private readonly _validUntil: number; 10 | private users: Api.TypeUser[]; 11 | private gallery: boolean; 12 | private nextOffset: string | undefined; 13 | private switchPm: Api.TypeInlineBotSwitchPM | undefined; 14 | 15 | constructor( 16 | client: AbstractTelegramClient, 17 | original: Api.messages.TypeBotResults, 18 | entity?: Api.TypeEntityLike, 19 | ) { 20 | super( 21 | ...original.results.map( 22 | (res) => new InlineResult(client, res, original.queryId, entity), 23 | ), 24 | ); 25 | this.result = original; 26 | this.queryId = original.queryId; 27 | this.cacheTime = original.cacheTime; 28 | this._validUntil = new Date().getTime() / 1000 + this.cacheTime; 29 | this.users = original.users; 30 | this.gallery = Boolean(original.gallery); 31 | this.nextOffset = original.nextOffset; 32 | this.switchPm = original.switchPm; 33 | } 34 | 35 | resultsValid() { 36 | return new Date().getTime() / 1000 < this._validUntil; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tl/custom/message_button.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { Button } from "./button.ts"; 3 | import { computeCheck } from "../../password.ts"; 4 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 5 | 6 | export class MessageButton { 7 | private readonly _client: AbstractTelegramClient; 8 | private readonly _chat: Api.TypeEntityLike; 9 | public readonly button: Api.TypeButtonLike; 10 | private readonly _bot?: Api.TypeEntityLike; 11 | private readonly _msgId: Api.TypeMessageIDLike; 12 | 13 | constructor( 14 | client: AbstractTelegramClient, 15 | original: Api.TypeButtonLike, 16 | chat: Api.TypeEntityLike, 17 | bot: Api.TypeEntityLike | undefined, 18 | msgId: Api.TypeMessageIDLike, 19 | ) { 20 | this.button = original; 21 | this._bot = bot; 22 | this._chat = chat; 23 | this._msgId = msgId; 24 | this._client = client; 25 | } 26 | 27 | get client() { 28 | return this._client; 29 | } 30 | 31 | get text() { 32 | // @ts-expect-error: TODO 33 | return !(this.button instanceof Button) ? this.button.text : ""; 34 | } 35 | 36 | // deno-lint-ignore getter-return 37 | get data() { 38 | if (this.button instanceof Api.KeyboardButtonCallback) { 39 | return this.button.data; 40 | } 41 | } 42 | 43 | // deno-lint-ignore getter-return 44 | get inlineQuery() { 45 | if (this.button instanceof Api.KeyboardButtonSwitchInline) { 46 | return this.button.query; 47 | } 48 | } 49 | 50 | // deno-lint-ignore getter-return 51 | get url() { 52 | if (this.button instanceof Api.KeyboardButtonUrl) { 53 | return this.button.url; 54 | } 55 | } 56 | 57 | async click({ 58 | sharePhone = false, 59 | shareGeo = [0, 0], 60 | password, 61 | }: { 62 | sharePhone?: boolean | string | Api.InputMediaContact; 63 | shareGeo?: [number, number] | Api.InputMediaGeoPoint; 64 | password?: string; 65 | }) { 66 | if (this.button instanceof Api.KeyboardButton) { 67 | return this._client.sendMessage(this._chat, { 68 | message: this.button.text, 69 | parseMode: undefined, 70 | }); 71 | } else if (this.button instanceof Api.KeyboardButtonCallback) { 72 | let encryptedPassword; 73 | if (password !== undefined) { 74 | const pwd = await this.client.invoke( 75 | new Api.account.GetPassword(), 76 | ); 77 | encryptedPassword = await computeCheck(pwd, password); 78 | } 79 | const request = new Api.messages.GetBotCallbackAnswer({ 80 | peer: this._chat, 81 | msgId: this._msgId, 82 | data: this.button.data, 83 | password: encryptedPassword, 84 | }); 85 | try { 86 | return await this._client.invoke(request); 87 | } catch (e) { 88 | if (e.errorMessage === "BOT_RESPONSE_TIMEOUT") { 89 | return null; 90 | } 91 | throw e; 92 | } 93 | } else if (this.button instanceof Api.KeyboardButtonSwitchInline) { 94 | return this._client.invoke( 95 | new Api.messages.StartBot({ 96 | bot: this._bot, 97 | peer: this._chat, 98 | startParam: this.button.query, 99 | }), 100 | ); 101 | } else if (this.button instanceof Api.KeyboardButtonUrl) { 102 | return this.button.url; 103 | } else if (this.button instanceof Api.KeyboardButtonGame) { 104 | const request = new Api.messages.GetBotCallbackAnswer({ 105 | peer: this._chat, 106 | msgId: this._msgId, 107 | game: true, 108 | }); 109 | try { 110 | return await this._client.invoke(request); 111 | } catch (e) { 112 | if (e.errorMessage == "BOT_RESPONSE_TIMEOUT") { 113 | return null; 114 | } 115 | throw e; 116 | } 117 | } else if (this.button instanceof Api.KeyboardButtonRequestPhone) { 118 | if (!sharePhone) { 119 | throw new Error( 120 | "cannot click on phone buttons unless sharePhone=true", 121 | ); 122 | } 123 | if (sharePhone === true || typeof sharePhone === "string") { 124 | const me = (await this._client.getMe()) as Api.User; 125 | sharePhone = new Api.InputMediaContact({ 126 | phoneNumber: (sharePhone === true ? me.phone : sharePhone) || "", 127 | firstName: me.firstName || "", 128 | lastName: me.lastName || "", 129 | vcard: "", 130 | }); 131 | } 132 | throw new Error("Not supported for now"); 133 | } else if (this.button instanceof Api.InputWebFileGeoPointLocation) { 134 | if (!shareGeo) { 135 | throw new Error( 136 | "cannot click on geo buttons unless shareGeo=[longitude, latitude]", 137 | ); 138 | } 139 | throw new Error("Not supported for now"); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/tl/custom/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./button.ts"; 2 | export * from "./chat_getter.ts"; 3 | export * from "./dialog.ts"; 4 | export * from "./draft.ts"; 5 | export * from "./file.ts"; 6 | export * from "./forward.ts"; 7 | export * from "./inline_result.ts"; 8 | export * from "./inline_results.ts"; 9 | export * from "./message.ts"; 10 | export * from "./message_button.ts"; 11 | export * from "./sender_getter.ts"; 12 | -------------------------------------------------------------------------------- /src/tl/custom/sender_getter.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "../api.js"; 2 | import { ChatGetter } from "./chat_getter.ts"; 3 | import { bigInt } from "../../deps.ts"; 4 | import { AbstractTelegramClient } from "../../client/abstract_telegram_client.ts"; 5 | 6 | interface SenderGetterConstructorInterface { 7 | senderId?: bigInt.BigInteger; 8 | sender?: Api.TypeEntity; 9 | inputSender?: Api.TypeInputPeer; 10 | } 11 | 12 | export class SenderGetter extends ChatGetter { 13 | _senderId?: bigInt.BigInteger; 14 | _sender?: Api.TypeEntity; 15 | _inputSender?: Api.TypeInputPeer; 16 | declare public _client?: AbstractTelegramClient; 17 | 18 | static initSenderClass( 19 | // deno-lint-ignore no-explicit-any 20 | c: any, 21 | { senderId, sender, inputSender }: SenderGetterConstructorInterface, 22 | ) { 23 | c._senderId = senderId; 24 | c._sender = sender; 25 | c._inputSender = inputSender; 26 | c._client = undefined; 27 | } 28 | 29 | get sender() { 30 | return this._sender; 31 | } 32 | 33 | async getSender() { 34 | if ( 35 | this._client && 36 | (!this._sender || 37 | (this._sender instanceof Api.Channel && this._sender.min)) && 38 | (await this.getInputSender()) 39 | ) { 40 | try { 41 | this._sender = await this._client.getEntity(this._inputSender!); 42 | } catch (_e) { 43 | await this._refetchSender(); 44 | } 45 | } 46 | 47 | return this._sender; 48 | } 49 | 50 | get inputSender() { 51 | if (!this._inputSender && this._senderId && this._client) { 52 | try { 53 | this._inputSender = this._client._entityCache.get(this._senderId); 54 | } catch (_e) { 55 | // 56 | } 57 | } 58 | return this._inputSender; 59 | } 60 | 61 | async getInputSender() { 62 | if (!this.inputSender && this._senderId && this._client) { 63 | await this._refetchSender(); 64 | } 65 | return this._inputSender; 66 | } 67 | 68 | get senderId() { 69 | return this._senderId; 70 | } 71 | 72 | async _refetchSender() {} 73 | } 74 | -------------------------------------------------------------------------------- /src/tl/generate_module.ts: -------------------------------------------------------------------------------- 1 | import { dirname, fromFileUrl, resolve } from "../deps.ts"; 2 | 3 | const DIR_PATH = dirname(fromFileUrl(import.meta.url)); 4 | 5 | // Generate module 6 | import "./types_generator/generate.ts"; 7 | 8 | function stripTl(tl: string) { 9 | return tl 10 | .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, "") 11 | .replace(/\n\s*\n/g, "\n") 12 | .replace(/`/g, "\\`"); 13 | } 14 | 15 | function main() { 16 | const apiTl = Deno.readTextFileSync( 17 | resolve(DIR_PATH, "./static/api.tl"), 18 | ); 19 | 20 | Deno.writeTextFileSync( 21 | resolve(DIR_PATH, "./api_tl.ts"), 22 | `export default \`${stripTl(apiTl)}\`;\n`, 23 | ); 24 | 25 | const schemaTl = Deno.readTextFileSync( 26 | resolve(DIR_PATH, "./static/schema.tl"), 27 | ); 28 | 29 | Deno.writeTextFileSync( 30 | resolve(DIR_PATH, "./schema_tl.ts"), 31 | `export default \`${stripTl(schemaTl)}\`;\n`, 32 | ); 33 | } 34 | 35 | main(); 36 | -------------------------------------------------------------------------------- /src/tl/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Api } from "./api.js"; 2 | 3 | export const EntityType_ = { 4 | USER: 0, 5 | CHAT: 1, 6 | CHANNEL: 2, 7 | }; 8 | 9 | Object.freeze(EntityType_); 10 | 11 | export function entityType_(entity: Api.TypeEntityLike) { 12 | if (typeof entity !== "object" || !("SUBCLASS_OF_ID" in entity)) { 13 | throw new Error( 14 | `${entity} is not a TLObject, cannot determine entity type`, 15 | ); 16 | } 17 | if ( 18 | ![ 19 | 0x2d45687, // crc32('Peer') 20 | 0xc91c90b6, // crc32('InputPeer') 21 | 0xe669bf46, // crc32('InputUser') 22 | 0x40f202fd, // crc32('InputChannel') 23 | 0x2da17977, // crc32('User') 24 | 0xc5af5d94, // crc32('Chat') 25 | 0x1f4661b9, // crc32('UserFull') 26 | 0xd49a2697, // crc32('ChatFull') 27 | ].includes(entity.SUBCLASS_OF_ID) 28 | ) { 29 | throw new Error(`${entity} does not have any entity type`); 30 | } 31 | const name = entity.className; 32 | if (name.includes("User")) { 33 | return EntityType_.USER; 34 | } else if (name.includes("Chat")) { 35 | return EntityType_.CHAT; 36 | } else if (name.includes("Channel")) { 37 | return EntityType_.CHANNEL; 38 | } else if (name.includes("Self")) { 39 | return EntityType_.USER; 40 | } 41 | // 'Empty' in name or not found, we don't care, not a valid entity. 42 | throw new Error(`${entity} does not have any entity type`); 43 | } 44 | -------------------------------------------------------------------------------- /src/tl/mod.ts: -------------------------------------------------------------------------------- 1 | import { patchAll } from "./patcher.ts"; 2 | patchAll(); 3 | 4 | export * from "./api.js"; 5 | export * from "./core/mod.ts"; 6 | export * from "./custom/mod.ts"; 7 | export * from "./generation_helpers.ts"; 8 | -------------------------------------------------------------------------------- /src/tl/patcher.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { Api } from "./api.js"; 3 | import { CustomMessage } from "./custom/message.ts"; 4 | 5 | function getGetter(obj: any, prop: string) { 6 | while (obj) { 7 | const getter = Object.getOwnPropertyDescriptor(obj, prop); 8 | if (getter && getter.get) { 9 | return getter.get; 10 | } 11 | obj = Object.getPrototypeOf(obj); 12 | } 13 | } 14 | 15 | function getSetter(obj: any, prop: string) { 16 | while (obj) { 17 | const getter = Object.getOwnPropertyDescriptor(obj, prop); 18 | if (getter && getter.set) { 19 | return getter.set; 20 | } 21 | obj = Object.getPrototypeOf(obj); 22 | } 23 | } 24 | 25 | function getInstanceMethods(obj: any) { 26 | const keys = { 27 | methods: new Set(), 28 | setters: new Set(), 29 | getters: new Set(), 30 | }; 31 | const topObject = obj; 32 | 33 | const mapAllMethods = (property: string) => { 34 | const getter = getGetter(topObject, property); 35 | const setter = getSetter(topObject, property); 36 | if (getter) { 37 | keys["getters"].add(property); 38 | } else if (setter) { 39 | keys["setters"].add(property); 40 | } else { 41 | if (!(property == "constructor")) { 42 | keys["methods"].add(property); 43 | } 44 | } 45 | }; 46 | 47 | do { 48 | Object.getOwnPropertyNames(obj).map(mapAllMethods); 49 | obj = Object.getPrototypeOf(obj); 50 | } while (obj && Object.getPrototypeOf(obj)); 51 | 52 | return keys; 53 | } 54 | 55 | // deno-lint-ignore ban-types 56 | function patchClass(clazz: Function) { 57 | const { getters, setters, methods } = getInstanceMethods( 58 | CustomMessage.prototype, 59 | ); 60 | for (const getter of getters) { 61 | Object.defineProperty(clazz.prototype, getter, { 62 | get: getGetter(CustomMessage.prototype, getter), 63 | }); 64 | } 65 | for (const setter of setters) { 66 | Object.defineProperty(clazz.prototype, setter, { 67 | set: getSetter(CustomMessage.prototype, setter), 68 | }); 69 | } 70 | for (const method of methods) { 71 | clazz.prototype[method] = (CustomMessage.prototype as any)[method]; 72 | } 73 | } 74 | 75 | export function patchAll() { 76 | patchClass(Api.Message); 77 | patchClass(Api.MessageService); 78 | } 79 | -------------------------------------------------------------------------------- /src/tl/schema_tl.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector = ResPQ; 3 | p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; 4 | p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; 5 | p_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data; 6 | p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data; 7 | bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner; 8 | server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params; 9 | server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params; 10 | server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data; 11 | client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data; 12 | dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer; 13 | dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer; 14 | dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer; 15 | destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes; 16 | destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes; 17 | destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; 18 | ---functions--- 19 | req_pq#60469778 nonce:int128 = ResPQ; 20 | req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; 21 | req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params; 22 | set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer; 23 | destroy_auth_key#d1435160 = DestroyAuthKeyRes; 24 | ---types--- 25 | msgs_ack#62d6b459 msg_ids:Vector = MsgsAck; 26 | bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification; 27 | bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification; 28 | msgs_state_req#da69fb52 msg_ids:Vector = MsgsStateReq; 29 | msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo; 30 | msgs_all_info#8cc0d131 msg_ids:Vector info:string = MsgsAllInfo; 31 | msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo; 32 | msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo; 33 | msg_resend_req#7d861a08 msg_ids:Vector = MsgResendReq; 34 | rpc_error#2144ca19 error_code:int error_message:string = RpcError; 35 | rpc_answer_unknown#5e2ad36e = RpcDropAnswer; 36 | rpc_answer_dropped_running#cd78e586 = RpcDropAnswer; 37 | rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer; 38 | future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt; 39 | future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts; 40 | pong#347773c5 msg_id:long ping_id:long = Pong; 41 | destroy_session_ok#e22045fc session_id:long = DestroySessionRes; 42 | destroy_session_none#62d350c9 session_id:long = DestroySessionRes; 43 | new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession; 44 | http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; 45 | ipPort#d433ad73 ipv4:int port:int = IpPort; 46 | ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort; 47 | accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector = AccessPointRule; 48 | help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple; 49 | tlsClientHello blocks:vector = TlsClientHello; 50 | tlsBlockString data:string = TlsBlock; 51 | tlsBlockRandom length:int = TlsBlock; 52 | tlsBlockZero length:int = TlsBlock; 53 | tlsBlockDomain = TlsBlock; 54 | tlsBlockGrease seed:int = TlsBlock; 55 | tlsBlockPublicKey = TlsBlock; 56 | tlsBlockScope entries:Vector = TlsBlock; 57 | ---functions--- 58 | rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer; 59 | get_future_salts#b921bd04 num:int = FutureSalts; 60 | ping#7abe77ec ping_id:long = Pong; 61 | ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong; 62 | destroy_session#e7512126 session_id:long = DestroySessionRes; 63 | `; 64 | -------------------------------------------------------------------------------- /src/tl/static/schema.tl: -------------------------------------------------------------------------------- 1 | // Core types (no need to gen) 2 | 3 | //vector#1cb5c415 {t:Type} # [ t ] = Vector t; 4 | 5 | /////////////////////////////// 6 | /// Authorization key creation 7 | /////////////////////////////// 8 | 9 | resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector = ResPQ; 10 | 11 | p_q_inner_data#83c95aec pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data; 12 | p_q_inner_data_dc#a9f55f95 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data; 13 | p_q_inner_data_temp#3c6a84d4 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data; 14 | p_q_inner_data_temp_dc#56fddf88 pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data; 15 | 16 | bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner; 17 | 18 | server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params; 19 | server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:string = Server_DH_Params; 20 | 21 | server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:string g_a:string server_time:int = Server_DH_inner_data; 22 | 23 | client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:string = Client_DH_Inner_Data; 24 | 25 | dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer; 26 | dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer; 27 | dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer; 28 | 29 | destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes; 30 | destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes; 31 | destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes; 32 | 33 | ---functions--- 34 | 35 | req_pq#60469778 nonce:int128 = ResPQ; 36 | req_pq_multi#be7e8ef1 nonce:int128 = ResPQ; 37 | 38 | req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string public_key_fingerprint:long encrypted_data:string = Server_DH_Params; 39 | 40 | set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:string = Set_client_DH_params_answer; 41 | 42 | destroy_auth_key#d1435160 = DestroyAuthKeyRes; 43 | 44 | /////////////////////////////// 45 | ////////////// System messages 46 | /////////////////////////////// 47 | 48 | ---types--- 49 | 50 | msgs_ack#62d6b459 msg_ids:Vector = MsgsAck; 51 | 52 | bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification; 53 | bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification; 54 | 55 | msgs_state_req#da69fb52 msg_ids:Vector = MsgsStateReq; 56 | msgs_state_info#04deb57d req_msg_id:long info:string = MsgsStateInfo; 57 | msgs_all_info#8cc0d131 msg_ids:Vector info:string = MsgsAllInfo; 58 | 59 | msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo; 60 | msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo; 61 | 62 | msg_resend_req#7d861a08 msg_ids:Vector = MsgResendReq; 63 | 64 | //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult; // parsed manually 65 | 66 | rpc_error#2144ca19 error_code:int error_message:string = RpcError; 67 | 68 | rpc_answer_unknown#5e2ad36e = RpcDropAnswer; 69 | rpc_answer_dropped_running#cd78e586 = RpcDropAnswer; 70 | rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer; 71 | 72 | future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt; 73 | future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts; 74 | 75 | pong#347773c5 msg_id:long ping_id:long = Pong; 76 | 77 | destroy_session_ok#e22045fc session_id:long = DestroySessionRes; 78 | destroy_session_none#62d350c9 session_id:long = DestroySessionRes; 79 | 80 | new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession; 81 | 82 | //message msg_id:long seqno:int bytes:int body:Object = Message; // parsed manually 83 | //msg_container#73f1f8dc messages:vector = MessageContainer; // parsed manually 84 | //msg_copy#e06046b2 orig_message:Message = MessageCopy; // parsed manually, not used - use msg_container 85 | //gzip_packed#3072cfa1 packed_data:string = Object; // parsed manually 86 | 87 | http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait; 88 | 89 | //ipPort ipv4:int port:int = IpPort; 90 | //help.configSimple#d997c3c5 date:int expires:int dc_id:int ip_port_list:Vector = help.ConfigSimple; 91 | 92 | ipPort#d433ad73 ipv4:int port:int = IpPort; 93 | ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort; 94 | accessPointRule#4679b65f phone_prefix_rules:string dc_id:int ips:vector = AccessPointRule; 95 | help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple; 96 | 97 | tlsClientHello blocks:vector = TlsClientHello; 98 | 99 | tlsBlockString data:string = TlsBlock; 100 | tlsBlockRandom length:int = TlsBlock; 101 | tlsBlockZero length:int = TlsBlock; 102 | tlsBlockDomain = TlsBlock; 103 | tlsBlockGrease seed:int = TlsBlock; 104 | tlsBlockPublicKey = TlsBlock; 105 | tlsBlockScope entries:Vector = TlsBlock; 106 | 107 | ---functions--- 108 | 109 | rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer; 110 | 111 | get_future_salts#b921bd04 num:int = FutureSalts; 112 | 113 | ping#7abe77ec ping_id:long = Pong; 114 | ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong; 115 | 116 | destroy_session#e7512126 session_id:long = DestroySessionRes; 117 | -------------------------------------------------------------------------------- /src/tl/types_generator/generate.ts: -------------------------------------------------------------------------------- 1 | import { template } from "./template.ts"; 2 | import { Config, parseTl } from "../generation_helpers.ts"; 3 | import { dirname, fromFileUrl, resolve } from "../../deps.ts"; 4 | 5 | const DIR_PATH = dirname(fromFileUrl(import.meta.url)); 6 | const INPUT_FILE = resolve(DIR_PATH, "../static/api.tl"); 7 | const SCHEMA_FILE = resolve(DIR_PATH, "../static/schema.tl"); 8 | const OUTPUT_FILE = resolve(DIR_PATH, "../api.d.ts"); 9 | 10 | const peersToPatch = [ 11 | "InputPeer", 12 | "Peer", 13 | "InputUser", 14 | "User", 15 | "UserFull", 16 | "Chat", 17 | "ChatFull", 18 | "InputChannel", 19 | ]; 20 | 21 | function patchMethods(methods: Array) { 22 | for (const method of methods) { 23 | for (const arg in method["argsConfig"]) { 24 | if (peersToPatch.includes(method.argsConfig[arg].type!)) { 25 | method.argsConfig[arg].type = "EntityLike"; 26 | } else if ( 27 | method.argsConfig[arg].type && 28 | arg.toLowerCase().includes("msgid") 29 | ) { 30 | if (method.argsConfig[arg].type !== "long") { 31 | method.argsConfig[arg].type = "TypeMessageIDLike"; 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | function extractParams(fileContent: string) { 39 | const defIterator = parseTl(fileContent, "109"); 40 | const types: Record< 41 | string, 42 | { namespace?: string; name: string; constructors: Array } 43 | > = {}; 44 | const constructors = new Array(); 45 | const functions = new Array(); 46 | 47 | for (const def of defIterator) { 48 | if (def.isFunction) { 49 | functions.push(def); 50 | continue; 51 | } 52 | 53 | if (!types[def.result]) { 54 | const [namespace, name] = def.result.includes(".") 55 | ? def.result.split(".") 56 | : [undefined, def.result]; 57 | 58 | types[def.result] = { 59 | namespace, 60 | name, 61 | constructors: [], 62 | }; 63 | } 64 | 65 | types[def.result].constructors.push( 66 | def.namespace ? `${def.namespace}.${def.name}` : def.name, 67 | ); 68 | constructors.push(def); 69 | } 70 | 71 | return { 72 | types: Object.values(types), 73 | constructors, 74 | functions, 75 | }; 76 | } 77 | 78 | function generateTypes() { 79 | const tlContent = Deno.readTextFileSync(INPUT_FILE); 80 | const apiConfig = extractParams(tlContent); 81 | 82 | const schemaContent = Deno.readTextFileSync(SCHEMA_FILE); 83 | const schemaConfig = extractParams(schemaContent); 84 | 85 | const types = [...apiConfig.types, ...schemaConfig.types]; 86 | const functions = [...apiConfig.functions, ...schemaConfig.functions]; 87 | const constructors = [ 88 | ...apiConfig.constructors, 89 | ...schemaConfig.constructors, 90 | ]; 91 | 92 | // Patching custom types 93 | patchMethods(functions); 94 | const generated = template({ 95 | constructors, 96 | functions, 97 | types, 98 | }); 99 | 100 | Deno.writeTextFileSync(OUTPUT_FILE, generated); 101 | } 102 | 103 | generateTypes(); 104 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = "0.8.2"; 2 | export const GRAM_BASE_VERSION = "2.15.3"; 3 | --------------------------------------------------------------------------------