├── .editorconfig ├── .flowconfig ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── prebuilt-tdlib.yml │ └── publish-tdl.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── examples ├── async-iterators.js ├── custom-login.js ├── deno-example.ts ├── get-chats.js ├── multiple-accounts.js ├── send-message.js ├── using-prebuilt-tdlib.js ├── with-proxy.js └── with-typescript.ts ├── flow-typed └── tdlib-types_vx.x.x.js ├── package-lock.json ├── package.json ├── packages ├── prebuilt-tdlib │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── ci │ │ ├── build-linux.sh │ │ ├── build-macos.sh │ │ ├── tdlib-zig-wrapper.nix │ │ ├── tdlib-zig.patch │ │ ├── tdlib.nix │ │ └── zig-toolchain.nix │ ├── index.d.ts │ ├── index.js │ ├── package.json │ ├── prebuild-list.js │ ├── prebuild-template │ │ ├── LICENSE │ │ ├── README.md │ │ └── package.json │ ├── prebuilds │ │ └── .gitkeep │ └── publish.js ├── tdl-install-types │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── tdl-install-types │ ├── package.json │ ├── src │ │ ├── gen.js │ │ └── index.js │ └── tests │ │ ├── __snapshots__ │ │ ├── flow-tdlib-1-8-0.js.shot │ │ └── ts-tdlib-1-8-0.js.shot │ │ ├── generator.test.ts │ │ └── schema │ │ └── v1.8.0.tl └── tdl │ ├── .gitignore │ ├── LICENSE │ ├── addon │ ├── td.cpp │ ├── win32-dlfcn.cpp │ └── win32-dlfcn.h │ ├── binding.gyp │ ├── index.d.ts │ ├── package.json │ ├── scripts │ ├── copy-readme.js │ └── generate-flow.js │ ├── src │ ├── addon.ts │ ├── client.ts │ ├── index.ts │ ├── prompt.ts │ ├── queue.ts │ ├── util.ts │ └── version.ts │ ├── tests │ └── rename.test.ts │ └── tsconfig.json ├── scripts ├── run-prebuilt-tdlib.sh ├── update-license-year.sh └── update-types.sh ├── test.cfg.example ├── tests ├── auth-only │ └── bot.test.ts ├── integration │ ├── shared.ts │ ├── tdjson-old.test.ts │ └── tdjson.test.ts └── types │ ├── flow.js │ └── ts.ts ├── tsconfig.json ├── typings ├── node-gyp-build.d.ts └── tdlib-types.d.ts └── vitest.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | module.use_strict=true 11 | include_warnings=true 12 | 13 | [strict] 14 | 15 | [declarations] 16 | /node_modules/.* 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.flow linguist-language=JavaScript 2 | *.h linguist-language=C++ 3 | *.snap linguist-generated=true 4 | *.shot linguist-generated=true 5 | tdlib-types.d.ts linguist-generated=true 6 | flow-typed/*.js linguist-generated=true 7 | examples/*.js linguist-vendored=true 8 | examples/*.ts linguist-vendored=true 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build-and-test: 5 | name: 'Test / Node v${{ matrix.node }} / ${{ matrix.os }}' 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | # Weirdly arm64 macos-14 doesn't really work because typescript fails with 13 | # RangeError: Maximum call stack size exceeded at getOuterTypeParameters 14 | - macos-13 15 | - windows-latest 16 | node: 17 | - 20 18 | include: 19 | # The packages themselves should still work with Node v16, but the 20 | # dev dependencies don't support v16 anymore 21 | - node: 18 22 | os: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node }} 28 | cache: npm 29 | - name: Update npm 30 | run: npm install -g npm@9 31 | - run: npm install # also runs the `prepare` npm script, which runs `build` 32 | - run: npm test 33 | - run: npm uninstall -D prebuilt-tdlib 34 | - name: Run integration tests 35 | shell: pwsh 36 | run: | 37 | $ErrorActionPreference = "Stop" 38 | $tds = @( 39 | "td-1.8.33", 40 | "td-1.8.30", 41 | "td-1.8.27", 42 | "td-1.8.26", 43 | "td-1.8.25", 44 | "td-1.8.19", 45 | "td-1.8.14", 46 | "td-1.8.0" 47 | ) 48 | foreach ($td in $tds) { 49 | mkdir $td 50 | npm install -D prebuilt-tdlib@$td 51 | npm run test:integration 52 | if ($LastExitCode -ne 0) { 53 | exit $LastExitCode 54 | } 55 | npm uninstall -D prebuilt-tdlib 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/prebuilt-tdlib.yml: -------------------------------------------------------------------------------- 1 | name: Pre-built TDLib 2 | # This workflow can be executed using a command like this: 3 | # gh workflow run prebuilt-tdlib.yml --ref develop -f tdlib=v1.8.0 -f npm-patch=0 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | tdlib: 8 | description: 'TDLib git ref (e.g. v1.8.0 or a commit hash)' 9 | type: string 10 | required: true 11 | npm-patch: 12 | description: 'prebuilt-tdlib patch version, required to publish (e.g. 0, 1)' 13 | type: string 14 | required: false 15 | npm-tag: 16 | description: 'npm tag (e.g. latest, beta)' 17 | type: string 18 | required: false 19 | default: 'latest' 20 | jobs: 21 | build-linux-x86_64-glibc: 22 | name: 'Build TDLib / Linux x86_64 glibc' 23 | runs-on: ubuntu-24.04 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: cachix/install-nix-action@v27 27 | with: 28 | nix_path: nixpkgs=channel:nixos-unstable 29 | - name: Build TDLib 30 | run: | 31 | cd packages/prebuilt-tdlib/ci 32 | ./build-linux.sh ${{ inputs.tdlib }} x86_64 gnu 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: tdlib-linux-x86_64-glibc 36 | path: ${{ env.TO_UPLOAD }} 37 | 38 | build-linux-arm64-glibc: 39 | name: 'Build TDLib / Linux arm64 glibc (cross)' 40 | runs-on: ubuntu-24.04 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: cachix/install-nix-action@v27 44 | with: 45 | nix_path: nixpkgs=channel:nixos-unstable 46 | - name: Build TDLib 47 | run: | 48 | cd packages/prebuilt-tdlib/ci 49 | ./build-linux.sh ${{ inputs.tdlib }} aarch64 gnu 50 | - uses: actions/upload-artifact@v4 51 | with: 52 | name: tdlib-linux-arm64-glibc 53 | path: ${{ env.TO_UPLOAD }} 54 | 55 | build-macos-x86_64: 56 | name: 'Build TDLib / macOS x86_64' 57 | runs-on: macos-13 58 | steps: 59 | - uses: actions/checkout@v4 60 | - uses: cachix/install-nix-action@v27 61 | with: 62 | nix_path: nixpkgs=channel:nixpkgs-unstable 63 | - name: Build TDLib 64 | run: | 65 | cd packages/prebuilt-tdlib/ci 66 | ./build-macos.sh ${{ inputs.tdlib }} 67 | - uses: actions/upload-artifact@v4 68 | with: 69 | name: tdlib-macos-x86_64 70 | path: ${{ env.TO_UPLOAD }} 71 | 72 | build-macos-arm64: 73 | name: 'Build TDLib / macOS arm64' 74 | runs-on: macos-14 75 | steps: 76 | - uses: actions/checkout@v4 77 | - uses: cachix/install-nix-action@v27 78 | with: 79 | nix_path: nixpkgs=channel:nixpkgs-unstable 80 | - name: Build TDLib 81 | run: | 82 | cd packages/prebuilt-tdlib/ci 83 | ./build-macos.sh ${{ inputs.tdlib }} 84 | - name: Verify codesigning 85 | run: codesign -v ${{ env.TO_UPLOAD }}/libtdjson.dylib 86 | - uses: actions/upload-artifact@v4 87 | with: 88 | name: tdlib-macos-arm64 89 | path: ${{ env.TO_UPLOAD }} 90 | 91 | build-windows-x86_64: 92 | name: 'Build TDLib / Windows x86_64' 93 | runs-on: windows-2022 94 | steps: 95 | - uses: actions/checkout@v4 96 | with: 97 | repository: 'tdlib/td' 98 | ref: ${{ inputs.tdlib }} 99 | - name: vcpkg cache 100 | uses: actions/cache@v4 101 | with: 102 | path: '~\AppData\Local\vcpkg\archives' 103 | key: windows-vcpkg-${{ github.run_id }} 104 | restore-keys: | 105 | windows-vcpkg- 106 | - name: Install dependencies using vcpkg 107 | run: vcpkg install gperf:x64-windows-static openssl:x64-windows-static zlib:x64-windows-static 108 | - name: CMake version 109 | run: cmake --version 110 | # NOTE: The ZLIB_USE_STATIC_LIBS option requires CMake >= 3.24 111 | - name: Build TDLib 112 | run: | 113 | mkdir to-upload 114 | mkdir build 115 | cd build 116 | cmake -A x64 ` 117 | -DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake ` 118 | -DVCPKG_TARGET_TRIPLET=x64-windows-static ` 119 | -DOPENSSL_USE_STATIC_LIBS=TRUE -DZLIB_USE_STATIC_LIBS=TRUE .. 120 | cmake --build . --target tdjson --config Release --parallel 4 121 | cd .. 122 | cp build\Release\tdjson.dll to-upload\tdjson.dll 123 | vcpkg list | Select-String openssl,zlib 124 | - uses: actions/upload-artifact@v4 125 | with: 126 | name: tdlib-windows-x86_64 127 | path: to-upload 128 | 129 | test: 130 | name: 'Test / ${{ matrix.v.bin }} / ${{ matrix.v.os }}' 131 | needs: 132 | - build-linux-x86_64-glibc 133 | - build-linux-arm64-glibc 134 | - build-macos-x86_64 135 | - build-macos-arm64 136 | - build-windows-x86_64 137 | runs-on: ${{ matrix.v.os }} 138 | strategy: 139 | fail-fast: false 140 | matrix: 141 | v: 142 | # use an older ubuntu version to test the library working with 143 | # previous glibc version 144 | - os: ubuntu-22.04 145 | bin: tdlib-linux-x86_64-glibc 146 | - os: ubuntu-24.04-arm 147 | bin: tdlib-linux-arm64-glibc 148 | - os: macos-13 149 | bin: tdlib-macos-x86_64 150 | - os: macos-latest 151 | bin: tdlib-macos-arm64 152 | - os: windows-latest 153 | bin: tdlib-windows-x86_64 154 | steps: 155 | - uses: actions/checkout@v4 156 | - uses: actions/setup-node@v4 157 | with: 158 | node-version: lts/* 159 | cache: npm 160 | - run: npm install 161 | - name: Uninstall the prebuilt-tdlib dev dependency 162 | run: npm uninstall -D prebuilt-tdlib # just in case 163 | - uses: actions/download-artifact@v4 164 | with: 165 | name: ${{ matrix.v.bin }} 166 | merge-multiple: true 167 | - run: npm run test:integration 168 | env: 169 | LIBDIR_PATH: '.' 170 | 171 | publish: 172 | name: 'Publish to npm' 173 | needs: [test] 174 | runs-on: ubuntu-latest 175 | permissions: 176 | contents: read 177 | id-token: write 178 | steps: 179 | - uses: actions/checkout@v4 180 | - uses: actions/setup-node@v4 181 | with: 182 | node-version: lts/* 183 | cache: npm 184 | # registry-url is mandatory here 185 | registry-url: 'https://registry.npmjs.org' 186 | - run: npm install 187 | - run: npm uninstall -D prebuilt-tdlib 188 | - uses: actions/download-artifact@v4 189 | with: 190 | pattern: tdlib-* 191 | path: packages/prebuilt-tdlib/prebuilds 192 | - run: tree packages/prebuilt-tdlib 193 | - run: du -hsc packages/prebuilt-tdlib/prebuilds/* 194 | env: 195 | PREBUILT_PATH: packages/prebuilt-tdlib 196 | - run: | 197 | git clone https://github.com/tdlib/td td 198 | cd td 199 | git checkout ${{ inputs.tdlib }} 200 | echo "TDLIB_COMMIT_HASH=$(git rev-parse ${{ inputs.tdlib }})" >> "$GITHUB_ENV" 201 | __ver=`grep -Po "(?<=TDLib VERSION )\d+\.\d+\.\d+" ./CMakeLists.txt` 202 | echo "TDLIB_VERSION=$__ver" >> "$GITHUB_ENV" 203 | echo NPM_TAG=${{ inputs.npm-tag }} >> "$GITHUB_ENV" 204 | - name: Publish 205 | run: node packages/prebuilt-tdlib/publish.js ${{ inputs.npm-patch }} 206 | env: 207 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 208 | if: "${{ inputs.npm-patch != '' }}" 209 | -------------------------------------------------------------------------------- /.github/workflows/publish-tdl.yml: -------------------------------------------------------------------------------- 1 | name: Publish tdl 2 | # This workflow can be executed using a command like this: 3 | # gh workflow run publish-tdl.yml --ref develop -f npm-tag=latest 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | npm-tag: 8 | description: 'npm tag (e.g. latest, beta), required to publish' 9 | type: string 10 | required: false 11 | 12 | jobs: 13 | build-and-test: 14 | name: Build the node addon on ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-latest, macos-13, windows-latest] 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | cache: npm 26 | - uses: goto-bus-stop/setup-zig@v2 27 | with: 28 | version: '0.13.0' 29 | if: runner.os == 'Linux' 30 | - name: Install dependencies 31 | run: npm install 32 | - name: Clean old binaries 33 | run: npm run clean -w tdl 34 | - name: Prebuildify (Linux via zig) 35 | if: runner.os == 'Linux' 36 | run: | 37 | CC="zig cc -target x86_64-linux-gnu.2.22" CXX="zig c++ -target x86_64-linux-gnu.2.22 -s" \ 38 | npm exec -w tdl -c 'prebuildify --napi -t 18.20.5 --arch x64 --tag-libc --strip' 39 | CC="zig cc -target aarch64-linux-gnu.2.22" CXX="zig c++ -target aarch64-linux-gnu.2.22 -s" \ 40 | npm exec -w tdl -c 'prebuildify --napi -t 18.20.5 --arch arm64 --armv 8 --tag-armv --tag-libc' 41 | ldd packages/tdl/prebuilds/*-x64/*.node 42 | file packages/tdl/prebuilds/*/*.node 43 | du -hsc packages/tdl/prebuilds/*/*.node 44 | - name: Prebuildify (macOS x86_64 and Windows x86_64) 45 | if: runner.os != 'Linux' 46 | run: npm exec -w tdl -c 'prebuildify --napi -t 18.20.5 --arch x64 --strip' 47 | - name: "Prebuildify: Crosscompile to arm64 Apple Silicon" 48 | if: runner.os == 'macOS' 49 | run: | 50 | npm exec -w tdl -c 'prebuildify --napi -t 18.20.5 --arch arm64 --strip' 51 | file packages/tdl/prebuilds/*/*.node 52 | du -hsc packages/tdl/prebuilds/*/*.node 53 | - name: Run tests (unit tests + integration tests) 54 | run: npm run test:all 55 | - uses: actions/upload-artifact@v4 56 | with: 57 | name: tdl-prebuilds-${{ matrix.os }} 58 | path: packages/tdl/prebuilds 59 | 60 | publish: 61 | name: 'Publish to npm' 62 | needs: [build-and-test] 63 | runs-on: ubuntu-latest 64 | permissions: 65 | contents: read 66 | id-token: write 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: actions/setup-node@v4 70 | with: 71 | node-version: lts/* 72 | cache: npm 73 | # registry-url is mandatory here 74 | registry-url: 'https://registry.npmjs.org' 75 | - name: Install dependencies 76 | run: npm install 77 | - uses: actions/download-artifact@v4 78 | with: 79 | pattern: tdl-prebuilds-* 80 | path: packages/tdl/prebuilds 81 | merge-multiple: true 82 | - run: tree packages/tdl/prebuilds 83 | - name: Ensure prebuilts exist 84 | run: (( $(ls packages/tdl/prebuilds | wc -l) > 3 )) 85 | - name: Tests (excluding integration tests) 86 | run: npm test 87 | - name: Publish 88 | if: "${{ inputs.npm-tag != '' }}" 89 | run: npm publish --provenance --access public --tag ${{ inputs.npm-tag }} -w tdl 90 | env: 91 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | dist/ 4 | flow-typed/npm/ 5 | *.lerna_backup 6 | /coverage/ 7 | *.bak 8 | /*.txt 9 | _td_database/ 10 | _td_files/ 11 | /test.cfg 12 | /tdlib/ 13 | *.dylib 14 | *.so 15 | *.dll 16 | compile_commands.json 17 | .ccls 18 | .clangd/ 19 | /.vim 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## tdl@8.0.2 (2024-12-20) 6 | 7 | - Fixed a possible `TypeError: null is not an object` error in `client.login()` 8 | if called from a database with an incomplete login attempt. 9 | 10 | ## tdl-install-types@0.3.0 (2024-07-19) 11 | 12 | - No longer generates `number | string` instead of `string` for `int64` types 13 | in output positions (the input int64 is still `number | string`). 14 | - Better detection of `prebuilt-tdlib`: can now use TDLib commit and version 15 | from `prebuilt-tdlib/package.json` instead of always extracting the commit 16 | hash from the library file. The `--prebuilt-tdlib` option is replaced with the 17 | `prebuilt-tdlib` argument (`--prebuilt-tdlib` is deprecated). 18 | 19 | ## tdl@8.0.1 (2024-06-18) 20 | 21 | - In bun (instead of Node.js), fixed updates/responses stop being received if 22 | none of those have been received in the last 10 (`receiveTimeout`) seconds. 23 | - In bun, fixed the old tdjson interface exiting the process instantly. 24 | 25 | This is related to bun < v1.1.14 only. 26 | 27 | ## tdl@8.0.0 (2024-05-28) 28 | 29 | - The compatibility with `tdl-tdlib-addon` is removed. The `Client` constructor 30 | is no longer exported. The non-deprecated API mostly remains the same. 31 | - TDLib < v1.8.0 is no longer supported. 32 | - Node.js < v16.0.0 is no longer supported. 33 | - The new tdjson interface is now used by default. The `useNewTdjsonInterface` 34 | option in `tdl.configure` is removed; `useOldTdjsonInterface` is added 35 | instead. 36 | - Added `client.iterUpdates()` to receive updates via async iterators as an 37 | alternative to `client.on('update', ...)`. Example: 38 | ```javascript 39 | for await (const update of client.iterUpdates()) { 40 | if (update._ === 'updateOption' && update.name === 'my_id') { 41 | console.log(`My ID is ${update.value.value}!`) 42 | break 43 | } 44 | } 45 | ``` 46 | - `client.login()` can now accept the object directly besides a function 47 | returning the object. 48 | - The TDLib errors are now wrapped in a new `TDLibError` class; `TdlError` 49 | is removed and replaced with `UnknownError`. 50 | - Changed the default verbosity level from 2 to 1. 51 | - Added `client.isClosed()`. 52 | - Pre-built node addons now support aarch64 GNU/Linux. 53 | - Removed deprecated client options: `receiveTimeout`, `useDefaultVerbosityLevel`, 54 | `disableAuth`, `useMutableRename`, `verbosityLevel`. 55 | - Removed the `bare` client option. `tdl.createBareClient(): Client` is added 56 | instead. 57 | - Removed deprecated client methods: `destroy`, `pause`, `resume`, `connect`, 58 | `connectAndLogin`, `getBackendName`, `setLogFatalErrorCallback`. 59 | - Removed deprecated exports: `TDL`, `Tdl`. 60 | - Removed deprecated events: `response`, `auth-needed`, `auth-not-needed`. The 61 | `destroy` event is renamed to `close`. 62 | - `client.off` now returns `boolean` instead of `void`; the `once` parameter is 63 | removed. (The `eventemitter3` dependency is also dropped.) 64 | - The old tdjson interface should work in multiple threads of 65 | `node:worker_threads` now. 66 | - Now published with `--provenance`. 67 | - Internal: The `tdl` client code was rewritten in TypeScript. 68 | 69 | ## tdl-install-types@0.2.0 (2024-05-28) 70 | 71 | - Added a `--github-repo` CLI option. 72 | - Added `-h` and `--version` aliases. 73 | - The generator now indicates that the `bytes` parameters are base64. 74 | - The version of `tdl-install-types` is now included in the header of the 75 | generated files. 76 | - No longer generates `null` for return types of `Execute`. 77 | - Functions not marked as synchronous are no longer generated in `Execute`. 78 | - Added some special types: `$Function`, `$FunctionResultByName`, 79 | `$FunctionInputByName`, `$FunctionName`, `$SyncFunctionName`. `Invoke` and 80 | `Execute` are implemented in terms of these types and may be somewhat faster 81 | now (in typecheck time). 82 | 83 | ## tdl@7.4.1 (2024-02-16) 84 | 85 | - Fixed a race condition (first reported when used in bun). This was publised as 86 | `tdl@7.4.1-beta.0` on 2023-11-07. 87 | 88 | ## tdl@7.4.0 (2023-10-10) 89 | 90 | - Added `tdl.setLogMessageCallback` that allows to pass a callback to the 91 | `td_set_log_message_callback` TDLib function using Node-API's thread-safe 92 | functions. (TDLib v1.8.0+ only) 93 | - `tdl.configure`: Added an experimental option `useNewTdjsonInterface` that 94 | enables the use of `td_create_client_id`/`td_send`/`td_receive`/`td_execute` 95 | interface with a client manager and global receive loop, though the old 96 | interface still works well. (TDLib v1.7.0+ only) 97 | - Changed the implementation of the old tdjson interface bindings so that the 98 | limitation of max `UV_THREADPOOL_SIZE` clients is lifted. 99 | - `tdl.configure`: Added a `receiveTimeout` advanced option. 100 | - `tdl.createClient`: `receiveTimeout` in the client options is deprecated. 101 | - `tdl.createClient`: Deprecated the `useMutableRename` advanced option. 102 | 103 | ## tdl-install-types@0.1.0 (2023-09-26) 104 | 105 | Initial release of a new cli utility that is now recommended to use instead of 106 | installing the `tdlib-types` package. It can generate a `tdlib-types.d.ts` file 107 | when given a shared library / tdlib git ref / td_api.tl file and can be called 108 | via `npx`. (See the "Types" section in the README for more information.) Changes 109 | were also made to the generator to make the autocompletion faster. 110 | 111 | ## tdl@7.3.2 (2023-09-21) 112 | 113 | - Fixed `Symbol not found: node_register_module_v…` errors on some platforms. 114 | - Fixed passing falsy values to `tdl.configure`. 115 | 116 | ## tdl@7.3.1 (2023-06-22) 117 | 118 | This update introduces some significant and long-planned changes to the 119 | interface, while retaining backward compatiblity. 120 | 121 | - No longer need to separately install `tdl` and `tdl-tdlib-addon`; just install 122 | `tdl`. `tdl-tdlib-addon` is deprecated. The library is mostly focused to 123 | Node.js only now, deno support can be added later as a separate library. This 124 | simplifies tdl. 125 | - To better reflect the distinction between TDLib-global and instance-specific 126 | functions, TDLib-global options are now passed in the special `configure` 127 | function, and `execute` is decoupled from clients. As an example: 128 | ```javascript 129 | const tdl = require('tdl') 130 | tdl.configure({ 131 | tdjson: 'libtdjson.dylib', 132 | libdir: '/usr/local/lib', 133 | verbosityLevel: 3 /* the default is 2 */ 134 | }) 135 | tdl.execute({ _: 'setLogStream', /* ... */ }) 136 | const client = tdl.createClient({ 137 | apiId: /* your api id */, 138 | apiHash: /* your api hash */ 139 | }) 140 | await client.login() 141 | ``` 142 | The full documentation for the `configure` function is available in the 143 | TypeScript typings. The old `new Client` approach is still supported but 144 | deprecated. 145 | - The `verbosityLevel` client option is deprecated (moved to `tdl.configure`). 146 | - Added pre-built binaries for the node addon using `prebuildify` and `node-gyp-build`. 147 | - Updated README to be somewhat more user-friendly. Aside of documentation 148 | changes, the library also should be simpler to use now. 149 | - The packages `tdl-tdlib-wasm` and `tdl-shared` are deprecated. Any webassembly 150 | support is removed. 151 | - Deprecated `Client#getBackendName`. 152 | 153 | Old code: 154 | 155 | ```javascript 156 | const { Client } = require('tdl') 157 | const { TDLib } = require('tdl-tdlib-addon') 158 | const client = new Client(new TDLib('path/to/libtdjson'), { 159 | apiId, 160 | apiHash, 161 | verbosityLevel: 0, 162 | // ... 163 | }) 164 | ``` 165 | 166 | New code: 167 | 168 | ```javascript 169 | const tdl = require('tdl') 170 | tdl.configure({ tdjson: 'path/to/libtdjson', verbosityLevel: 0 }) 171 | const client = tdl.createClient({ 172 | apiId, 173 | apiHash, 174 | // ... 175 | }) 176 | ``` 177 | 178 | If the default values of `tdjson` and `verbosityLevel` are used, then calling 179 | `configure` is optional. 180 | 181 | ## tdl-tdlib-addon@1.2.2 (2023-03-23) 182 | 183 | - Fixed freeing unallocated memory on app shutdown if callbacks were set. 184 | - Updated dependencies. 185 | 186 | ## tdl@7.2.0 (2022-10-11) 187 | 188 | - It is no longer needed to call `client.connect` or `client.connectAndLogin`, 189 | these functions are deprecated. 190 | - Added a `client.loginAsBot` function. 191 | - Added documentation to the `.d.ts` declaration file. 192 | - Deprecated `client.setLogFatalErrorCallback` and `client.destroy`. 193 | - Deprecated the `useDefaultVerbosityLevel` advanced option, it is replaced 194 | with `verbosityLevel: 'default'`. 195 | - Renamed the `disableAuth` advanced option to `bare`, `disableAuth` is now 196 | deprecated. 197 | 198 | ## tdl@7.1.0 (2022-09-18) 199 | 200 | - Added support for TDLib >= v1.8.6. 201 | - New functions in `LoginDetails`: `getEmailAddress`, `getEmailCode`, 202 | `confirmOnAnotherDevice`. 203 | - Added a `client.getVersion(): string` function, undocumented in the README for 204 | now. 205 | 206 | ## tdl-tdlib-addon@1.2.1 (2022-09-01) 207 | 208 | - On Linux, `dlopen` with `RTLD_DEEPBIND` (if available) is now used instead of 209 | `dlmopen`. `dlmopen` was not stable enough. This fixes some segmentation 210 | faults on GNU/Linux. 211 | 212 | ## tdl-tdlib-addon@1.2.0 (2022-08-29) 213 | 214 | - Exports the `defaultLibraryFile` string. It can be imported using 215 | `const { defaultLibraryFile } = require('tdl-tdlib-addon')`. 216 | - On GNU/Linux with glibc, `dlmopen` is used instead of `dlopen` to prevent 217 | OpenSSL compatibility issues. 218 | 219 | ## tdl-tdlib-addon@1.1.0 (2022-07-27) 220 | 221 | - Fixes `Check failed: result.second` errors on Node.js v16+ (and sometimes v14). 222 | This changes the representation of the `client`. 223 | 224 | ## tdl-tdlib-addon@1.0.1 (2021-02-16) 225 | 226 | - Fixes node-gyp's error log output when installing using npm v7. 227 | 228 | ## tdl@7.0.0 (2021-01-14) 229 | 230 | - **Important**: TypeScript and Flow users now need to install `tdlib-types` for the TDLib typings to work. 231 | It is now possible to install the typings for other TDLib versions. 232 | - `client.pause()` and `client.resume()` are now deprecated. 233 | - Removed deprecated `client.invokeFuture`. 234 | - Added a `client.getBackendName(): string` function. 235 | - Dropped support for TDLib v1.4.0. TDLib v1.5.0 or newer is required now. 236 | - Requires `tdl-shared@0.10.0`. 237 | - Multiple documentation improvements. 238 | - Internal: 239 | - - Updated Flow to v0.138.0. 240 | 241 | ## tdlib-types@1 (2021-01-14) 242 | 243 | - This is the first release of `tdlib-types`, see its [README](packages/tdlib-types/README.md). 244 | - The TDLib typings have been split from the `tdl` package into this package. 245 | - The generator now parses "may be null" comments. 246 | - Added typings for TDLib v1.7.0. 247 | 248 | ## tdl-shared@0.10.0, tdl-tdlib-addon@1.0.0, tdl-tdlib-ffi@3.0.0, tdl-tdlib-wasm@0.6.0 (2021-01-14) 249 | 250 | - Removed deprecated `setLogFilePath`, `setLogMaxFileSize`, and `setLogVerbosityLevel`. 251 | - New method: `getName(): string`. 252 | - `create()` now doesn't need to return a promise. 253 | 254 | ## tdl-tdlib-addon@0.8.2 (2020-12-04) 255 | 256 | - Fixed not building on Windows. 257 | 258 | ## tdl@6.2.0 (2020-11-08) 259 | 260 | - New method: `client.close()`, returns a promise. 261 | - `client.invokeFuture` is deprecated. 262 | - `tdl-tdlib-addon` is now recommended instead of `tdl-tdlib-ffi`. 263 | Note that the libraryFile extension in tdl-tdlib-addon is mandatory (ffi-napi automatically appends the extension). 264 | - Documentation improvements. 265 | - Other improvements. 266 | 267 | ## tdl-tdlib-addon@0.8.0 (2020-11-08) 268 | 269 | - Uses dlopen instead of rpath. 270 | - Added support for `td_set_log_fatal_error_callback`. 271 | - Major improvements. 272 | 273 | ## tdl-tdlib-ffi@2.0.1 (2020-11-08) 274 | 275 | - Minor improvements. 276 | 277 | ## tdl-tdlib-ffi@2.0.0 (2020-06-30) 278 | 279 | - ffi-napi updated to v3. 280 | This adds support for Node.js v14 and drops support for Node.js older than v10. 281 | 282 | ## tdl@6.1.0 (2020-03-22) 283 | 284 | - Adds full support for TDLib v1.6.0: 285 | - - TDLib typings are updated to TDLib v1.6.0. 286 | - - Registration in the `login` function now works with TDLib v1.5.0+ (and doesn't support tdlib <1.5.0). PR: [#71][]. 287 | - Several improvements to the documentation. 288 | - Internal: 289 | - - Moved the `tdlib-typings` project into this repository. 290 | 291 | [#71]: https://github.com/eilvelia/tdl/pull/71 292 | 293 | ## tdl@6.0.1 (2019-05-07) 294 | 295 | - Fixed renaming of objects in an array (see [#48][]). 296 | 297 | [#48]: https://github.com/eilvelia/tdl/issues/48 298 | 299 | ## v6.0.0 (2019-05-02) 300 | 301 | - **Important**: Allowed recovery from invalid phone numbers (see [#33][]). `loginDetails.phoneNumber`/`loginDetails.token` field replaced with `loginDetails.getPhoneNumber`/`loginDetails.getToken` function. 302 | - **Important**: `new Client(options)` -> `new Client(tdlibInstance, options)` 303 | - Splitted `tdl` into two packages: `tdl` and `tdl-tdlib-ffi`. Users should manually install both. 304 | - Now the `tdl` core package can work in browser. 305 | - Added the `tdl-tdlib-wasm` package. 306 | - (very unstable) Added the `tdl-tdlib-addon` package. 307 | - Removed static method `Client.fromTDLib`. 308 | - Changed behaviour of `client.on('update')` and `client.on('error')`. 309 | - `client.login()` (and `client.connectAndLogin()`) argument is optional now. 310 | - Added `client.off` (alias of `client.removeListener`) and `client.addListener` (alias of `client.on`). `Rx.fromEvent` from RxJS 6 can now work with `tdl` out of the box. Example: `Rx.fromEvent(client, 'update')`. 311 | - Removed `client.setLogFilePath`, `client.setLogMaxFileSize`, `client.setLogVerbosityLevel` methods, which are deprecated in TDLib. 312 | - Added the `disableAuth` advanced option. 313 | - Many documentation improvements. 314 | - Added basic contributing guidelines. 315 | - TDLib typings for TS/Flow: 316 | - - Updated to tdlib v1.4.0. 317 | - - Changed names of "input" kind of types: `xxxOptional` -> `xxx$Input` 318 | - - Properties of input types are read-only now. 319 | - - Flow: Now all types are exact. Previously, only input types were exact. 320 | - - Flow: Now non-input types are subtypes of input. 321 | - Other minor improvements. 322 | 323 | [#33]: https://github.com/eilvelia/tdl/issues/33 324 | 325 | ## v5.2.0 (2018-10-24) 326 | 327 | - Added `client.connectAndLogin` method. 328 | - File `types/tdlib.ts` renamed to `types/tdlib.d.ts` (see [#31][]). 329 | 330 | [#31]: https://github.com/eilvelia/tdl/issues/31 331 | 332 | ## v5.1.0 (2018-10-13) 333 | 334 | - Added `client.pause()` and `client.resume()` functions, which allow you to pause update receiving. 335 | 336 | ## v5.0.1 (2018-10-09) 337 | 338 | - Fixed [#24][]. 339 | 340 | [#24]: https://github.com/eilvelia/tdl/issues/24 341 | 342 | ## v5.0.0 (2018-09-22) 343 | 344 | - **Important**: The `client.connect` function is now splitted into two functions: `client.connect` and `client.login`. Removed `beforeAuth` argument from `client.connect` function. Removed `loginDetails` option from `Client` constructor; now `client.login` accepts a function that returns `LoginDetails`. 345 | - Added OS detection. Now `tdl` searches for `tdjson` on Windows and for `libtdjson` on other OSes. 346 | - Added `response` event that allows reading pure updates (i.e. "events" or "responses") that come from tdlib `client_receive()`. 347 | - Removed `logFilePath` option. `client.setLogFilePath()` method can be used instead. 348 | - `tdlib.send` now returns `undefined` instead of `null`. 349 | - Added `databaseEncryptionKey` option. 350 | - Added `useDefaultVerbosityLevel` option for advanced users. When it's set to true, `tdl` doesn't invoke `setLogVerbosityLevel` during `connect()`. 351 | - TDLib typings updated to TDLib v1.3.0. Documentation comments added to TDLib typings. 352 | - Many documentation improvements. 353 | - `tdlib.setLogFatalErrorCallback` now accepts `null`. 354 | - `client.setLogFatalErrorCallback` now accepts `null`. 355 | - Internal: 356 | - - Added internal package `tdl-shared` with TS / Flow types. 357 | - - Added [`debug`](https://github.com/visionmedia/debug#readme) namespaces: `tdl:client:emitter`, `tdl:client:response`, `tdl:client:request`. 358 | - - Many improvements of tests for TS / Flow typings. 359 | 360 | ## v4.1.0 (2018-07-28) 361 | 362 | - Added `client.removeListener` function. 363 | - Internal: 364 | - - Flow updated to v0.76.0. 365 | 366 | ## v4.0.0 (2018-07-19) 367 | 368 | - Added `beforeAuth` argument in `client.connect` function. 369 | - `dev` option renamed to `useTestDc`. 370 | - Added `Client.create` and `Client.fromTDLib` static methods. 371 | - Added `auth-not-needed` event. 372 | - Changed `client.on('error')` signature: `(event: 'error', listener: (err: TDError) => void) => Client` to `(event: 'error', listener: (err: TDError | Error) => void) => Client`. 373 | - Internal: 374 | - - Now names of private fields start with `_`. 375 | - - Added tests for Flow typings. 376 | 377 | ## v3.8.2 (2018-07-19) 378 | 379 | - Now errors in `_loop` are correctly handled. See [#16][]. 380 | 381 | [#16]: https://github.com/eilvelia/tdl/pull/16 382 | 383 | ## v3.8.1 (2018-06-27) 384 | 385 | - Some fixes in TDlib TS typings. 386 | 387 | ## v3.8.0 (2018-06-19) 388 | 389 | - Added TypeScript typings. 390 | 391 | ## v3.7.0 (2018-06-18) 392 | 393 | - Minimum Node.js version is `8.6.0`. 394 | - Added sign up support. Added `loginDetails.getName` function. 395 | - Added support of previously unsupported tdjson functions: 396 | - - Added `client.setLogMaxFileSize`. 397 | - - Added `client.setLogFatalErrorCallback`. 398 | - - Added `tdlib.setLogMaxFileSize`. 399 | 400 | ## v3.6.3 (2018-06-11) 401 | 402 | - Bugfixes. See [#6][]. 403 | - Internal: 404 | - - Added `.gitattributes`. 405 | 406 | [#6]: https://github.com/eilvelia/tdl/pull/6 407 | 408 | ## v3.6.2 (2018-06-04) 409 | 410 | - Fixed value of `main` field in `package.json`. 411 | 412 | ## v3.6.1 (2018-06-04) 413 | 414 | - Some improvements of Flow typings. 415 | 416 | ## v3.6.0 (2018-06-04) 417 | 418 | - `src/tdlib-types.js` renamed to `types/tdlib.js`. 419 | - Added `tdlib.setLogFatalErrorCallback` method. 420 | - Documentation: Added 'typings' and 'examples' section in README. 421 | 422 | ## v3.5.2 (2018-05-24) 423 | 424 | - Bugfixes. 425 | 426 | ## v3.5.1 (2018-05-24) 427 | 428 | - `prepublish` script in package.json replaced with `prepare` and `prepack`. 429 | - Internal: 430 | - - Added eslint. 431 | - - Added `.editorconfig`. 432 | 433 | ## v3.5.0 (2018-05-23) 434 | 435 | - Added `client.invokeFuture` function. 436 | - Added `multiple-accounts.js` and `using-fluture.js` examples. 437 | - Added `tdlibIntance` option. 438 | - Some documentation improvements. 439 | - Some bugfixes. 440 | 441 | ## v3.4.0 (2018-05-20) 442 | 443 | - Added event `auth-needed`. 444 | - Fixed arrays in TDLib flow typings. 445 | 446 | ## v3.3.0 (2018-05-19) 447 | 448 | - Added `TDLib_API.md` file. 449 | - `inquirer` replaced with `promptly`. 450 | - Added `useMutableRename` option. 451 | - Travis activated. `build` badge added to README. 452 | - Added `skipOldUpdates` option. 453 | - Node.js `events` replaced with `eventemitter3`. 454 | 455 | ## v3.2.2 (2018-05-16) 456 | 457 | - Fixes in TDLib flow typings. 458 | - Internal: 459 | - - Flow updated to v0.69.0. 460 | 461 | … 462 | 463 | ## v2.0.0 (2018-05-02) 464 | 465 | First release of tdl as a fork of [tglib](https://github.com/nodegin/tglib). 466 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2025 eilvelia 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 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js' 2 | import tseslint from 'typescript-eslint' 3 | 4 | export default [ 5 | // Config for JS files 6 | { 7 | ...eslint.configs.recommended, 8 | files: ['packages/**/*.js'] 9 | }, 10 | // Config for TS files 11 | ...[ 12 | eslint.configs.recommended, 13 | ...tseslint.configs.recommended 14 | ].map(conf => ({ 15 | ...conf, 16 | files: ['packages/**/*.ts'] 17 | })), 18 | // Rule overrides 19 | { 20 | rules: { 21 | 'semi': ['warn', 'never'], 22 | 'no-redeclare': 0, 23 | 'valid-jsdoc': 0, 24 | 'no-undef': 0, 25 | 'no-constant-condition': 0, 26 | 'no-case-declarations': 0, 27 | '@typescript-eslint/no-explicit-any': 0, 28 | '@typescript-eslint/no-var-requires': 0, 29 | '@typescript-eslint/no-unused-vars': 0, 30 | '@typescript-eslint/ban-ts-comment': 0 31 | } 32 | }, 33 | // Global ignores 34 | { 35 | ignores: [ 36 | '**/*.d.ts', 37 | '**/*.js.flow', 38 | '**/node_modules/', 39 | '**/dist/', 40 | '**/coverage/', 41 | 'flow-typed/', 42 | 'tests/', 43 | 'packages/prebuilt-tdlib/index.js', 44 | ] 45 | }, 46 | ] 47 | -------------------------------------------------------------------------------- /examples/async-iterators.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | 3 | const client = tdl.createClient({ 4 | apiId: 2222, // Your api_id 5 | apiHash: 'YOUR_API_HASH' 6 | }) 7 | 8 | client.on('error', console.error) 9 | 10 | client.loginAsBot('YOUR_BOT_TOKEN') 11 | 12 | async function main() { 13 | for await (const update of client.iterUpdates()) { 14 | if (update._ === 'updateOption' && update.name === 'my_id') { 15 | console.log(`My ID is ${update.value.value}!`) 16 | break 17 | } 18 | } 19 | await client.close() 20 | } 21 | 22 | main() 23 | -------------------------------------------------------------------------------- /examples/custom-login.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | const readline = require('node:readline/promises') 3 | 4 | tdl.configure({ verbosityLevel: 1 }) 5 | 6 | const client = tdl.createClient({ 7 | apiId: 2222, // Your api_id 8 | apiHash: 'YOUR_API_HASH' 9 | }) 10 | 11 | // One example 12 | 13 | async function prompt (query) { 14 | const rl = readline.createInterface({ 15 | input: process.stdin, 16 | output: process.stdout 17 | }) 18 | try { 19 | const answer = await rl.question(query) 20 | return answer 21 | } finally { 22 | rl.close() 23 | } 24 | } 25 | 26 | async function login () { 27 | for await (const update of client.iterUpdates()) { 28 | if (update._ !== 'updateAuthorizationState') continue 29 | const authorizationState = update.authorization_state 30 | switch (authorizationState._) { 31 | case 'authorizationStateWaitPhoneNumber': 32 | await client.invoke({ 33 | _: 'setAuthenticationPhoneNumber', 34 | phone_number: await prompt('phone number: ') 35 | }) 36 | break 37 | case 'authorizationStateWaitCode': 38 | await client.invoke({ 39 | _: 'checkAuthenticationCode', 40 | code: await prompt('code: ') 41 | }) 42 | break 43 | case 'authorizationStateWaitPassword': 44 | await client.invoke({ 45 | _: 'checkAuthenticationPassword', 46 | password: await prompt('password: ') 47 | }) 48 | break 49 | case 'authorizationStateReady': 50 | return // Success 51 | } 52 | } 53 | throw new Error('Failed to log in') 54 | } 55 | 56 | (async function () { 57 | await login() 58 | console.log('Logged in') 59 | await client.close() 60 | })() 61 | -------------------------------------------------------------------------------- /examples/deno-example.ts: -------------------------------------------------------------------------------- 1 | // Simple deno example. 2 | // $ deno run --allow-read --allow-env --allow-ffi deno-example.ts 3 | 4 | import * as tdl from 'npm:tdl@8' 5 | // Also use prebuilt TDLib: 6 | import { getTdjson } from 'npm:prebuilt-tdlib@td-1.8.46' 7 | 8 | tdl.configure({ tdjson: getTdjson() }) 9 | 10 | const client = tdl.createClient({ 11 | apiId: 2222, // Your api_id 12 | apiHash: 'YOUR_API_HASH' 13 | }) 14 | 15 | client.on('error', console.error) 16 | client.on('update', update => { 17 | console.log('Got update:', update) 18 | }) 19 | 20 | await client.loginAsBot('') 21 | 22 | const me = await client.invoke({ _: 'getMe' }) 23 | console.log('I am', me) 24 | 25 | await client.close() 26 | -------------------------------------------------------------------------------- /examples/get-chats.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | 3 | const client = tdl.createClient({ 4 | apiId: 2222, // Your api_id 5 | apiHash: 'YOUR_API_HASH' 6 | }) 7 | 8 | client.on('error', console.error) 9 | 10 | async function main() { 11 | await client.login() 12 | 13 | const result = await client.invoke({ 14 | _: 'getChats', 15 | chat_list: { _: 'chatListMain' }, 16 | limit: 9000 17 | }) 18 | 19 | console.log(result) 20 | 21 | await client.close() 22 | } 23 | 24 | main().catch(console.error) 25 | -------------------------------------------------------------------------------- /examples/multiple-accounts.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | 3 | const API_ID = 2222 // Your api_id 4 | const API_HASH = '0123456789abcdef0123456789abcdef' // Your api_hash 5 | 6 | const client1 = tdl.createClient({ 7 | apiId: API_ID, 8 | apiHash: API_HASH, 9 | databaseDirectory: '_td_database1', 10 | filesDirectory: '_td_files1' 11 | }) 12 | 13 | const client2 = tdl.createClient({ 14 | apiId: API_ID, 15 | apiHash: API_HASH, 16 | databaseDirectory: '_td_database2', 17 | filesDirectory: '_td_files2' 18 | }) 19 | 20 | client1 21 | .on('error', e => console.error('Client1 error:', e)) 22 | .on('update', u => console.log('Client1 update:', u)) 23 | 24 | client2 25 | .on('error', e => console.error('Client2 error:', e)) 26 | .on('update', u => console.log('Client2 update:', u)) 27 | 28 | async function main() { 29 | await client1.login() 30 | await client2.login() 31 | 32 | // ... 33 | } 34 | 35 | main().catch(console.error) 36 | -------------------------------------------------------------------------------- /examples/send-message.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | 3 | const client = tdl.createClient({ 4 | apiId: 2222, // Your api_id 5 | apiHash: 'YOUR_API_HASH' 6 | }) 7 | 8 | client.on('error', console.error) 9 | 10 | async function main() { 11 | await client.login() 12 | 13 | await client.invoke({ 14 | _: 'sendMessage', 15 | chat_id: 123456789, 16 | input_message_content: { 17 | _: 'inputMessageText', 18 | text: { 19 | _: 'formattedText', 20 | text: 'Hi' 21 | } 22 | } 23 | }) 24 | 25 | await client.close() 26 | } 27 | 28 | main().catch(console.error) 29 | -------------------------------------------------------------------------------- /examples/using-prebuilt-tdlib.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | const { getTdjson } = require('prebuilt-tdlib') 3 | 4 | tdl.configure({ tdjson: getTdjson() }) 5 | 6 | const client = tdl.createClient({ 7 | apiId: 2222, // Your api_id 8 | apiHash: 'YOUR_API_HASH' 9 | }) 10 | 11 | client.on('error', console.error) 12 | 13 | async function main() { 14 | await client.login() 15 | 16 | console.log(await client.invoke({ _: 'getMe' })) 17 | 18 | await client.close() 19 | } 20 | 21 | main().catch(console.error) 22 | -------------------------------------------------------------------------------- /examples/with-proxy.js: -------------------------------------------------------------------------------- 1 | const tdl = require('tdl') 2 | 3 | const client = tdl.createClient({ 4 | apiId: 2222, // Your api_id 5 | apiHash: 'YOUR_API_HASH' 6 | }) 7 | 8 | client.on('error', console.error) 9 | 10 | // Works at least in TDLib v1.3.0 11 | 12 | async function main() { 13 | const proxy = await client.invoke({ 14 | _: 'addProxy', 15 | server: '127.0.0.1', 16 | port: 443, 17 | enable: true, 18 | type: { _: 'proxyTypeMtproto', secret: '15abcdef1234567890deadbeef123456' } 19 | }) 20 | 21 | console.log('Proxy:', proxy) 22 | 23 | await client.login() 24 | 25 | // ... 26 | } 27 | 28 | main() 29 | -------------------------------------------------------------------------------- /examples/with-typescript.ts: -------------------------------------------------------------------------------- 1 | import * as tdl from 'tdl' 2 | // import TDLib types: 3 | // import type * as Td from 'tdlib-types' 4 | 5 | const client = tdl.createClient({ 6 | apiId: 2222, // Your api_id 7 | apiHash: 'YOUR_API_HASH' 8 | }) 9 | 10 | client.on('error', console.error) 11 | client.on('update', console.log) 12 | 13 | async function main() { 14 | await client.login() 15 | 16 | console.log(await client.invoke({ _: 'getMe' })) 17 | 18 | await client.close() 19 | } 20 | 21 | main().catch(console.error) 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdl-dev", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "A JavaScript wrapper for TDLib", 6 | "scripts": { 7 | "clean": "rimraf --glob packages/*/dist/ && npm run clean -w tdl", 8 | "clean-modules": "rm -rf node_modules/ && rm -rf packages/node_modules/", 9 | "clean-all": "npm run clean && npm run clean-modules", 10 | "build": "npm run build -ws", 11 | "ts:check": "tsc --noEmit", 12 | "flow:check": "flow check --max-warnings 0", 13 | "lint": "eslint . --max-warnings 0", 14 | "test:unit": "vitest run --exclude \"tests/integration|tests/auth-only\"", 15 | "test": "tsc --noEmit && npm run flow:check && npm run lint && npm run test:unit", 16 | "test:integration": "vitest run tests/integration", 17 | "test:all": "npm run test && npm run test:integration", 18 | "test:auth-only": "vitest run tests/auth-only", 19 | "prepare": "npm run build", 20 | "prepack": "npm test" 21 | }, 22 | "workspaces": [ 23 | "packages/tdl", 24 | "packages/prebuilt-tdlib", 25 | "packages/tdl-install-types" 26 | ], 27 | "devDependencies": { 28 | "@eslint/js": "^9.23.0", 29 | "@types/debug": "^4.1.12", 30 | "@types/node": "^22.13.16", 31 | "@vitest/coverage-v8": "^3.1.1", 32 | "eslint": "^9.23.0", 33 | "flow-bin": "^0.238.3", 34 | "node-gyp": "^11.2.0", 35 | "prebuildify": "^6.0.1", 36 | "prebuilt-tdlib": "^0.1008046.0", 37 | "rimraf": "^6.0.1", 38 | "typescript": "^5.8.2", 39 | "typescript-eslint": "^8.29.0", 40 | "vite": "^6.2.4", 41 | "vitest": "^3.1.1" 42 | }, 43 | "author": "eilvelia ", 44 | "license": "MIT", 45 | "directories": { 46 | "example": "examples" 47 | }, 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/eilvelia/tdl.git" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/eilvelia/tdl/issues" 54 | }, 55 | "homepage": "https://github.com/eilvelia/tdl#readme" 56 | } 57 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/.gitignore: -------------------------------------------------------------------------------- 1 | prebuilds/tdlib-* 2 | to-upload/ 3 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 eilvelia 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 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/README.md: -------------------------------------------------------------------------------- 1 | # Pre-built TDLib   [![npm](https://img.shields.io/npm/v/prebuilt-tdlib/latest.svg)](https://www.npmjs.com/package/prebuilt-tdlib) [![npm](https://img.shields.io/npm/v/prebuilt-tdlib/tagged.svg)](https://www.npmjs.com/package/prebuilt-tdlib) 2 | 3 | This package distributes pre-built [TDLib][] shared libraries through npm. 4 | The libraries are built on GitHub Actions ([prebuilt-tdlib.yml][]) and published 5 | using [npm publish --provenance][npm-provenance]. 6 | 7 | [TDLib]: https://github.com/tdlib/td 8 | [prebuilt-tdlib.yml]: ../../.github/workflows/prebuilt-tdlib.yml 9 | [npm-provenance]: https://docs.npmjs.com/generating-provenance-statements 10 | 11 | Supported systems: 12 | - Linux x86_64, arm64 (requires glibc >= 2.22) 13 | - macOS x86_64, arm64 (requires macOS >= 11.0) 14 | - Windows x86_64 15 | 16 | To install `prebuilt-tdlib` for the latest TDLib version that `prebuilt-tdlib` 17 | supports, run: 18 | 19 | ```console 20 | $ npm install prebuilt-tdlib 21 | ``` 22 | 23 | To install `prebuilt-tdlib` for a specific TDLib version, e.g. TDLib v1.8.33, 24 | run: 25 | 26 | ```console 27 | $ npm install prebuilt-tdlib@td-1.8.33 28 | ``` 29 | 30 | `prebuilt-tdlib` can be installed for other TDLib versions, execute 31 | `$ npm info prebuilt-tdlib dist-tags`[^1] to get the list of available versions. 32 | 33 | [^1]: Or, with more convenient output, using jq: 34 | ```console 35 | $ npm info prebuilt-tdlib dist-tags --json | jq 'to_entries | sort_by(.value) | .[].key | select(startswith("td-"))' 36 | ``` 37 | 38 | The TDLib version is important: there is no backward compatibility and the 39 | interface you use may significantly change after an update. It is, though, 40 | recommended to use the latest TDLib version. 41 | 42 | The shared libraries are statically linked against OpenSSL and zlib (for one, to 43 | prevent compatibility issues in Node.js). libstdc++ is also linked statically 44 | on Linux. 45 | 46 | For the dependencies of TDLib (zlib and openssl), except on Windows, the 47 | `nixpkgs-unstable` package repository is used, and the versions of those 48 | dependencies reflect the state of `nixpkgs-unstable` at the time of building of 49 | `prebuilt-tdlib`. 50 | 51 | ## Usage 52 | 53 | The `prebuilt-tdlib` package exports a single function `getTdjson`, which 54 | returns the path to the `tdjson` shared library. 55 | 56 | ```javascript 57 | const { getTdjson } = require('prebuilt-tdlib') 58 | console.log(getTdjson()) 59 | // Prints a path like: 60 | // '/home/user/proj/node_modules/@prebuilt-tdlib/linux-x64-glibc/libtdjson.so' 61 | ``` 62 | 63 | This package can be used with, for example, [tdl][]. You can pass the 64 | path to `tdl.configure` (since tdl v7.3.0): 65 | 66 | [tdl]: https://github.com/eilvelia/tdl 67 | 68 | ```javascript 69 | const tdl = require('tdl') 70 | const { getTdjson } = require('prebuilt-tdlib') 71 | tdl.configure({ tdjson: getTdjson() }) 72 | // ... 73 | ``` 74 | 75 | The pre-built libraries can also be extracted and used with any other library 76 | or programming language. 77 | 78 | ## Versioning conventions 79 | 80 | > This information is present mostly for maintaining `prebuilt-tdlib`, 81 | > it is not strictly necessary for using this package. 82 | 83 | Because TDLib does not follow SemVer, not to require the users to manually 84 | specify the exact version of `prebuilt-tdlib` in their `package.json`, the TDLib 85 | version is packed into a single minor version. 86 | 87 | `prebuilt-tdlib` is published to npm under versions `0.xyyyzzz.v`, where 88 | 89 | - `x`, `y`, `z` correspond to the `x.y.z` TDLib version (e.g., 1.8.0). The 90 | leading zeros are appended to `y` and `z` (y=`8` becomes y=`008`). 91 | - `v` corresponds to the version of `prebuilt-tdlib` itself, these updates can 92 | contain fixes in case some of the builds were broken or include new pre-built 93 | libraries for other platforms. 94 | - The major version is always `0`. 95 | 96 | Example: the npm release for TDLib `v1.8.5` is `0.1008005.0`. 97 | 98 | For convenience, `td-X` dist-tags are available. To install `prebuilt-tdlib` for 99 | TDLib v1.8.5, just run `npm install prebuilt-tdlib@td-1.8.5`, or 100 | `npm install prebuilt-tdlib@td-1.8.0` for TDLib v1.8.0. This will automatically 101 | install the needed version of `prebuilt-tdlib`. 102 | 103 | The releases of the `prebuilt-tdlib` npm package are not git-tagged. 104 | 105 | Additionally, TDLib's releasing process is [unusual][], and most 106 | `prebuilt-tdlib` releases are not connected to any tag release in the TDLib 107 | repository. Usually, the prebuilt packages are generated based on the "Update 108 | version to x.y.z." TDLib commit ([example][commit-example]) or, if subsequent 109 | commits have been made before such a commit is pushed to `tdlib/td`, on the last 110 | pushed commit. The commit hash is indicated in the prebuilt-tdlib's 111 | package.json, see e.g. `npm info prebuilt-tdlib tdlib` (or `tdlib.commit`). 112 | 113 | [unusual]: https://github.com/tdlib/td/issues/2215 114 | [commit-example]: https://github.com/tdlib/td/commit/b3b63bbdc14dc377d2de6b78e5844fec1564f95d 115 | 116 | ## Changes 117 | 118 | Changes to the building process of `prebuilt-tdlib` are noted below. 119 | 120 | ### 2024-11-17 121 | 122 | First published as `prebuilt-tdlib@td-1.8.40`. 123 | 124 | - Minimum macOS version is now 11.0 instead of 10.12. 125 | - The macOS package is split into `darwin-x64` and `darwin-arm64` instead of 126 | using a universal binary. 127 | - The Windows binary is built on the `windows-2022` GitHub Actions runner 128 | instead of `windows-2019`. 129 | 130 | ### 2024-07-19 131 | 132 | First published as `prebuilt-tdlib@td-1.8.33`. 133 | 134 | The building process is significantly changed in this update. 135 | 136 | - Changed the structure of the package: instead of packing all binaries into the 137 | prebuilt-tdlib package, every binary is split into a separate package, and all 138 | the packages are specified in `optionalDependencies` of `prebuilt-tdlib`. The 139 | same approach is used by, e.g., esbuild and swc. This installs a binary for 140 | the user's system only, allowing `prebuilt-tdlib` to potentially scale for 141 | more architectures and libc variants. One downside is that `node_modules` 142 | can't simply be copied to a different platform anymore. The `prebuilds` 143 | directory in the `prebuilt-tdlib` package is removed. 144 | - On macOS, TDLib is built using macOS SDK from nixpkgs, and the minimal 145 | supported macOS version is now 10.12 instead of 10.14. The arm64 macOS 146 | library is now tested in the CI using the macos-14 GitHub runner (and not 147 | crosscompiled anymore). 148 | - On Linux, TDLib is now built using zig. The minimal glibc version is 2.22 149 | instead of 2.17. 150 | - Added a crosscompiled prebuild for Linux arm64. 151 | 152 | Fix (2024-07-21): Fixed codesigning on macOS arm64. 153 | 154 | ### 2024-05-08 155 | 156 | First published as `prebuilt-tdlib@td-1.8.29`. 157 | 158 | - Added a `tdlib: { commit: ..., version: ... }` field to `package.json`. This 159 | allows to query information using `npm info prebuilt-tdlib tdlib.commit`, for 160 | example. 161 | - Added `commit` as an alias for `ref` to `tdlib.json`. 162 | - The packages are now published with `--provenance`. 163 | 164 | ### 2023-09-26 165 | 166 | First published as `prebuilt-tdlib@td-1.8.19`. 167 | 168 | - The packages now include a `prebuilds/tdlib.json` file specifying the TDLib 169 | commit hash and version. 170 | 171 | ### 2023-06-26 172 | 173 | First published as `prebuilt-tdlib@td-1.8.14`. 174 | 175 | - Added support for macOS arm64 (M1 / Apple silicon); a universal binary is 176 | shipped. However, the arm64 binary is not tested in the CI. 177 | - The Linux binaries are now built on environment with glibc 2.17 instead of 178 | 2.31 and work on older Linux distributions. Some cloud environments such as 179 | Amazon Linux 2 or Google Cloud Functions (nodejs <= 16) use older glibc, 180 | `prebuilt-tdlib` should run out of the box on these systems now. 181 | - Restored support for older versions of macOS, >= 10.14 is now supported. 182 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/ci/build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | tdlib=$1 6 | arch=$2 7 | abi="${3:-gnu}" 8 | 9 | if [ -z "$tdlib" ]; then 10 | echo "Not enough arguments: expected TDLib rev" 11 | exit 1 12 | fi 13 | 14 | if [ -z "$arch" ]; then 15 | echo "Not enough arguments: expected arch" 16 | exit 1 17 | fi 18 | 19 | if [ "$abi" = "gnu" ]; then 20 | libc_version="2.22" 21 | else 22 | libc_version="" 23 | fi 24 | 25 | set -x 26 | 27 | nix-build tdlib-zig-wrapper.nix -v \ 28 | --argstr rev "$tdlib" \ 29 | --argstr arch "$arch" \ 30 | --argstr abi "$abi" \ 31 | --argstr libcVersion "$libc_version" 32 | 33 | mkdir to-upload 34 | cp -L ./result/lib/libtdjson.so to-upload/libtdjson.so 35 | cd to-upload 36 | 37 | # Info 38 | ldd libtdjson.so || true 39 | 40 | if [ -z "$GITHUB_ENV" ]; then 41 | echo "Note: GITHUB_ENV not found" 42 | exit 43 | fi 44 | 45 | # Resulting directory 46 | echo "TO_UPLOAD=$(pwd)" >> $GITHUB_ENV 47 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/ci/build-macos.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | tdlib=$1 6 | 7 | if [ -z "$tdlib" ]; then 8 | echo "Not enough arguments: expected TDLib rev" 9 | exit 1 10 | fi 11 | 12 | set -x 13 | 14 | nix-build -v -E "(import { }).callPackage ./tdlib.nix { rev = \"$tdlib\"; }" 15 | 16 | mkdir to-upload 17 | cp -L ./result/lib/libtdjson.dylib to-upload/libtdjson.dylib 18 | cd to-upload 19 | 20 | install_name_tool -id @rpath/libtdjson.dylib libtdjson.dylib 21 | install_name_tool -change \ 22 | "$(otool -L libtdjson.dylib | grep -o '/nix/store.*libc++.*.dylib')" \ 23 | '/usr/lib/libc++.1.dylib' \ 24 | libtdjson.dylib 25 | 26 | codesign -s - --force libtdjson.dylib 27 | 28 | # Info 29 | otool -L libtdjson.dylib 30 | otool -l libtdjson.dylib 31 | 32 | if [ -z "$GITHUB_ENV" ]; then 33 | echo "Note: GITHUB_ENV not found" 34 | exit 35 | fi 36 | 37 | # Resulting directory 38 | echo "TO_UPLOAD=$(pwd)" >> $GITHUB_ENV 39 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/ci/tdlib-zig-wrapper.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}, rev, arch, abi, libcVersion ? "" }: 2 | # Linux-only 3 | let 4 | target = "${arch}-linux-${abi}" + 5 | (if builtins.stringLength libcVersion > 0 then ".${libcVersion}" else ""); 6 | pkgs' = import pkgs.path { crossSystem.config = "${arch}-unknown-linux-${abi}"; }; 7 | zig-toolchain = import ./zig-toolchain.nix { 8 | inherit target; 9 | pkgs = pkgs'.buildPackages.buildPackages; 10 | }; 11 | openssl = pkgs'.openssl.overrideAttrs (final: prev: { 12 | preConfigure = (prev.preConfigure or "") + zig-toolchain.env; 13 | doCheck = false; 14 | }); 15 | zlib = pkgs'.zlib.overrideAttrs (final: prev: { 16 | preConfigure = (prev.preConfigure or "") + zig-toolchain.env; 17 | doCheck = false; 18 | }); 19 | in 20 | (pkgs'.callPackage ./tdlib.nix { inherit rev openssl zlib; }).overrideAttrs (final: prev: { 21 | preConfigure = (prev.preConfigure or "") + zig-toolchain.env; 22 | patches = [ ./tdlib-zig.patch ]; 23 | }) 24 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/ci/tdlib-zig.patch: -------------------------------------------------------------------------------- 1 | diff --git a/CMake/TdSetUpCompiler.cmake b/CMake/TdSetUpCompiler.cmake 2 | index ff5991cfb..e1f7ea28b 100644 3 | --- a/CMake/TdSetUpCompiler.cmake 4 | +++ b/CMake/TdSetUpCompiler.cmake 5 | @@ -63,9 +63,9 @@ function(td_set_up_compiler) 6 | elseif (EMSCRIPTEN) 7 | set(TD_LINKER_FLAGS "-Wl,--gc-sections") 8 | elseif (ANDROID) 9 | - set(TD_LINKER_FLAGS "-Wl,--gc-sections -Wl,--exclude-libs,ALL -Wl,--icf=safe") 10 | + set(TD_LINKER_FLAGS "-Wl,--gc-sections -Wl,--icf=safe") 11 | else() 12 | - set(TD_LINKER_FLAGS "-Wl,--gc-sections -Wl,--exclude-libs,ALL") 13 | + set(TD_LINKER_FLAGS "-Wl,--gc-sections") 14 | endif() 15 | endif() 16 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${TD_LINKER_FLAGS}") 17 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/ci/tdlib.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenv, 3 | lib, 4 | openssl, 5 | zlib, 6 | cmake, 7 | gperf, 8 | apple-sdk_11, 9 | buildPackages, 10 | 11 | rev, 12 | }: 13 | # Based on https://github.com/NixOS/nixpkgs/blob/af51e23ce535b1bfa8484021ff3913d876e09082/pkgs/development/libraries/tdlib/default.nix 14 | stdenv.mkDerivation { 15 | pname = "tdlib"; 16 | version = "0.0"; 17 | 18 | src = builtins.fetchTarball "https://github.com/tdlib/td/archive/${rev}.tar.gz"; 19 | 20 | buildInputs = [ 21 | (openssl.override { static = true; }).dev 22 | (zlib.override { static = true; shared = false; }) 23 | ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ apple-sdk_11 ]; 24 | 25 | nativeBuildInputs = [ 26 | cmake 27 | gperf 28 | ]; 29 | 30 | depsBuildBuild = [ buildPackages.stdenv.cc ]; 31 | 32 | # Note: Even if we compile for the same architecture, the hostPlatform 33 | # compiler can be zig cc targeting a different glibc version that doesn't 34 | # produce runnable executables 35 | preConfigure = '' 36 | cmake -B native-build \ 37 | -DCMAKE_C_COMPILER=$CC_FOR_BUILD \ 38 | -DCMAKE_CXX_COMPILER=$CXX_FOR_BUILD \ 39 | -DCMAKE_AR=$(command -v $AR_FOR_BUILD) \ 40 | -DCMAKE_RANLIB=$(command -v $RANLIB_FOR_BUILD) \ 41 | -DCMAKE_STRIP=$(command -v $STRIP_FOR_BUILD) \ 42 | -DTD_GENERATE_SOURCE_FILES=ON . 43 | cmake --build native-build -j $NIX_BUILD_CORES 44 | ''; 45 | 46 | cmakeFlags = [ 47 | "-DCMAKE_BUILD_TYPE=Release" 48 | "-DOPENSSL_USE_STATIC_LIBS=TRUE" 49 | "-DZLIB_USE_STATIC_LIBS=TRUE" 50 | "-DCMAKE_CROSSCOMPILING=TRUE" 51 | ] ++ lib.optionals stdenv.hostPlatform.isLinux [ 52 | "-DCMAKE_SYSTEM_NAME=Linux" 53 | ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ 54 | "-DCMAKE_SYSTEM_NAME=Darwin" 55 | ]; 56 | 57 | buildPhase = '' 58 | runHook preBuild 59 | cmake --build . --target tdjson -j $NIX_BUILD_CORES 60 | runHook postBuild 61 | ''; 62 | 63 | installPhase = '' 64 | runHook preInstall 65 | mkdir -p $out/lib 66 | '' + (if stdenv.hostPlatform.isDarwin then '' 67 | cp -L libtdjson.dylib $out/lib 68 | '' else '' 69 | cp -L libtdjson.so $out/lib 70 | '') + '' 71 | runHook postInstall 72 | ''; 73 | 74 | # https://github.com/tdlib/td/issues/1974 75 | postPatch = '' 76 | substituteInPlace CMake/GeneratePkgConfig.cmake \ 77 | --replace 'function(generate_pkgconfig' \ 78 | 'include(GNUInstallDirs) 79 | function(generate_pkgconfig' \ 80 | --replace '\$'{prefix}/'$'{CMAKE_INSTALL_LIBDIR} '$'{CMAKE_INSTALL_FULL_LIBDIR} \ 81 | --replace '\$'{prefix}/'$'{CMAKE_INSTALL_INCLUDEDIR} '$'{CMAKE_INSTALL_FULL_INCLUDEDIR} 82 | '' + lib.optionalString (stdenv.hostPlatform.isDarwin && stdenv.hostPlatform.isAarch64) '' 83 | sed -i "/vptr/d" test/CMakeLists.txt 84 | ''; 85 | } 86 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/ci/zig-toolchain.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {}, target }: 2 | # See also https://gist.github.com/Cloudef/acb74ff9e36ab41709479240596ab501 3 | let 4 | write-zig-compiler-wrapper = cmd: 5 | pkgs.writeShellScript "zig-${cmd}" '' 6 | ZIG_LOCAL_CACHE_DIR="$TMPDIR/zig-cache-${target}" \ 7 | ZIG_GLOBAL_CACHE_DIR="$ZIG_LOCAL_CACHE_DIR" \ 8 | ${pkgs.zig}/bin/zig ${cmd} -target ${target} -s "$@" 9 | ''; 10 | write-zig-wrapper = cmd: 11 | pkgs.writeShellScript "zig-${cmd}" '' 12 | ZIG_LOCAL_CACHE_DIR="$TMPDIR/zig-cache-${target}" \ 13 | ZIG_GLOBAL_CACHE_DIR="$ZIG_LOCAL_CACHE_DIR" \ 14 | ${pkgs.zig}/bin/zig ${cmd} "$@" 15 | ''; 16 | in 17 | rec { 18 | zig-cc = write-zig-compiler-wrapper "cc"; 19 | zig-cxx = write-zig-compiler-wrapper "c++"; 20 | zig-ar = write-zig-wrapper "ar"; 21 | zig-ranlib = write-zig-wrapper "ranlib"; 22 | env = '' 23 | export CC="${zig-cc}" 24 | export CXX="${zig-cxx}" 25 | export AR="${zig-ar}" 26 | export RANLIB="${zig-ranlib}" 27 | ''; 28 | } 29 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/index.d.ts: -------------------------------------------------------------------------------- 1 | /** Currently present for forward compatibility only. */ 2 | export type Options = { 3 | readonly forceLibc?: 'glibc' | 'musl' 4 | } 5 | 6 | export declare function getTdjson(options?: Options): string 7 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | const { prebuilds } = require('./prebuild-list') 6 | 7 | const SCOPE = '@prebuilt-tdlib' 8 | 9 | /*:: 10 | export type Options = { 11 | +forceLibc?: 'glibc' | 'musl' 12 | } 13 | */ 14 | 15 | let libcNameCache = null 16 | 17 | function detectLibc ()/*: 'glibc' | 'musl' */ { 18 | // This function can return false results: it currently only checks for 19 | // Alpine Linux as a heuristic (the same approach is used by node-gyp-build). 20 | if (libcNameCache != null) return libcNameCache 21 | let result = 'glibc' 22 | try { 23 | if (fs.existsSync('/etc/alpine-release')) 24 | result = 'musl' 25 | } catch (e) { 26 | // Intentionally empty (defaults to 'glibc') 27 | } 28 | libcNameCache = result 29 | return result 30 | } 31 | 32 | exports.getTdjson = function getTdjson (options/*:: ?: Options */)/*: string */ { 33 | let { platform, arch } = process 34 | if (platform === 'android') platform = 'linux' 35 | const libc = options?.forceLibc || (platform === 'linux' ? detectLibc() : null) 36 | for (const prebuild of prebuilds) { 37 | if (prebuild.requirements.os != null) 38 | if (!prebuild.requirements.os.includes(platform)) continue 39 | if (prebuild.requirements.cpu != null) 40 | if (!prebuild.requirements.cpu.includes(arch)) continue 41 | if (prebuild.libc != null && prebuild.libc !== libc) continue 42 | // Found a prebuild for the current platform 43 | const pkg = `${SCOPE}/${prebuild.packageName}/${prebuild.libfile}` 44 | try { 45 | return require.resolve(pkg) 46 | } catch (e) { 47 | throw new Error(`Could not load ${pkg} (are optionalDependencies installed?): ${e?.message}`) 48 | } 49 | } 50 | let entirePlatform = `${platform}-${arch}` 51 | if (libc != null) entirePlatform += '-' + libc 52 | throw new Error(`The ${entirePlatform} platform is not supported`) 53 | } 54 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prebuilt-tdlib-dev", 3 | "version": "0.0.0", 4 | "description": "Pre-built TDLib libraries", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "build": "" 9 | }, 10 | "files": [ 11 | "index.js", 12 | "prebuild-list.js", 13 | "index.d.ts", 14 | "LICENSE.md" 15 | ], 16 | "optionalDependencies": {}, 17 | "author": "eilvelia ", 18 | "license": "MIT", 19 | "keywords": [ 20 | "telegram", 21 | "telegram-api", 22 | "tdlib", 23 | "prebuilt" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/eilvelia/tdl.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/eilvelia/tdl/issues" 31 | }, 32 | "homepage": "https://github.com/eilvelia/tdl/tree/main/packages/prebuilt-tdlib#readme", 33 | "tdlib": { 34 | "commit": "unknown", 35 | "version": "unknown" 36 | }, 37 | "private": true 38 | } 39 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/prebuild-list.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /*:: 4 | export type PrebuildInfo = { 5 | packageName: string, 6 | prebuildDir: string, 7 | libfile: string, 8 | descr: string, 9 | requirements: { os?: string[], cpu?: string[] }, 10 | libc?: 'glibc' | 'musl' 11 | } 12 | */ 13 | 14 | const SHARED_LINUX = 'libtdjson.so' 15 | const SHARED_MACOS = 'libtdjson.dylib' 16 | const SHARED_WINDOWS = 'tdjson.dll' 17 | 18 | const prebuilds/*: PrebuildInfo[] */ = [ 19 | { 20 | packageName: 'win32-x64', 21 | prebuildDir: 'tdlib-windows-x86_64', 22 | libfile: SHARED_WINDOWS, 23 | descr: 'Windows x86_64', 24 | requirements: { 25 | os: ['win32'], 26 | cpu: ['x64'] 27 | } 28 | }, 29 | { 30 | packageName: 'darwin-x64', 31 | prebuildDir: 'tdlib-macos-x86_64', 32 | libfile: SHARED_MACOS, 33 | descr: 'macOS x86_64', 34 | requirements: { 35 | os: ['darwin'], 36 | cpu: ['x64'] 37 | } 38 | }, 39 | { 40 | packageName: 'darwin-arm64', 41 | prebuildDir: 'tdlib-macos-arm64', 42 | libfile: SHARED_MACOS, 43 | descr: 'macOS arm64', 44 | requirements: { 45 | os: ['darwin'], 46 | cpu: ['arm64'] 47 | } 48 | }, 49 | { 50 | packageName: 'linux-x64-glibc', 51 | prebuildDir: 'tdlib-linux-x86_64-glibc', 52 | libfile: SHARED_LINUX, 53 | descr: 'Linux x86_64 (glibc)', 54 | requirements: { 55 | os: ['linux'], 56 | cpu: ['x64'] 57 | }, 58 | libc: 'glibc' 59 | }, 60 | { 61 | packageName: 'linux-arm64-glibc', 62 | prebuildDir: 'tdlib-linux-arm64-glibc', 63 | libfile: SHARED_LINUX, 64 | descr: 'Linux arm64 (glibc)', 65 | requirements: { 66 | os: ['linux'], 67 | cpu: ['arm64'] 68 | }, 69 | libc: 'glibc' 70 | } 71 | ] 72 | 73 | exports.prebuilds = prebuilds 74 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/prebuild-template/LICENSE: -------------------------------------------------------------------------------- 1 | Zero-Clause BSD 2 | ============= 3 | 4 | Permission to use, copy, modify, and/or distribute this software for 5 | any purpose with or without fee is hereby granted. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 8 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 9 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 10 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 11 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 12 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 13 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/prebuild-template/README.md: -------------------------------------------------------------------------------- 1 | This distributes a pre-built TDLib shared library that is part of the [prebuilt-tdlib][] package. 2 | 3 | [prebuilt-tdlib]: https://npmjs.com/package/prebuilt-tdlib 4 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/prebuild-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prebuilt-tdlib/dev", 3 | "version": "0.0.0", 4 | "description": "Pre-built TDLib shared library", 5 | "scripts": { 6 | "build": "" 7 | }, 8 | "files": [ 9 | "LICENSE.md", 10 | "README.md" 11 | ], 12 | "preferUnplugged": true, 13 | "license": "0BSD", 14 | "keywords": [ 15 | "tdlib", 16 | "prebuilt-tdlib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/eilvelia/tdl.git" 21 | }, 22 | "homepage": "https://github.com/eilvelia/tdl/tree/main/packages/prebuilt-tdlib#readme", 23 | "tdlib": { 24 | "commit": "unknown", 25 | "version": "unknown" 26 | }, 27 | "private": true 28 | } 29 | -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/prebuilds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eilvelia/tdl/610e6ee48465c350a7c13ce578bdc5f1f78fe978/packages/prebuilt-tdlib/prebuilds/.gitkeep -------------------------------------------------------------------------------- /packages/prebuilt-tdlib/publish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/node 2 | // @flow 3 | 4 | const path = require('path') 5 | const fs = require('fs') 6 | const { execSync } = require('child_process') 7 | const { prebuilds } = require('./prebuild-list') 8 | /*:: import type { PrebuildInfo } from './prebuild-list' */ 9 | 10 | const MAIN_PACKAGE_NAME = 'prebuilt-tdlib' 11 | const SCOPE = '@prebuilt-tdlib' 12 | 13 | const tdlibCommit = process.env.TDLIB_COMMIT_HASH 14 | let tdlibVersion = process.env.TDLIB_VERSION 15 | const npmTag = process.env.NPM_TAG 16 | const patchVersion = process.argv[2] 17 | 18 | if (!tdlibCommit) throw new Error('Expected TDLIB_COMMIT_HASH') 19 | if (!tdlibVersion) throw new Error('Expected TDLIB_VERSION') 20 | if (!npmTag) throw new Error('Expected NPM_TAG') 21 | if (!patchVersion) throw new Error('Expected the patch version argument') 22 | 23 | if (Number.isNaN(Number(patchVersion))) 24 | throw new Error(`Incorrect patch version: ${patchVersion}`) 25 | 26 | if (tdlibCommit.length < 40) throw new Error('Too short TDLib commit') 27 | 28 | tdlibVersion = tdlibVersion.replace(/^v/, '') 29 | 30 | let [tdlibMajor, tdlibMinor, tdlibPatch] = tdlibVersion.split('.') 31 | 32 | if (tdlibMajor == null || tdlibMinor == null || tdlibPatch == null) 33 | throw new Error(`Incorrect TDLib version: ${tdlibVersion}`) 34 | 35 | tdlibMinor = tdlibMinor.padStart(3, '0') 36 | tdlibPatch = tdlibPatch.padStart(3, '0') 37 | 38 | const npmVersion = `0.${tdlibMajor}${tdlibMinor}${tdlibPatch}.${patchVersion}` 39 | 40 | const prebuildPackageJson = require('./prebuild-template/package.json') 41 | 42 | delete prebuildPackageJson.private 43 | prebuildPackageJson.version = npmVersion 44 | prebuildPackageJson.tdlib = { 45 | commit: tdlibCommit, 46 | version: tdlibVersion 47 | } 48 | 49 | const prebuildTemplateDir = path.join(__dirname, 'prebuild-template') 50 | 51 | function publishPrebuild (info/*: PrebuildInfo */) { 52 | console.log(`Preparing to publish ${SCOPE}/${info.packageName}@${npmVersion}`) 53 | 54 | // Writing package.json 55 | const pkg = { ...prebuildPackageJson } 56 | pkg.name = SCOPE + '/' + info.packageName 57 | pkg.description += ' for ' + info.descr 58 | pkg.files = [info.libfile, 'LICENSE.md', 'README.md'] 59 | for (const [k, v] of Object.entries(info.requirements)) 60 | pkg[k] = v 61 | fs.writeFileSync( 62 | path.join(prebuildTemplateDir, 'package.json'), 63 | JSON.stringify(pkg, null, ' ') + '\n' 64 | ) 65 | 66 | const lib = path.join(__dirname, 'prebuilds', info.prebuildDir, info.libfile) 67 | 68 | if (!fs.existsSync(lib)) 69 | throw new Error(`'${lib}' does not exist`) 70 | 71 | fs.copyFileSync(lib, path.join('.', info.libfile)) 72 | 73 | const files = fs.readdirSync(prebuildTemplateDir) 74 | 75 | if (files.length < 4 || !files.includes(info.libfile)) 76 | throw new Error(`No shared library found (files: ${files.join()})`) 77 | 78 | execSync('npm publish --provenance --access public', { stdio: 'inherit' }) 79 | 80 | fs.rmSync(info.libfile) 81 | } 82 | 83 | const prebuildCount = fs.readdirSync(path.join(__dirname, 'prebuilds')) 84 | .filter(name => name.startsWith('tdlib-')) 85 | .length 86 | 87 | if (prebuildCount < prebuilds.length) 88 | throw new Error(`Expected ${prebuilds.length} prebuilds, found ${prebuildCount}`) 89 | 90 | // Publish 91 | 92 | const oldCwd = process.cwd() 93 | 94 | process.chdir(prebuildTemplateDir) 95 | 96 | for (const prebuild of prebuilds) 97 | publishPrebuild(prebuild) 98 | 99 | console.log(`Preparing to publish ${MAIN_PACKAGE_NAME}@${npmVersion}`) 100 | 101 | const mainPackageJson = require('./package.json') 102 | 103 | delete mainPackageJson.private 104 | mainPackageJson.name = MAIN_PACKAGE_NAME 105 | mainPackageJson.version = npmVersion 106 | mainPackageJson.tdlib = { 107 | commit: tdlibCommit, 108 | version: tdlibVersion 109 | } 110 | mainPackageJson.optionalDependencies = prebuilds.reduce((obj, info) => { 111 | return { ...obj, [SCOPE + '/' + info.packageName]: npmVersion } 112 | }, {}) 113 | 114 | fs.writeFileSync( 115 | path.join(__dirname, 'package.json'), 116 | JSON.stringify(mainPackageJson, null, ' ') + '\n' 117 | ) 118 | 119 | process.chdir(__dirname) 120 | 121 | const publishCommand = 122 | `npm publish --provenance --access public --tag ${npmTag}` 123 | 124 | const addTagCommand = 125 | `npm dist-tag add ${MAIN_PACKAGE_NAME}@${npmVersion} td-${tdlibVersion}` 126 | 127 | execSync(publishCommand, { stdio: 'inherit' }) 128 | execSync(addTagCommand, { stdio: 'inherit' }) 129 | 130 | try { 131 | process.chdir(oldCwd) 132 | } catch (e) { 133 | console.error(`Note: failed to chdir to ${oldCwd}: ${e?.message}`) 134 | } 135 | -------------------------------------------------------------------------------- /packages/tdl-install-types/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 eilvelia 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 | -------------------------------------------------------------------------------- /packages/tdl-install-types/README.md: -------------------------------------------------------------------------------- 1 | # tdl-install-types   [![npm](https://img.shields.io/npm/v/tdl-install-types.svg)](https://www.npmjs.com/package/tdl-install-types) 2 | 3 | A cli utility that generates TypeScript (and optionally Flow) types for TDLib, 4 | potentially fetching the needed schema from the TDLib's GitHub repository. 5 | These types can be used with the tdl library. 6 | 7 | This utility can be launched via `npx` without manually installing it. 8 | 9 | By default, a `tdlib-types.d.ts` file is created that you can git-commit. 10 | 11 | ## Usage 12 | 13 | ```console 14 | $ npx tdl-install-types [] [] 15 | ``` 16 | 17 | Examples: 18 | 19 | ```console 20 | $ npx tdl-install-types # tries to use prebult-tdlib 21 | $ npx tdl-install-types prebuilt-tdlib # same as above 22 | $ npx tdl-install-types ./libtdjson.so # generate types for this shared library 23 | $ npx tdl-install-types 0ada45c3618108a62806ce7d9ab435a18dac1aab # commit hash 24 | $ npx tdl-install-types v1.8.0 # git tag in the TDLib repository 25 | $ npx tdl-install-types ./td_api.tl # types for the given TDLib tl schema 26 | ``` 27 | 28 | For more information, see `npx tdl-install-types --help`. 29 | -------------------------------------------------------------------------------- /packages/tdl-install-types/bin/tdl-install-types: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../src/index') 3 | -------------------------------------------------------------------------------- /packages/tdl-install-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdl-install-types", 3 | "version": "0.3.0", 4 | "description": "Generate TypeScript (and Flow) types for TDLib", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "" 8 | }, 9 | "files": [ 10 | "src", 11 | "bin", 12 | "README.md", 13 | "LICENSE" 14 | ], 15 | "keywords": [ 16 | "cli", 17 | "typings", 18 | "types", 19 | "tdlib", 20 | "tdl", 21 | "typescript", 22 | "flow" 23 | ], 24 | "dependencies": { 25 | "node-fetch": "^3.3.2", 26 | "ti-el": "^0.9.1" 27 | }, 28 | "bin": { 29 | "tdl-install-types": "bin/tdl-install-types" 30 | }, 31 | "engines": { 32 | "node": ">=16.0.0" 33 | }, 34 | "author": "eilvelia ", 35 | "license": "MIT", 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/eilvelia/tdl.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/eilvelia/tdl/issues" 42 | }, 43 | "homepage": "https://github.com/eilvelia/tdl/tree/main/packages/tdl-install-types" 44 | } 45 | -------------------------------------------------------------------------------- /packages/tdl-install-types/src/gen.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | /*:: import type { Parameter, TdClass, BaseClass } from 'ti-el' */ 4 | const { tldoc } = require('ti-el') 5 | 6 | const EOL = '\n' 7 | const INPUT_SUFFIX = '$Input' 8 | 9 | const addIndent = (n/*: number */, str/*: string */) => str 10 | .split(EOL) 11 | .map(l => l.length > 0 ? ' '.repeat(n) + l : l) 12 | .join(EOL) 13 | 14 | function formatDesc (desc/*: string */, indent/*: number */ = 0) { 15 | const space = ' '.repeat(indent) 16 | let length = 0 17 | const lines = [''] 18 | 19 | for (const ch of desc.split('')) { 20 | length++ 21 | if (length > 75 && ch === ' ') { 22 | lines.push('') 23 | length = 0 24 | } 25 | lines[lines.length - 1] += ch 26 | } 27 | 28 | if (lines.length > 1) { 29 | const str = lines 30 | .map(line => space + ' * ' + line.trim()) 31 | .join(EOL) 32 | return `${space}/**${EOL}${str}${EOL}${space} */` 33 | } 34 | 35 | return `${space}/** ${lines.join(EOL)} */` 36 | } 37 | 38 | function generate ( 39 | source/*: string */, 40 | lang/*: 'typescript' | 'flow' */, 41 | header/*: string[] */ 42 | )/*: string */ { 43 | const TS = lang !== 'flow' 44 | 45 | const READONLY_ARRAY = TS ? 'ReadonlyArray' : '$ReadOnlyArray' 46 | const READONLY = TS ? 'readonly ' : '+' 47 | const EXACT = TS ? '' : '|' 48 | const INEXACT = TS ? '' : ', ...' 49 | const EXPORT = TS ? 'export' : 'declare export' 50 | const EXTENDS = TS ? ' extends' : ':' 51 | 52 | // console.log(JSON.stringify(tldoc(source), null, ' ')) 53 | 54 | function genParameter (p/*: Parameter */, input/*: boolean */ = false) { 55 | let typ = (() => { 56 | switch (p.type) { 57 | case 'double': return 'number' 58 | case 'string': return 'string' 59 | case 'int32': return 'number' 60 | case 'int53': return 'number' 61 | case 'int64': return input ? 'number | string' : 'string' 62 | case 'Bool': return 'boolean' 63 | case 'bytes': return 'string /* base64 */' 64 | default: return input ? p.type + INPUT_SUFFIX : p.type 65 | } 66 | })() 67 | // Ad-hoc parsing of 'may be null' 68 | // https://github.com/tdlib/td/issues/1087 69 | const mayBeNull = p.description.includes('may be null') 70 | if (mayBeNull && p.vector > 0) 71 | typ += ' | null' 72 | for (let i = 0; i < p.vector; i++) 73 | typ = input ? `${READONLY_ARRAY}<${typ}>` : `Array<${typ}>` 74 | const optional = input || (mayBeNull && p.vector <= 0) ? '?' : '' 75 | const readonly = input ? READONLY : '' 76 | return [ 77 | formatDesc(p.description, 2), 78 | ` ${readonly}${p.name}${optional}: ${typ},` 79 | ].join(EOL) 80 | } 81 | 82 | function genTdClass (c/*: TdClass */, input/*: boolean */ = false, preserveName/*: boolean */ = false) { 83 | const typeName = input && !preserveName ? c.name + INPUT_SUFFIX : c.name 84 | const readonly = input ? READONLY : '' 85 | return [ 86 | `${EXPORT} type ${typeName} = {${EXACT}`, 87 | formatDesc(c.description, 2), 88 | ` ${readonly}_: '${c.name}',`, 89 | ...c.parameters.map(p => genParameter(p, input)), 90 | `${EXACT}}` 91 | ].filter(x => x.length > 0).join(EOL) 92 | } 93 | 94 | function genTdBaseClass ( 95 | name/*: string */, 96 | children/*: TdClass[] */, 97 | description/*: ?string */, 98 | input/*: boolean */ = false 99 | ) { 100 | const typeName = input ? name + INPUT_SUFFIX : name 101 | const childSuffix = input ? INPUT_SUFFIX : '' 102 | if (children.length === 1) { 103 | const c = children[0] 104 | return [ 105 | description && formatDesc(description), 106 | `${EXPORT} type ${typeName} = ${c.name}${childSuffix}` 107 | ].filter(Boolean).join(EOL) 108 | } 109 | return [ 110 | description && formatDesc(description), 111 | `${EXPORT} type ${typeName} =`, 112 | ...children.map(c => ` | ${c.name}${childSuffix}`) 113 | ].filter(Boolean).join(EOL) 114 | } 115 | 116 | const { baseClasses, classes } = tldoc(source) 117 | 118 | const baseClassesNameLookup/*: Map */ = new Map() 119 | for (const c of baseClasses) baseClassesNameLookup.set(c.name, c) 120 | 121 | const classesNameLookup/*: Map */ = new Map() 122 | for (const c of classes) classesNameLookup.set(c.name, c) 123 | 124 | const baseClassChildren/*: Map */ = new Map() 125 | for (const c of classes) { 126 | if (c.kind !== 'constructor') continue 127 | const arr = baseClassChildren.get(c.result) 128 | if (arr) 129 | arr.push(c) 130 | else 131 | baseClassChildren.set(c.result, [c]) 132 | } 133 | 134 | function getAllDeps (stack/*: string[] */)/*: Set */ { 135 | const set/*: Set */ = new Set() 136 | while (stack.length > 0) { 137 | const typ = stack.pop() 138 | if (set.has(typ)) continue 139 | set.add(typ) 140 | const children = baseClassChildren.get(typ) 141 | if (!children) { 142 | const c = classesNameLookup.get(typ) 143 | if (c != null && c.kind === 'constructor') 144 | for (const p of c.parameters) stack.push(p.type) 145 | continue 146 | } 147 | for (const c of children) { 148 | stack.push(c.name) 149 | for (const p of c.parameters) stack.push(p.type) 150 | } 151 | } 152 | return set 153 | } 154 | 155 | const funcs = classes.filter(e => e.kind === 'function') 156 | 157 | const syncFuncs = 158 | funcs.filter(e => e.description.includes('Can be called synchronously')) 159 | 160 | const inputTypes = getAllDeps(funcs.flatMap(f => f.parameters.map(p => p.type))) 161 | const outputTypes = getAllDeps(['Update', 'Error', ...funcs.map(f => f.result)]) 162 | 163 | const objectTypes = classes 164 | .map(c => { 165 | if (c.kind === 'function') 166 | return genTdClass(c, true, true) 167 | if (inputTypes.has(c.name) && outputTypes.has(c.name)) 168 | return [genTdClass(c), '', genTdClass(c, true)].join(EOL) 169 | if (inputTypes.has(c.name)) 170 | return genTdClass(c, true) 171 | if (outputTypes.has(c.name)) 172 | return genTdClass(c) 173 | console.error(`WARN ${c.name} is neither an input or output type`) 174 | return null 175 | }) 176 | .filter(x => x != null) 177 | .join(EOL + EOL) 178 | 179 | const unionTypes = [...baseClassChildren.entries()] 180 | .map(([name, children]) => { 181 | const description = baseClassesNameLookup.get(name)?.description 182 | if (inputTypes.has(name) && outputTypes.has(name)) { 183 | const input = genTdBaseClass(name, children, description, true) 184 | const output = genTdBaseClass(name, children, description) 185 | return [output, '', input].join(EOL) 186 | } 187 | if (inputTypes.has(name)) 188 | return genTdBaseClass(name, children, description, true) 189 | if (outputTypes.has(name)) 190 | return genTdBaseClass(name, children, description) 191 | return null 192 | }) 193 | .filter(x => x != null) 194 | .join(EOL + EOL) 195 | 196 | // function genApiObjectFunction (c/*: TdClass */) { 197 | // const param = c.parameters.length < 1 198 | // ? `params?: ${EMPTY_OBJ}` 199 | // : `params: ${c.name}` 200 | // return [ 201 | // formatDesc(c.description, 2), 202 | // ` ${c.name}(${param}): Promise<${c.result}>,` 203 | // ].join(EOL) 204 | // } 205 | // const apiObject = [ 206 | // `${EXPORT} type ApiObject = {${EXACT}`, 207 | // ...funcs.map(genApiObjectFunction), 208 | // `${EXACT}}` 209 | // ].join(EOL) 210 | 211 | const functionUnionType = [ 212 | `${EXPORT} type $Function =`, 213 | funcs.map(c => ' | ' + c.name).join(EOL) 214 | ].join(EOL) 215 | 216 | const nameToResultTable = [ 217 | `${EXPORT} type $FunctionResultByName = {`, 218 | funcs.map(c => ` ${c.name}: ${c.result},`).join(EOL), 219 | '}' 220 | ].join(EOL) 221 | 222 | const nameToInputTable = [ 223 | `${EXPORT} type $FunctionInputByName = {`, 224 | funcs.map(c => ` ${c.name}: ${c.name},`).join(EOL), 225 | '}' 226 | ].join(EOL) 227 | 228 | const funName = TS 229 | ? `${EXPORT} type $FunctionName = keyof $FunctionResultByName` 230 | : `${EXPORT} type $FunctionName = $Keys<$FunctionResultByName>` 231 | 232 | const syncFuncName = [ 233 | `${EXPORT} type $SyncFunctionName =`, 234 | syncFuncs.map(c => ` | '${c.name}'`).join(EOL) 235 | ].join(EOL) 236 | 237 | const invoke = [ 238 | `${EXPORT} type Invoke = (`, 239 | ` query: { ${READONLY}_: T${INEXACT} } & $FunctionInputByName[T]`, 240 | ') => Promise<$FunctionResultByName[T]>' 241 | ].join(EOL) 242 | 243 | const execute = [ 244 | `${EXPORT} type Execute = (`, 245 | ` query: { ${READONLY}_: T${INEXACT} } & $FunctionInputByName[T]`, 246 | ') => error | $FunctionResultByName[T]' 247 | ].join(EOL) 248 | 249 | return [ 250 | TS ? null : '// @flow', 251 | header.join(EOL), 252 | 'declare module \'tdlib-types\' {', 253 | addIndent(2, [ 254 | objectTypes, 255 | '', 256 | '// --- ---', 257 | '', 258 | unionTypes, 259 | '', 260 | '// --- Special types ---', 261 | ...[ 262 | functionUnionType, 263 | nameToResultTable, 264 | nameToInputTable, 265 | funName, 266 | syncFuncName, 267 | invoke, 268 | execute 269 | ].flatMap(x => ['', x]) 270 | ].join(EOL)), 271 | '}', 272 | '' 273 | ].filter(x => x != null).join(EOL) 274 | } 275 | 276 | module.exports = { generate } 277 | -------------------------------------------------------------------------------- /packages/tdl-install-types/src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs') 2 | const path = require('node:path') 3 | const { createRequire } = require('node:module') 4 | const fetch = (...args) => import('node-fetch') 5 | .then(({ default: fetch }) => fetch(...args)) 6 | const { generate } = require('./gen') 7 | 8 | const packageVersion = require('../package.json').version 9 | 10 | const argv = [...process.argv].slice(2) 11 | 12 | const help = `\ 13 | Usage: tdl-install-types [] [] 14 | 15 | Generate TypeScript (and optionally Flow) types for TDLib, 16 | potentially fetching the needed schema from the TDLib's GitHub repository. 17 | These types can be used with the tdl library. 18 | 19 | By default, a tdlib-types.d.ts file is created that you can git-commit. 20 | 21 | can be one of: 22 | - a file path to the .so/.dylib/.dll tdjson shared library file 23 | - a file path to the td_api.tl file 24 | - a git ref in the TDLib repository 25 | - "prebuilt-tdlib": special value to generate types for the installed 26 | version of prebuilt-tdlib (this is the default) 27 | 28 | Examples: 29 | tdl-install-types ./libtdjson.so # generate types for this shared library 30 | tdl-install-types prebuilt-tdlib # use version of installed prebuilt-tdlib 31 | tdl-install-types # similar to above: try to use prebuilt-tdlib 32 | tdl-install-types 0ada45c3618108a62806ce7d9ab435a18dac1aab # commit hash 33 | tdl-install-types v1.8.0 # git tag in the TDLib repository 34 | tdl-install-types td_api.tl # generate directly from the tl file 35 | npx tdl-install-types # can be called via npx without installing 36 | 37 | Options: 38 | --lib 39 | Interpret as a file path to the tdjson shared library. This is 40 | the default behavior if ends with .so, .dylib, or .dll. 41 | Can fail if the heuristics cannot find the version inside the shared 42 | library, especially for older TDLib versions. 43 | 44 | --tl 45 | Interpret as a file path to a td_api.tl file describing the TDLib 46 | schema in the TL language. This is the default behavior if ends 47 | with .tl. 48 | 49 | --git-ref 50 | Interpret as a git ref (e.g., a commit hash or a git tag or a 51 | branch name) in the TDLib repository. This is the default behavior if 52 | neither of the above conditions is met and is not prebuilt-tdlib. 53 | 54 | -o 55 | Output generated TypeScript types to . 56 | Defaults to tdlib-types.d.ts in the current working directory. 57 | 58 | --flow 59 | Also generate Flow types besides the TypeScript types. 60 | 61 | --flow-output 62 | Output generated Flow types to . 63 | Defaults to flow-typed/tdlib-types_vx.x.x.js. 64 | 65 | --github-repo / 66 | Set the TDLib GitHub repository. 67 | Can be set, for example, to the tdlight's repository. 68 | Defaults to tdlib/td. 69 | 70 | --prebuilt-tdlib Deprecated. Same as setting to prebuilt-tdlib. 71 | 72 | -h, --help Print this text and exit. 73 | -v, --version Print version and exit.` 74 | 75 | const LIB = 'lib' 76 | const GIT_REF = 'git-ref' 77 | const TL = 'tl' 78 | const PREBUILT_TDLIB = 'prebuilt-tdlib' 79 | 80 | let target 81 | let type 82 | let tsOutput = 'tdlib-types.d.ts' 83 | let flow = false 84 | let flowOutput = 'flow-typed/tdlib-types_vx.x.x.js' 85 | let githubRepo = 'tdlib/td' 86 | let defaultTarget = false 87 | 88 | function parseArgs () { 89 | if (argv.includes('--help') || argv.includes('-h')) { 90 | console.log(help) 91 | return false 92 | } 93 | if (argv.includes('--version') || argv.includes('-v')) { 94 | console.log(packageVersion) 95 | return false 96 | } 97 | for (let i = 0; i < argv.length; i++) { 98 | const arg = argv[i] 99 | switch (arg) { 100 | case '--lib': 101 | type = LIB 102 | break 103 | case '--tl': 104 | type = TL 105 | break 106 | case '--git-ref': 107 | type = GIT_REF 108 | break 109 | case '--prebuilt-tdlib': 110 | type = PREBUILT_TDLIB 111 | target = 'prebuilt-tdlib' 112 | break 113 | case '-o': 114 | if (!argv[i + 1]) { 115 | console.error(`${arg} expects a file path`) 116 | process.exitCode = 1 117 | return false 118 | } 119 | tsOutput = argv[i + 1] 120 | i++ 121 | break 122 | case '--flow': 123 | flow = true 124 | break 125 | case '--flow-output': 126 | if (!argv[i + 1]) { 127 | console.error(`${arg} expects a file path`) 128 | process.exitCode = 1 129 | return false 130 | } 131 | flowOutput = argv[i + 1] 132 | i++ 133 | break 134 | case '--github-repo': 135 | if (!argv[i + 1] || argv[i + 1].startsWith('-')) { 136 | console.error(`${arg} expects a value`) 137 | process.exitCode = 1 138 | return false 139 | } 140 | githubRepo = argv[i + 1] 141 | i++ 142 | break 143 | default: 144 | if (arg.startsWith('-')) { 145 | console.error(`WARN Unrecognized option ${arg}`) 146 | break 147 | } 148 | if (target) { 149 | console.error('Too many arguments') 150 | return false 151 | } 152 | target = arg 153 | if (type != null) break 154 | if (target == 'prebuilt-tdlib') 155 | type = PREBUILT_TDLIB 156 | else if (target.endsWith('.so') || target.endsWith('.dylib') || target.endsWith('.dll')) 157 | type = LIB 158 | else if (target.endsWith('.tl')) 159 | type = TL 160 | else 161 | type = GIT_REF 162 | } 163 | } 164 | if (!target) { 165 | type = PREBUILT_TDLIB 166 | defaultTarget = true 167 | } 168 | return true 169 | } 170 | 171 | function writeFile (file, contents) { 172 | const dir = path.dirname(file) 173 | if (dir !== '.' && dir !== '..') 174 | fs.mkdirSync(dir, { recursive: true }) 175 | fs.writeFileSync(file, contents) 176 | } 177 | 178 | function fromSchema (schema, { ver, hash } = {}) { 179 | let line1 = '// Types for TDLib' 180 | if (ver) line1 += ` v${ver}` 181 | if (hash) line1 += ` (${hash})` 182 | const line2 = `// Generated using tdl-install-types v${packageVersion}` 183 | const header = [line1, line2] 184 | const tsGenerated = generate(schema, 'typescript', header) 185 | writeFile(tsOutput, tsGenerated) 186 | if (flow) { 187 | const flowGenerated = generate(schema, 'flow', header) 188 | writeFile(flowOutput, flowGenerated) 189 | } 190 | } 191 | 192 | async function fetchTdVersion (ref) { 193 | try { 194 | const url = `https://raw.githubusercontent.com/${githubRepo}/${ref}/CMakeLists.txt` 195 | const res = await fetch(url) 196 | const text = await res.text() 197 | if (text.includes('404: Not Found')) throw Error('404 Not found') 198 | const version = text.match(/TDLib VERSION ([\w.]+) /)?.[1] 199 | if (!version) throw Error('Could not find version in CMakeLists.txt') 200 | return version 201 | } catch (e) { 202 | console.error(`WARN Failed to get TDLib version: ${e?.message || String(e)}`) 203 | return '' 204 | } 205 | } 206 | 207 | async function fromGitRef (ref, ver = null) { 208 | const url = `https://raw.githubusercontent.com/${githubRepo}/${ref}/td/generate/scheme/td_api.tl` 209 | const res = await fetch(url) 210 | const schema = await res.text() 211 | if (schema.length < 2000) 212 | throw Error(`Failed to fetch schema from ${url}\n ${schema}`) 213 | ver ||= await fetchTdVersion(ref) 214 | const hash = (ref.length >= 30 && ref !== ver) ? ref : null 215 | fromSchema(schema, { ver, hash }) 216 | } 217 | 218 | // > $ strings libtdjson.dylib | grep -iE '^[0123456789abcdef]{40}$' 219 | // > 66234ae2537a99ec0eaf7b0857245a6e5c2d2bc9 220 | 221 | function fromDynamicLib (lib) { 222 | // Try to parse the hash/version from the so file itself 223 | const data = fs.readFileSync(lib).toString('binary') 224 | const hash = data.match(/[^\dabcdef]([\dabcdef]{40})[^\dabcdef]/)?.[1] 225 | const version = data.match(/\W([1234]\.\d{1,2}\.\d{1,3})\W/)?.[1] 226 | // console.log('hash', hash) 227 | // console.log('version', version) 228 | if (version?.endsWith?.('.0')) { 229 | const parsed = version.split('.').map(Number) 230 | const useVersionRef = (parsed?.[0] === 1 && parsed?.[1] < 8) 231 | || (!hash && parsed.every(x => !Number.isNaN(x))) 232 | if (useVersionRef) 233 | return fromGitRef('v' + version, version) 234 | } 235 | if (hash) { 236 | console.log(`INFO Using TDLib ${hash}`) 237 | return fromGitRef(hash) 238 | } 239 | throw Error('Could not determine TDLib commit hash') 240 | } 241 | 242 | async function fromPrebuiltTdlib () { 243 | try { 244 | // Find path of the prebuilt-tdlib module 245 | const cwdRequire = createRequire(path.resolve('index.js')) 246 | const prebuiltTdlib = path.dirname(cwdRequire.resolve('prebuilt-tdlib')) 247 | 248 | // Try to use the tdlib.commit and tdlib.version fields in package.json 249 | const packageJsonPath = path.join(prebuiltTdlib, 'package.json') 250 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()) 251 | const tdlibCommit = packageJson?.tdlib?.commit 252 | const tdlibVersion = packageJson?.tdlib?.version 253 | if (typeof tdlibCommit === 'string' && typeof tdlibVersion === 'string') { 254 | await fromGitRef(tdlibCommit, tdlibVersion) 255 | return 256 | } 257 | 258 | // If not existent, fallback to executing getTdjson() and extracting tdlib 259 | // commit from the shared library 260 | const tdjsonFile = cwdRequire('prebuilt-tdlib').getTdjson() 261 | await fromDynamicLib(tdjsonFile) 262 | } catch (e) { 263 | if (defaultTarget && e?.code === 'MODULE_NOT_FOUND') { 264 | console.error('Error: Cannot find prebuilt-tdlib. Help:') 265 | console.error(help) 266 | } else if (defaultTarget) { 267 | console.error('Could not generate types for prebuilt-tdlib:') 268 | console.error(e?.message || String(e)) 269 | console.error(help) 270 | } else { 271 | console.error(String(e)) 272 | } 273 | process.exitCode = 1 274 | } 275 | } 276 | 277 | const shouldContinue = parseArgs() 278 | 279 | if (!shouldContinue) { 280 | // Nothing, exit 281 | } else if (type === PREBUILT_TDLIB) { 282 | fromPrebuiltTdlib() 283 | } else if (type === TL) { 284 | const schema = fs.readFileSync(target).toString() 285 | fromSchema(schema) 286 | } else if (type === GIT_REF) { 287 | fromGitRef(target).catch(console.error) 288 | } else if (type === LIB) { 289 | fromDynamicLib(target) 290 | } 291 | -------------------------------------------------------------------------------- /packages/tdl-install-types/tests/generator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from 'vitest' 2 | import * as fs from 'node:fs' 3 | import * as path from 'node:path' 4 | // @ts-expect-error 5 | import { generate } from '../src/gen.js' 6 | 7 | const schemaDir = path.join(__dirname, 'schema') 8 | 9 | const schema180 = fs.readFileSync(path.join(schemaDir, 'v1.8.0.tl')) 10 | .toString() 11 | 12 | describe('types generator', () => { 13 | test('should generate TypeScript types for TDLib v1.8.0', async () => { 14 | await expect(generate(schema180, 'typescript', ['// TDLib v1.8.0'])) 15 | .toMatchFileSnapshot('__snapshots__/ts-tdlib-1-8-0.js.shot') 16 | }) 17 | 18 | test('should generate Flow types for TDLib v1.8.0', async () => { 19 | await expect(generate(schema180, 'flow', ['// TDLib v1.8.0'])) 20 | .toMatchFileSnapshot('__snapshots__/flow-tdlib-1-8-0.js.shot') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /packages/tdl/.gitignore: -------------------------------------------------------------------------------- 1 | /README.md 2 | build/ 3 | prebuilds/ 4 | -------------------------------------------------------------------------------- /packages/tdl/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2025 eilvelia 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 | -------------------------------------------------------------------------------- /packages/tdl/addon/td.cpp: -------------------------------------------------------------------------------- 1 | #define NAPI_VERSION 5 2 | #define NODE_API_NO_EXTERNAL_BUFFERS_ALLOWED 1 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) 13 | # include "win32-dlfcn.h" 14 | #else 15 | # include 16 | #endif 17 | 18 | #ifdef RTLD_DEEPBIND 19 | # pragma message("Using RTLD_DEEPBIND") 20 | # define DLOPEN(FILE) dlopen(FILE, RTLD_LAZY | RTLD_LOCAL | RTLD_DEEPBIND) 21 | #else 22 | # pragma message("Using standard dlopen") 23 | # define DLOPEN(FILE) dlopen(FILE, RTLD_LAZY | RTLD_LOCAL) 24 | #endif 25 | 26 | typedef void * (*td_json_client_create_t)(); 27 | typedef void (*td_json_client_send_t)(void *client, const char *request); 28 | typedef const char * (*td_json_client_receive_t)(void *client, double timeout); 29 | typedef const char * (*td_json_client_execute_t)(void *client, const char *request); 30 | typedef void (*td_json_client_destroy_t)(void *client); 31 | 32 | // New tdjson interface 33 | typedef int (*td_create_client_id_t)(); 34 | typedef void (*td_send_t)(int client_id, const char *request); 35 | typedef const char * (*td_receive_t)(double timeout); 36 | typedef const char * (*td_execute_t)(const char *request); 37 | 38 | typedef void (*td_log_message_callback_ptr)(int verbosity_level, const char *message); 39 | typedef void (*td_set_log_message_callback_t)(int max_verbosity_level, td_log_message_callback_ptr callback); 40 | 41 | td_json_client_create_t td_json_client_create; 42 | td_json_client_send_t td_json_client_send; 43 | td_json_client_receive_t td_json_client_receive; 44 | td_json_client_execute_t td_json_client_execute; 45 | td_json_client_destroy_t td_json_client_destroy; 46 | td_create_client_id_t td_create_client_id; 47 | td_send_t td_send; 48 | td_receive_t td_receive; 49 | td_execute_t td_execute; 50 | td_set_log_message_callback_t td_set_log_message_callback; 51 | 52 | #define FAIL(MSG, ...) \ 53 | NAPI_THROW(Napi::Error::New(env, MSG), __VA_ARGS__); 54 | #define TYPEFAIL(MSG, ...) \ 55 | NAPI_THROW(Napi::TypeError::New(env, MSG), __VA_ARGS__); 56 | 57 | static const char empty_str[] = ""; 58 | 59 | class ReceiveWorker { 60 | public: 61 | ReceiveWorker(const Napi::Env& env, void *client, double timeout) 62 | : client(client), timeout(timeout), 63 | tsfn(Tsfn::New(env, "ReceiveTSFN", 0, 1, this)), 64 | thread(&ReceiveWorker::loop, this) 65 | { 66 | if (client == nullptr) // New tdjson interface 67 | thread.detach(); 68 | tsfn.Ref(env); // note: bun ( lock(mutex); 73 | stop = true; 74 | ready = true; 75 | } 76 | cv.notify_all(); 77 | if (client != nullptr) 78 | td_json_client_destroy(client); 79 | if (thread.joinable()) 80 | thread.join(); 81 | } 82 | 83 | // A task can be added only after the previous task is finished. 84 | Napi::Promise NewTask(const Napi::Env& env) { 85 | if (deferred != nullptr) { 86 | auto error = Napi::Error::New(env, "receive is not finished yet"); 87 | auto fail_deferred = Napi::Promise::Deferred::New(env); 88 | fail_deferred.Reject(error.Value()); 89 | return fail_deferred.Promise(); 90 | } 91 | deferred = std::make_unique(env); 92 | { 93 | std::lock_guard lock(mutex); 94 | ready = true; 95 | } 96 | cv.notify_all(); 97 | return deferred->Promise(); 98 | } 99 | 100 | void Ref(const Napi::Env& env) { tsfn.Ref(env); } 101 | void Unref(const Napi::Env& env) { tsfn.Unref(env); } 102 | inline void * GetClient() { return client; } 103 | private: 104 | using TsfnCtx = ReceiveWorker; 105 | // Called on the main thread 106 | static void CallJs(Napi::Env env, Napi::Function, TsfnCtx *ctx, char *data) { 107 | if (env != nullptr && ctx != nullptr) { 108 | const char *res = data; 109 | auto val = res == nullptr || *res == '\0' ? env.Null() : Napi::String::New(env, res); 110 | auto deferred = std::move(ctx->deferred); 111 | // Note that this can potentially execute the JS code (it does in deno) 112 | deferred->Resolve(val); 113 | // ctx may not exist anymore 114 | } 115 | } 116 | using Tsfn = Napi::TypedThreadSafeFunction; 117 | 118 | void loop() { 119 | std::unique_lock lock(mutex); 120 | while (true) { 121 | cv.wait(lock, [this] { return ready; }); 122 | if (stop) break; 123 | ready = false; 124 | lock.unlock(); 125 | const char *response = client == nullptr 126 | ? td_receive(timeout) 127 | : td_json_client_receive(client, timeout); 128 | // TDLib stores the response in thread-local storage that is deallocated 129 | // on execute() and receive(). Since we never call execute() in this 130 | // thread, it should be safe not to copy the response here. 131 | lock.lock(); 132 | // note: bun ((response == nullptr ? empty_str : response); 135 | tsfn.NonBlockingCall(data); 136 | } 137 | tsfn.Release(); 138 | // NOTE: Given that this thread is not calling receive anymore, the last 139 | // response (if not null) probably will stay to be allocated forever. That 140 | // should not be a big deal. 141 | } 142 | 143 | void *client; 144 | double timeout; 145 | Tsfn tsfn; 146 | std::unique_ptr deferred; 147 | bool ready {false}; 148 | bool stop {false}; 149 | std::mutex mutex; 150 | std::condition_variable cv; 151 | std::thread thread; 152 | }; 153 | 154 | // Old tdjson interface 155 | namespace Tdo { 156 | Napi::Value ClientCreate(const Napi::CallbackInfo& info) { 157 | Napi::Env env = info.Env(); 158 | if (!info[0].IsNumber()) 159 | TYPEFAIL("Expected first argument to be a number", Napi::Value()); 160 | double timeout = info[0].As().DoubleValue(); 161 | void *client = td_json_client_create(); 162 | if (client == nullptr) 163 | FAIL("td_json_client_create returned null", Napi::Value()); 164 | auto worker = new ReceiveWorker(env, client, timeout); 165 | return Napi::External::New(env, worker); 166 | } 167 | 168 | void ClientSend(const Napi::CallbackInfo& info) { 169 | auto *worker = info[0].As>().Data(); 170 | std::string request = info[1].As().Utf8Value(); 171 | td_json_client_send(worker->GetClient(), request.c_str()); 172 | } 173 | 174 | // Do not call again until the promise is resolved/rejected. 175 | Napi::Value ClientReceive(const Napi::CallbackInfo& info) { 176 | Napi::Env env = info.Env(); 177 | auto *worker = info[0].As>().Data(); 178 | return worker->NewTask(env); 179 | } 180 | 181 | Napi::Value ClientExecute(const Napi::CallbackInfo& info) { 182 | Napi::Env env = info.Env(); 183 | void *client = info[0].IsNull() || info[0].IsUndefined() 184 | ? nullptr 185 | : info[0].As>().Data()->GetClient(); 186 | if (!info[1].IsString()) 187 | TYPEFAIL("Expected second argument to be a string", Napi::Value()); 188 | std::string request = info[1].As().Utf8Value(); 189 | const char *response = td_json_client_execute(client, request.c_str()); 190 | if (response == nullptr) 191 | FAIL("td_json_client_execute returned null", Napi::Value()); 192 | return Napi::String::New(env, response); 193 | } 194 | 195 | // Preferably do not call this if the receive promise is pending. 196 | void ClientDestroy(const Napi::CallbackInfo& info) { 197 | auto *worker = info[0].As>().Data(); 198 | delete worker; 199 | } 200 | } 201 | 202 | // New tdjson interface 203 | namespace Tdn { 204 | static ReceiveWorker *worker = nullptr; 205 | 206 | // Create the worker and set the receive timeout explicitly. 207 | void Init(const Napi::CallbackInfo& info) { 208 | Napi::Env env = info.Env(); 209 | if (worker != nullptr) 210 | FAIL("The worker is already initialized"); 211 | if (!info[0].IsNumber()) 212 | TYPEFAIL("Expected first argument (timeout) to be a number"); 213 | double timeout = info[0].As().DoubleValue(); 214 | worker = new ReceiveWorker(env, nullptr, timeout); 215 | } 216 | 217 | void Ref(const Napi::CallbackInfo& info) { 218 | Napi::Env env = info.Env(); 219 | if (worker == nullptr) FAIL("The worker is uninitialized"); 220 | worker->Ref(env); 221 | } 222 | 223 | void Unref(const Napi::CallbackInfo& info) { 224 | Napi::Env env = info.Env(); 225 | if (worker == nullptr) FAIL("The worker is uninitialized"); 226 | worker->Unref(env); 227 | } 228 | 229 | Napi::Value CreateClientId(const Napi::CallbackInfo& info) { 230 | Napi::Env env = info.Env(); 231 | if (td_create_client_id == nullptr) 232 | FAIL("td_create_client_id is not available", Napi::Value()); 233 | if (td_send == nullptr) 234 | FAIL("td_send is not available", Napi::Value()); 235 | if (td_receive == nullptr) 236 | FAIL("td_receive is not available", Napi::Value()); 237 | if (worker == nullptr) 238 | FAIL("The worker is uninitialized", Napi::Value()); 239 | int client_id = td_create_client_id(); 240 | return Napi::Number::New(env, client_id); 241 | } 242 | 243 | void Send(const Napi::CallbackInfo& info) { 244 | int client_id = info[0].As().Int32Value(); 245 | std::string request = info[1].As().Utf8Value(); 246 | td_send(client_id, request.c_str()); 247 | } 248 | 249 | // Should not be called again until promise is resolved/rejected. 250 | Napi::Value Receive(const Napi::CallbackInfo& info) { 251 | Napi::Env env = info.Env(); 252 | return worker->NewTask(env); 253 | } 254 | 255 | Napi::Value Execute(const Napi::CallbackInfo& info) { 256 | Napi::Env env = info.Env(); 257 | if (!info[0].IsString()) 258 | TYPEFAIL("Expected first argument to be a string", Napi::Value()); 259 | std::string request = info[0].As().Utf8Value(); 260 | const char *response = td_execute(request.c_str()); 261 | if (response == nullptr) 262 | FAIL("td_execute returned null", Napi::Value()); 263 | return Napi::String::New(env, response); 264 | } 265 | 266 | // void Stop(const Napi::CallbackInfo& info) { delete worker; } 267 | } 268 | 269 | namespace TdCallbacks { 270 | using TsfnCtx = std::nullptr_t; 271 | struct TsfnData { 272 | int verbosity_level; 273 | std::string message; 274 | }; 275 | void CallJs(Napi::Env env, Napi::Function callback, TsfnCtx *, TsfnData *data) { 276 | if (data == nullptr) return; 277 | if (env != nullptr && callback != nullptr) { 278 | // NOTE: Without --force-node-api-uncaught-exceptions-policy=true, this will 279 | // print a warning and won't rethrow an exception from inside the callback 280 | // https://github.com/nodejs/node-addon-api/pull/1345 281 | callback.Call({ 282 | Napi::Number::New(env, data->verbosity_level), 283 | Napi::String::New(env, data->message) 284 | }); 285 | } 286 | delete data; 287 | } 288 | using Tsfn = Napi::TypedThreadSafeFunction; 289 | 290 | static Tsfn tsfn = nullptr; 291 | static std::mutex tsfn_mutex; 292 | 293 | // NOTE: If TDLib exits with SIGABRT right after the verbosity_level=0 message, 294 | // we won't actually have a chance to pass the message to the main thread. 295 | extern "C" void c_message_callback (int verbosity_level, const char *message) { 296 | { 297 | std::lock_guard lock(tsfn_mutex); 298 | if (tsfn == nullptr) return; 299 | auto *data = new TsfnData { verbosity_level, std::string(message) }; 300 | tsfn.NonBlockingCall(data); 301 | } 302 | if (verbosity_level == 0) { 303 | // Hack for the aforementioned issue. Note that there is still no guarantee 304 | // that the callback will be executed. For example, td_execute(addLogMessage) 305 | // with verbosity_level=0 runs this function on the main thread. 306 | std::this_thread::sleep_for(std::chrono::seconds(5)); 307 | } 308 | } 309 | 310 | void SetLogMessageCallback(const Napi::CallbackInfo& info) { 311 | Napi::Env env = info.Env(); 312 | if (info.Length() < 2) 313 | TYPEFAIL("Expected two arguments"); 314 | if (!info[0].IsNumber()) 315 | TYPEFAIL("Expected first argument to be a number"); 316 | int max_verbosity_level = info[0].As().Int32Value(); 317 | if (info[1].IsNull() || info[1].IsUndefined()) { 318 | td_set_log_message_callback(max_verbosity_level, nullptr); 319 | std::lock_guard lock(tsfn_mutex); 320 | if (tsfn != nullptr) { 321 | tsfn.Release(); 322 | tsfn = nullptr; 323 | } 324 | return; 325 | } 326 | if (!info[1].IsFunction()) 327 | TYPEFAIL("Expected second argument to be one of: a function, null, undefined"); 328 | std::lock_guard lock(tsfn_mutex); 329 | if (tsfn != nullptr) 330 | tsfn.Release(); 331 | tsfn = Tsfn::New(env, info[1].As(), "TdCallbackTSFN", 0, 1); 332 | tsfn.Unref(env); 333 | td_set_log_message_callback(max_verbosity_level, &c_message_callback); 334 | } 335 | } 336 | 337 | #define FINDFUNC(F) \ 338 | F = (F##_t) dlsym(handle, #F); \ 339 | if ((dlsym_err_cstr = dlerror()) != nullptr) { \ 340 | std::string dlsym_err(dlsym_err_cstr); \ 341 | FAIL("Failed to get " #F ": " + dlsym_err, Napi::Value()); \ 342 | } \ 343 | if (F == nullptr) { \ 344 | FAIL("Failed to get " #F " (null)", Napi::Value()); \ 345 | } 346 | 347 | // #define FINDFUNC_OPT(F) \ 348 | // F = (F##_t) dlsym(handle, #F); \ 349 | // dlerror(); 350 | 351 | Napi::Value LoadTdjson(const Napi::CallbackInfo& info) { 352 | Napi::Env env = info.Env(); 353 | if (td_create_client_id != nullptr) // Already loaded 354 | return Napi::Boolean::New(env, false); 355 | std::string library_file = info[0].As().Utf8Value(); 356 | dlerror(); // Clear errors 357 | void *handle = DLOPEN(library_file.c_str()); 358 | char *dlopen_err_cstr = dlerror(); 359 | if (handle == nullptr) { 360 | std::string dlopen_err(dlopen_err_cstr == nullptr ? "NULL" : dlopen_err_cstr); 361 | FAIL("Dynamic Loading Error: " + dlopen_err, Napi::Value()); 362 | } 363 | char *dlsym_err_cstr; 364 | FINDFUNC(td_json_client_create); 365 | FINDFUNC(td_json_client_send); 366 | FINDFUNC(td_json_client_receive); 367 | FINDFUNC(td_json_client_execute); 368 | FINDFUNC(td_json_client_destroy); 369 | FINDFUNC(td_create_client_id); 370 | FINDFUNC(td_send); 371 | FINDFUNC(td_receive); 372 | FINDFUNC(td_execute); 373 | FINDFUNC(td_set_log_message_callback); 374 | return Napi::Boolean::New(env, true); 375 | } 376 | 377 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 378 | exports["tdoCreate"] = Napi::Function::New(env, Tdo::ClientCreate, "create"); 379 | exports["tdoSend"] = Napi::Function::New(env, Tdo::ClientSend, "send"); 380 | exports["tdoReceive"] = Napi::Function::New(env, Tdo::ClientReceive, "receive"); 381 | exports["tdoExecute"] = Napi::Function::New(env, Tdo::ClientExecute, "execute"); 382 | exports["tdoDestroy"] = Napi::Function::New(env, Tdo::ClientDestroy, "destroy"); 383 | exports["tdnInit"] = Napi::Function::New(env, Tdn::Init, "init"); 384 | exports["tdnRef"] = Napi::Function::New(env, Tdn::Ref, "ref"); 385 | exports["tdnUnref"] = Napi::Function::New(env, Tdn::Unref, "unref"); 386 | exports["tdnCreateClientId"] = 387 | Napi::Function::New(env, Tdn::CreateClientId, "createClientId"); 388 | exports["tdnSend"] = Napi::Function::New(env, Tdn::Send, "send"); 389 | exports["tdnReceive"] = Napi::Function::New(env, Tdn::Receive, "receive"); 390 | exports["tdnExecute"] = Napi::Function::New(env, Tdn::Execute, "execute"); 391 | exports["setLogMessageCallback"] = 392 | Napi::Function::New(env, TdCallbacks::SetLogMessageCallback, "setLogMessageCallback"); 393 | exports["loadTdjson"] = Napi::Function::New(env, LoadTdjson, "loadTdjson"); 394 | return exports; 395 | } 396 | 397 | NODE_API_MODULE(addon, Init) 398 | -------------------------------------------------------------------------------- /packages/tdl/addon/win32-dlfcn.cpp: -------------------------------------------------------------------------------- 1 | // From https://github.com/node-ffi-napi/node-ffi-napi/blob/1abb30b5e578e7ef3cf48d2892a295b0357ac3c1/src/win32-dlfcn.cc 2 | 3 | /** 4 | * @file Minimal emulation of POSIX dlopen/dlsym/dlclose on Windows. 5 | * @license Public domain. 6 | * 7 | * This code works fine for the common scenario of loading a 8 | * specific DLL and calling one (or more) functions within it. 9 | * 10 | * No attempt is made to emulate POSIX symbol table semantics. 11 | * The way Windows thinks about dynamic linking is fundamentally 12 | * different, and there's no way to emulate the useful aspects of 13 | * POSIX semantics. 14 | */ 15 | 16 | #ifndef WIN32_LEAN_AND_MEAN 17 | #define WIN32_LEAN_AND_MEAN 18 | #endif 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include "win32-dlfcn.h" 25 | 26 | /** 27 | * Win32 error code from last failure. 28 | */ 29 | 30 | static DWORD lastError = 0; 31 | 32 | #ifdef __cplusplus 33 | extern "C" { 34 | #endif 35 | 36 | /** 37 | * Convert UTF-8 string to Windows UNICODE (UCS-2 LE). 38 | * 39 | * Caller must free() the returned string. 40 | */ 41 | 42 | static 43 | WCHAR* 44 | UTF8toWCHAR( 45 | const char* inputString /** UTF-8 string. */ 46 | ) 47 | { 48 | int outputSize; 49 | WCHAR* outputString; 50 | 51 | outputSize = MultiByteToWideChar(CP_UTF8, 0, inputString, -1, NULL, 0); 52 | 53 | if (outputSize == 0) 54 | return NULL; 55 | 56 | outputString = (WCHAR*) malloc(outputSize * sizeof(WCHAR)); 57 | 58 | if (outputString == NULL) { 59 | SetLastError(ERROR_OUTOFMEMORY); 60 | return NULL; 61 | } 62 | 63 | if (MultiByteToWideChar(CP_UTF8, 0, inputString, -1, outputString, outputSize) != outputSize) 64 | { 65 | free(outputString); 66 | return NULL; 67 | } 68 | 69 | return outputString; 70 | } 71 | 72 | /** 73 | * Open DLL, returning a handle. 74 | */ 75 | 76 | void* 77 | dlopen( 78 | const char* file, /** DLL filename (UTF-8). */ 79 | int mode /** mode flags (ignored). */ 80 | ) 81 | { 82 | WCHAR* unicodeFilename; 83 | UINT errorMode; 84 | void* handle; 85 | 86 | UNREFERENCED_PARAMETER(mode); 87 | 88 | if (file == NULL) 89 | return (void*) GetModuleHandle(NULL); 90 | 91 | unicodeFilename = UTF8toWCHAR(file); 92 | 93 | if (unicodeFilename == NULL) { 94 | lastError = GetLastError(); 95 | return NULL; 96 | } 97 | 98 | errorMode = GetErrorMode(); 99 | 100 | /* Have LoadLibrary return NULL on failure; prevent GUI error message. */ 101 | SetErrorMode(errorMode | SEM_FAILCRITICALERRORS); 102 | 103 | handle = (void*) LoadLibraryW(unicodeFilename); 104 | 105 | if (handle == NULL) 106 | lastError = GetLastError(); 107 | 108 | SetErrorMode(errorMode); 109 | 110 | free(unicodeFilename); 111 | 112 | return handle; 113 | } 114 | 115 | /** 116 | * Close DLL. 117 | */ 118 | 119 | int 120 | dlclose( 121 | void* handle /** Handle from dlopen(). */ 122 | ) 123 | { 124 | int rc = 0; 125 | 126 | if (handle != (void*) GetModuleHandle(NULL)) 127 | rc = !FreeLibrary((HMODULE) handle); 128 | 129 | if (rc) 130 | lastError = GetLastError(); 131 | 132 | return rc; 133 | } 134 | 135 | /** 136 | * Look up symbol exported by DLL. 137 | */ 138 | 139 | void* 140 | dlsym( 141 | void* handle, /** Handle from dlopen(). */ 142 | const char* name /** Name of exported symbol (ASCII). */ 143 | ) 144 | { 145 | void* address = (void*) GetProcAddress((HMODULE) handle, name); 146 | 147 | if (address == NULL) 148 | lastError = GetLastError(); 149 | 150 | return address; 151 | } 152 | 153 | /** 154 | * Return message describing last error. 155 | */ 156 | 157 | char* 158 | dlerror(void) 159 | { 160 | static char errorMessage[64]; 161 | 162 | if (lastError != 0) { 163 | sprintf(errorMessage, "Win32 error %lu", lastError); 164 | lastError = 0; 165 | return errorMessage; 166 | } else { 167 | return NULL; 168 | } 169 | } 170 | 171 | #ifdef __cplusplus 172 | } 173 | #endif 174 | -------------------------------------------------------------------------------- /packages/tdl/addon/win32-dlfcn.h: -------------------------------------------------------------------------------- 1 | // From https://github.com/node-ffi-napi/node-ffi-napi/blob/1abb30b5e578e7ef3cf48d2892a295b0357ac3c1/src/win32-dlfcn.h 2 | 3 | /** 4 | * @file Minimal emulation of POSIX dlopen/dlsym/dlclose on Windows. 5 | * @license Public domain. 6 | * 7 | * This code works fine for the common scenario of loading a 8 | * specific DLL and calling one (or more) functions within it. 9 | * No attempt is made to emulate POSIX symbol table semantics. 10 | */ 11 | 12 | #ifndef _INCLUDE_DLFCN_H_ 13 | #define _INCLUDE_DLFCN_H_ 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | 19 | #define RTLD_LAZY 0 20 | #define RTLD_NOW 0 21 | #define RTLD_GLOBAL 0 22 | #define RTLD_LOCAL 0 23 | 24 | #define RTLD_DEFAULT ((void*) NULL) 25 | #define RTLD_NEXT ((void*) NULL) 26 | 27 | /** 28 | * Open DLL, returning a handle. 29 | */ 30 | 31 | void* 32 | dlopen( 33 | const char *file, /** DLL filename. */ 34 | int mode /** mode flags (ignored). */ 35 | ); 36 | 37 | /** 38 | * Close DLL. 39 | */ 40 | 41 | int 42 | dlclose( 43 | void* handle /** Handle from dlopen(). */ 44 | ); 45 | 46 | /** 47 | * Look up symbol exported by DLL. 48 | */ 49 | 50 | void* 51 | dlsym( 52 | void* handle, /** Handle from dlopen(). */ 53 | const char* name /** Name of exported symbol. */ 54 | ); 55 | 56 | /** 57 | * Return message describing last error. 58 | */ 59 | 60 | char* 61 | dlerror(void); 62 | 63 | #ifdef __cplusplus 64 | } 65 | #endif 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /packages/tdl/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'variables': { 3 | 'openssl_fips': '' 4 | }, 5 | 'targets': [{ 6 | 'target_name': 'td', 7 | 'cflags!': [ '-fno-exceptions' ], 8 | 'cflags_cc!': [ '-fno-exceptions' ], 9 | 'cflags': [ '-Wall', '-Wextra' ], 10 | 'cflags_cc': [ '-Wall', '-Wextra', '-std=c++14' ], 11 | 'sources': [ 12 | 'addon/td.cpp' 13 | ], 14 | 'include_dirs': [ 15 | " 9 | 10 | export type TDLibConfiguration = { 11 | /** 12 | * The path to libtdjson. Defaults to `'tdjson.dll'` on Windows, 13 | * `'libtdjson.dylib'` on macOS, or `'libtdjson.so'` on a different OS. 14 | */ 15 | tdjson?: string, 16 | /** 17 | * The path to the directory with libtdjson. Defaults to `''`. Can be set to, 18 | * for example, `'/usr/local/lib'` or `__dirname` while keeping the `tdjson` 19 | * option unchanged. 20 | */ 21 | libdir?: string, 22 | /** 23 | * Set the verbosity level of TDLib. Defaults to `1`. From the TDLib 24 | * documentation: 25 | * - 0 corresponds to fatal errors, 26 | * - 1 corresponds to errors, 27 | * - 2 corresponds to warnings and debug warnings, 28 | * - 3 corresponds to informational, 29 | * - 4 corresponds to debug, 30 | * - 5 corresponds to verbose deubg, 31 | * - value greater than 5 and up to 1024 can be used to enable even more 32 | * logging. 33 | * 34 | * Another possible option is `'default'`, `tdl` then won't send any 35 | * verbosity level to TDLib. 36 | */ 37 | verbosityLevel?: number | 'default', 38 | /** 39 | * Advanced option. Use the old tdjson interface (`td_json_client_create`, 40 | * etc.) instead of the one that was added in TDLib v1.7.0 41 | * (`td_create_client_id`, etc). Defaults to `false`. */ 42 | useOldTdjsonInterface?: boolean, 43 | /** 44 | * Advanced option. Configures the delay for the `receive` tdjson function. 45 | * Defaults to `10.0` seconds. 46 | */ 47 | receiveTimeout?: number, 48 | } 49 | 50 | /** 51 | * Configure options such as path to the tdjson library or the verbosity level. 52 | * Only options passed in the object are set; can be called none or multiple 53 | * times. The shared library will be loaded using `path.join(libdir, tdjson)` as 54 | * `filename`. Should be called before `createClient`, `execute`, `init`, etc. 55 | */ 56 | export function configure(cfg: TDLibConfiguration): void 57 | 58 | /** 59 | * Call a TDLib method synchronously. Can be used only with the methods 60 | * marked as "can be called synchronously" in the TDLib documentation. 61 | */ 62 | export const execute: Execute 63 | 64 | /** 65 | * The `td_set_log_message_callback` tdjson function, sets the callback that 66 | * will be called when a message is added to the internal TDLib log. Note that 67 | * setting a callback overrides the previous callback. Pass null to remove the 68 | * callback. 69 | */ 70 | export function setLogMessageCallback( 71 | maxVerbosityLevel: number, 72 | callback: null | ((verbosityLevel: number, message: string) => void) 73 | ): void 74 | 75 | /** Create a TDLib client. */ 76 | export function createClient(opts: ClientOptions): Client 77 | 78 | export interface Client { 79 | /** 80 | * Set up an update handler that will log you in to your Telegram account. The 81 | * promise resolves in case the account is successfully authenticated. By 82 | * default, this asks for the credentials in the console; that can be 83 | * overrided by passing the callbacks. If a function is passed as 84 | * `loginDetails`, it will not be called unless any of the handlers trigger. 85 | * `login` supports only a subset of available authentication methods. 86 | */ 87 | login(loginDetails?: LoginDetails | (() => LoginDetails)): Promise; 88 | /** 89 | * Log in as a bot. You can get the token from `@BotFather`. 90 | * If the client is already logged in as a user, it will not be relogged 91 | * as a bot. In case a function is passed instead of string, it will not be 92 | * called if you are already logged in. 93 | * This function is short for 94 | * ``` 95 | * client.login({ 96 | * type: 'bot', 97 | * getToken: retry => retry 98 | * ? Promise.reject(new Error('Invalid bot token')) 99 | * : Promise.resolve(typeof token === 'string' ? token : token()) 100 | * }) 101 | * ``` 102 | */ 103 | loginAsBot(token: string | (() => string | Promise)): Promise; 104 | /** 105 | * Call a TDLib method. Example: 106 | * ``` 107 | * const me = await client.invoke({ _: 'getMe' }) 108 | * console.log(`I am ${me.first_name}`) 109 | * ``` 110 | */ 111 | invoke: Invoke; 112 | /** 113 | * Attach an event listener. This function can be used to iterate through 114 | * updates. Possible events: `update`, `error`, `close`. 115 | * 116 | * The `error` event fires in case an exception is thrown in the `update` or 117 | * `close` handlers, an error occured during the initialization 118 | * `setTdlibParameters` phase (e.g. if some of the client options are 119 | * incorrect), or potentially in case of some internal tdl error. 120 | */ 121 | on: On; 122 | /** 123 | * Same as `client.on`, but attaches a one-time event listener instead. The 124 | * listener is removed after it is called. A single function cannot be reused 125 | * for both `on` and `once`. 126 | */ 127 | once: On; 128 | /** 129 | * Remove an event listener. Returns `true` if the listener has been 130 | * successfully removed, `false` otherwise. 131 | */ 132 | off: Off; 133 | /** Alias for `client.on`. */ 134 | addListener: On; 135 | /** Alias for `client.off`. */ 136 | removeListener: Off; 137 | /** 138 | * Iterate received updates using JavaScript async iterators. This is an 139 | * alternative to `client.on('update', ...)`, one can use either. Example: 140 | * ``` 141 | * for await (const update of client.iterUpdates()) { 142 | * if (update._ === 'updateOption' && update.name === 'my_id') { 143 | * console.log(`My ID is ${update.value.value}!`) 144 | * break 145 | * } 146 | * } 147 | * ``` 148 | */ 149 | iterUpdates(): AsyncIterableIterator; 150 | /** Alias for `tdl.execute`. */ 151 | execute: Execute; 152 | /** 153 | * Close the client. This sends `{ _: 'close' }` and waits for 154 | * `authorizationStateClosed`. 155 | * 156 | * There are also other methods to close the TDLib instance, namely 157 | * `client.invoke({ _: 'logOut' })` and `client.invoke({ _: 'destroy' })`. 158 | */ 159 | close(): Promise; 160 | /** 161 | * Get the TDLib version in the `major.minor.patch` format. Can throw an 162 | * exception if the version (the `updateOption` update) is not (yet) received. 163 | */ 164 | getVersion(): string; 165 | /** 166 | * Test whether the client is closed (that is, `authorizationStateClosed` has 167 | * been received, and the TDLib client does not exist anymore). When true, 168 | * it is no longer possible to send requests or receive updates. 169 | */ 170 | isClosed(): boolean; 171 | /** Emit an event. For advanced use only. */ 172 | emit: Emit; 173 | } 174 | 175 | export type ClientOptions = { 176 | /** Required. Can be obtained at https://my.telegram.org/ */ 177 | apiId: number, 178 | /** Required. Can be obtained at https://my.telegram.org/ */ 179 | apiHash: string, 180 | /** 181 | * Path to the TDLib's database directory (relative to the current working 182 | * directory). Defaults to `'_td_database'`. 183 | */ 184 | databaseDirectory?: string, 185 | /** 186 | * Path to the TDLib's files directory (relative to the current working 187 | * directory). Defaults to `'_td_files'`. 188 | */ 189 | filesDirectory?: string, 190 | /** An optional key for database encryption. */ 191 | databaseEncryptionKey?: string, 192 | /** Use test telegram server. */ 193 | useTestDc?: boolean, 194 | /** 195 | * Raw TDLib parameters. These contain fields like application_version, 196 | * device_model, etc. Defaults to: 197 | * ``` 198 | * { use_message_database: true 199 | * , use_secret_chats: false 200 | * , system_language_code: 'en' 201 | * , application_version: '1.0' 202 | * , device_model: 'Unknown device' 203 | * , system_version: 'Unknown' } 204 | * ``` 205 | */ 206 | tdlibParameters?: TDLibParameters, 207 | /** 208 | * Advanced option. When set to true, the client does not emit updates if 209 | * `connectionState` equals to `connectionStateUpdating`. See also the 210 | * `ignore_background_updates` option in TDLib. 211 | */ 212 | skipOldUpdates?: boolean, 213 | } 214 | 215 | /** 216 | * Advanced function. Create a client without handling the initialization auth* 217 | * updates. You will need to handle the `authorizationStateWaitTdlibParameters` 218 | * update and send parameters manually. A client created by `createClient` 219 | * allows invoking requests only after the client options have been sent to 220 | * TDLib; this disables that. 221 | */ 222 | export function createBareClient(): Client 223 | 224 | export type LoginUser = { 225 | type: 'user', 226 | /** Handler for `authorizationStateWaitPhoneNumber`, will be recalled on error. */ 227 | getPhoneNumber: (retry?: boolean) => Promise, 228 | /** Handler for `authorizationStateWaitEmailAddress`, TDLib v1.8.6+ only. */ 229 | getEmailAddress: () => Promise, 230 | /** Handler for `authorizationStateWaitEmailCode`, TDLib v1.8.6+ only. */ 231 | getEmailCode: () => Promise, 232 | /** Handler for `authorizationStateWaitOtherDeviceConfirmation`, sends nothing. */ 233 | confirmOnAnotherDevice: (link: string) => void, 234 | /** Handler for `authorizationStateWaitCode`, will be recalled on error. */ 235 | getAuthCode: (retry?: boolean) => Promise, 236 | /** Handler for `authorizationStateWaitPassword`, will be recalled on error. */ 237 | getPassword: (passwordHint: string, retry?: boolean) => Promise, 238 | /** Handler for `authorizationStateWaitRegistration`. */ 239 | getName: () => Promise<{ firstName: string, lastName?: string }> 240 | } 241 | 242 | export type LoginBot = { 243 | /** You will be logged in as a bot. */ 244 | type: 'bot', 245 | /** 246 | * Handler for `authorizationStateWaitPhoneNumber`, 247 | * sends `checkAuthenticationBotToken`, will be recalled on error. 248 | */ 249 | getToken: (retry?: boolean) => Promise 250 | } 251 | 252 | export type LoginDetails = Partial | LoginBot 253 | 254 | /** 255 | * Error that is sent by TDLib as an `("error", code, message)` triple. 256 | * 257 | * A `client.invoke` promise can be rejected with an instance of this class; 258 | * if the received error is not connected to any request, it is passed to the 259 | * `client.on('error', ...)` handler. 260 | */ 261 | export class TDLibError extends Error { 262 | readonly _: 'error'; 263 | readonly code: number; 264 | constructor(code: number, message: string); 265 | } 266 | 267 | /** 268 | * Wraps any non-Error exceptions that were about to be emitted as the 269 | * `error` event. 270 | */ 271 | export class UnknownError extends Error { 272 | readonly err: any; 273 | private constructor(err: unknown); 274 | } 275 | 276 | export type On = 277 | & ((event: 'update', listener: (update: Td$Update) => void) => Client) 278 | & ((event: 'error', listener: (err: Error) => void) => Client) 279 | & ((event: 'close', listener: () => void) => Client) 280 | 281 | export type Emit = 282 | & ((event: 'update', update: Td$Update) => void) 283 | & ((event: 'error', err: Error) => void) 284 | & ((event: 'close') => void) 285 | 286 | export type Off = 287 | & ((event: 'update', listener: (...args: any[]) => any) => boolean) 288 | & ((event: 'error', listener: (...args: any[]) => any) => boolean) 289 | & ((event: 'close', listener: (...args: any[]) => any) => boolean) 290 | 291 | /** 292 | * Initialize the node addon explicitly. This function is entirely optional to 293 | * call, TDLib will be initialized automatically on the first call of 294 | * `createClient`, `execute`, `setLogMessageCallback`, or `createBareClient`. 295 | */ 296 | export function init(): void 297 | -------------------------------------------------------------------------------- /packages/tdl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdl", 3 | "version": "8.0.2", 4 | "description": "Node.js bindings to TDLib (Telegram Database library)", 5 | "main": "dist/index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "build:lib": "tsc", 9 | "build:addon": "node-gyp rebuild", 10 | "build": "tsc && node scripts/copy-readme.js && node scripts/generate-flow.js && node-gyp rebuild", 11 | "clean": "node-gyp clean", 12 | "install": "node-gyp-build" 13 | }, 14 | "files": [ 15 | "index.d.ts", 16 | "dist", 17 | "addon", 18 | "binding.gyp", 19 | "prebuilds", 20 | "README.md", 21 | "LICENSE" 22 | ], 23 | "dependencies": { 24 | "debug": "^4.4.0", 25 | "node-addon-api": "^7.1.1", 26 | "node-gyp-build": "^4.8.4" 27 | }, 28 | "engines": { 29 | "node": ">=16.14.0" 30 | }, 31 | "author": "eilvelia ", 32 | "license": "MIT", 33 | "keywords": [ 34 | "telegram", 35 | "telegram-api", 36 | "telegram-client-api", 37 | "tdlib", 38 | "tglib", 39 | "bindings", 40 | "node-addon", 41 | "backend" 42 | ], 43 | "repository": { 44 | "type": "git", 45 | "url": "git+https://github.com/eilvelia/tdl.git" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/eilvelia/tdl/issues" 49 | }, 50 | "homepage": "https://github.com/eilvelia/tdl#readme" 51 | } 52 | -------------------------------------------------------------------------------- /packages/tdl/scripts/copy-readme.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | fs.copyFileSync( 5 | path.join(__dirname, '..', '..', '..', 'README.md'), 6 | path.join(__dirname, '..', 'README.md')) 7 | -------------------------------------------------------------------------------- /packages/tdl/scripts/generate-flow.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | const inputFile = path.join(__dirname, '..', 'index.d.ts') 7 | const outputFile = path.join(__dirname, '..', 'dist', 'index.js.flow') 8 | 9 | const inputStr = fs.readFileSync(inputFile).toString() 10 | 11 | const HEADER = '// @flow\n// AUTOGENERATED, edit index.d.ts instead\n\n' 12 | 13 | const outputStr = HEADER + inputStr 14 | .replace(/readonly /g, '+') 15 | .replace(/export function/g, 'declare export function') 16 | .replace(/export const/g, 'declare export const') 17 | .replace(/export class/g, 'declare export class') 18 | .replace(/(?:private )?constructor\((.+)\);/g, 'constructor($1): void;') 19 | .replace(/: unknown/g, ': mixed') 20 | .replace(/AsyncIterableIterator, 15 | send(client: TdjsonOldClient, request: string): void 16 | } 17 | 18 | /** New tdjson interface */ 19 | export type TdjsonNew = { 20 | init(receiveTimeout: number): void, 21 | ref(): void, 22 | /** Allow the process to exit. */ 23 | unref(): void, 24 | createClientId(): number, 25 | send(clientId: number, request: string): void, 26 | /** Do not call receive again until the promise is completed. */ 27 | receive(): Promise, 28 | execute(request: string): string 29 | } 30 | 31 | export type Tdjson = { 32 | tdold: TdjsonOld, 33 | tdnew: TdjsonNew, 34 | setLogMessageCallback( 35 | maxVerbosityLevel: number, 36 | callback: null | ((verbosityLevel: number, message: string) => void) 37 | ): void 38 | } 39 | 40 | export function loadAddon (libraryFile: string, ignoreAlreadyLoaded = false): Tdjson { 41 | const addon: any = nodeGypBuild(packageDir) 42 | const success = addon.loadTdjson(libraryFile) 43 | if (!success && !ignoreAlreadyLoaded) 44 | throw new Error('tdjson is already loaded') 45 | return { 46 | tdold: { 47 | create: addon.tdoCreate, 48 | send: addon.tdoSend, 49 | receive: addon.tdoReceive, 50 | execute: addon.tdoExecute, 51 | destroy: addon.tdoDestroy 52 | }, 53 | tdnew: { 54 | init: addon.tdnInit, 55 | ref: addon.tdnRef, 56 | unref: addon.tdnUnref, 57 | createClientId: addon.tdnCreateClientId, 58 | send: addon.tdnSend, 59 | receive: addon.tdnReceive, 60 | execute: addon.tdnExecute 61 | }, 62 | setLogMessageCallback: addon.setLogMessageCallback 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/tdl/src/client.ts: -------------------------------------------------------------------------------- 1 | import { resolve as resolvePath } from 'node:path' 2 | import Debug from 'debug' 3 | import { deepRenameKey, mergeDeepRight } from './util' 4 | import * as prompt from './prompt' 5 | import { Version } from './version' 6 | import { Queue } from './queue' 7 | import type { Tdjson, TdjsonOldClient } from './addon' 8 | import type * as Td from 'tdlib-types' 9 | 10 | // NOTE: if needed, this client can be abstracted into a different package later 11 | 12 | const debug = Debug('tdl:client') 13 | const debugReceive = Debug('tdl:client:receive') 14 | const debugReq = Debug('tdl:client:request') 15 | 16 | export type TDLibParameters = Omit 17 | 18 | type StrictClientOptions = { 19 | apiId?: number, 20 | apiHash?: string, 21 | databaseDirectory: string, 22 | filesDirectory: string, 23 | databaseEncryptionKey: string, 24 | useTestDc: boolean, 25 | tdlibParameters: TDLibParameters, 26 | skipOldUpdates: boolean, 27 | } 28 | 29 | export type ClientOptions = Partial 30 | 31 | const defaultOptions: StrictClientOptions = { 32 | databaseDirectory: '_td_database', 33 | filesDirectory: '_td_files', 34 | databaseEncryptionKey: '', 35 | useTestDc: false, 36 | skipOldUpdates: false, 37 | tdlibParameters: { 38 | use_message_database: true, 39 | use_secret_chats: false, 40 | system_language_code: 'en', 41 | application_version: '1.0', 42 | device_model: 'Unknown device', 43 | system_version: 'Unknown' 44 | } 45 | } 46 | 47 | export type LoginUser = { 48 | type: 'user', 49 | getPhoneNumber: (retry?: boolean) => Promise, 50 | getEmailAddress: () => Promise, 51 | getEmailCode: () => Promise, 52 | confirmOnAnotherDevice: (link: string) => void, 53 | getAuthCode: (retry?: boolean) => Promise, 54 | getPassword: (passwordHint: string, retry?: boolean) => Promise, 55 | getName: () => Promise<{ firstName: string, lastName?: string }> 56 | } 57 | 58 | export type LoginBot = { 59 | type: 'bot', 60 | getToken: (retry?: boolean) => Promise, 61 | } 62 | 63 | export type LoginDetails = Partial | LoginBot 64 | type StrictLoginDetails = LoginUser | LoginBot 65 | 66 | const defaultLoginDetails: StrictLoginDetails = { 67 | type: 'user', 68 | getPhoneNumber: prompt.getPhoneNumber, 69 | getEmailAddress: prompt.getEmailAddress, 70 | getEmailCode: prompt.getEmailCode, 71 | confirmOnAnotherDevice: prompt.confirmOnAnotherDevice, 72 | getAuthCode: prompt.getAuthCode, 73 | getPassword: prompt.getPassword, 74 | getName: prompt.getName 75 | } 76 | 77 | export class TDLibError extends Error { 78 | readonly _ = 'error' as const 79 | readonly code: number 80 | constructor (code: number, message: string) { 81 | super(message) 82 | this.code = code 83 | this.name = 'TDLibError' 84 | } 85 | toString(): string { 86 | return `TDLibError: ${this.code} ${this.message}` 87 | } 88 | } 89 | 90 | export class UnknownError extends Error { 91 | readonly err: any 92 | constructor (err: unknown) { 93 | if (typeof err === 'string') super(err) 94 | else super() 95 | this.err = err 96 | this.name = 'UnknownError' 97 | } 98 | } 99 | 100 | type DeferredPromise = { 101 | resolve: (result: R) => void, 102 | reject: (error: E) => void 103 | } 104 | 105 | type PendingRequest = DeferredPromise 106 | 107 | export type On = 108 | & ((event: 'update', listener: (update: Td.Update) => void) => Client) 109 | & ((event: 'error', listener: (err: Error) => void) => Client) 110 | & ((event: 'close', listener: () => void) => Client) 111 | 112 | export type Emit = 113 | & ((event: 'update', update: Td.Update) => void) 114 | & ((event: 'error', err: Error) => void) 115 | & ((event: 'close') => void) 116 | 117 | export type Off = 118 | & ((event: 'update', listener: (...args: any[]) => any) => boolean) 119 | & ((event: 'error', listener: (...args: any[]) => any) => boolean) 120 | & ((event: 'close', listener: (...args: any[]) => any) => boolean) 121 | 122 | export type ManagingParameters = { 123 | useOldTdjsonInterface: boolean, 124 | receiveTimeout: number, 125 | bare: boolean, 126 | executeFunc: Td.Execute 127 | } 128 | 129 | const TDLIB_1_8_6 = new Version('1.8.6') 130 | const TDLIB_DEFAULT = new Version('1.8.27') 131 | 132 | const TDL_MAGIC = '-tdl-' 133 | 134 | type EventListener = ((arg: any) => any) & { once?: boolean } 135 | type Events = { 136 | [key: string]: Set | undefined 137 | } 138 | 139 | type TdjsonAnyClient = 140 | | { isTdn: true, val: number | null } 141 | | { isTdn: false, val: TdjsonOldClient | null } 142 | 143 | // All package-public methods in the Client class are meant to be defined as 144 | // properties. 145 | export class Client { 146 | private readonly _tdjson: Tdjson 147 | private readonly _options: StrictClientOptions 148 | private readonly _pending: Map = new Map() 149 | private readonly _client: TdjsonAnyClient 150 | private _requestId: number = 0 151 | private _initialized: boolean = false 152 | private _preinitRequests: Array<{ request: any, id: unknown }> = [] 153 | private _version: Version = TDLIB_DEFAULT 154 | private _connectionStateName: Td.ConnectionState['_'] = 'connectionStateWaitingForNetwork' 155 | private _authorizationState: Td.AuthorizationState | null = null 156 | private _events: Events = { 157 | update: new Set(), 158 | error: new Set(), 159 | close: new Set() 160 | } 161 | 162 | public execute: Td.Execute 163 | 164 | constructor ( 165 | tdjson: Tdjson, 166 | managing: ManagingParameters, 167 | options: ClientOptions = {}, 168 | ) { 169 | this._options = mergeDeepRight(defaultOptions, options) 170 | this._tdjson = tdjson 171 | this._client = { isTdn: !managing.useOldTdjsonInterface, val: null } 172 | this.execute = managing.executeFunc 173 | 174 | if (managing.bare) { 175 | this._initialized = true 176 | } else { 177 | if (!options.apiId && !options.tdlibParameters?.api_id) 178 | throw new TypeError('Valid api_id must be provided.') 179 | 180 | if (!options.apiHash && !options.tdlibParameters?.api_hash) 181 | throw new TypeError('Valid api_hash must be provided.') 182 | } 183 | 184 | if ((options as any).verbosityLevel != null) { 185 | throw new TypeError('Set verbosityLevel in tdl.configure instead') 186 | } 187 | 188 | if (!this._client.isTdn) { 189 | this._client.val = this._tdjson.tdold.create(managing.receiveTimeout) 190 | 191 | if (this._client.val == null) 192 | throw new Error('Failed to create a TDLib client') 193 | 194 | // Note: To allow defining listeners before the first update, we must 195 | // ensure that emit is not executed in the current tick. process.nextTick 196 | // or queueMicrotask are redundant here because of await in the _loop 197 | // function. 198 | this._loop() 199 | } else { 200 | this._client.val = this._tdjson.tdnew.createClientId() 201 | // The new tdjson interface requires to send a dummy request first 202 | this._sendTdl({ _: 'getOption', name: 'version' }) 203 | } 204 | } 205 | 206 | // Called by the client manager in case the new interface is used 207 | getClientId (): number { 208 | if (!this._client.isTdn) 209 | throw new Error('Cannot get id of a client in the old tdjson interface') 210 | if (this._client.val == null) 211 | throw new Error('Cannot get id of a closed client') 212 | return this._client.val 213 | } 214 | 215 | getVersion = (): string => { 216 | if (this._version === TDLIB_DEFAULT) 217 | throw new Error('Unknown TDLib version') 218 | return this._version.toString() 219 | } 220 | 221 | on: On = (event, fn) => { 222 | let listeners = this._events[event] 223 | if (listeners == null) 224 | listeners = this._events[event] = new Set() 225 | listeners.add(fn) 226 | return this 227 | } 228 | 229 | once: On = (event, fn: EventListener) => { 230 | let listeners = this._events[event] 231 | if (listeners == null) 232 | listeners = this._events[event] = new Set() 233 | fn.once = true 234 | listeners.add(fn) 235 | return this 236 | } 237 | 238 | off: Off = (event, fn) => { 239 | const listeners = this._events[event] 240 | if (listeners == null) return false 241 | return listeners.delete(fn) 242 | } 243 | 244 | emit: Emit = (event, value?: any) => { 245 | const listeners = this._events[event] 246 | if (event === 'error' && (listeners == null || listeners.size === 0)) { 247 | // Creating unhandled promise rejection if no error handlers are set 248 | Promise.reject(value) 249 | } 250 | if (listeners == null) return 251 | for (const listener of listeners) { 252 | if (listener.once === true) 253 | listeners.delete(listener) 254 | listener(value) 255 | } 256 | } 257 | 258 | addListener: On = this.on 259 | 260 | removeListener: Off = this.off 261 | 262 | iterUpdates = (): AsyncIterableIterator => { 263 | if (this._client.val == null) throw new Error('The client is closed') 264 | 265 | const unconsumedEvents = new Queue() 266 | let defer: DeferredPromise, Error> | null = null 267 | let finished = false 268 | 269 | const finish = () => { 270 | this.off('update', onUpdate) 271 | finished = true 272 | debug('Finished an async iterator') 273 | } 274 | 275 | function onUpdate (update: Td.Update) { 276 | if (update._ === 'updateAuthorizationState' && 277 | update.authorization_state._ == 'authorizationStateClosed') { 278 | finish() 279 | } 280 | if (defer != null) { 281 | defer.resolve({ done: false, value: update }) 282 | defer = null 283 | } else { 284 | unconsumedEvents.push(update) 285 | } 286 | } 287 | 288 | this.on('update', onUpdate) 289 | 290 | const iterator: AsyncIterableIterator = { 291 | next () { 292 | if (!unconsumedEvents.isEmpty()) { 293 | const update = unconsumedEvents.shift()! 294 | return Promise.resolve({ done: false, value: update }) 295 | } 296 | if (finished) 297 | return Promise.resolve({ done: true, value: undefined }) 298 | if (defer != null) { 299 | finish() 300 | throw new Error('Cannot call next() twice in succession') 301 | } 302 | return new Promise((resolve, reject) => { 303 | defer = { resolve, reject } 304 | }) 305 | }, 306 | 307 | return () { 308 | finish() 309 | return Promise.resolve({ done: true, value: undefined }) 310 | }, 311 | 312 | [Symbol.asyncIterator]() { 313 | return iterator 314 | } 315 | } 316 | 317 | return iterator 318 | } 319 | 320 | private _finishInit (): void { 321 | debug('Finished initialization') 322 | this._initialized = true 323 | for (const r of this._preinitRequests) 324 | this._send(r.request, r.id) 325 | this._preinitRequests = [] 326 | } 327 | 328 | invoke: Td.Invoke = (request: any): Promise => { 329 | const id = this._requestId 330 | this._requestId++ 331 | if (id >= Number.MAX_SAFE_INTEGER) 332 | throw new Error('Too large request id') 333 | const responsePromise = new Promise((resolve, reject) => { 334 | this._pending.set(id, { resolve, reject }) 335 | }) 336 | if (this._initialized === false) { 337 | this._preinitRequests.push({ request, id }) 338 | return responsePromise 339 | } 340 | this._send(request, id) 341 | return responsePromise 342 | } 343 | 344 | // Sends { _: 'close' } and waits until the client gets destroyed 345 | close = (): Promise => { 346 | debug('close') 347 | return new Promise(resolve => { 348 | if (this._client.val == null) return resolve() 349 | this._sendTdl({ _: 'close' }) 350 | this.once('close', () => resolve()) 351 | }) 352 | } 353 | 354 | // There's a bit of history behind this renaming of @type to _ in tdl. 355 | // Initially, it was because this code was written in Flow which had a bug 356 | // with disjoint unions (https://flow.org/en/docs/lang/refinements/) 357 | // not working if the tag is referenced via square brackets. _ has been chosen 358 | // because it is already an old convention in JS MTProto libraries and 359 | // webogram. The bug in Flow was later fixed, however the renaming is kept, 360 | // since it is more convenient to write if (o._ === '...') instead of 361 | // if (o['@type'] === '...'). Funny, other JS TDLib libraries also followed 362 | // with this renaming to _. 363 | 364 | private _send (request: Td.$Function, extra: unknown): void { 365 | debugReq('send', request) 366 | const renamedRequest = deepRenameKey('_', '@type', request) 367 | renamedRequest['@extra'] = extra 368 | const tdRequest = JSON.stringify(renamedRequest) 369 | if (this._client.val == null) 370 | throw new Error('A closed client cannot be reused, create a new client') 371 | if (this._client.isTdn) 372 | this._tdjson.tdnew.send(this._client.val, tdRequest) 373 | else 374 | this._tdjson.tdold.send(this._client.val, tdRequest) 375 | } 376 | 377 | private _sendTdl (request: Td.$Function): void { 378 | this._send(request, TDL_MAGIC) 379 | } 380 | 381 | private _handleClose (): void { 382 | if (this._client.val == null) { 383 | debug('Trying to close an already closed client') 384 | return 385 | } 386 | if (!this._client.isTdn) 387 | this._tdjson.tdold.destroy(this._client.val) 388 | this._client.val = null 389 | this.emit('close') 390 | debug('closed') 391 | } 392 | 393 | // Used with the old tdjson interface 394 | private async _loop (): Promise { 395 | if (this._client.isTdn) 396 | throw new Error('Can start the loop in the old tdjson interface only') 397 | try { 398 | while (true) { 399 | if (this._client.val === null) { 400 | debug('receive loop: destroyed client') 401 | break 402 | } 403 | const responseString = await this._tdjson.tdold.receive(this._client.val) 404 | if (responseString == null) { 405 | debug('receive loop: response is empty') 406 | continue 407 | } 408 | const res = JSON.parse(responseString) 409 | this.handleReceive(res) 410 | } 411 | } catch (e) { 412 | this._handleClose() 413 | throw e 414 | } 415 | } 416 | 417 | // Can be called by the client manager in case the new interface is used 418 | handleReceive (res: object): void { 419 | try { 420 | this._handleReceive(deepRenameKey('@type', '_', res)) 421 | } catch (e: unknown) { 422 | debug('handleReceive: caught error', e) 423 | const error = e instanceof Error ? e : new UnknownError(e) 424 | this.emit('error', error) 425 | } 426 | } 427 | 428 | // This function can be called with any TDLib object 429 | private _handleReceive (res: any): void { 430 | debugReceive(res) 431 | 432 | const isError = res._ === 'error' 433 | 434 | const id = res['@extra'] 435 | const defer = id != null ? this._pending.get(id) : undefined 436 | 437 | if (defer != null) { 438 | // a response to a request made by client.invoke 439 | delete res['@extra'] 440 | this._pending.delete(id) 441 | if (isError) 442 | defer.reject(new TDLibError(res.code, res.message)) 443 | else 444 | defer.resolve(res) 445 | return 446 | } 447 | 448 | if (isError) { 449 | // error not connected to any request. we'll emit it 450 | // the error may still potentially have @extra and it's good to save that 451 | const resError: Td.error = res 452 | const error = new TDLibError(resError.code, resError.message) 453 | if (id != null) (error as any)['@extra'] = id 454 | throw error 455 | } 456 | 457 | if (id === TDL_MAGIC) { 458 | // a response to a request sent by tdl itself (during initialization) 459 | // it's irrelevant, just ignoring it (it's most likely `{ _: 'ok' }`) 460 | debug('(TDL_MAGIC) Not emitting response', res) 461 | return 462 | } 463 | 464 | // if the object is not connected to any known request, we treat it as an 465 | // update. note that in a weird case (maybe if the @extra was manually set) 466 | // it still can contain the @extra field, this is intended and we want to 467 | // pass it further to client.on('update') 468 | this._handleUpdate(res) 469 | } 470 | 471 | private _handleUpdate (update: Td.Update): void { 472 | // updateOption, updateConnectionState, updateAuthorizationState 473 | // are always emitted, even with skipOldUpdates set to true 474 | switch (update._) { 475 | case 'updateOption': 476 | if (update.name === 'version' && update.value._ === 'optionValueString') { 477 | debug('Received version:', update.value.value) 478 | this._version = new Version(update.value.value) 479 | } 480 | break 481 | 482 | case 'updateConnectionState': 483 | debug('New connection state:', update.state) 484 | this._connectionStateName = update.state._ 485 | break 486 | 487 | case 'updateAuthorizationState': 488 | debug('New authorization state:', update.authorization_state._) 489 | this._authorizationState = update.authorization_state 490 | if (update.authorization_state._ === 'authorizationStateClosed') 491 | this._handleClose() 492 | else if (!this._initialized) 493 | this._handleAuthInit(update.authorization_state) 494 | break 495 | 496 | default: 497 | const shouldSkip = this._options.skipOldUpdates 498 | && this._connectionStateName === 'connectionStateUpdating' 499 | if (shouldSkip) return 500 | } 501 | this.emit('update', update) 502 | } 503 | 504 | private _handleAuthInit (authState: Td.AuthorizationState): void { 505 | // Note: pre-initialization requests should not call client.invoke 506 | switch (authState._) { 507 | case 'authorizationStateWaitTdlibParameters': 508 | if (this._version.lt(TDLIB_1_8_6)) { 509 | this._sendTdl({ 510 | _: 'setTdlibParameters', 511 | parameters: { 512 | database_directory: resolvePath(this._options.databaseDirectory), 513 | files_directory: resolvePath(this._options.filesDirectory), 514 | api_id: this._options.apiId, 515 | api_hash: this._options.apiHash, 516 | use_test_dc: this._options.useTestDc, 517 | ...this._options.tdlibParameters, 518 | _: 'tdlibParameters' 519 | } 520 | } as any) 521 | } else { 522 | this._sendTdl({ 523 | database_directory: resolvePath(this._options.databaseDirectory), 524 | files_directory: resolvePath(this._options.filesDirectory), 525 | api_id: this._options.apiId, 526 | api_hash: this._options.apiHash, 527 | use_test_dc: this._options.useTestDc, 528 | database_encryption_key: this._options.databaseEncryptionKey, 529 | ...this._options.tdlibParameters, 530 | _: 'setTdlibParameters' 531 | }) 532 | this._finishInit() 533 | } 534 | return 535 | 536 | // @ts-expect-error: This update can be received in TDLib <= v1.8.5 only 537 | case 'authorizationStateWaitEncryptionKey': 538 | this._sendTdl({ 539 | _: 'checkDatabaseEncryptionKey', 540 | encryption_key: this._options.databaseEncryptionKey 541 | } as any) 542 | this._finishInit() 543 | } 544 | } 545 | 546 | login = (arg: LoginDetails | (() => LoginDetails) = {}): Promise => { 547 | return new Promise((resolve, reject) => { 548 | if (this._client.val == null) 549 | return reject(new Error('The client is closed')) 550 | let cachedLoginDetails: StrictLoginDetails | null = null 551 | function needLoginDetails (): StrictLoginDetails { 552 | if (cachedLoginDetails == null) { 553 | cachedLoginDetails = mergeDeepRight( 554 | defaultLoginDetails, 555 | typeof arg === 'function' ? arg() : arg 556 | ) as StrictLoginDetails 557 | } 558 | return cachedLoginDetails 559 | } 560 | function needUserLogin (): LoginUser { 561 | const loginDetails = needLoginDetails() 562 | if (loginDetails.type !== 'user') 563 | throw new Error('Expected to log in as a bot, received user auth update') 564 | return loginDetails 565 | } 566 | const processAuthorizationState = async (authState: Td.AuthorizationState) => { 567 | // Note: authorizationStateWaitPhoneNumber may not be the first update 568 | // in the login flow in case of a previous incomplete login attempt 569 | try { 570 | switch (authState._) { 571 | case 'authorizationStateReady': { 572 | // Finished (this may be the first update if already logged in) 573 | this.off('update', onUpdate) 574 | resolve(undefined) 575 | return 576 | } 577 | 578 | case 'authorizationStateClosed': { 579 | throw new Error('Received authorizationStateClosed') 580 | } 581 | 582 | case 'authorizationStateWaitPhoneNumber': { 583 | const loginDetails = needLoginDetails() 584 | let retry = false 585 | if (loginDetails.type === 'user') { 586 | while (true) { 587 | const phoneNumber = await loginDetails.getPhoneNumber(retry) 588 | try { 589 | await this.invoke({ 590 | _: 'setAuthenticationPhoneNumber', 591 | phone_number: phoneNumber 592 | }) 593 | return 594 | } catch (e: any) { 595 | if (e?.message === 'PHONE_NUMBER_INVALID') retry = true 596 | else throw e 597 | } 598 | } 599 | } else { 600 | while (true) { 601 | const token = await loginDetails.getToken(retry) 602 | try { 603 | await this.invoke({ 604 | _: 'checkAuthenticationBotToken', 605 | token 606 | }) 607 | return 608 | } catch (e: any) { 609 | if (e?.message === 'ACCESS_TOKEN_INVALID') retry = true 610 | else throw e 611 | } 612 | } 613 | } 614 | } 615 | 616 | // TDLib >= v1.8.6 only 617 | case 'authorizationStateWaitEmailAddress': { 618 | const loginDetails = needUserLogin() 619 | await this.invoke({ 620 | _: 'setAuthenticationEmailAddress', 621 | email_address: await loginDetails.getEmailAddress() 622 | }) 623 | return 624 | } 625 | 626 | // TDLib >= v1.8.6 only 627 | case 'authorizationStateWaitEmailCode': { 628 | const loginDetails = needUserLogin() 629 | await this.invoke({ 630 | _: 'checkAuthenticationEmailCode', 631 | code: { 632 | // Apple ID and Google ID are not supported 633 | _: 'emailAddressAuthenticationCode', 634 | code: await loginDetails.getEmailCode() 635 | } 636 | }) 637 | return 638 | } 639 | 640 | case 'authorizationStateWaitOtherDeviceConfirmation': { 641 | const loginDetails = needUserLogin() 642 | loginDetails.confirmOnAnotherDevice(authState.link) 643 | return 644 | } 645 | 646 | case 'authorizationStateWaitCode': { 647 | const loginDetails = needUserLogin() 648 | let retry = false 649 | while (true) { 650 | const code = await loginDetails.getAuthCode(retry) 651 | try { 652 | await this.invoke({ 653 | _: 'checkAuthenticationCode', 654 | code 655 | }) 656 | return 657 | } catch (e: any) { 658 | if (e?.message === 'PHONE_CODE_EMPTY' || e?.message === 'PHONE_CODE_INVALID') 659 | retry = true 660 | else 661 | throw e 662 | } 663 | } 664 | } 665 | 666 | case 'authorizationStateWaitRegistration': { 667 | const loginDetails = needUserLogin() 668 | const { firstName, lastName = '' } = await loginDetails.getName() 669 | await this.invoke({ 670 | _: 'registerUser', 671 | first_name: firstName, 672 | last_name: lastName 673 | }) 674 | return 675 | } 676 | 677 | case 'authorizationStateWaitPassword': { 678 | const loginDetails = needUserLogin() 679 | const passwordHint = authState.password_hint 680 | let retry = false 681 | while (true) { 682 | const password = await loginDetails.getPassword(passwordHint, retry) 683 | try { 684 | await this.invoke({ 685 | _: 'checkAuthenticationPassword', 686 | password 687 | }) 688 | return 689 | } catch (e: any) { 690 | if (e?.message === 'PASSWORD_HASH_INVALID') retry = true 691 | else throw e 692 | } 693 | } 694 | } 695 | } 696 | } catch (e) { 697 | this.off('update', onUpdate) 698 | reject(e) 699 | } 700 | } 701 | function onUpdate (update: Td.Update): void { 702 | if (update._ !== 'updateAuthorizationState') return 703 | processAuthorizationState(update.authorization_state) 704 | } 705 | // Process last received authorization state first 706 | if (this._authorizationState != null) 707 | processAuthorizationState(this._authorizationState) 708 | this.on('update', onUpdate) 709 | }) 710 | } 711 | 712 | loginAsBot = (token: string | (() => string | Promise)): Promise => { 713 | return this.login({ 714 | type: 'bot', 715 | getToken: retry => retry 716 | ? Promise.reject(new Error('Invalid bot token')) 717 | : Promise.resolve(typeof token === 'string' ? token : token()) 718 | }) 719 | } 720 | 721 | isClosed = (): boolean => { 722 | return this._client.val == null 723 | } 724 | } 725 | -------------------------------------------------------------------------------- /packages/tdl/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import Debug from 'debug' 3 | import { Client, TDLibError, UnknownError, type ClientOptions } from './client' 4 | import { loadAddon, type Tdjson } from './addon' 5 | import { deepRenameKey } from './util' 6 | import type { Execute } from 'tdlib-types' 7 | 8 | const debug = Debug('tdl') 9 | 10 | let tdjsonAddon: Tdjson | null = null 11 | 12 | const defaultLibraryFile = (() => { 13 | switch (process.platform) { 14 | case 'win32': return 'tdjson.dll' 15 | case 'darwin': return 'libtdjson.dylib' 16 | default: return 'libtdjson.so' 17 | } 18 | })() 19 | 20 | export type TDLibConfiguration = { 21 | tdjson?: string, 22 | libdir?: string, 23 | verbosityLevel?: number | 'default', 24 | receiveTimeout?: number, 25 | useOldTdjsonInterface?: boolean 26 | } 27 | 28 | const cfg: Required = { 29 | tdjson: defaultLibraryFile, 30 | libdir: '', 31 | verbosityLevel: 1, 32 | receiveTimeout: 10, 33 | useOldTdjsonInterface: false 34 | } 35 | 36 | export function configure (opts: TDLibConfiguration = {}): void { 37 | if (tdjsonAddon) 38 | throw Error('TDLib is already initialized; too late to configure') 39 | if (opts.tdjson != null) cfg.tdjson = opts.tdjson 40 | if (opts.libdir != null) cfg.libdir = opts.libdir 41 | if (opts.verbosityLevel != null) cfg.verbosityLevel = opts.verbosityLevel 42 | if (opts.receiveTimeout != null) cfg.receiveTimeout = opts.receiveTimeout 43 | if (opts.useOldTdjsonInterface != null) cfg.useOldTdjsonInterface = opts.useOldTdjsonInterface 44 | } 45 | 46 | export function init (): void { 47 | if (tdjsonAddon != null) return 48 | debug('Initializing the node addon') 49 | const lib = path.join(cfg.libdir, cfg.tdjson) 50 | tdjsonAddon = loadAddon(lib, cfg.useOldTdjsonInterface) 51 | if (cfg.verbosityLevel !== 'default') { 52 | debug('Executing setLogVerbosityLevel', cfg.verbosityLevel) 53 | const request = JSON.stringify({ 54 | '@type': 'setLogVerbosityLevel', 55 | new_verbosity_level: cfg.verbosityLevel 56 | }) 57 | const response = !cfg.useOldTdjsonInterface 58 | ? tdjsonAddon.tdnew.execute(request) 59 | : tdjsonAddon.tdold.execute(null, request) 60 | debug('setLogVerbosityLevel response:', response) 61 | } 62 | } 63 | 64 | export const execute: Execute = function execute (request: any): any { 65 | if (tdjsonAddon == null) { 66 | init() 67 | if (tdjsonAddon == null) throw Error('TDLib is uninitialized') 68 | } 69 | debug('execute', request) 70 | request = JSON.stringify(deepRenameKey('_', '@type', request)) 71 | const response = !cfg.useOldTdjsonInterface 72 | ? tdjsonAddon.tdnew.execute(request) 73 | : tdjsonAddon.tdold.execute(null, request) 74 | return deepRenameKey('@type', '_', JSON.parse(response)) 75 | } 76 | 77 | export function setLogMessageCallback ( 78 | maxVerbosityLevel: number, 79 | callback: null | ((verbosityLevel: number, message: string) => void) 80 | ): void { 81 | if (tdjsonAddon == null) { 82 | init() 83 | if (tdjsonAddon == null) throw Error('TDLib is uninitialized') 84 | } 85 | tdjsonAddon.setLogMessageCallback(maxVerbosityLevel, callback) 86 | } 87 | 88 | const clientMap: Map = new Map() 89 | let tdnInitialized = false 90 | let runningReceiveLoop = false 91 | 92 | // Loop for the new tdjson interface 93 | async function receiveLoop () { 94 | debug('Starting tdn receive loop') 95 | if (tdjsonAddon == null) throw new Error('TDLib is uninitialized') 96 | runningReceiveLoop = true 97 | try { 98 | tdjsonAddon.tdnew.ref() 99 | while (true) { 100 | if (clientMap.size < 1) { 101 | debug('Stopping receive loop') 102 | break 103 | } 104 | const responseString = await tdjsonAddon.tdnew.receive() 105 | if (responseString == null) { 106 | debug('Receive loop: got empty response') 107 | continue 108 | } 109 | const res = JSON.parse(responseString) 110 | const clientId = res['@client_id'] 111 | const client = clientId != null ? clientMap.get(clientId) : undefined 112 | if (client == null) { 113 | debug(`Cannot find client_id ${clientId}`) 114 | continue 115 | } 116 | delete res['@client_id'] // Note that delete is somewhat slow 117 | client.handleReceive(res) 118 | } 119 | } finally { 120 | runningReceiveLoop = false 121 | tdjsonAddon.tdnew.unref() 122 | } 123 | } 124 | 125 | function createAnyClient (opts: ClientOptions, bare = false): Client { 126 | if (tdjsonAddon == null) { 127 | init() 128 | if (tdjsonAddon == null) throw Error('TDLib is uninitialized') 129 | } 130 | const managingOpts = { 131 | bare, 132 | receiveTimeout: cfg.receiveTimeout, 133 | executeFunc: execute, 134 | useOldTdjsonInterface: false 135 | } 136 | if (cfg.useOldTdjsonInterface) { 137 | const tdoManaging = { ...managingOpts, useOldTdjsonInterface: true } 138 | return new Client(tdjsonAddon, tdoManaging, opts) 139 | } 140 | if (!tdnInitialized) { 141 | tdjsonAddon.tdnew.init(cfg.receiveTimeout) 142 | tdnInitialized = true 143 | } 144 | const client = new Client(tdjsonAddon, managingOpts, opts) 145 | const clientId = client.getClientId() 146 | clientMap.set(clientId, client) 147 | client.once('close', () => { 148 | debug(`Deleting client_id ${clientId}`) 149 | clientMap.delete(clientId) 150 | }) 151 | if (!runningReceiveLoop) 152 | receiveLoop() 153 | return client 154 | } 155 | 156 | export function createClient (opts: ClientOptions): Client { 157 | return createAnyClient(opts) 158 | } 159 | 160 | export function createBareClient (): Client { 161 | return createAnyClient({}, true) 162 | } 163 | 164 | export { TDLibError, UnknownError } 165 | 166 | // TODO: We could possibly export an unsafe/unstable getRawTdjson() : Tdjson 167 | // function that allows to access underlying tdjson functions 168 | -------------------------------------------------------------------------------- /packages/tdl/src/prompt.ts: -------------------------------------------------------------------------------- 1 | import * as readline from 'node:readline' 2 | 3 | function prompt (query: string): Promise { 4 | return new Promise((resolve, reject) => { 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout 8 | }) 9 | 10 | rl.on('SIGINT', () => { 11 | rl.close() 12 | reject(new Error('Cancelled')) 13 | }) 14 | 15 | rl.on('error', reject) 16 | 17 | rl.question(query, answer => { 18 | rl.close() 19 | resolve(answer.replace(/[\r\n]*$/, '')) 20 | }) 21 | }) 22 | } 23 | 24 | export const getPhoneNumber = (retry?: boolean): Promise => 25 | prompt(retry 26 | ? 'Invalid phone number, please re-enter: ' 27 | : 'Please enter your phone number: ') 28 | 29 | export const getEmailAddress = (): Promise => 30 | prompt('Please enter your email address: ') 31 | 32 | export const getEmailCode = (): Promise => 33 | prompt('Please enter the email auth code you received: ') 34 | 35 | export const confirmOnAnotherDevice = (link: string): void => 36 | console.log(`Please confirm on another device: ${link}`) 37 | 38 | export const getAuthCode = (retry?: boolean): Promise => 39 | prompt(retry 40 | ? 'Wrong auth code, please re-enter: ' 41 | : 'Please enter the auth code you received: ') 42 | 43 | export const getPassword = (passwordHint: string, retry?: boolean): Promise => { 44 | const hint = passwordHint ? ` (hint: "${passwordHint}")` : '' 45 | const query = retry 46 | ? `Wrong password, please re-enter${hint}: ` 47 | : `Please enter your 2FA password${hint}: ` 48 | return prompt(query) 49 | } 50 | 51 | type GetName = () => Promise<{ firstName: string, lastName?: string }> 52 | export const getName: GetName = async () => ({ 53 | firstName: await prompt('Please enter your first name: '), 54 | lastName: await prompt('Please enter your last name (optional): ') || undefined 55 | }) 56 | -------------------------------------------------------------------------------- /packages/tdl/src/queue.ts: -------------------------------------------------------------------------------- 1 | // The queue implementation is taken from Node.js 2 | // https://github.com/nodejs/node/blob/bae03c4e30f927676203f61ff5a34fe0a0c0bbc9/lib/internal/fixed_queue.js 3 | // The authors are Node.js contributors, MIT license. 4 | 5 | const kSize = 2048 6 | const kMask = kSize - 1 7 | 8 | class FixedCircularBuffer { 9 | private bottom: number = 0 10 | private top: number = 0 11 | private list: (T | undefined)[] = new Array(kSize) 12 | public next: FixedCircularBuffer | null = null 13 | 14 | constructor() {} 15 | 16 | isEmpty() { 17 | return this.top === this.bottom 18 | } 19 | 20 | isFull() { 21 | return ((this.top + 1) & kMask) === this.bottom 22 | } 23 | 24 | push(data: T) { 25 | this.list[this.top] = data 26 | this.top = (this.top + 1) & kMask 27 | } 28 | 29 | shift(): T | null { 30 | const nextItem = this.list[this.bottom] 31 | if (nextItem === undefined) 32 | return null 33 | this.list[this.bottom] = undefined 34 | this.bottom = (this.bottom + 1) & kMask 35 | return nextItem 36 | } 37 | } 38 | 39 | export class Queue { 40 | private head: FixedCircularBuffer 41 | private tail: FixedCircularBuffer 42 | 43 | constructor() { 44 | this.head = this.tail = new FixedCircularBuffer() 45 | } 46 | 47 | isEmpty(): boolean { 48 | return this.head.isEmpty() 49 | } 50 | 51 | push(data: T) { 52 | if (this.head.isFull()) { 53 | // Head is full: Creates a new queue, sets the old queue's `.next` to it, 54 | // and sets it as the new main queue. 55 | this.head = this.head.next = new FixedCircularBuffer() 56 | } 57 | this.head.push(data) 58 | } 59 | 60 | shift(): T | null { 61 | const tail = this.tail 62 | const next = tail.shift() 63 | if (tail.isEmpty() && tail.next !== null) { 64 | // If there is another queue, it forms the new tail. 65 | this.tail = tail.next 66 | tail.next = null 67 | } 68 | return next 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/tdl/src/util.ts: -------------------------------------------------------------------------------- 1 | function isObject (item: unknown): item is object { 2 | return item != null && typeof item === 'object' && !Array.isArray(item) 3 | } 4 | 5 | export function mergeDeepRight < 6 | A extends { [key: string]: any }, 7 | B extends { [key: string]: any } 8 | >(obj1: A, obj2: B): B & A { 9 | if (isObject(obj1) && isObject(obj2)) { 10 | const result: any = {} 11 | for (const key2 in obj2) { 12 | const val1 = obj1[key2] 13 | const val2 = obj2[key2] 14 | result[key2] = mergeDeepRight(val1, val2) 15 | } 16 | for (const key1 in obj1) { 17 | if (!(key1 in result)) 18 | result[key1] = obj1[key1] 19 | } 20 | return result 21 | } 22 | return obj2 as any 23 | } 24 | 25 | /** Renames `oldKey` to `newKey` deeply. The objects should not contain functions. */ 26 | export function deepRenameKey ( 27 | oldKey: string, 28 | newKey: string, 29 | v: { readonly [key: string]: any } 30 | ): { [key: string]: any } { 31 | if (Array.isArray(v)) 32 | return v.map(x => deepRenameKey(oldKey, newKey, x)) 33 | if (typeof v === 'object' && v !== null) { 34 | const newObj: any = {} 35 | for (const k in v) 36 | newObj[k === oldKey ? newKey : k] = deepRenameKey(oldKey, newKey, v[k]) 37 | return newObj 38 | } 39 | return v 40 | } 41 | -------------------------------------------------------------------------------- /packages/tdl/src/version.ts: -------------------------------------------------------------------------------- 1 | export class Version { 2 | private readonly _major: number 3 | private readonly _minor: number 4 | private readonly _patch: number 5 | 6 | /** Parse version from a string */ 7 | constructor (ver: string) { 8 | const [majorStr, minorStr, patchStr] = ver.split('.') 9 | const major = Number(majorStr) 10 | const minor = Number(minorStr) 11 | const patch = Number(patchStr) || 0 12 | if (!majorStr || !minorStr || Number.isNaN(major) || Number.isNaN(minor)) 13 | throw new Error(`Invalid TDLib version '${ver}'`) 14 | this._major = major 15 | this._minor = minor 16 | this._patch = patch 17 | } 18 | 19 | /** v1.gt(v2) is v1 > v2 */ 20 | gt (other: Version): boolean { 21 | if (this._major > other._major) return true 22 | if (this._major < other._major) return false 23 | if (this._minor > other._minor) return true 24 | if (this._minor < other._minor) return false 25 | return this._patch > other._patch 26 | } 27 | 28 | /** v1.lt(v2) is v1 < v2 */ 29 | lt (other: Version): boolean { 30 | if (this._major < other._major) return true 31 | if (this._major > other._major) return false 32 | if (this._minor < other._minor) return true 33 | if (this._minor > other._minor) return false 34 | return this._patch < other._patch 35 | } 36 | 37 | /** v1.gte(v2) is v1 >= v2 */ 38 | gte (other: Version): boolean { 39 | if (this._major > other._major) return true 40 | if (this._major < other._major) return false 41 | if (this._minor > other._minor) return true 42 | if (this._minor < other._minor) return false 43 | return this._patch >= other._patch 44 | } 45 | 46 | /** v1.lte(v2) is v1 <= v2 */ 47 | lte (other: Version): boolean { 48 | if (this._major < other._major) return true 49 | if (this._major > other._major) return false 50 | if (this._minor < other._minor) return true 51 | if (this._minor > other._minor) return false 52 | return this._patch <= other._patch 53 | } 54 | 55 | /** v1.eq(v2) is v1 == v2 */ 56 | eq (other: Version): boolean { 57 | return this._major === other._major 58 | && this._minor === other._minor 59 | && this._patch === other._patch 60 | } 61 | 62 | toString (): string { 63 | return `${this._major}.${this._minor}.${this._patch}` 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/tdl/tests/rename.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest' 2 | // @ts-expect-error 3 | import { deepRenameKey } from '../dist/util.js' 4 | 5 | const OBJ = Object.freeze({ 6 | _: 'chat', 7 | order: '0', 8 | is_pinned: false, 9 | type: { 10 | _: 'chatTypeSupergroup', 11 | is_channel: true, 12 | }, 13 | innerObjWithoutType: { 14 | k1: 'v1', 15 | k2: 'v2', 16 | nil: null, 17 | und: undefined, 18 | }, 19 | nullField: null, 20 | undefinedField: undefined, 21 | numbers: 1234, 22 | emptyStr: '', 23 | typeInStr: '@type', 24 | underscoreInStr: '_', 25 | arrayTest: [ 26 | { 27 | _: 'chatPermissions', 28 | can_send_messages: false, 29 | can_send_media_messages: false, 30 | can_send_polls: false, 31 | innerArray: [ 32 | { 33 | _: 'type', 34 | }, 35 | ], 36 | }, 37 | [ 38 | [ 39 | { 40 | _: 'hello', 41 | }, 42 | {}, 43 | ], 44 | ], 45 | { 46 | _: 'chatNotificationSettings', 47 | use_default_mute_for: true, 48 | mute_for: 0, 49 | }, 50 | ], 51 | }) 52 | 53 | test('deepRenameKey', () => { 54 | const obj = { ...OBJ } 55 | const newObj = deepRenameKey('_', '@type', obj) 56 | // Should not change the original object 57 | expect(obj).toStrictEqual(OBJ) 58 | expect(newObj).toMatchInlineSnapshot(` 59 | { 60 | "@type": "chat", 61 | "arrayTest": [ 62 | { 63 | "@type": "chatPermissions", 64 | "can_send_media_messages": false, 65 | "can_send_messages": false, 66 | "can_send_polls": false, 67 | "innerArray": [ 68 | { 69 | "@type": "type", 70 | }, 71 | ], 72 | }, 73 | [ 74 | [ 75 | { 76 | "@type": "hello", 77 | }, 78 | {}, 79 | ], 80 | ], 81 | { 82 | "@type": "chatNotificationSettings", 83 | "mute_for": 0, 84 | "use_default_mute_for": true, 85 | }, 86 | ], 87 | "emptyStr": "", 88 | "innerObjWithoutType": { 89 | "k1": "v1", 90 | "k2": "v2", 91 | "nil": null, 92 | "und": undefined, 93 | }, 94 | "is_pinned": false, 95 | "nullField": null, 96 | "numbers": 1234, 97 | "order": "0", 98 | "type": { 99 | "@type": "chatTypeSupergroup", 100 | "is_channel": true, 101 | }, 102 | "typeInStr": "@type", 103 | "undefinedField": undefined, 104 | "underscoreInStr": "_", 105 | } 106 | `) 107 | }) 108 | -------------------------------------------------------------------------------- /packages/tdl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "../../typings/*", 9 | "src/**/*" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /scripts/run-prebuilt-tdlib.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Requires gh, jq 3 | 4 | set -euo pipefail 5 | 6 | if [ $# -lt 1 ]; then 7 | echo "Too few arguments: expected TDLib version" 8 | exit 1 9 | fi 10 | 11 | version=$1 12 | npm_patch="${2:-0}" 13 | 14 | if [ "$version" = "latest" ]; then 15 | commit=$(gh api repos/tdlib/td/commits) 16 | else 17 | commit=$(gh search commits "Update version to $version" -R tdlib/td --json commit,sha) 18 | fi 19 | 20 | if [ "$commit" = "[]" ]; then 21 | echo "Cannot find version $version" 22 | exit 1 23 | fi 24 | 25 | message=$(echo $commit | jq -r '.[0].commit.message') 26 | sha=$(echo $commit | jq -r '.[0].sha') 27 | 28 | command="gh workflow run prebuilt-tdlib.yml --ref main -f tdlib=$sha -f npm-patch=$npm_patch" 29 | 30 | echo "TDLib commit: https://github.com/tdlib/td/commit/$sha" 31 | echo "Message: \"$message\"" 32 | echo "Command: $command" 33 | read -p "Run? (Ctrl-c or n to exit) " answer 34 | case $answer in 35 | [nN] ) exit ;; 36 | * ) eval $command ;; 37 | esac 38 | -------------------------------------------------------------------------------- /scripts/update-license-year.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Requires fd and sd 4 | 5 | fd LICENSE | xargs -L 1 sd ' (\d{4})-\d{4} ' " \$1-$(date '+%Y') " 6 | -------------------------------------------------------------------------------- /scripts/update-types.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Should be executed with project root as the cwd 3 | npx tdl-install-types -o typings/tdlib-types.d.ts --flow prebuilt-tdlib 4 | -------------------------------------------------------------------------------- /test.cfg.example: -------------------------------------------------------------------------------- 1 | TEST_API_ID=51235 2 | TEST_API_HASH=******************************* 3 | TEST_BOT_TOKEN=1234566:FOOBARFOOBARFOOBAR 4 | -------------------------------------------------------------------------------- /tests/auth-only/bot.test.ts: -------------------------------------------------------------------------------- 1 | // This file includes tests that require telegram authentication (and also 2 | // database creation). It takes API_ID, API_HASH, and the bot token from the 3 | // test.cfg file. These tests are run manually only. 4 | 5 | import { describe, test, expect, beforeEach, afterEach } from 'vitest' 6 | import * as path from 'node:path' 7 | import * as fs from 'node:fs' 8 | import * as fsp from 'node:fs/promises' 9 | import * as tdl from '../../packages/tdl' 10 | import { getTdjson } from 'prebuilt-tdlib' 11 | 12 | const projectRoot = path.join(__dirname, '..', '..') 13 | 14 | tdl.configure({ tdjson: getTdjson() }) 15 | 16 | const databaseDirectory = path.join(__dirname, '..', '_td_database') 17 | const filesDirectory = path.join(__dirname, '..', '_td_files') 18 | 19 | const envrcLines = fs.readFileSync(path.join(projectRoot, 'test.cfg')) 20 | .toString() 21 | .trim() 22 | .split(/\r?\n/) 23 | .map(l => l.trim()) 24 | 25 | let apiId: number | undefined 26 | let apiHash: string | undefined 27 | let botToken: string | undefined 28 | 29 | for (const line of envrcLines) { 30 | if (line[0] === '#') continue 31 | const [k, v] = line.split('=') 32 | switch (k) { 33 | case 'TEST_API_ID': apiId = Number(v); break 34 | case 'TEST_API_HASH': apiHash = v; break 35 | case 'TEST_BOT_TOKEN': botToken = v; break 36 | } 37 | } 38 | 39 | if (apiId == null) throw new Error('No api id') 40 | if (apiHash == null) throw new Error('No api hash') 41 | if (botToken == null) throw new Error('No bot token') 42 | 43 | describe('tdl with an authenticated client', () => { 44 | let client: tdl.Client 45 | 46 | beforeEach(() => { 47 | client = tdl.createClient({ 48 | apiId, 49 | apiHash, 50 | databaseDirectory, 51 | filesDirectory, 52 | tdlibParameters: { 53 | use_file_database: false, 54 | use_chat_info_database: false, 55 | use_message_database: false 56 | } 57 | }) 58 | }) 59 | 60 | afterEach(async () => { 61 | await client.close() 62 | await fsp.rm(databaseDirectory, { recursive: true, force: true }) 63 | await fsp.rm(filesDirectory, { recursive: true, force: true }) 64 | }) 65 | 66 | test('make pre-auth request, log in successfully', async () => { 67 | const option = await client.invoke({ 68 | _: 'getOption', 69 | name: 'message_caption_length_max' 70 | }) 71 | expect(option._).toBe('optionValueInteger') 72 | await client.loginAsBot(botToken) 73 | const me = await client.invoke({ _: 'getMe' }) 74 | expect(me).toMatchObject({ 75 | _: 'user', 76 | phone_number: '' 77 | }) 78 | expect(me.type._).toBe('userTypeBot') 79 | }) 80 | 81 | test('can receive auth-only updates', async () => { 82 | (async () => { 83 | for await (const update of client.iterUpdates()) { 84 | // Can queue updates 85 | await new Promise(resolve => setTimeout(resolve, 50)) 86 | if (update._ === 'updateOption' && update.name === 'my_id') { 87 | expect(update.value._).toBe('optionValueInteger') 88 | return 89 | } 90 | } 91 | throw new Error('Failed to receive updateOption(my_id)') 92 | })() 93 | await client.loginAsBot(botToken) 94 | }) 95 | 96 | test('an exception in the client.on(update) handler is emitted as error', () => { 97 | return new Promise((resolve, reject) => { 98 | client.once('error', e => { 99 | try { 100 | expect(e).toBeInstanceOf(Error) 101 | expect(e.message).toBe('##TEST##') 102 | resolve(undefined) 103 | } catch (err) { 104 | reject(err) 105 | } 106 | }) 107 | client.on('error', () => { 108 | // Since we are not waiting for initialization to finish, TDLib will 109 | // send "Request aborted" 110 | }) 111 | client.once('update', () => { 112 | throw new Error('##TEST##') 113 | }) 114 | }) 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /tests/integration/shared.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, beforeAll, afterAll } from 'vitest' 2 | import * as path from 'node:path' 3 | import * as tdl from 'tdl' 4 | import type * as Td from 'tdlib-types' 5 | 6 | const projectRoot = path.join(__dirname, '..', '..') 7 | 8 | if (process.env.PREBUILT_PATH) { 9 | const prebuiltPath = process.env.PREBUILT_PATH 10 | console.log(`Testing prebuilt-tdlib from '${prebuiltPath}'`) 11 | const prebuiltTdlib = require(path.join(projectRoot, prebuiltPath)) 12 | tdl.configure({ tdjson: prebuiltTdlib.getTdjson() }) 13 | } else if (process.env.LIBTDJSON_PATH) { 14 | const tdjsonPath = process.env.LIBTDJSON_PATH 15 | console.log(`Testing tdjson from ${tdjsonPath}`) 16 | tdl.configure({ tdjson: path.resolve(projectRoot, tdjsonPath) }) 17 | } else if (process.env.LIBDIR_PATH) { 18 | const libdirPath = process.env.LIBDIR_PATH 19 | console.log(`Testing tdjson from the ${libdirPath} directory`) 20 | tdl.configure({ libdir: path.resolve(projectRoot, libdirPath) }) 21 | } else { 22 | tdl.configure({ tdjson: require('prebuilt-tdlib').getTdjson() }) 23 | } 24 | 25 | export function addTests (oldTdjson: boolean = false) { 26 | if (oldTdjson) 27 | tdl.configure({ useOldTdjsonInterface: true }) 28 | 29 | const client = tdl.createBareClient() 30 | const updates: Td.Update[] = [] 31 | 32 | client.on('error', e => console.error('error', e)) 33 | client.on('update', u => { 34 | // console.log('update', u) 35 | updates.push(u) 36 | }) 37 | 38 | async function expectUpdate (pred: (u: Td.Update) => boolean) { 39 | for (const u of updates) 40 | if (pred(u)) return 41 | for await (const u of client.iterUpdates()) 42 | if (pred(u)) return 43 | } 44 | 45 | beforeAll(() => { 46 | return expectUpdate(u => u._ === 'updateOption' && u.name === 'version') 47 | }, 2000) 48 | 49 | afterAll(async () => { 50 | await client.close() 51 | expect(client.isClosed()).toBe(true) 52 | }) 53 | 54 | test('The client should not be closed', () => { 55 | expect(client.isClosed()).toBe(false) 56 | }) 57 | 58 | test('authorizationStateWaitTdlibParameters has been received', () => { 59 | return expectUpdate(u => u._ === 'updateAuthorizationState' 60 | && u?.authorization_state._ === 'authorizationStateWaitTdlibParameters') 61 | }, 2000) 62 | 63 | test('getVersion() should return a version string', () => { 64 | const version = client.getVersion() 65 | expect(version).toBeTypeOf('string') 66 | expect(version).toMatch(/^1\./) 67 | }) 68 | 69 | test('invoke(testCallString) should respond with the same value', async () => { 70 | const response = await client.invoke({ _: 'testCallString', x: 'hi' }) 71 | expect(response).toStrictEqual({ _: 'testString', value: 'hi' }) 72 | }) 73 | 74 | test('invoke(testReturnError) should fail with the same error', async () => { 75 | const error = { _: 'error', code: 222, message: 'hi-error' } as const 76 | const responseP = client.invoke({ _: 'testReturnError', error }) 77 | await expect(responseP).rejects.toBeInstanceOf(tdl.TDLibError) 78 | return expect(responseP).rejects.toMatchObject(error) 79 | }) 80 | 81 | test('tdl.execute(getTextEntities) should synchronously return a textEntities object', () => { 82 | const response = tdl.execute({ _: 'getTextEntities', text: 'hi @mybot' }) 83 | expect(response).toMatchObject({ _: 'textEntities' }) 84 | }) 85 | 86 | test('tdl.setLogMessageCallback should set the callback', () => { 87 | return new Promise((resolve, reject) => { 88 | let receivedSpecial = 0 89 | tdl.setLogMessageCallback(5, (verbosityLevel, message) => { 90 | try { 91 | // console.log(verbosityLevel, 'Received message:', message) 92 | expect(verbosityLevel).toBeTypeOf('number') 93 | expect(message).toBeTypeOf('string') 94 | if (message.includes('TDL-SPECIAL')) receivedSpecial++ 95 | if (receivedSpecial >= 3) { 96 | tdl.setLogMessageCallback(5, null) 97 | // The process should exit with the callback attached 98 | tdl.setLogMessageCallback(1, () => {}) 99 | resolve(undefined) 100 | } 101 | } catch (err) { 102 | reject(err) 103 | } 104 | }) 105 | tdl.execute({ _: 'addLogMessage', verbosity_level: 1, text: 'TDL-SPECIAL 1' }) 106 | tdl.execute({ _: 'addLogMessage', verbosity_level: 1, text: 'TDL-SPECIAL 2' }) 107 | tdl.execute({ _: 'addLogMessage', verbosity_level: 1, text: 'TDL-SPECIAL 3' }) 108 | // client.invoke({ _: 'addLogMessage', verbosity_level: 0, text: 'TDL-SPECIAL 3' }) 109 | }) 110 | }, 2000) 111 | 112 | test('client.execute(getTextEntities) should synchronously return a textEntities object', () => { 113 | const response = client.execute({ _: 'getTextEntities', text: 'hi @mybot' }) 114 | expect(response).toMatchObject({ _: 'textEntities' }) 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /tests/integration/tdjson-old.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest' 2 | import { addTests } from './shared' 3 | 4 | describe('tdl + tdjson (useOldTdjsonInterface = true)', () => { 5 | addTests(true) 6 | }) 7 | -------------------------------------------------------------------------------- /tests/integration/tdjson.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest' 2 | import { addTests } from './shared' 3 | 4 | describe('tdl + tdjson', () => { 5 | addTests() 6 | }) 7 | -------------------------------------------------------------------------------- /tests/types/flow.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as tdl from '../../packages/tdl' 4 | import { getTdjson } from '../../packages/prebuilt-tdlib' 5 | import * as Td from 'tdlib-types' 6 | 7 | const { TDLibError, UnknownError } = tdl 8 | 9 | tdl.configure({ tdjson: 'libtdjson.dylib', libdir: '/usr/local/lib' }) 10 | tdl.configure({ verbosityLevel: 'default' }) 11 | tdl.configure({ libdir: __dirname }) 12 | tdl.configure({ libdir: '' }) 13 | tdl.configure({ tdjson: getTdjson() }) 14 | 15 | tdl.configure({ receiveTimeout: 10 }) 16 | 17 | getTdjson({ forceLibc: 'glibc' }) 18 | 19 | const client = tdl.createClient({ 20 | apiId: 2, 21 | apiHash: 'hash' 22 | }) 23 | 24 | tdl.createClient({ 25 | apiId: 222, 26 | apiHash: 'abc', 27 | useTestDc: true, 28 | tdlibParameters: { 29 | device_model: 'unknown' 30 | } 31 | }) 32 | // $FlowExpectedError[incompatible-call] 33 | tdl.createClient() 34 | 35 | tdl.createBareClient() 36 | 37 | tdl.init() 38 | 39 | const execRes = tdl.execute({ 40 | _: 'getTextEntities', 41 | text: '@telegram /test_command https://telegram.org telegram.me' 42 | }) 43 | 44 | ;(execRes: Td.error | Td.textEntities) 45 | 46 | tdl.setLogMessageCallback(4, a => console.log(a)) 47 | tdl.setLogMessageCallback(4, null) 48 | // $FlowExpectedError[incompatible-call] 49 | tdl.setLogMessageCallback('1234') 50 | 51 | async function main () { 52 | await client.login(() => ({ 53 | type: 'user', 54 | getPhoneNumber: () => Promise.resolve('+1234'), 55 | getAuthCode: () => Promise.resolve('123') 56 | })) 57 | 58 | await client.login(() => ({ 59 | type: 'bot', 60 | getToken: () => Promise.resolve('token') 61 | })) 62 | 63 | await client.login() 64 | 65 | await client.login(() => ({})) 66 | 67 | await client.login(() => ({ 68 | getPhoneNumber: retry => retry 69 | ? Promise.reject('Invalid phone number') 70 | : Promise.resolve('+9996620001'), 71 | getAuthCode: retry => retry 72 | ? Promise.reject('Invalid auth code') 73 | : Promise.resolve('22222'), 74 | getPassword: (passwordHint, retry) => retry 75 | ? Promise.reject('Invalid password') 76 | : Promise.resolve('abcdef'), 77 | getName: () => 78 | Promise.resolve({ firstName: 'John', lastName: 'Doe' }) 79 | })) 80 | 81 | // $FlowExpectedError[incompatible-call] 82 | await client.login(() => ({ getToken: () => Promise.resolve('token') })) 83 | // $FlowExpectedError[incompatible-call] 84 | await client.login(() => {}) 85 | // $FlowExpectedError[incompatible-call] 86 | await client.login(() => ({ a: 2 })) 87 | // $FlowExpectedError[incompatible-call] 88 | await client.login(() => ({ type: 'abc' })) 89 | 90 | // $FlowExpectedError[incompatible-call] 91 | client.login(123) 92 | // $FlowExpectedError[incompatible-call] 93 | client.login(() => 2) 94 | 95 | await client.loginAsBot('token') 96 | await client.loginAsBot(() => 'token') 97 | await client.loginAsBot(() => Promise.resolve('token')) 98 | // $FlowExpectedError[incompatible-call] 99 | await client.loginAsBot(Promise.resolve('token')) 100 | 101 | const res = client.execute({ 102 | _: 'getTextEntities', 103 | text: '@telegram /test_command https://telegram.org telegram.me' 104 | }) 105 | console.log(res) 106 | 107 | const result = await client.invoke({ 108 | _: 'getChats', 109 | chat_list: { _: 'chatListMain' }, 110 | limit: 100 111 | }) 112 | 113 | const msg = await client.invoke({ 114 | _: 'sendMessage', 115 | chat_id: 123456789, 116 | input_message_content: { 117 | _: 'inputMessageText', 118 | text: { 119 | _: 'formattedText', 120 | text: 'Hi', 121 | } 122 | } 123 | }) 124 | 125 | const user: Promise = client.invoke({ _: 'getMe' }) 126 | 127 | client.invoke({ 128 | _: 'addProxy', 129 | server: '127.0.0.1', 130 | port: 443, 131 | enable: true, 132 | type: { _: 'proxyTypeMtproto', secret: '15abcdef1234567890deadbeef123456' } 133 | }) 134 | 135 | const ver: string = client.getVersion() 136 | 137 | await client.close() 138 | } 139 | 140 | client.on('error', e => console.log('error', e)) 141 | 142 | // $FlowExpectedError[incompatible-call] 143 | client.on('foo') 144 | // $FlowExpectedError[incompatible-call] 145 | client.on('error') 146 | 147 | client.once('update', e => { 148 | ;(e: Td.Update) 149 | // $FlowExpectedError[incompatible-cast] 150 | ;(e: number) 151 | }) 152 | 153 | client.on('error', e => { 154 | if (e instanceof TDLibError) { 155 | console.log((e: TDLibError)) 156 | console.log(e.code, e.message) 157 | // $FlowExpectedError[prop-missing] 158 | console.log(e.ab) 159 | return 160 | } 161 | console.log(e.message) 162 | // $FlowExpectedError[prop-missing] 163 | console.log(e.abc) 164 | }) 165 | 166 | client.removeListener('update', () => {}) 167 | // $FlowExpectedError[incompatible-call] 168 | client.removeListener('update', () => {}, 'abc') 169 | // $FlowExpectedError[incompatible-call] 170 | client.removeListener('myevent', () => {}) 171 | // $FlowExpectedError[incompatible-call] 172 | client.removeListener('update', 'abc') 173 | 174 | client.emit('close') 175 | 176 | main().catch(console.error) 177 | 178 | // Td.formattedText <: Td.formattedText$Input 179 | declare var fmt: Td.formattedText 180 | const fmtInput: Td.formattedText$Input = fmt 181 | 182 | // subtyping should also work correctly with 'may be null' fields 183 | declare var chatFolder: Td.chatFolder 184 | const chatFolderInput: Td.chatFolder$Input = chatFolder 185 | -------------------------------------------------------------------------------- /tests/types/ts.ts: -------------------------------------------------------------------------------- 1 | import * as tdl from '../../packages/tdl' 2 | import { getTdjson } from '../../packages/prebuilt-tdlib' 3 | import type * as Td from 'tdlib-types' 4 | 5 | const { TDLibError, UnknownError } = tdl 6 | 7 | tdl.configure({ tdjson: 'libtdjson.dylib', libdir: '/usr/local/lib' }) 8 | tdl.configure({ verbosityLevel: 'default' }) 9 | tdl.configure({ libdir: __dirname }) 10 | tdl.configure({ libdir: '' }) 11 | tdl.configure({ tdjson: getTdjson() }) 12 | 13 | getTdjson({ forceLibc: 'glibc' }) 14 | 15 | const client = tdl.createClient({ 16 | apiId: 2, 17 | apiHash: 'hash' 18 | }) 19 | 20 | tdl.createClient({ 21 | apiId: 222, 22 | apiHash: 'abc', 23 | useTestDc: true, 24 | tdlibParameters: { 25 | device_model: 'unknown' 26 | } 27 | }) 28 | 29 | const bareCl: tdl.Client = tdl.createBareClient() 30 | 31 | tdl.init() 32 | 33 | tdl.execute({ 34 | _: 'getTextEntities', 35 | text: '@telegram /test_command https://telegram.org telegram.me' 36 | }) 37 | 38 | tdl.setLogMessageCallback(2, a => console.log(a)) 39 | tdl.setLogMessageCallback(3, null) 40 | 41 | async function main () { 42 | await client.login(() => ({ 43 | type: 'user', 44 | getPhoneNumber: () => Promise.resolve('+1234'), 45 | getAuthCode: () => Promise.resolve('123') 46 | })) 47 | 48 | await client.login(() => ({ 49 | type: 'bot', 50 | getToken: () => Promise.resolve('token') 51 | })) 52 | 53 | await client.login() 54 | 55 | await client.login(() => ({})) 56 | 57 | await client.login(() => ({ 58 | getPhoneNumber: retry => retry 59 | ? Promise.reject('Invalid phone number') 60 | : Promise.resolve('+9996620001'), 61 | getAuthCode: retry => retry 62 | ? Promise.reject('Invalid auth code') 63 | : Promise.resolve('22222'), 64 | getPassword: (passwordHint, retry) => retry 65 | ? Promise.reject('Invalid password') 66 | : Promise.resolve('abcdef'), 67 | getName: () => 68 | Promise.resolve({ firstName: 'John', lastName: 'Doe' }) 69 | })) 70 | 71 | await client.loginAsBot('token') 72 | await client.loginAsBot(() => 'token') 73 | await client.loginAsBot(() => Promise.resolve('token')) 74 | 75 | const res = client.execute({ 76 | _: 'getTextEntities', 77 | text: '@telegram /test_command https://telegram.org telegram.me' 78 | }) 79 | console.log(res) 80 | 81 | const result = await client.invoke({ 82 | _: 'getChats', 83 | chat_list: { _: 'chatListMain' }, 84 | limit: 100 85 | }) 86 | 87 | const msg = await client.invoke({ 88 | _: 'sendMessage', 89 | chat_id: 123456789, 90 | input_message_content: { 91 | _: 'inputMessageText', 92 | text: { 93 | _: 'formattedText', 94 | text: 'Hi', 95 | } 96 | } 97 | }) 98 | 99 | const user: Promise = client.invoke({ _: 'getMe' }) 100 | 101 | client.invoke({ 102 | _: 'addProxy', 103 | server: '127.0.0.1', 104 | port: 443, 105 | enable: true, 106 | type: { _: 'proxyTypeMtproto', secret: '15abcdef1234567890deadbeef123456' } 107 | }) 108 | 109 | const ver: string = client.getVersion() 110 | 111 | await client.close() 112 | } 113 | 114 | client.on('error', e => console.log('error', e)) 115 | 116 | client.on('close', () => {}) 117 | 118 | client.once('update', e => { 119 | const e2: Td.Update = e 120 | }) 121 | 122 | client.on('error', e => { 123 | if (e instanceof TDLibError) { 124 | console.log(e.code, e.message) 125 | return 126 | } 127 | console.log(e.message) 128 | }) 129 | 130 | client.removeListener('update', () => {}) 131 | 132 | main().catch(console.error) 133 | 134 | // Td.formattedText <: Td.formattedText$Input 135 | declare var fmt: Td.formattedText 136 | const fmtInp: Td.formattedText$Input = fmt 137 | 138 | const invoke: ( 139 | query: { readonly _: T } & Td.$FunctionInputByName[T] 140 | ) => Promise = ( 141 | req: any 142 | ): any => 143 | client.invoke(req).catch(e => { 144 | if (e instanceof tdl.TDLibError) 145 | return { _: e._, code: e.code, message: e.message } 146 | throw e 147 | }) 148 | 149 | async function test () { 150 | const res = await invoke({ _: 'getMe' }) 151 | if (res._ === 'error') { 152 | console.error('Error', res.code, res.message) 153 | return 154 | } 155 | console.log('Id:', res.id) 156 | } 157 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "Node16", 5 | "lib": ["ES2021"], 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "alwaysStrict": true, 10 | "skipLibCheck": true, 11 | "types": ["node"], 12 | "esModuleInterop": true, 13 | "noEmit": true, 14 | "outDir": "dist" 15 | }, 16 | "include": [ 17 | "packages/**/*", 18 | "tests/**/*", 19 | "typings/*", 20 | "examples/with-typescript.ts" 21 | ], 22 | "exclude": [ 23 | "examples/deno-example.ts", 24 | "node_modules", 25 | "**/node_modules/*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /typings/node-gyp-build.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'node-gyp-build' { 2 | function load(path: string): unknown 3 | export = load 4 | } 5 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | test: { 6 | fileParallelism: false 7 | } 8 | }) 9 | --------------------------------------------------------------------------------