@patricktobias86/node-red-telegram-account 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,4 @@
1
+ # Repository Instructions
2
+
3
+ - Always run `npm test` before creating a commit.
4
+ - If a code change alters user-facing behavior, update the README and any relevant docs to reflect the change.
package/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.1.7] - 2025-07-22
6
+ ### Added
7
+ - Mocha tests for the configuration node ensure sessions are reused correctly.
8
+
9
+ ### Changed
10
+ - Session management now tracks active clients in a `Map` for safer reuse.
11
+
package/README.md CHANGED
@@ -10,13 +10,44 @@
10
10
  npm i @patricktobias86/node-red-telegram-account
11
11
  ```
12
12
 
13
- ## Nodes
13
+ This package contains a collection of Node‑RED nodes built on top of [GramJS](https://gram.js.org/). They make it easier to interact with the Telegram MTProto API from your flows.
14
14
 
15
- - resolve-userid – resolve a Telegram username to its numeric user ID
15
+ ## Node overview
16
+
17
+ See [docs/NODES.md](docs/NODES.md) for a detailed description of every node. Below is a quick summary:
18
+
19
+ - **config** – stores your API credentials and caches sessions for reuse.
20
+ - **auth** – interactive login that outputs a `stringSession`.
21
+ - **receiver** – emits messages for every incoming update (with optional ignore list).
22
+ - **command** – triggers when an incoming message matches a command or regex.
23
+ - **send-message** – sends text or media messages with rich options.
24
+ - **send-files** – uploads one or more files with captions and buttons.
25
+ - **get-entity** – resolves usernames, IDs or t.me links into Telegram entities.
26
+ - **delete-message** – deletes one or more messages, optionally revoking them.
27
+ - **iter-dialogs** – iterates over your dialogs (chats, groups, channels).
28
+ - **iter-messages** – iterates over messages in a chat with filtering options.
29
+ - **promote-admin** – promotes a user to admin with configurable rights.
30
+ - **resolve-userid** – converts a username to a numeric user ID.
16
31
 
17
32
  ## Session management
18
33
 
19
- Connections to Telegram are cached by the configuration node. When the flow is
20
- redeployed, the existing session is reused instead of creating a new one.
21
- The client is only disconnected once no nodes reference that session anymore.
34
+ Connections to Telegram are cached by the configuration node. A Map keyed by the `stringSession` tracks each client together with a reference count and the connection promise. If a node is created while another one is still connecting, it waits for that promise and then reuses the same client.
35
+
36
+ A single `TelegramClient` instance is therefore shared between all flows that point to the same configuration node, even after a redeploy. When Node‑RED restarts it checks the cache and returns the existing client rather than creating a new connection. The reference count is decreased whenever a node using the session is closed. Once all nodes have closed and the count reaches zero, the cached client is disconnected.
37
+
38
+ Example flows can be found in the [examples](examples) folder.
39
+
40
+ ## Running tests
41
+
42
+ After cloning the repository, install dependencies and run the test suite with:
43
+
44
+ ```bash
45
+ npm install
46
+ npm test
47
+ ```
48
+
49
+ The tests use Mocha and verify that sessions are properly cached across nodes.
50
+
51
+ ## Changelog
22
52
 
53
+ See [CHANGELOG.md](CHANGELOG.md) for release notes.
package/docs/NODES.md ADDED
@@ -0,0 +1,22 @@
1
+ # Node Overview
2
+
3
+ The package provides a set of custom Node-RED nodes built around the [GramJS](https://gram.js.org/) library. Each node exposes a small part of the Telegram API, making it easier to build Telegram bots and automation flows.
4
+
5
+ Below is a short description of each node. For a full list of configuration options see the built‑in help inside Node‑RED or open the corresponding HTML files in the `nodes/` directory.
6
+
7
+ | Node | Description |
8
+ |------|-------------|
9
+ | **config** | Configuration node storing API credentials and connection options. Other nodes reference this to share a Telegram client and reuse the session. Connections are tracked in a Map with a reference count so multiple nodes can wait for the same connection. |
10
+ | **auth** | Starts an interactive login flow. Produces a `stringSession` that can be reused with the `config` node. |
11
+ | **receiver** | Emits an output message for every incoming Telegram message. Can ignore specific user IDs. |
12
+ | **command** | Listens for new messages and triggers when a message matches a configured command or regular expression. |
13
+ | **send-message** | Sends text messages or media files to a chat. Supports parse mode, buttons, scheduling, and more. |
14
+ | **send-files** | Uploads one or more files to a chat with optional caption, thumbnails and other parameters. |
15
+ | **get-entity** | Resolves a username, user ID or t.me URL into a Telegram entity object. |
16
+ | **delete-message** | Deletes one or multiple messages from a chat. Can revoke messages for all participants. |
17
+ | **iter-dialogs** | Iterates through the user’s dialogs (chats, groups, channels) and outputs the collected list. |
18
+ | **iter-messages** | Iterates over messages in a chat with various filtering and pagination options. |
19
+ | **promote-admin** | Grants admin rights to a user in a group or channel with configurable permissions. |
20
+ | **resolve-userid** | Converts a Telegram username to its numeric user ID. |
21
+
22
+
package/nodes/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const { TelegramClient } = require("telegram");
2
2
  const { StringSession } = require("telegram/sessions");
3
3
 
4
- const activeClients = {}; // Cache: session string → { client, refCount }
4
+ const activeClients = new Map(); // Cache: session string → { client, refCount, connecting }
5
5
 
6
6
  module.exports = function (RED) {
7
7
  function TelegramClientConfig(config) {
@@ -16,12 +16,19 @@ module.exports = function (RED) {
16
16
 
17
17
  const node = this;
18
18
 
19
- if (activeClients[sessionStr]) {
19
+ if (activeClients.has(sessionStr)) {
20
20
  // Reuse existing client
21
- const record = activeClients[sessionStr];
21
+ const record = activeClients.get(sessionStr);
22
22
  this.client = record.client;
23
23
  record.refCount += 1;
24
- node.status({ fill: "green", shape: "dot", text: "Reused existing client" });
24
+ if (record.connecting) {
25
+ node.status({ fill: "yellow", shape: "dot", text: "Waiting for connection" });
26
+ record.connecting.then(() => {
27
+ node.status({ fill: "green", shape: "dot", text: "Reused existing client" });
28
+ }).catch(err => node.error("Connection error: " + err.message));
29
+ } else {
30
+ node.status({ fill: "green", shape: "dot", text: "Reused existing client" });
31
+ }
25
32
  } else {
26
33
  // Create and connect new client
27
34
  this.client = new TelegramClient(this.session, apiId, apiHash, {
@@ -30,24 +37,31 @@ module.exports = function (RED) {
30
37
  requestRetries: config.requestRetries || 5,
31
38
  });
32
39
 
33
- // Pre-store with refCount to ensure reuse during connection setup
34
- activeClients[sessionStr] = { client: this.client, refCount: 1 };
40
+ node.status({ fill: "yellow", shape: "ring", text: "Connecting" });
35
41
 
36
- this.client.connect().then(async () => {
37
- const authorized = await this.client.isUserAuthorized();
38
- if (!authorized) {
39
- node.error("Session is invalid");
40
- } else {
42
+ const record = { client: this.client, refCount: 1, connecting: null };
43
+ record.connecting = this.client.connect()
44
+ .then(async () => {
45
+ const authorized = await this.client.isUserAuthorized();
46
+ if (!authorized) {
47
+ throw new Error("Session is invalid");
48
+ }
49
+ })
50
+ .then(() => {
41
51
  node.status({ fill: "green", shape: "dot", text: "Connected" });
42
- }
43
- }).catch(err => {
44
- node.error("Connection error: " + err.message);
45
- delete activeClients[sessionStr];
46
- });
52
+ record.connecting = null;
53
+ })
54
+ .catch(err => {
55
+ node.error("Connection error: " + err.message);
56
+ activeClients.delete(sessionStr);
57
+ record.connecting = null;
58
+ });
59
+
60
+ activeClients.set(sessionStr, record);
47
61
  }
48
62
 
49
63
  this.on("close", async () => {
50
- const record = activeClients[sessionStr];
64
+ const record = activeClients.get(sessionStr);
51
65
  if (record && record.client === this.client) {
52
66
  record.refCount -= 1;
53
67
  if (record.refCount <= 0) {
@@ -56,7 +70,7 @@ module.exports = function (RED) {
56
70
  } catch (err) {
57
71
  node.error("Disconnect error: " + err.message);
58
72
  }
59
- delete activeClients[sessionStr];
73
+ activeClients.delete(sessionStr);
60
74
  node.status({ fill: "red", shape: "ring", text: "Disconnected" });
61
75
  }
62
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patricktobias86/node-red-telegram-account",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Node-RED nodes to communicate with GramJS.",
5
5
  "main": "nodes/config.js",
6
6
  "keywords": [
@@ -45,5 +45,12 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "telegram": "^2.17.10"
48
+ },
49
+ "devDependencies": {
50
+ "mocha": "^11.7.1",
51
+ "proxyquire": "^2.1.3"
52
+ },
53
+ "scripts": {
54
+ "test": "mocha"
48
55
  }
49
56
  }
@@ -0,0 +1,62 @@
1
+ const assert = require('assert');
2
+ const proxyquire = require('proxyquire').noPreserveCache();
3
+
4
+ function load() {
5
+ const instances = [];
6
+ class TelegramClientStub {
7
+ constructor(session, id, hash, opts) {
8
+ this.session = session;
9
+ this.id = id;
10
+ this.hash = hash;
11
+ this.opts = opts;
12
+ instances.push(this);
13
+ }
14
+ connect() { return Promise.resolve(); }
15
+ isUserAuthorized() { return Promise.resolve(true); }
16
+ disconnect() { return Promise.resolve(); }
17
+ }
18
+ class StringSessionStub {
19
+ constructor(str) { this.str = str; }
20
+ }
21
+
22
+ let NodeCtor;
23
+ const RED = {
24
+ nodes: {
25
+ createNode(node) {
26
+ node._events = {};
27
+ node.on = (e, fn) => { node._events[e] = fn; };
28
+ node.status = () => {};
29
+ },
30
+ registerType(name, ctor) { NodeCtor = ctor; }
31
+ }
32
+ };
33
+
34
+ proxyquire('../nodes/config.js', {
35
+ telegram: { TelegramClient: TelegramClientStub },
36
+ 'telegram/sessions': { StringSession: StringSessionStub }
37
+ })(RED);
38
+
39
+ return { NodeCtor, instances };
40
+ }
41
+
42
+ describe('TelegramClientConfig', function() {
43
+ it('creates only one client for identical sessions', async function() {
44
+ const { NodeCtor, instances } = load();
45
+ const cfg = { session: 'sess', api_id: 1, api_hash: 'hash' };
46
+ const a = new NodeCtor(cfg);
47
+ const b = new NodeCtor(cfg);
48
+ assert.strictEqual(instances.length, 1);
49
+ assert.strictEqual(a.client, b.client);
50
+ });
51
+
52
+ it('reuses session after node redeploy', async function() {
53
+ const { NodeCtor, instances } = load();
54
+ const cfg = { session: 'sess', api_id: 1, api_hash: 'hash' };
55
+ const a = new NodeCtor(cfg);
56
+ const b = new NodeCtor(cfg);
57
+ await a._events.close();
58
+ const c = new NodeCtor(cfg);
59
+ assert.strictEqual(instances.length, 1);
60
+ assert.strictEqual(b.client, c.client);
61
+ });
62
+ });