@patricktobias86/node-red-telegram-account 1.1.6 → 1.1.8
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 +15 -0
- package/README.md +36 -5
- package/docs/NODES.md +22 -0
- package/nodes/command.js +15 -13
- package/nodes/config.js +32 -18
- package/nodes/receiver.js +20 -13
- package/package.json +8 -1
- package/test/command.test.js +43 -0
- package/test/config.test.js +62 -0
- package/test/receiver.test.js +43 -0
package/AGENTS.md
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
|
|
12
|
+
## [1.1.8] - 2025-07-22
|
|
13
|
+
### Fixed
|
|
14
|
+
- Receiver and Command nodes now remove their event listeners when closed to prevent duplicate messages after redeploys.
|
|
15
|
+
|
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). Event listeners are cleaned up on node close so redeploys won't duplicate messages.
|
|
22
|
+
- **command** – triggers when an incoming message matches a command or regex. Event listeners are removed on redeploy to prevent duplicates.
|
|
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. Event handlers are automatically removed when the node is closed. |
|
|
12
|
+
| **command** | Listens for new messages and triggers when a message matches a configured command or regular expression. The event listener is cleaned up on node close to avoid duplicates. |
|
|
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/command.js
CHANGED
|
@@ -9,44 +9,46 @@ module.exports = function (RED) {
|
|
|
9
9
|
/** @type {TelegramClient} */
|
|
10
10
|
const client = this.config.client;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
client.addEventHandler((update) => {
|
|
16
|
-
const message = update.message.message
|
|
12
|
+
const event = new NewMessage();
|
|
13
|
+
const handler = (update) => {
|
|
14
|
+
const message = update.message.message;
|
|
17
15
|
if (message) {
|
|
18
16
|
if (config.regex) {
|
|
19
17
|
const regex = new RegExp(config.command);
|
|
20
18
|
|
|
21
19
|
if (regex.test(message)) {
|
|
22
|
-
|
|
23
20
|
var msg = {
|
|
24
21
|
payload: {
|
|
25
22
|
update
|
|
26
23
|
}
|
|
27
24
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
25
|
node.send(msg);
|
|
31
26
|
}
|
|
32
27
|
} else if (message === config.command) {
|
|
33
|
-
|
|
34
28
|
var msg = {
|
|
35
29
|
payload: {
|
|
36
30
|
update
|
|
37
31
|
}
|
|
38
32
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
33
|
node.send(msg);
|
|
42
34
|
}
|
|
43
35
|
}
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
client.addEventHandler(handler, event);
|
|
46
40
|
} catch (err) {
|
|
47
41
|
node.error('Authorization error: ' + err.message);
|
|
48
42
|
}
|
|
49
43
|
|
|
44
|
+
this.on('close', () => {
|
|
45
|
+
try {
|
|
46
|
+
client.removeEventHandler(handler, event);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
node.error('Handler removal error: ' + err.message);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
RED.nodes.registerType('command', Command);
|
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/nodes/receiver.js
CHANGED
|
@@ -8,24 +8,31 @@ module.exports = function (RED) {
|
|
|
8
8
|
const client = this.config.client;
|
|
9
9
|
const ignore = config.ignore.split(/\n/);
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
} )
|
|
11
|
+
const event = new NewMessage();
|
|
12
|
+
const handler = (update) => {
|
|
13
|
+
if (update.message.fromId != null && !ignore.includes(update.message.fromId.userId.toString())) {
|
|
14
|
+
node.send({
|
|
15
|
+
payload: {
|
|
16
|
+
update
|
|
17
|
+
}
|
|
18
|
+
});
|
|
21
19
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
client.addEventHandler(handler, event);
|
|
25
24
|
} catch (err) {
|
|
26
25
|
node.error('Authorization error: ' + err.message);
|
|
27
26
|
}
|
|
28
27
|
|
|
28
|
+
this.on('close', () => {
|
|
29
|
+
try {
|
|
30
|
+
client.removeEventHandler(handler, event);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
node.error('Handler removal error: ' + err.message);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
RED.nodes.registerType('receiver', Receiver);
|
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.8",
|
|
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,43 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const proxyquire = require('proxyquire').noPreserveCache();
|
|
3
|
+
|
|
4
|
+
function load() {
|
|
5
|
+
const addCalls = [];
|
|
6
|
+
const removeCalls = [];
|
|
7
|
+
class TelegramClientStub {
|
|
8
|
+
addEventHandler(fn, event) { addCalls.push({fn, event}); }
|
|
9
|
+
removeEventHandler(fn, event) { removeCalls.push({fn, event}); }
|
|
10
|
+
}
|
|
11
|
+
class NewMessageStub {}
|
|
12
|
+
|
|
13
|
+
let NodeCtor;
|
|
14
|
+
const configNode = { client: new TelegramClientStub() };
|
|
15
|
+
const RED = {
|
|
16
|
+
nodes: {
|
|
17
|
+
createNode(node) {
|
|
18
|
+
node._events = {};
|
|
19
|
+
node.on = (e, fn) => { node._events[e] = fn; };
|
|
20
|
+
},
|
|
21
|
+
registerType(name, ctor) { NodeCtor = ctor; },
|
|
22
|
+
getNode() { return configNode; }
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
proxyquire('../nodes/command.js', {
|
|
27
|
+
'telegram/events': { NewMessage: NewMessageStub }
|
|
28
|
+
})(RED);
|
|
29
|
+
|
|
30
|
+
return { NodeCtor, addCalls, removeCalls };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Command node', function() {
|
|
34
|
+
it('removes event handler on close', function() {
|
|
35
|
+
const { NodeCtor, addCalls, removeCalls } = load();
|
|
36
|
+
const node = new NodeCtor({config:'c', command:'cmd', regex:false});
|
|
37
|
+
assert.strictEqual(addCalls.length, 1);
|
|
38
|
+
node._events.close();
|
|
39
|
+
assert.strictEqual(removeCalls.length, 1);
|
|
40
|
+
assert.strictEqual(removeCalls[0].fn, addCalls[0].fn);
|
|
41
|
+
assert.strictEqual(removeCalls[0].event, addCalls[0].event);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const proxyquire = require('proxyquire').noPreserveCache();
|
|
3
|
+
|
|
4
|
+
function load() {
|
|
5
|
+
const addCalls = [];
|
|
6
|
+
const removeCalls = [];
|
|
7
|
+
class TelegramClientStub {
|
|
8
|
+
addEventHandler(fn, event) { addCalls.push({fn, event}); }
|
|
9
|
+
removeEventHandler(fn, event) { removeCalls.push({fn, event}); }
|
|
10
|
+
}
|
|
11
|
+
class NewMessageStub {}
|
|
12
|
+
|
|
13
|
+
let NodeCtor;
|
|
14
|
+
const configNode = { client: new TelegramClientStub() };
|
|
15
|
+
const RED = {
|
|
16
|
+
nodes: {
|
|
17
|
+
createNode(node) {
|
|
18
|
+
node._events = {};
|
|
19
|
+
node.on = (e, fn) => { node._events[e] = fn; };
|
|
20
|
+
},
|
|
21
|
+
registerType(name, ctor) { NodeCtor = ctor; },
|
|
22
|
+
getNode() { return configNode; }
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
proxyquire('../nodes/receiver.js', {
|
|
27
|
+
'telegram/events': { NewMessage: NewMessageStub }
|
|
28
|
+
})(RED);
|
|
29
|
+
|
|
30
|
+
return { NodeCtor, addCalls, removeCalls };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('Receiver node', function() {
|
|
34
|
+
it('removes event handler on close', function() {
|
|
35
|
+
const { NodeCtor, addCalls, removeCalls } = load();
|
|
36
|
+
const node = new NodeCtor({config:'c', ignore:''});
|
|
37
|
+
assert.strictEqual(addCalls.length, 1);
|
|
38
|
+
node._events.close();
|
|
39
|
+
assert.strictEqual(removeCalls.length, 1);
|
|
40
|
+
assert.strictEqual(removeCalls[0].fn, addCalls[0].fn);
|
|
41
|
+
assert.strictEqual(removeCalls[0].event, addCalls[0].event);
|
|
42
|
+
});
|
|
43
|
+
});
|