├── .gitignore ├── src ├── structs │ ├── index.js │ └── TextStruct.js ├── errors │ ├── ClientFetchError.js │ ├── ClientNotCreatedError.js │ ├── InvalidBotTokenError.js │ ├── ClientCreateError.js │ ├── InvalidCallbackError.js │ └── index.js ├── main.module.js ├── main.node.js ├── ClientNode │ ├── utils.js │ └── index.js ├── ClientWasm │ └── index.js ├── ClientBase.js └── TG.js ├── rollup.config.js ├── examples ├── getChats.js ├── handleUpdates.js ├── sendMessage.js ├── botCommand.js └── multiClients.js ├── package.json ├── test └── test.js ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | /node.js 5 | /wasm.js 6 | -------------------------------------------------------------------------------- /src/structs/index.js: -------------------------------------------------------------------------------- 1 | import TextStruct from './TextStruct' 2 | 3 | export { 4 | TextStruct, 5 | } 6 | -------------------------------------------------------------------------------- /src/errors/ClientFetchError.js: -------------------------------------------------------------------------------- 1 | export default class ClientFetchError extends Error { 2 | constructor(update) { 3 | super() 4 | this.message = JSON.stringify(update) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/ClientNotCreatedError.js: -------------------------------------------------------------------------------- 1 | export default class ClientNotCreatedError extends Error { 2 | constructor(error) { 3 | super(error) 4 | this.message = 'Client is not created' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/InvalidBotTokenError.js: -------------------------------------------------------------------------------- 1 | export default class InvalidBotTokenError extends Error { 2 | constructor() { 3 | super() 4 | this.message = 'Provided Bot Token is not valid' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main.module.js: -------------------------------------------------------------------------------- 1 | import Client from './ClientWasm/index.js' 2 | import * as Errors from './errors/index.js' 3 | import * as Structs from './structs/index.js' 4 | 5 | export { Client, Errors, Structs } 6 | -------------------------------------------------------------------------------- /src/main.node.js: -------------------------------------------------------------------------------- 1 | import Client from './ClientNode/index.js' 2 | import * as Errors from './errors/index.js' 3 | import * as Structs from './structs/index.js' 4 | 5 | export { Client, Errors, Structs } 6 | -------------------------------------------------------------------------------- /src/errors/ClientCreateError.js: -------------------------------------------------------------------------------- 1 | export default class ClientCreateError extends Error { 2 | constructor(error) { 3 | super(error) 4 | this.message = `Unable to create client: ${error.message}` 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/InvalidCallbackError.js: -------------------------------------------------------------------------------- 1 | export default class InvalidCallbackError extends Error { 2 | constructor(eventName) { 3 | super() 4 | this.message = `"${eventName}" is not a valid callback.` 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/errors/index.js: -------------------------------------------------------------------------------- 1 | import InvalidCallbackError from './InvalidCallbackError' 2 | import InvalidBotTokenError from './InvalidBotTokenError' 3 | import ClientCreateError from './ClientCreateError' 4 | import ClientNotCreatedError from './ClientNotCreatedError' 5 | import ClientFetchError from './ClientFetchError' 6 | 7 | export { 8 | InvalidCallbackError, 9 | InvalidBotTokenError, 10 | ClientCreateError, 11 | ClientNotCreatedError, 12 | ClientFetchError, 13 | } 14 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import json from 'rollup-plugin-json'; 2 | import pkg from './package.json'; 3 | 4 | export default [ 5 | // ES module (for Node) build. 6 | { 7 | input: 'src/main.node.js', 8 | external: [], 9 | output: [ 10 | { file: pkg.main, format: 'cjs' } 11 | ], 12 | plugins: [ 13 | json() 14 | ] 15 | }, 16 | // ES module (for browser bundlers) build. 17 | { 18 | input: 'src/main.module.js', 19 | external: [], 20 | output: [ 21 | { file: pkg.module, format: 'es' } 22 | ], 23 | plugins: [ 24 | json() 25 | ] 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /src/structs/TextStruct.js: -------------------------------------------------------------------------------- 1 | export default class TextStruct { 2 | constructor(text, parseMode) { 3 | this.text = text 4 | if (['textParseModeHTML', 'textParseModeMarkdown'].indexOf(parseMode) >= 0) { 5 | this.parseMode = parseMode 6 | } 7 | } 8 | 9 | // Internal method 10 | async _format(_client) { 11 | let args 12 | if (this.parseMode) { 13 | args = await _client._execute({ 14 | '@type': 'parseTextEntities', 15 | 'text': this.text, 16 | 'parse_mode': { '@type': this.parseMode }, 17 | }) 18 | } else { 19 | args = { 20 | '@type': 'formattedText', 21 | 'text': this.text, 22 | } 23 | } 24 | return args 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ClientNode/utils.js: -------------------------------------------------------------------------------- 1 | import ref from 'ref-napi' 2 | import inquirer from 'inquirer' 3 | 4 | export function buildQuery(query) { 5 | const buffer = Buffer.from(JSON.stringify(query) + '\0', 'utf-8') 6 | buffer.type = ref.types.CString 7 | return buffer 8 | } 9 | 10 | export async function getInput({ string, extras: { hint } = {} }) { 11 | let input = '' 12 | while (!input.length) { 13 | const type = ( 14 | string.startsWith('tglib.input.AuthorizationCode') || 15 | string.startsWith('tglib.input.AuthorizationPassword') 16 | ) ? 'password' : 'input' 17 | const message = `${string}${ hint ? ` (${hint})` : '' }` 18 | const result = await inquirer.prompt([ 19 | { type, name: 'input', message }, 20 | ]) 21 | input = result.input 22 | } 23 | return input 24 | } 25 | -------------------------------------------------------------------------------- /examples/getChats.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('tglib') 2 | 3 | void async function() { 4 | const client = new Client({ 5 | apiId: 'YOUR_API_ID', 6 | apiHash: 'YOUR_API_HASH', 7 | }) 8 | 9 | // Save tglib default handler which prompt input at console 10 | const defaultHandler = client.callbacks['td:getInput'] 11 | 12 | // Register own callback for returning auth details 13 | client.registerCallback('td:getInput', async (args) => { 14 | if (args.string === 'tglib.input.AuthorizationType') { 15 | return 'user' 16 | } else if (args.string === 'tglib.input.AuthorizationValue') { 17 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER' 18 | } 19 | return await defaultHandler(args) 20 | }) 21 | 22 | await client.ready 23 | 24 | const result = await client.fetch({ 25 | '@type': 'getChats', 26 | 'offset_order': '9223372036854775807', 27 | 'offset_chat_id': 0, 28 | 'limit': 100, 29 | }) 30 | 31 | // latest 100 chat id will be returned 32 | console.log(result) 33 | }() 34 | -------------------------------------------------------------------------------- /examples/handleUpdates.js: -------------------------------------------------------------------------------- 1 | const { Client } = require('tglib') 2 | 3 | void async function() { 4 | const client = new Client({ 5 | apiId: 'YOUR_API_ID', 6 | apiHash: 'YOUR_API_HASH', 7 | }) 8 | 9 | // Save tglib default handler which prompt input at console 10 | const defaultHandler = client.callbacks['td:getInput'] 11 | 12 | // Register own callback for returning auth details 13 | client.registerCallback('td:getInput', async (args) => { 14 | if (args.string === 'tglib.input.AuthorizationType') { 15 | return 'user' 16 | } else if (args.string === 'tglib.input.AuthorizationValue') { 17 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER' 18 | } 19 | return await defaultHandler(args) 20 | }) 21 | 22 | client.registerCallback('td:update', (update) => { 23 | console.log('Got update:', JSON.stringify(update, null, 2)) 24 | }) 25 | 26 | client.registerCallback('td:error', (update) => { 27 | console.error('Got error:', JSON.stringify(update, null, 2)) 28 | }) 29 | 30 | await client.ready 31 | }() 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tglib", 3 | "version": "3.0.5", 4 | "author": "nodegin", 5 | "license": "MIT", 6 | "description": "TDLib (Telegram Database library) bindings for Node.js", 7 | "homepage": "https://github.com/nodegin/tglib#readme", 8 | "bugs": { 9 | "url": "https://github.com/nodegin/tglib/issues" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/nodegin/tglib.git" 14 | }, 15 | "keywords": [ 16 | "tdlib", 17 | "telegram" 18 | ], 19 | "main": "node.js", 20 | "module": "wasm.js", 21 | "scripts": { 22 | "build": "rollup -c", 23 | "dev": "rollup -c -w", 24 | "test": "node test/test.js", 25 | "pretest": "npm run build", 26 | "prepublish": "npm run build" 27 | }, 28 | "dependencies": { 29 | "crc": "^3.8.0", 30 | "event-emitter": "^0.3.5", 31 | "ffi-napi": "^2.4.5", 32 | "fs-extra": "^7.0.1", 33 | "inquirer": "^6.2.2", 34 | "ref-napi": "^1.4.1" 35 | }, 36 | "devDependencies": { 37 | "rollup": "^1.0.0", 38 | "rollup-plugin-json": "^4.0.0" 39 | }, 40 | "files": [ 41 | "node.js", 42 | "wasm.js" 43 | ], 44 | "engines": { 45 | "node": ">= 9.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/sendMessage.js: -------------------------------------------------------------------------------- 1 | const { Client, Structs } = require('tglib') 2 | 3 | void async function() { 4 | const client = new Client({ 5 | apiId: 'YOUR_API_ID', 6 | apiHash: 'YOUR_API_HASH', 7 | }) 8 | 9 | // Save tglib default handler which prompt input at console 10 | const defaultHandler = client.callbacks['td:getInput'] 11 | 12 | // Register own callback for returning auth details 13 | client.registerCallback('td:getInput', async (args) => { 14 | if (args.string === 'tglib.input.AuthorizationType') { 15 | return 'user' 16 | } else if (args.string === 'tglib.input.AuthorizationValue') { 17 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER' 18 | } 19 | return await defaultHandler(args) 20 | }) 21 | 22 | await client.ready 23 | 24 | await client._send({ 25 | '@type': 'sendMessage', 26 | 'chat_id': -123456789, 27 | 'input_message_content': { 28 | '@type': 'inputMessageText', 29 | 'text': { 30 | '@type': 'formattedText', 31 | 'text': 'Hi', 32 | }, 33 | }, 34 | }) 35 | 36 | // or use tglib API 37 | await client.tg.sendTextMessage({ 38 | '$text': new Structs.TextStruct('Hi bold', 'textParseModeHTML'), 39 | 'chat_id': -123456789, 40 | }) 41 | }() 42 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const howLongTillLunch = require('..'); 3 | 4 | function MockDate () { 5 | this.date = 0; 6 | this.hours = 0; 7 | this.minutes = 0; 8 | this.seconds = 0; 9 | this.milliseconds = 0; 10 | }; 11 | 12 | Object.assign(MockDate.prototype, { 13 | getDate () { return this.date; }, 14 | setDate (date) { this.date = date; }, 15 | setHours (h) { this.hours = h; }, 16 | setMinutes (m) { this.minutes = m; }, 17 | setSeconds (s) { this.seconds = s; }, 18 | setMilliseconds (ms) { this.milliseconds = ms; }, 19 | valueOf () { 20 | return ( 21 | this.milliseconds + 22 | this.seconds * 1e3 + 23 | this.minutes * 1e3 * 60 + 24 | this.hours * 1e3 * 60 * 60 + 25 | this.date * 1e3 * 60 * 60 * 24 26 | ); 27 | } 28 | }); 29 | 30 | const now = new MockDate(); 31 | MockDate.now = () => now.valueOf(); 32 | 33 | global.Date = MockDate; 34 | 35 | function test(hours, minutes, seconds, expected) { 36 | now.setHours(hours); 37 | now.setMinutes(minutes); 38 | now.setSeconds(seconds); 39 | 40 | assert.equal(howLongTillLunch(...lunchtime), expected); 41 | console.log(`\u001B[32m✓\u001B[39m ${expected}`); 42 | } 43 | 44 | let lunchtime = [ 12, 30 ]; 45 | test(11, 30, 0, '1 hour'); 46 | test(10, 30, 0, '2 hours'); 47 | test(12, 25, 0, '5 minutes'); 48 | test(12, 29, 15, '45 seconds'); 49 | test(13, 30, 0, '23 hours'); 50 | 51 | // some of us like an early lunch 52 | lunchtime = [ 11, 0 ]; 53 | test(10, 30, 0, '30 minutes'); -------------------------------------------------------------------------------- /examples/botCommand.js: -------------------------------------------------------------------------------- 1 | const { Client, Structs } = require('tglib') 2 | 3 | void async function() { 4 | const client = new Client({ 5 | apiId: 'YOUR_API_ID', 6 | apiHash: 'YOUR_API_HASH', 7 | }) 8 | 9 | // Save tglib default handler which prompt input at console 10 | const defaultHandler = client.callbacks['td:getInput'] 11 | 12 | // Register own callback for returning auth details 13 | client.registerCallback('td:getInput', async (args) => { 14 | if (args.string === 'tglib.input.AuthorizationType') { 15 | return 'bot' 16 | } else if (args.string === 'tglib.input.AuthorizationValue') { 17 | return 'YOUR_BOT_TOKEN' 18 | } 19 | return await defaultHandler(args) 20 | }) 21 | 22 | await client.ready 23 | 24 | const { id: myId } = await client.fetch({ '@type': 'getMe' }) 25 | 26 | client.registerCallback('td:update', async (update) => { 27 | if (update['@type'] === 'updateNewMessage') { 28 | // check if message is sent from self 29 | const sender = update['message']['sender_user_id'] 30 | if (sender !== myId) { 31 | const { text: { text } } = update['message']['content'] 32 | let replyText 33 | if (text.startsWith('/')) { 34 | replyText = `Are you requested ${text}?` 35 | } else { 36 | replyText = `Sorry I do not understand ${text}.` 37 | } 38 | await client.tg.sendTextMessage({ 39 | '$text': new Structs.TextStruct(replyText, 'textParseModeHTML'), 40 | 'chat_id': 123456789, 41 | 'disable_notification': true, 42 | 'clear_draft': false, 43 | }) 44 | } 45 | } 46 | }) 47 | }() 48 | -------------------------------------------------------------------------------- /examples/multiClients.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { Client, Structs } = require('tglib') 3 | 4 | async function initClients() { 5 | const clients = {} 6 | const credentials = { 7 | alice: { type: 'user', value: 'YOUR_INTERNATIONAL_PHONE_NUMBER' }, 8 | } 9 | 10 | for (const key in credentials) { 11 | try { 12 | const { type, value } = credentials[key] 13 | 14 | // Create new client for each credential 15 | // Storing data in corresponding directories 16 | const client = new Client({ 17 | apiId: 'YOUR_API_ID', 18 | apiHash: 'YOUR_API_HASH', 19 | appDir: path.resolve(process.cwd(), `__tglib-${type}-${value}__`), 20 | }) 21 | 22 | // Save tglib default handler which prompt input at console 23 | const defaultHandler = client.callbacks['td:getInput'] 24 | 25 | // Register own callback for returning auth details 26 | client.registerCallback('td:getInput', async (args) => { 27 | if (args.string === 'tglib.input.AuthorizationType') { 28 | return type 29 | } else if (args.string === 'tglib.input.AuthorizationValue') { 30 | return value 31 | } 32 | return await defaultHandler(args) 33 | }) 34 | 35 | // Wait for client ready 36 | await client.ready 37 | clients[key] = client 38 | } catch (e) { 39 | console.log(`Cannot create ${key}: `, e.message) 40 | } 41 | } 42 | 43 | return clients 44 | } 45 | 46 | void async function() { 47 | const clients = await initClients() 48 | 49 | await clients.alice.sendTextMessage({ 50 | '$text': new Structs.TextStruct('hello'), 51 | 'chat_id': 123456789, 52 | 'disable_notification': true, 53 | 'clear_draft': false, 54 | }) 55 | }() 56 | -------------------------------------------------------------------------------- /src/ClientWasm/index.js: -------------------------------------------------------------------------------- 1 | import ClientBase from '../ClientBase' 2 | 3 | import { 4 | ClientCreateError, 5 | ClientNotCreatedError, 6 | } from '../errors/index.js' 7 | 8 | class ClientWASM extends ClientBase { 9 | constructor(options = {}) { 10 | super('wasm', options) 11 | } 12 | 13 | async init() { 14 | try { 15 | const { 16 | verbosityLevel, 17 | wasmModule, 18 | } = this.options 19 | 20 | // See also: 21 | // https://github.com/tdlib/td/blob/master/td/telegram/td_emscripten.cpp 22 | // https://github.com/tdlib/td/blob/master/example/web/tdweb/src/worker.js#L495 23 | this.tdlib = { 24 | td_create: wasmModule.cwrap('td_create', 'number', []), 25 | td_send: wasmModule.cwrap('td_send', null, ['number', 'string']), 26 | td_receive: wasmModule.cwrap('td_receive', 'string', ['number']), 27 | td_execute: wasmModule.cwrap('td_execute', 'string', ['number', 'string']), 28 | td_destroy: wasmModule.cwrap('td_destroy', null, ['number']), 29 | } 30 | this.tdlib.td_execute( 31 | 0, 32 | JSON.stringify({ 33 | '@type': 'setLogVerbosityLevel', 34 | new_verbosity_level: verbosityLevel, 35 | }) 36 | ) 37 | 38 | this.client = await this._create() 39 | } catch (error) { 40 | this.rejector(new ClientCreateError(error)) 41 | return 42 | } 43 | this.loop() 44 | } 45 | 46 | async _create() { 47 | const client = await this.tdlib.td_create() 48 | return client 49 | } 50 | 51 | async _send(query) { 52 | if (!this.client) { 53 | throw new ClientNotCreatedError() 54 | } 55 | const response = await this.tdlib.td_send(this.client, JSON.stringify(query)) 56 | if (!response) { 57 | return null 58 | } 59 | return JSON.parse(response) 60 | } 61 | 62 | async _receive() { 63 | if (!this.client) { 64 | throw new ClientNotCreatedError() 65 | } 66 | const response = await this.tdlib.td_receive(this.client) 67 | if (!response) { 68 | return null 69 | } 70 | return JSON.parse(response) 71 | } 72 | 73 | async _execute(query) { 74 | if (!this.client) { 75 | throw new ClientNotCreatedError() 76 | } 77 | const response = await this.tdlib.td_execute(this.client, JSON.stringify(query)) 78 | if (!response) { 79 | return null 80 | } 81 | return JSON.parse(response) 82 | } 83 | 84 | async _destroy() { 85 | if (this.client) { 86 | await this.tdlib.td_destroy(this.client) 87 | this.client = null 88 | } 89 | } 90 | } 91 | 92 | export default ClientWASM 93 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | #### tglib v3.0.3 4 | 5 | - Added a new high level API `client.tg.download` for downloading files 6 | 7 | ----- 8 | 9 | #### tglib v3.0.2 10 | 11 | - Fix WASM verbosity setting for TDLib 1.4.0 (provides official WASM suppor & example) 12 | 13 | ----- 14 | 15 | #### tglib v3.0.1 16 | 17 | - Added a new high level API `client.tg.call` 18 | 19 | ----- 20 | 21 | #### tglib v3.0.0 22 | 23 | WARNING: This release came with multiple breaking changes, please see README.md / examples for updated usages. 24 | 25 | - Breaking changes: Removed `dataDir` option in flavor of `appDir` 26 | - Breaking changes: Removed `auth` option 27 | - Added support for WebAssembly 28 | 29 | ----- 30 | 31 | #### tglib v2.1.0 32 | 33 | - Added a new option `dataDir` for specifying `__tglib__` data directory. 34 | - Added a new option `binaryPath` for specifying TDLib binary. 35 | - Added a new high level API `client.tg.getChat`. 36 | - Fixed issue #36, update loop will now stop when client is destroyed. 37 | 38 | ----- 39 | 40 | #### tglib v2.0.0 41 | 42 | - Breaking changes: Rewrote `client.tg.sendTextMessage`. 43 | 44 | *The old way to send text message:* 45 | ```js 46 | await client.tg.sendTextMessage('123456789', 'Hello *World*', { 47 | 'parse_mode': 'markdown', 48 | 'disable_notification': true, 49 | 'clear_draft': false, 50 | }) 51 | ``` 52 | *The new way to send text message:* 53 | ```js 54 | const { TextStruct } = require('tglib/structs') 55 | 56 | await client.tg.sendTextMessage({ 57 | '$text': new TextStruct('`Hello` world!', 'textParseModeMarkdown'), 58 | 'chat_id': 123456789, 59 | 'disable_notification': true, 60 | 'clear_draft': false, 61 | }) 62 | ``` 63 | - Added `tglib/structs` for common structures. 64 | - Added `TextStruct` for formatted text struct, used in `inputMessageText.text`, `inputMessagePhoto.caption`, etc. 65 | ```js 66 | const text1 = new TextStruct('Normal text') 67 | const text2 = new TextStruct('`Markdown` text', 'textParseModeMarkdown') 68 | const text3 = new TextStruct('HTML text', 'textParseModeHTML') 69 | ``` 70 | - Added a new high level API `client.tg.sendPhotoMessage`. 71 | ```js 72 | const { TextStruct } = require('tglib/structs') 73 | 74 | await client.tg.sendPhotoMessage({ 75 | '$caption': new TextStruct('Such doge much wow'), 76 | 'chat_id': 123456789, 77 | 'path': '/tmp/doge.jpg', 78 | 'ttl': 5, 79 | }) 80 | ``` 81 | - Breaking changes: `client.on` now renamed to `client.registerCallback`. 82 | 83 | *The old way to receive updates and errors:* 84 | ```js 85 | client.on('_update', (update) => console.log(update)) 86 | client.on('_error', (error) => console.error(error)) 87 | ``` 88 | 89 | *The new way to receive updates and errors:* 90 | ```js 91 | client.registerCallback('td:update', (update) => console.log(update)) 92 | client.registerCallback('td:error', (error) => console.error(error)) 93 | ``` 94 | - Added a new callback `td:getInput` to split out the `getInput` function from core, you can now register your own callback function to handle the login process. 95 | ```js 96 | client.registerCallback('td:getInput', async (args) => { 97 | const result = await getInputFromUser(args) 98 | return result 99 | }) 100 | ``` 101 | ----- 102 | 103 | #### tglib v1.4 104 | 105 | - Added a new high level API `client.tg.updateUsername`. 106 | - Added a new high level API `client.tg.getAllChats`. 107 | - Added a new high level API `client.tg.openSecretChat`. 108 | - Added a new high level API `client.tg.deleteChat`. 109 | - Fixes new user authorization. (#24) 110 | - Improve documentations. 111 | 112 | ----- 113 | 114 | #### tglib v1.3 115 | 116 | - Now support multiple session login by storing files in separate directories. (#16, #19) 117 | - Ability to login as Bots. 118 | ```diff 119 | const client = new Client({ 120 | apiId: 'YOUR_API_ID', 121 | apiHash: 'YOUR_API_HASH', 122 | - phoneNumber: 'YOUR_PHONE_NUMBER', 123 | + auth: { 124 | + type: 'user', 125 | + value: 'YOUR_PHONE_NUMBER', 126 | + }, 127 | }) 128 | ``` 129 | - Error events for `client.fetch` are now handled. `ClientFetchError` will be thrown if `client.fetch` fails. 130 | - `client.tg.sendMessageTypeText` renamed to `client.tg.sendTextMessage`. 131 | - `client.connect()` changed to `client.ready`. 132 | -------------------------------------------------------------------------------- /src/ClientNode/index.js: -------------------------------------------------------------------------------- 1 | import ClientBase from '../ClientBase' 2 | 3 | import ffi from 'ffi-napi' 4 | import fs from 'fs-extra' 5 | import path from 'path' 6 | import { buildQuery, getInput } from './utils' 7 | import { 8 | ClientCreateError, 9 | ClientNotCreatedError, 10 | } from '../errors/index.js' 11 | 12 | class ClientNode extends ClientBase { 13 | constructor(options = {}) { 14 | super('node', { 15 | appDir: path.resolve(process.cwd(), '__tglib__'), 16 | binaryPath: path.resolve(process.cwd(), 'libtdjson'), 17 | ...options, 18 | }) 19 | // register inquirer getInput function as default 20 | this.registerCallback('td:getInput', getInput) 21 | } 22 | 23 | async init() { 24 | try { 25 | const { 26 | appDir, 27 | binaryPath, 28 | verbosityLevel, 29 | } = this.options 30 | 31 | await fs.ensureDir(appDir) 32 | 33 | // update options 34 | this.options.filesDir = path.resolve(appDir, 'files') 35 | this.options.databaseDir = path.resolve(appDir, 'database') 36 | 37 | this.tdlib = ffi.Library(binaryPath, { 38 | 'td_json_client_create' : ['pointer', []], 39 | 'td_json_client_send' : ['void' , ['pointer', 'string']], 40 | 'td_json_client_receive' : ['string' , ['pointer', 'double']], 41 | 'td_json_client_execute' : ['string' , ['pointer', 'string']], 42 | 'td_json_client_destroy' : ['void' , ['pointer']], 43 | 'td_set_log_file_path' : ['int' , ['string']], 44 | 'td_set_log_verbosity_level' : ['void' , ['int']], 45 | 'td_set_log_fatal_error_callback': ['void' , ['pointer']], 46 | }) 47 | this.tdlib.td_set_log_file_path(path.resolve(appDir, 'logs.txt')) 48 | this.tdlib.td_set_log_verbosity_level(verbosityLevel) 49 | this.tdlib.td_set_log_fatal_error_callback(ffi.Callback('void', ['string'], (message) => { 50 | console.error('TDLib Fatal Error:', message) 51 | })) 52 | 53 | this.client = await this._create() 54 | } catch (error) { 55 | this.rejector(new ClientCreateError(error)) 56 | return 57 | } 58 | this.loop() 59 | } 60 | 61 | _create() { 62 | return new Promise((resolve, reject) => { 63 | this.tdlib.td_json_client_create.async((err, client) => { 64 | if (err) { 65 | return reject(err) 66 | } 67 | resolve(client) 68 | }) 69 | }) 70 | } 71 | 72 | _send(query) { 73 | return new Promise((resolve, reject) => { 74 | if (!this.client) { 75 | return reject(new ClientNotCreatedError()) 76 | } 77 | this.tdlib.td_json_client_send.async(this.client, buildQuery(query), (err, response) => { 78 | if (err) { 79 | return reject(err) 80 | } 81 | if (!response) { 82 | return resolve(null) 83 | } 84 | resolve(JSON.parse(response)) 85 | }) 86 | }) 87 | } 88 | 89 | _receive(timeout = 10) { 90 | return new Promise((resolve, reject) => { 91 | if (!this.client) { 92 | return reject(new ClientNotCreatedError()) 93 | } 94 | this.tdlib.td_json_client_receive.async(this.client, timeout, (err, response) => { 95 | if (err) { 96 | return reject(err) 97 | } 98 | if (!response) { 99 | return resolve(null) 100 | } 101 | resolve(JSON.parse(response)) 102 | }) 103 | }) 104 | } 105 | 106 | _execute(query) { 107 | return new Promise((resolve, reject) => { 108 | if (!this.client) { 109 | return reject(new ClientNotCreatedError()) 110 | } 111 | try { 112 | const response = this.tdlib.td_json_client_execute(this.client, buildQuery(query)) 113 | if (!response) { 114 | return resolve(null) 115 | } 116 | resolve(JSON.parse(response)) 117 | } catch (err) { 118 | reject(err) 119 | } 120 | }) 121 | } 122 | 123 | _destroy() { 124 | return new Promise((resolve) => { 125 | if (this.client) { 126 | this.tdlib.td_json_client_destroy(this.client) 127 | this.client = null 128 | } 129 | resolve() 130 | }) 131 | } 132 | } 133 | 134 | export default ClientNode 135 | -------------------------------------------------------------------------------- /src/ClientBase.js: -------------------------------------------------------------------------------- 1 | import { crc32 } from 'crc' 2 | import TG from './TG' 3 | import { version as appVersion } from '../package.json' 4 | import { 5 | InvalidCallbackError, 6 | InvalidBotTokenError, 7 | ClientFetchError, 8 | } from './errors/index.js' 9 | 10 | class Client { 11 | constructor(mode, options = {}) { 12 | const defaultOptions = { 13 | apiId: null, 14 | apiHash: null, 15 | verbosityLevel: 2, 16 | tdlibParameters: { 17 | 'enable_storage_optimizer': true, 18 | 'use_message_database': true, 19 | 'use_secret_chats': true, 20 | 'system_language_code': 'en', 21 | 'application_version': '1.0', 22 | 'device_model': 'tglib', 23 | 'system_version': appVersion, 24 | }, 25 | } 26 | this.options = { 27 | ...defaultOptions, 28 | ...options, 29 | } 30 | this.ready = new Promise((resolve, reject) => { 31 | // Add some delay to allow telegram get ready. (Issue #20) 32 | this.resolver = () => setTimeout(resolve, 500) 33 | this.rejector = reject 34 | }) 35 | this.client = null 36 | this.tg = new TG(this) 37 | this.hijackers = {} 38 | this.downloading = {} 39 | this.fetching = {} 40 | this.callbacks = { 41 | 'td:update': () => {}, 42 | 'td:error': () => {}, 43 | 'td:getInput': () => { throw new Error('td:getInput callback is not set.') }, 44 | } 45 | this.init() 46 | } 47 | 48 | registerCallback(key, callback) { 49 | const validNames = Object.keys(this.callbacks) 50 | if (validNames.indexOf(key) < 0) { 51 | throw new InvalidCallbackError(key) 52 | } 53 | this.callbacks[key] = callback 54 | } 55 | 56 | async loop() { 57 | if (!this.client) { 58 | // when client has been destroyed, stop the update loop 59 | return 60 | } 61 | const update = await this._receive() 62 | if (update) { 63 | if (this.hijackers[update['@type']]) { 64 | // for tglib update hijacking 65 | this.hijackers[update['@type']](update) 66 | } else { 67 | // handle update normally 68 | switch (update['@type']) { 69 | case 'updateAuthorizationState': { 70 | await this.handleAuth(update) 71 | break 72 | } 73 | case 'error': { 74 | await this.handleError(update) 75 | break 76 | } 77 | default: 78 | await this.handleUpdate(update) 79 | break 80 | } 81 | } 82 | } 83 | setTimeout(() => this.loop(), 1) 84 | } 85 | 86 | _hijackUpdate(type, callback) { 87 | if (typeof callback === 'function') { 88 | this.hijackers[type] = callback 89 | } else { 90 | delete this.hijackers[type] 91 | } 92 | } 93 | 94 | async handleAuth(update) { 95 | switch (update['authorization_state']['@type']) { 96 | case 'authorizationStateWaitTdlibParameters': { 97 | this._send({ 98 | '@type': 'setTdlibParameters', 99 | 'parameters': { 100 | ...this.options.tdlibParameters, 101 | '@type': 'tdlibParameters', 102 | 'api_id': this.options.apiId, 103 | 'api_hash': this.options.apiHash, 104 | 'database_directory': this.options.databaseDir, 105 | 'files_directory': this.options.filesDir, 106 | }, 107 | }) 108 | break 109 | } 110 | case 'authorizationStateWaitEncryptionKey': { 111 | this._send({ '@type': 'checkDatabaseEncryptionKey' }) 112 | break 113 | } 114 | case 'authorizationStateWaitPhoneNumber': { 115 | const type = await this.callbacks['td:getInput']({ 116 | string: 'tglib.input.AuthorizationType', 117 | }) 118 | const value = await this.callbacks['td:getInput']({ 119 | string: 'tglib.input.AuthorizationValue', 120 | }) 121 | if (type === 'user') { 122 | this._send({ 123 | '@type': 'setAuthenticationPhoneNumber', 124 | 'phone_number': value, 125 | }) 126 | } else { 127 | this._send({ 128 | '@type': 'checkAuthenticationBotToken', 129 | 'token': value, 130 | }) 131 | } 132 | break 133 | } 134 | case 'authorizationStateWaitCode': { 135 | const payload = { '@type': 'checkAuthenticationCode' } 136 | payload['code'] = await this.callbacks['td:getInput']({ 137 | string: 'tglib.input.AuthorizationCode', 138 | }) 139 | this._send(payload) 140 | break 141 | } 142 | case 'authorizationStateWaitRegistration': { 143 | const payload = { '@type': 'registerUser' }; 144 | console.log(`User has not yet been registered with Telegram`); 145 | payload['first_name'] = await this.callbacks['td:getInput']({ 146 | string: 'tglib.input.FirstName', 147 | }); 148 | this._send(payload); 149 | break 150 | } 151 | case 'authorizationStateWaitPassword': { 152 | this.authFlowPasswordHint = update['authorization_state']['password_hint'] 153 | const password = await this.callbacks['td:getInput']({ 154 | string: 'tglib.input.AuthorizationPassword', 155 | extras: { hint: this.authFlowPasswordHint }, 156 | }) 157 | this._send({ 158 | '@type': 'checkAuthenticationPassword', 159 | 'password': password, 160 | }) 161 | break 162 | } 163 | case 'authorizationStateReady': 164 | delete this.authFlowPasswordHint 165 | this.resolver() 166 | break 167 | } 168 | } 169 | 170 | async handleError(update) { 171 | const id = update['@extra'] 172 | if (this.fetching[id]) { 173 | delete update['@extra'] 174 | this.fetching[id].reject(new ClientFetchError(update)) 175 | delete this.fetching[id] 176 | return 177 | } 178 | switch (update['message']) { 179 | case 'PHONE_CODE_EMPTY': 180 | case 'PHONE_CODE_INVALID': { 181 | const code = await this.callbacks['td:getInput']({ 182 | string: 'tglib.input.AuthorizationCodeIncorrect', 183 | }) 184 | this._send({ 185 | '@type': 'checkAuthenticationCode', 186 | 'code': code, 187 | }) 188 | break 189 | } 190 | case 'PASSWORD_HASH_INVALID': { 191 | const password = await this.callbacks['td:getInput']({ 192 | string: 'tglib.input.AuthorizationPasswordIncorrect', 193 | extras: { hint: this.authFlowPasswordHint }, 194 | }) 195 | this._send({ 196 | '@type': 'checkAuthenticationPassword', 197 | 'password': password, 198 | }) 199 | break 200 | } 201 | case 'ACCESS_TOKEN_INVALID': { 202 | this.rejector(new InvalidBotTokenError()) 203 | break 204 | } 205 | default: { 206 | this.callbacks['td:error'].call(null, update) 207 | } 208 | } 209 | } 210 | 211 | async handleUpdate(update) { 212 | const id = update['@extra'] 213 | if (this.fetching[id]) { 214 | delete update['@extra'] 215 | this.fetching[id].resolve(update) 216 | delete this.fetching[id] 217 | return 218 | } 219 | switch (update['@type']) { 220 | case 'updateFile': { 221 | const fileId = update['file']['id'] 222 | if (!this.downloading[fileId]) { 223 | // default handle 224 | this.callbacks['td:update'].call(null, update) 225 | return 226 | } 227 | if (!update['file']['local']['path'].length) { 228 | // not yet downloaded 229 | return 230 | } 231 | // resolve downloaded 232 | this.downloading[fileId](update['file']) 233 | } 234 | case 'updateOption': { 235 | if (update['name'] === 'my_id' && update['value']['@type'] === 'optionValueEmpty') { 236 | // session has been signed out 237 | await this._destroy() 238 | break 239 | } 240 | } 241 | default: { 242 | this.callbacks['td:update'].call(null, update) 243 | } 244 | } 245 | } 246 | 247 | async fetch(query) { 248 | const id = crc32(Math.random().toString()).toString() 249 | query['@extra'] = id 250 | const receiveUpdate = new Promise((resolve, reject) => { 251 | this.fetching[id] = { resolve, reject } 252 | // timeout after 15 seconds 253 | setTimeout(() => { 254 | delete this.fetching[id] 255 | reject('Query timed out after 30 seconds.') 256 | }, 1000 * 30) 257 | }) 258 | await this._send(query) 259 | return receiveUpdate 260 | } 261 | } 262 | 263 | export default Client 264 | -------------------------------------------------------------------------------- /src/TG.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'event-emitter' 2 | 3 | // combine the user-supplied options with the referencing base options 4 | function combine(object, options, whitelistedKeys) { 5 | Object.keys(object).forEach((key) => { 6 | if (object[key] instanceof Object) { 7 | combine(object[key], options, whitelistedKeys) 8 | } else if (options[key] !== undefined && whitelistedKeys.indexOf(key) >= 0) { 9 | object[key] = options[key] 10 | } 11 | }) 12 | } 13 | 14 | class TG { 15 | constructor(client) { 16 | this.client = client 17 | } 18 | 19 | /* 20 | * Send text message to an existing chat. 21 | * Method "parseTextEntities" requires TDLib 1.1.0 (git.io/tdlibchanges). 22 | */ 23 | async sendTextMessage(args = {}) { 24 | const { $text, ...options } = args 25 | if (!$text) { 26 | throw new Error('No text defined for method "sendTextMessage".') 27 | } 28 | const payload = { 29 | '@type': 'sendMessage', 30 | 'chat_id': 0, 31 | 'reply_to_message_id': 0, 32 | 'disable_notification': false, 33 | 'from_background': true, 34 | 'reply_markup': null, 35 | 'input_message_content': { 36 | '@type': 'inputMessageText', 37 | 'text': await $text._format(this.client), 38 | 'disable_web_page_preview': true, 39 | 'clear_draft': true, 40 | }, 41 | } 42 | combine(payload, options, [ 43 | 'chat_id', 'reply_to_message_id', 'disable_notification', 'from_background', 44 | 'disable_web_page_preview', 'clear_draft', 45 | ]) 46 | return this.client.fetch(payload) 47 | } 48 | 49 | /* 50 | * Send photo message to an existing chat. 51 | */ 52 | async sendPhotoMessage(args = {}) { 53 | const { $caption, ...options } = args 54 | const payload = { 55 | '@type': 'sendMessage', 56 | 'chat_id': 0, 57 | 'reply_to_message_id': 0, 58 | 'disable_notification': false, 59 | 'from_background': true, 60 | 'reply_markup': null, 61 | 'input_message_content': { 62 | '@type': 'inputMessagePhoto', 63 | 'photo': { 64 | '@type': 'inputFileLocal', 65 | 'path': null, 66 | }, 67 | 'thumbnail': null, 68 | 'added_sticker_file_ids': [], 69 | 'width': 0, 70 | 'height': 0, 71 | 'caption': $caption ? await $caption._format(this.client) : null, 72 | 'ttl': 0, 73 | }, 74 | } 75 | combine(payload, options, [ 76 | 'chat_id', 'reply_to_message_id', 'disable_notification', 'from_background', 77 | 'path', 'thumbnail', 'added_sticker_file_ids', 'width', 'height', 'ttl', 78 | ]) 79 | return this.client.fetch(payload) 80 | } 81 | 82 | /* 83 | * Send sticker message to an existing chat. 84 | */ 85 | async sendStickerMessage(args = {}) { 86 | const { $caption, ...options } = args 87 | if (!options.path || !options.path.endsWith('webp')) { 88 | throw 'WebP image must be passed for [sendStickerMessage] method' 89 | } 90 | const payload = { 91 | '@type': 'sendMessage', 92 | 'chat_id': 0, 93 | 'reply_to_message_id': 0, 94 | 'disable_notification': false, 95 | 'from_background': true, 96 | 'reply_markup': null, 97 | 'input_message_content': { 98 | '@type': 'inputMessageSticker', 99 | 'sticker': { 100 | '@type': 'inputFileLocal', 101 | 'path': null, 102 | }, 103 | 'thumbnail': null, 104 | 'width': 0, 105 | 'height': 0, 106 | }, 107 | } 108 | combine(payload, options, [ 109 | 'chat_id', 'reply_to_message_id', 'disable_notification', 'from_background', 110 | 'path', 'thumbnail', 'width', 'height', 111 | ]) 112 | return this.client.fetch(payload) 113 | } 114 | 115 | /* 116 | * Get all chats. 117 | */ 118 | async getAllChats() { 119 | let { chat_ids: chats } = await this.client.fetch({ 120 | '@type': 'getChats', 121 | 'offset_order': '9223372036854775807', 122 | 'offset_chat_id': 0, 123 | 'limit': Math.floor(Math.random() * 9999999), 124 | }) 125 | chats = chats.map((chatId) => this.client.fetch({ 126 | '@type': 'getChat', 127 | 'chat_id': chatId, 128 | })) 129 | chats = await Promise.all(chats) 130 | return chats 131 | } 132 | 133 | /* 134 | * Update user's username. 135 | * Method "checkChatUsername" requires TDLib 1.2.0 (git.io/tdlibchanges). 136 | */ 137 | updateUsername(username, supergroupId = null) { 138 | return new Promise(async (resolve, reject) => { 139 | let payload 140 | // Check the username 141 | payload = { '@type': 'checkChatUsername', username } 142 | if (supergroupId) { 143 | const { id } = await this.client.fetch({ '@type': 'createSupergroupChat', 'supergroup_id': supergroupId }) 144 | payload['chat_id'] = id 145 | } else { 146 | const { id } = await this.client.fetch({ '@type': 'getMe' }) 147 | payload['chat_id'] = id 148 | } 149 | const { '@type': checkResult } = await this.client.fetch(payload) 150 | if (checkResult !== 'checkChatUsernameResultOk') { 151 | return reject(checkResult) 152 | } 153 | // Update the username 154 | payload = { '@type': 'setUsername', username } 155 | if (supergroupId) { 156 | payload['@type'] = 'setSupergroupUsername' 157 | payload['supergroup_id'] = supergroupId 158 | } 159 | const result = await this.client.fetch(payload) 160 | resolve(result) 161 | }) 162 | } 163 | 164 | /* 165 | * Opens a secret chat with given user ID. 166 | */ 167 | async openSecretChat(userId) { 168 | let secretChat = await this.getAllChats() 169 | secretChat = secretChat.find(({ type: { '@type': type, user_id: user } }) => { 170 | return type === 'chatTypeSecret' && user === userId 171 | }) 172 | // created already 173 | if (secretChat) { 174 | return secretChat 175 | } 176 | // create new secret chat 177 | secretChat = await this.client.fetch({ 178 | '@type': 'createNewSecretChat', 179 | 'user_id': userId, 180 | }) 181 | return secretChat 182 | } 183 | 184 | /* 185 | * Close and remove a chat. 186 | */ 187 | async deleteChat(chatId) { 188 | const chat = await this.client.fetch({ 189 | '@type': 'getChat', 190 | 'chat_id': chatId, 191 | }) 192 | 193 | let payload = {} 194 | switch (chat.type['@type']) { 195 | case 'chatTypeBasicGroup': 196 | case 'chatTypeSupergroup': { 197 | const { id } = await this.client.fetch({ '@type': 'getMe' }) 198 | payload['@type'] = 'setChatMemberStatus' 199 | payload['user_id'] = id 200 | payload['chat_id'] = chat.id 201 | payload['status'] = { '@type': 'chatMemberStatusLeft' } 202 | break 203 | } 204 | case 'chatTypeSecret': { 205 | payload['@type'] = 'closeSecretChat' 206 | payload['secret_chat_id'] = chat.type.secret_chat_id 207 | break 208 | } 209 | default: { 210 | payload['@type'] = 'closeChat' 211 | payload['chat_id'] = chat.id 212 | break 213 | } 214 | } 215 | await this.client.fetch(payload) 216 | await this.client.fetch({ 217 | '@type': 'deleteChatHistory', 218 | 'chat_id': chat.id, 219 | 'remove_from_chat_list': true, 220 | }) 221 | } 222 | 223 | /* 224 | * Get chat by username or id 225 | */ 226 | async getChat(args = {}) { 227 | const { username, chat_id } = args 228 | let chat = {} 229 | if (username) { 230 | chat = await this.client.fetch({ 231 | '@type': 'searchPublicChat', 232 | username, 233 | }) 234 | } else if (chat_id) { 235 | chat = await this.client.fetch({ 236 | '@type': 'getChat', 237 | chat_id, 238 | }) 239 | } else { 240 | throw new Error('Neither username nor chat_id were specified for method "getChat"') 241 | } 242 | return chat 243 | } 244 | 245 | /* 246 | * Download a file 247 | */ 248 | async download(remoteFileId) { 249 | const { id } = await this.client.fetch({ 250 | '@type': 'getRemoteFile', 251 | 'remote_file_id': remoteFileId, 252 | }) 253 | let file = await this.client.fetch({ 254 | '@type': 'downloadFile', 255 | 'file_id': id, 256 | 'priority': 1, 257 | }) 258 | if (!file['local']['path'].length) { 259 | const downloadPromise = new Promise((resolve) => { 260 | this.client.downloading[id] = resolve 261 | }) 262 | file = await downloadPromise 263 | } 264 | return file 265 | } 266 | 267 | 268 | /* 269 | * Call an user 270 | */ 271 | call(userId) { 272 | return new Promise(async (resolve, reject) => { 273 | try { 274 | const { id } = await this.client.fetch({ 275 | '@type': 'createCall', 276 | 'user_id': userId, 277 | 'protocol': { 278 | '@type': 'callProtocol', 279 | 'udp_p2p': true, 280 | 'udp_reflector': true, 281 | 'min_layer': 65, 282 | 'max_layer': 65, 283 | }, 284 | }) 285 | const emitter = EventEmitter() 286 | this.client._hijackUpdate('updateCall', (update) => { 287 | // If call failed due to USER_PRIVACY_RESTRICTED 288 | if (update.call.state['@type'] === 'callStateError') { 289 | this.client._hijackUpdate('updateCall', false) 290 | return reject(update.call.state.error) 291 | } 292 | if (update.call.state['@type'] === 'callStateReady') { 293 | emitter.emit('ready', update.call) 294 | } 295 | if (update.call.state['@type'] === 'callStateDiscarded') { 296 | emitter.emit('discarded', update.call) 297 | this.client._hijackUpdate('updateCall', false) 298 | } 299 | return resolve(emitter) 300 | }) 301 | } catch (err) { 302 | reject(err) 303 | } 304 | }) 305 | } 306 | } 307 | 308 | export default TG 309 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## tglib 2 | 3 | TDLib (Telegram Database library) bindings for Node.js 4 | 5 | [![npm](https://img.shields.io/npm/v/tglib.svg)](https://www.npmjs.com/package/tglib) 6 | 7 | ----- 8 | 9 | ### Getting started 10 | 11 | 1. Build the binary (https://github.com/tdlib/td#building) 12 | 2. `npm i -S tglib` 13 | 14 | ----- 15 | 16 | ### Node / WASM support 17 | 18 | tglib support both Node and WASM environments: 19 | 20 | ```js 21 | // CommonJS, loads Node version tglib 22 | const { Client } = require('tglib') 23 | 24 | // ESModule, loads WASM version tglib 25 | import { Client } from 'tglib' 26 | ``` 27 | 28 | By default, `package.json` will automatically handle it for you. 29 | 30 | If use CommonJS (`require()` function, usally Node.js), it loads `require('tglib/node')` 31 | 32 | If use ESModule (`import` syntax, usally bundlers), it loads `import ... from 'tglib/wasm'` 33 | 34 | In case something misimported, you can manually set to the correct version you need: 35 | 36 | ```js 37 | import { Client } from 'tglib/node' 38 | ``` 39 | 40 | ----- 41 | 42 | ### Options 43 | 44 | ```js 45 | new Client({ 46 | apiId: '', // specify your API ID 47 | apiHash: '', // specify your API Hash 48 | verbosityLevel: 2, // specify TDLib verbosity level to control logging, default 2 49 | tdlibParameters: {}, // specify custom tdlibParameters object 50 | 51 | // Node only options 52 | appDir: '', // specify where to place tglib files, default "__tglib__" folder 53 | binaryPath: '', // specify the TDLib static binary path, default "libtdjson" in cwd 54 | 55 | // WASM only options 56 | wasmModule: null, // specify the WebAssembly module 57 | filesDir: '', // specify the files directory path 58 | databaseDir: '', // specify the database directory path 59 | }) 60 | ``` 61 | 62 | ----- 63 | 64 | #### Callbacks / Authorization 65 | 66 | You can register callbacks for the following events: 67 | 68 | - `td:update`, default empty function 69 | - `td:error`, default empty function 70 | - `td:getInput`, default console prompt in Node, exception in WASM 71 | 72 | ```js 73 | // register callback for updates 74 | client.registerCallback('td:update', (update) => { 75 | console.log('[update]', update) 76 | }) 77 | 78 | // register callback for errors 79 | client.registerCallback('td:error', (error) => { 80 | console.log('[error]', error) 81 | }) 82 | ``` 83 | 84 | The `td:getInput` event can be configured to ask for user input. 85 | 86 | It also be used for the authorization flow too. 87 | 88 | You MUST register an `async` function or a function that returns a `Promise`. 89 | 90 | To authorize a bot: 91 | 92 | ```js 93 | // Save tglib default handler which prompt input at console 94 | const defaultHandler = client.callbacks['td:getInput'] 95 | 96 | // Register own callback for returning auth details 97 | client.registerCallback('td:getInput', async (args) => { 98 | if (args.string === 'tglib.input.AuthorizationType') { 99 | return 'bot' 100 | } else if (args.string === 'tglib.input.AuthorizationValue') { 101 | return 'YOUR_BOT_TOKEN' 102 | } 103 | return await defaultHandler(args) 104 | }) 105 | ``` 106 | 107 | To authorize an user: 108 | 109 | ```js 110 | // Save tglib default handler which prompt input at console 111 | const defaultHandler = client.callbacks['td:getInput'] 112 | 113 | // Register own callback for returning auth details 114 | client.registerCallback('td:getInput', async (args) => { 115 | if (args.string === 'tglib.input.AuthorizationType') { 116 | return 'user' 117 | } else if (args.string === 'tglib.input.AuthorizationValue') { 118 | return 'YOUR_INTERNATIONAL_PHONE_NUMBER' 119 | } 120 | return await defaultHandler(args) 121 | }) 122 | ``` 123 | 124 | The `string` property in `td:getInput` argument object is used to determine what the input is: 125 | 126 | - `tglib.input.AuthorizationType`, authorization type: `user` or `bot` 127 | - `tglib.input.AuthorizationValue`, authorization value: bot token or phone number 128 | - `tglib.input.FirstName`, first name for new account creation 129 | - `tglib.input.AuthorizationCode`, authorization code received in Telegram or SMS 130 | - `tglib.input.AuthorizationCodeIncorrect`, authorization code re-input if wrong 131 | - `tglib.input.AuthorizationPassword`, authorization password (two-step verification) 132 | - `tglib.input.AuthorizationPasswordIncorrect`, authorization password re-input if wrong 133 | 134 | These string can be used as identifier for i18n purpose. 135 | 136 | An `extras` property may present in `td:getInput` argument object as well in some events. 137 | 138 | Currently, a `extras` property will come up with `hint` property in the following events: 139 | 140 | - `tglib.input.AuthorizationPassword` 141 | - `tglib.input.AuthorizationPasswordIncorrect` 142 | 143 | The `hint` property here indicates user cloud password hint. 144 | 145 | ```js 146 | client.registerCallback('td:getInput', async ({ string, extras: { hint } = {} }) => { 147 | const result = window.prompt(`${string}${hint ? ` ${hint}` : ''}`) 148 | return result 149 | }) 150 | ``` 151 | 152 | ----- 153 | 154 | ### Connect with Telegram 155 | 156 | tglib provide a `ready` Promise in client instances. 157 | 158 | This Promise will resolve automatically when the authentication flow finishes. 159 | 160 | In order words, when user successfully logged into their account, the Promise will be resolved. 161 | 162 | ```js 163 | const client = new Client(...) 164 | 165 | await client.ready 166 | 167 | // You are now connected! 168 | await client.tg.sendTextMessage(...) 169 | ``` 170 | 171 | ----- 172 | 173 | ### APIs 174 | 175 | tglib provide some useful methods that makes your Telegram app development easier. 176 | 177 | Most API classes/methods can be found in the official [TDLib documentation](https://core.telegram.org/tdlib/docs/classes.html). 178 | 179 | #### ![](https://placehold.it/12/efcf39/000?text=+) Low Level APIs 180 | 181 | 182 | ##### `client._send(query)` -> Promise -> Object 183 | 184 | 185 |
186 | Expand 187 |

188 | 189 | This API is provided by TDLib, you can use this API to send asynchronous message to Telegram. 190 | 191 | ```js 192 | await client._send({ 193 | '@type': 'sendMessage', 194 | 'chat_id': -123456789, 195 | 'input_message_content': { 196 | '@type': 'inputMessageText', 197 | 'text': { 198 | '@type': 'formattedText', 199 |      'text': '👻', 200 | }, 201 | }, 202 | }) 203 | ``` 204 |

205 |
206 | 207 | 208 | ##### `client._execute(query)` -> Promise -> Object 209 | 210 | 211 |
212 | Expand 213 |

214 | 215 | This API is provided by TDLib, you can use this API to execute synchronous action to Telegram. 216 | 217 | ```js 218 | await client._execute({ 219 | '@type': 'getTextEntities', 220 | 'text': '@telegram /test_command https://telegram.org telegram.me', 221 | }) 222 | ``` 223 |

224 |
225 | 226 | 227 | ##### `client._destroy()` -> Promise -> Void 228 | 229 | 230 |
231 | Expand 232 |

233 | 234 | This API is provided by TDLib, you can use this API to destroy the client. 235 | 236 | ```js 237 | await client._destroy() 238 | ``` 239 |

240 |
241 | 242 | 243 | ##### `client.fetch(query)` -> Promise -> Object 244 | 245 | 246 |
247 | Expand 248 |

249 | 250 | This API is provided by tglib, you can use this API to send asynchronous message to Telegram and receive response. 251 | 252 | ```js 253 | const chats = await client.fetch({ 254 | '@type': 'getChats', 255 | 'offset_order': '9223372036854775807', 256 | 'offset_chat_id': 0, 257 | 'limit': 100, 258 | }) 259 | ``` 260 |

261 |
262 | 263 | 264 | #### ![](https://placehold.it/12/3abc64/000?text=+) High Level APIs 265 | 266 | 267 | tglib provides a collection of APIs that designed for ease of use and handiness. These APIs are located under `client.tg` property. 268 | 269 | 270 | ##### `client.tg.sendTextMessage(args = {})` -> Promise -> Void 271 | 272 | 273 |
274 | Expand 275 |

276 | 277 | This API is provided by tglib, you can use this API to send message to a chat. The function will combine custom options specified in `args` with its default. 278 | 279 | The `TextStruct` struct uses "parseTextEntities" method which requires TDLib 1.1.0 or above, see [TDLib changelog](https://git.io/tdlibchanges) for details. 280 | 281 | ```js 282 | const { TextStruct } = require('tglib/structs') 283 | 284 | await client.tg.sendTextMessage({ 285 | '$text': new TextStruct('`Hello` world!', 'textParseModeMarkdown'), 286 | 'chat_id': 123456789, 287 | 'disable_notification': true, 288 | 'clear_draft': false, 289 | }) 290 | ``` 291 |

292 |
293 | 294 | 295 | ##### `client.tg.sendPhotoMessage(args = {})` -> Promise -> Void 296 | 297 | 298 |
299 | Expand 300 |

301 | 302 | This API is provided by tglib, you can use this API to send photo to a chat. The function will combine custom options specified in `args` with its default. 303 | 304 | The `TextStruct` struct uses "parseTextEntities" method which requires TDLib 1.1.0 or above, see [TDLib changelog](https://git.io/tdlibchanges) for details. 305 | 306 | ```js 307 | const { TextStruct } = require('tglib/structs') 308 | 309 | await client.tg.sendPhotoMessage({ 310 | '$caption': new TextStruct('Such doge much wow'), 311 | 'chat_id': 123456789, 312 | 'path': '/tmp/doge.jpg', 313 | 'ttl': 5, 314 | }) 315 | ``` 316 |

317 |
318 | 319 | 320 | ##### `client.tg.sendStickerMessage(args = {})` -> Promise -> Void 321 | 322 | 323 |
324 | Expand 325 |

326 | 327 | This API is provided by tglib, you can use this API to send sticker to a chat. The function will combine custom options specified in `args` with its default. 328 | 329 | ```js 330 | await client.tg.sendStickerMessage({ 331 | 'chat_id': 123456789, 332 | 'path': '/tmp/doge.webp', 333 | }) 334 | ``` 335 |

336 |
337 | 338 | 339 | ##### `client.tg.updateUsername(username, supergroupId = null)` -> Promise -> Void 340 | 341 | 342 |
343 | Expand 344 |

345 | 346 | This API is provided by tglib, you can use this API to update the username for session user or a supergroup chat. 347 | 348 | This API uses "checkChatUsername" method which requires TDLib 1.2.0 or above, see [TDLib changelog](https://git.io/tdlibchanges) for details. 349 | 350 | ```js 351 | await client.tg.updateUsername('a_new_username') 352 | ``` 353 |

354 |
355 | 356 | 357 | ##### `client.tg.getAllChats()` -> Promise -> Array 358 | 359 | 360 |
361 | Expand 362 |

363 | 364 | This API is provided by tglib, you can use this API to get all available chats of session user. 365 | 366 | ```js 367 | const chats = await client.tg.getAllChats() 368 | ``` 369 |

370 |
371 | 372 | 373 | ##### `client.tg.openSecretChat(userId)` -> Promise -> Object 374 | 375 | 376 |
377 | Expand 378 |

379 | 380 | This API is provided by tglib, you can use this API to open a secret chat with given user ID. 381 | 382 | Note: Secret chats are associated with the corresponding TDLib folder. (i.e. only available on the same device). 383 | 384 | ```js 385 | const chat = await client.tg.openSecretChat(123456789) 386 | ``` 387 |

388 |
389 | 390 | 391 | ##### `client.tg.deleteChat(chatId)` -> Promise -> Void 392 | 393 | 394 |
395 | Expand 396 |

397 | 398 | This API is provided by tglib, you can use this API to delete a chat and remove it from the chat list. You can use this API to delete "private", "secret", "basicGroup", and "supergroup" chats. 399 | 400 | ```js 401 | await client.tg.deleteChat(-12345678901234) 402 | ``` 403 |

404 |
405 | 406 | 407 | ##### `client.tg.getChat(args = {})` -> Promise -> Object 408 | 409 | 410 |
411 | Expand 412 |

413 | 414 | This API is provided by tglib, you can use this API to get a chat by username or chat id. This method requires either `username` option, or `chat_id` option. 415 | 416 | ```js 417 | const chat1 = await client.tg.getChat({ username: 'chat_username' }) 418 | const chat2 = await client.tg.getChat({ chat_id: '-12345678901234' }) 419 | ``` 420 |

421 |
422 | 423 | 424 | ##### `client.tg.call(userId)` -> Promise -> EventEmitter 425 | 426 | 427 |
428 | Expand 429 |

430 | 431 | This API is provided by tglib, you can use this API to call an user. 432 | 433 | The promise will resolve with an EventEmitter when call succceeded. 434 | 435 | The EventEmitter will emit `ready` and `discarded` events. 436 | 437 | ```js 438 | const emitter = await client.tg.call(4000000001) 439 | emitter.on('ready', (call) => console.log(call)) 440 | emitter.on('discarded', (call) => console.log(call)) 441 | ``` 442 |

443 |
444 | 445 | ----- 446 | 447 | ### Requirements 448 | 449 | - Node.js 10 preferred (minimum >= 9.0.0) 450 | > Note: If you are using Node.js 9.x, you may encounter a warning message `Warning: N-API is an experimental feature and could change at any time.`, this can be suppressed by upgrading to version 10. 451 | 452 | - TDLib binary (see build instructions below) 453 | 454 | > [Windows](https://github.com/c0re100/F9TelegramUtils#compile-tdlib-on-windows) 455 | 456 | > [macOS](https://github.com/tdlib/td#macos) 457 | 458 | > [Linux - CentOS 7.5](https://gist.github.com/nodegin/e3849aa1e5170c2e05942ffe86e4f8c9) 459 | 460 | Note: building TDLib binary requires at least 8GB of memory (or more...), otherwise building process will fail. Build in containers are recommended. 461 | 462 | ----- 463 | 464 | ### License 465 | 466 | tglib uses the same license as TDLib. See [tdlib/td](https://github.com/tdlib/td) for more information. 467 | --------------------------------------------------------------------------------