tdl

A JavaScript wrapper for TDLib (Telegram Database library)

Usage no npm install needed!

<script type="module">
  import tdl from 'https://cdn.skypack.dev/tdl';
</script>

README

tdl

npm CI

A JavaScript wrapper for TDLib (Telegram Database library), a library to create Telegram clients or bots.
TDLib version 1.5.0 or newer is required.

Table of Contents


Installation

  1. Build TDLib (https://github.com/tdlib/td#building)
  2. npm i tdl tdl-tdlib-addon  (install both)
  3. npm i --save-dev tdlib-types if you use TypeScript or Flow  (recommended)

You can also use third-party pre-built binaries:


Requirements

  • Node.js v10+
  • A C++ compiler and Python installed
  • The tdjson shared library (libtdjson.so on Linux, libtdjson.dylib on macOS, tdjson.dll on Windows)

Note that Node.js exports OpenSSL symbols. If libtdjson is linked dynamically against openssl, it will use openssl symbols from the Node.js binary, not from your system. Therefore libtdjson's openssl version should be compatible with the openssl version that Node.js statically linked against (process.versions.openssl).
If you get segmentation faults, it's most likely due to openssl incompatibility.

If you linked TDLib against system OpenSSL, you may consider rebuilding Node.js with the system openssl.
For example, you can install Node.js v12 from source via nvm on GNU/Linux this way:

$ nvm install -s 12 --shared-openssl --shared-openssl-includes=/usr/include/ --shared-openssl-libpath=/usr/lib/x86_64-linux-gnu/

Or you can build TDLib with the same openssl version that Node.js linked against.

This doesn't apply to Electron, since it doesn't export openssl symbols.


API

new Client(tdlibInstance, options) => Client

// Example in Node.js:
const { Client } = require('tdl')
const { TDLib } = require('tdl-tdlib-addon')

const client = new Client(new TDLib(), {
  apiId: 2222, // Your api_id
  apiHash: '0123456789abcdef0123456789abcdef', // Your api_hash
})

api_id and api_hash can be obtained at https://my.telegram.org/.

The path to libtdjson can be specified in the TDLib constructor's argument. It is directly passed to dlopen / LoadLibrary. Check your OS documentation to see where it searches for the library.

client.connect() => Promise<undefined>

Initialize and connect your client with Telegram. Returns a promise.

await client.connect()

client.login(fn?: () => LoginDetails) => Promise<undefined>

Log in to your Telegram account.

await client.login()

By default, tdl asks the user for the phone number, auth code, and password (if specified) in the console. You can pass your functions:

// Example
await client.login(() => ({
  getPhoneNumber: retry => retry
    ? Promise.reject('Invalid phone number')
    : Promise.resolve('+9996620001'),
  getAuthCode: retry => retry
    ? Promise.reject('Invalid auth code')
    : Promise.resolve('22222'),
  getPassword: (passwordHint, retry) => retry
    ? Promise.reject('Invalid password')
    : Promise.resolve('abcdef'),
  getName: () =>
    Promise.resolve({ firstName: 'John', lastName: 'Doe' })
}))

The getName function is called if the user is not registered.

Also see the LoginDetails interface in the Options section.

It is possible to not use the client.login helper and implement login process manually.

client.connectAndLogin(fn?: () => LoginDetails) => Promise<undefined>

Same as client.connect().then(() => client.login(fn)).

client.on(event: string, callback: Function) => Client

Attach an event listener to receive the updates.

client.on('update', console.log)
client.on('error', console.error)

Ideally you should always have a listener on client.on('error').

client.addListener is an alias to this function.

client.once(event: string, callback: Function) => Client

Add a one-time listener.

client.off(event: string, listener: Function, once?: boolean) => Client

Remove an event listener.

const listener = v => {
  console.log('New update.', v)
  client.off('update', listener)
}
client.on('update', listener)

You can consider using reactive libraries like RxJS or most for convenient event processing.

client.removeListener is an alias to this function.

client.invoke(query: Object) => Promise<Object>

Asynchronously send a message to Telegram and receive a response.
Returns a promise, which resolves with the response, or rejects with an error.

The API list can be found at https://core.telegram.org/tdlib/docs/annotated.html or in the td_api.tl file. Note: the bytes type means you should pass a base64-encoded string.
Also, tdl renames @type to _.

const chats = await client.invoke({
  _: 'getChats',
  offset_order: '9223372036854775807',
  offset_chat_id: 0,
  limit: 100
})
await client.invoke({
  _: 'sendMessage',
  chat_id: 123456789,
  input_message_content: {
    _: 'inputMessageText',
    text: {
      _: 'formattedText',
      text: '👻'
    }
  }
})

client.execute(query: Object) => (Object | null)

Synchronously send a message to Telegram and receive a response. This function can be called only with methods that are marked as "can be called synchronously" in the TDLib documentation.

const res = client.execute({
  _: 'getTextEntities',
  text: '@telegram /test_command https://telegram.org telegram.me'
})

client.close() => Promise<undefined>

Close the TDLib instance.

This method sends { _: 'close' } and waits until the client gets destroyed.

await client.close()

client.setLogFatalErrorCallback(fn: (null | Function)) => undefined

Set the callback that will be called when a TDLib fatal error happens.

See the TDLib doc.

client.setLogFatalErrorCallback(errorMessage => {
  console.error('Fatal error:', errorMessage)
})

Low-level TDLib API

See TDLib_API.md.


Examples

const { Client } = require('tdl')
const { TDLib } = require('tdl-tdlib-addon')

const client = new Client(new TDLib(), {
  apiId: 2222, // Your api_id, get it at http://my.telegram.org/
  apiHash: '0123456789abcdef0123456789abcdef' // Your api_hash
})

client.on('error', console.error)
client.on('update', update => {
  console.log('Received update:', update)
})

async function main () {
  await client.connectAndLogin()

  console.log(await client.invoke({ _: 'getMe' }))
}

main()

See the examples/ directory.


Log in as a bot

const client = new Client(new TDLib(), {
  apiId: 2222, // Your api_id
  apiHash: '0123456789abcdef0123456789abcdef' // Your api_hash
})

await client.connectAndLogin(() => ({
  type: 'bot',
  getToken: retry => retry
    ? Promise.reject('Token is not valid')
    : Promise.resolve('YOUR_BOT_TOKEN') // Token from @BotFather
}))

Options

// The interface of the options you can pass to the Client constructor:
type Options = {
  apiId: number, // Can be obtained at https://my.telegram.org
  apiHash: string, // Can be obtained at https://my.telegram.org
  databaseDirectory: string, // Relative path (default is '_td_database')
  filesDirectory: string, // Relative path (default is '_td_files')
  databaseEncryptionKey: string, // Optional key for database encryption
  verbosityLevel: number, // Verbosity level (default is 2)
  useTestDc: boolean, // Use test telegram server (default is false)
  tdlibParameters: Object, // Raw TDLib parameters
  // Advanced options:
  skipOldUpdates: boolean, // Don't emit old updates on launch
  receiveTimeout: number,
  useMutableRename: boolean,
  useDefaultVerbosityLevel: boolean,
  disableAuth: boolean
}

// The `login` function accepts one of these two objects:
type LoginDetails = {
  type: 'user',
  getPhoneNumber: (retry?: boolean) => Promise<string>,
  getAuthCode: (retry?: boolean) => Promise<string>,
  getPassword: (passwordHint: string, retry?: boolean) => Promise<string>,
  getName: () => Promise<{ firstName: string, lastName?: string }>
} | {
  type: 'bot',
  getToken: (retry?: boolean) => Promise<string>
}

Only apiId and apiHash are required fields. Any other field can be omitted.

See https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1tdlib_parameters.html for parameters that can be specified in the tdlibParameters option.

Default tdlibParameters:

tdlibParameters: {
  use_message_database: true,
  use_secret_chats: false,
  system_language_code: 'en',
  application_version: '1.0',
  device_model: 'Unknown device',
  system_version: 'Unknown',
  enable_storage_optimizer: true,
  api_id: options.apiId,
  api_hash: options.apiHash,
  database_directory: options.databaseDirectory,
  files_directory: options.filesDirectory,
  use_test_dc: options.useTestDc
}

Typings

tdl fully supports TypeScript and Flow. tdlib-types should be installed to use the typings.

TDLib types can be imported using:

import type { updateMessageViews, messageInvoice /* ... */ } from 'tdlib-types'

The latest available typings are for TDLib v1.7.0.

You can install typings for other TDLib versions using npm install -D tdlib-types@td-<TDLIB_VERSION>. Example for TDLib v1.5.0: npm install -D tdlib-types@td-1.5.0.

See also packages/tdlib-types/README.md.


WebAssembly

tdl also has an experimental wrapper for tdlib in wasm, see packages/tdl-tdlib-wasm/.


Architecture notes

The library is designed to work with different "backends", their interface is described in TDLib_API.md file. So the same main wrapper can be used with node ffi, with node addons, and in the browser with webassembly.

Available "backends" in the tdl repository:

You can easily substitute one with another, since they follow the same interface.


Can I create multiple clients?

You can use multiple clients with tdl-tdlib-addon if the number of clients < UV_THREADPOOL_SIZE.

With tdl-tdlib-ffi it's not possible to use multiple clients simultaneously in one process, see #18. If you try, it will result in use after free. You can create multiple processes using child_process.fork.


Contributing

See CONTRIBUTING.md.


Windows

tdl-tdlib-ffi and tdl-tdlib-addon depend on node-gyp, which may be difficult to install on Windows. You should install Visual Studio (or just Build Tools) and Python first. E.g. see https://gist.github.com/jtrefry/fd0ea70a89e2c3b7779c, https://github.com/Microsoft/nodejs-guidelines/blob/dd5074c/windows-environment.md#compiling-native-addon-modules. npm also has a windows-build-tools package.