@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 +4 -0
- package/CHANGELOG.md +11 -0
- package/README.md +36 -5
- package/docs/NODES.md +22 -0
- package/nodes/config.js +32 -18
- package/package.json +8 -1
- package/test/config.test.js +62 -0
package/AGENTS.md
ADDED
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
|
-
|
|
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
|
-
|
|
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.
|
|
20
|
-
|
|
21
|
-
|
|
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 =
|
|
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
|
|
19
|
+
if (activeClients.has(sessionStr)) {
|
|
20
20
|
// Reuse existing client
|
|
21
|
-
const record = activeClients
|
|
21
|
+
const record = activeClients.get(sessionStr);
|
|
22
22
|
this.client = record.client;
|
|
23
23
|
record.refCount += 1;
|
|
24
|
-
|
|
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
|
-
|
|
34
|
-
activeClients[sessionStr] = { client: this.client, refCount: 1 };
|
|
40
|
+
node.status({ fill: "yellow", shape: "ring", text: "Connecting" });
|
|
35
41
|
|
|
36
|
-
this.client
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
});
|