Skip to content

Commit 7333920

Browse files
committed
Added possibility to listen on channels: #289
1 parent 7852156 commit 7333920

3 files changed

Lines changed: 93 additions & 89 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Use telegram service to communicate with ioBroker
3838
### **WORK IN PROGRESS**
3939
-->
4040
### **WORK IN PROGRESS**
41+
- (@GermanBluefox) Channel posts (from a channel where the bot is an admin) are now received and written to `communicate.request`/`communicate.requestChatId` (previously ignored)
42+
- (@GermanBluefox) Robustness: all `setState` calls now catch their errors (via a `setStateSafe` helper), so a failing state write can no longer cause an unhandled promise rejection
4143
- (@GermanBluefox) Added the state `communicate.chats`: every chat/group the bot receives a message from is remembered as JSON (`id => {title, type}`), so other adapters can offer a chat/group picker
4244
- (@GermanBluefox) Outgoing messages that fail because telegram is unreachable are now queued in memory and resent automatically once the connection is back (bounded queue, permanent errors like "chat not found" are not retried)
4345
- (@GermanBluefox) Documented that an unanswered `ask` returns the string `'__timeout__'`, and that the calling adapter's own `sendTo` timeout (JavaScript adapter defaults to ~20 s) must be larger than the configured answer timeout - otherwise the callback fires early (looks like a "No" answer)

docs/en/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,14 @@ on({ id: 'telegram.0.communicate.requestLocation', change: 'any' }, obj => {
348348
});
349349
```
350350

351+
## Receiving channel posts
352+
If the bot is an administrator of a channel, posts published in that channel are received as well and written
353+
to `telegram.INSTANCE.communicate.request` in the form `[channel title]text` (together with
354+
`communicate.requestChatId` and `communicate.requestMessageId`). Channel posts are anonymous (they have no
355+
sender user), so the authentication/command handling does not apply to them - they are only exposed as a
356+
request. Any attached media is saved like for normal messages, and the channel is added to
357+
`communicate.chats`.
358+
351359
## Known chats and groups
352360
Every chat or group the bot receives a message from is remembered in the state
353361
`telegram.INSTANCE.communicate.chats` as a JSON object `id => { title, type }` (where `type` is one of

src/main.ts

Lines changed: 83 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,13 @@ class Telegram extends Adapter {
166166
try {
167167
userObj = JSON.parse(state.val as string);
168168
delete userObj[userID];
169-
this.setState('communicate.users', JSON.stringify(userObj), true, err => {
170-
if (!err) {
169+
this.setState('communicate.users', JSON.stringify(userObj), true)
170+
.then(() => {
171171
this.sendTo(obj.from, obj.command, userID, obj.callback);
172-
this.updateUsers();
172+
void this.updateUsers();
173173
this.log.warn(`User ${userID} has been deleted!`);
174-
}
175-
});
174+
})
175+
.catch(e => this.log.error(`Cannot set state communicate.users: ${e}`));
176176
} catch (err) {
177177
this.log.error(err);
178178
this.log.error(`Cannot delete user ${userID}!`);
@@ -191,15 +191,15 @@ class Telegram extends Adapter {
191191
try {
192192
userObj = JSON.parse(state.val as string);
193193
userObj[userID].sysMessages = checked;
194-
this.setState('communicate.users', JSON.stringify(userObj), true, err => {
195-
if (!err) {
194+
this.setState('communicate.users', JSON.stringify(userObj), true)
195+
.then(() => {
196196
this.sendTo(obj.from, obj.command, userID, obj.callback);
197-
this.updateUsers();
197+
void this.updateUsers();
198198
this.log.info(
199199
`Receiving of system messages for user "${userID}" has been changed to ${checked}!`,
200200
);
201-
}
202-
});
201+
})
202+
.catch(e => this.log.error(`Cannot set state communicate.users: ${e}`));
203203
} catch (err) {
204204
this.log.error(err);
205205
this.log.error(`Cannot change user ${userID}!`);
@@ -208,15 +208,15 @@ class Telegram extends Adapter {
208208
});
209209
} else if (obj.command === 'delAllUser') {
210210
try {
211-
this.setState('communicate.users', '{}', true, err => {
212-
if (!err) {
211+
this.setState('communicate.users', '{}', true)
212+
.then(() => {
213213
this.sendTo(obj.from, obj.command, true, obj.callback);
214-
this.updateUsers();
214+
void this.updateUsers();
215215
this.log.warn(
216216
'List of saved users has been wiped. Every User has to reauthenticate with the new password!',
217217
);
218-
}
219-
});
218+
})
219+
.catch(e => this.log.error(`Cannot set state communicate.users: ${e}`));
220220
} catch (err) {
221221
this.log.error(err);
222222
this.log.error('Cannot wipe list of saved users!');
@@ -610,6 +610,17 @@ class Telegram extends Adapter {
610610
return `${cmd.alias} => ${val}${cmd.unit ? ` ${cmd.unit}` : ''}`;
611611
}
612612

613+
/**
614+
* Fire-and-forget `setState` that never rejects: a failure is logged instead of surfacing as an
615+
* unhandled promise rejection (which would otherwise terminate the adapter).
616+
*
617+
* @param id the (namespaced) state id
618+
* @param value the value or a settable-state object (e.g. `{ val, ack: true }`)
619+
*/
620+
setStateSafe(id: string, value: ioBroker.SettableState): void {
621+
this.setState(id, value).catch(e => this.log.error(`Cannot set state "${id}": ${e}`));
622+
}
623+
613624
connectionState(connected: boolean, logSuccess?: boolean): void {
614625
let errorCounter = 0;
615626

@@ -638,7 +649,7 @@ class Telegram extends Adapter {
638649
}
639650
if (this.isConnected !== connected) {
640651
this.isConnected = connected;
641-
this.setState('info.connection', this.isConnected, true);
652+
this.setStateSafe('info.connection', { val: this.isConnected, ack: true });
642653
if (this.isConnected) {
643654
if (this.pollConnectionStatus) {
644655
this.clearTimeout(this.pollConnectionStatus);
@@ -1670,14 +1681,14 @@ class Telegram extends Adapter {
16701681
msg.location ||
16711682
msg.venue
16721683
) {
1673-
this.setState('communicate.requestChatId', { val: msg.chat.id, ack: true });
1674-
this.setState('communicate.requestMessageId', { val: msg.message_id, ack: true });
1675-
this.setState('communicate.requestMessageThreadId', {
1684+
this.setStateSafe('communicate.requestChatId', { val: msg.chat.id, ack: true });
1685+
this.setStateSafe('communicate.requestMessageId', { val: msg.message_id, ack: true });
1686+
this.setStateSafe('communicate.requestMessageThreadId', {
16761687
val: msg.is_topic_message ? msg.message_thread_id : 0,
16771688
ack: true,
16781689
});
16791690
if (msg.from) {
1680-
this.setState('communicate.requestUserId', { val: msg.from.id.toString(), ack: true });
1691+
this.setStateSafe('communicate.requestUserId', { val: msg.from.id.toString(), ack: true });
16811692
}
16821693
}
16831694

@@ -1686,7 +1697,7 @@ class Telegram extends Adapter {
16861697
// vis/jarvis map. See https://github.com/iobroker-community-adapters/ioBroker.telegram/issues/853
16871698
const location = msg.venue?.location || msg.location;
16881699
if (location) {
1689-
this.setState('communicate.requestLocation', {
1700+
this.setStateSafe('communicate.requestLocation', {
16901701
val: `${location.latitude};${location.longitude}`,
16911702
ack: true,
16921703
});
@@ -1700,12 +1711,7 @@ class Telegram extends Adapter {
17001711
res => {
17011712
if (!res.error) {
17021713
this.log.info(res.info!);
1703-
this.setState(
1704-
'communicate.pathFile',
1705-
res.path!,
1706-
true,
1707-
err => err && this.log.error(err.message),
1708-
);
1714+
this.setStateSafe('communicate.pathFile', { val: res.path!, ack: true });
17091715
} else {
17101716
this.log.debug(res.error);
17111717
}
@@ -1766,12 +1772,7 @@ class Telegram extends Adapter {
17661772
this.saveFile(item.file_id, fileName, res => {
17671773
if (!res.error) {
17681774
this.log.info(res.info!);
1769-
this.setState(
1770-
'communicate.pathFile',
1771-
res.path!,
1772-
true,
1773-
err => err && this.log.error(err.message),
1774-
);
1775+
this.setStateSafe('communicate.pathFile', { val: res.path!, ack: true });
17751776
} else {
17761777
this.log.debug(res.error);
17771778
}
@@ -1785,12 +1786,7 @@ class Telegram extends Adapter {
17851786
this.saveFile(msg.video.file_id, `/video/${date}.mp4`, res => {
17861787
if (!res.error) {
17871788
this.log.info(res.info!);
1788-
this.setState(
1789-
'communicate.pathFile',
1790-
res.path!,
1791-
true,
1792-
err => err && this.log.error(err.message),
1793-
);
1789+
this.setStateSafe('communicate.pathFile', { val: res.path!, ack: true });
17941790
} else {
17951791
this.log.debug(res.error);
17961792
}
@@ -1803,12 +1799,7 @@ class Telegram extends Adapter {
18031799
this.saveFile(msg.video_note.file_id, `/video/${date}.mp4`, res => {
18041800
if (!res.error) {
18051801
this.log.info(res.info!);
1806-
this.setState(
1807-
'communicate.pathFile',
1808-
res.path!,
1809-
true,
1810-
err => err && this.log.error(err.message),
1811-
);
1802+
this.setStateSafe('communicate.pathFile', { val: res.path!, ack: true });
18121803
} else {
18131804
this.log.debug(res.error);
18141805
}
@@ -1821,12 +1812,7 @@ class Telegram extends Adapter {
18211812
this.saveFile(msg.audio.file_id, `/audio/${date}.mp3`, res => {
18221813
if (!res.error) {
18231814
this.log.info(res.info!);
1824-
this.setState(
1825-
'communicate.pathFile',
1826-
res.path!,
1827-
true,
1828-
err => err && this.log.error(err.message),
1829-
);
1815+
this.setStateSafe('communicate.pathFile', { val: res.path!, ack: true });
18301816
} else {
18311817
this.log.debug(res.error);
18321818
}
@@ -1839,12 +1825,7 @@ class Telegram extends Adapter {
18391825
this.saveFile(msg.document.file_id, `/document/${msg.document.file_name}`, res => {
18401826
if (!res.error) {
18411827
this.log.info(res.info!);
1842-
this.setState(
1843-
'communicate.pathFile',
1844-
res.path!,
1845-
true,
1846-
err => err && this.log.error(err.message),
1847-
);
1828+
this.setStateSafe('communicate.pathFile', { val: res.path!, ack: true });
18481829
} else {
18491830
this.log.debug(res.error);
18501831
}
@@ -2084,7 +2065,7 @@ class Telegram extends Adapter {
20842065
this.storedUsers[id] = { firstName, userName, sysMessages: false };
20852066

20862067
if (this.config.rememberUsers) {
2087-
this.setState('communicate.users', JSON.stringify(this.storedUsers), true);
2068+
this.setStateSafe('communicate.users', { val: JSON.stringify(this.storedUsers), ack: true });
20882069
}
20892070
}
20902071
}
@@ -2105,7 +2086,7 @@ class Telegram extends Adapter {
21052086

21062087
if (!this.storedChats[id] || this.storedChats[id].title !== title || this.storedChats[id].type !== type) {
21072088
this.storedChats[id] = { title, type };
2108-
this.setState('communicate.chats', { val: JSON.stringify(this.storedChats), ack: true });
2089+
this.setStateSafe('communicate.chats', { val: JSON.stringify(this.storedChats), ack: true });
21092090
}
21102091
}
21112092

@@ -2703,17 +2684,17 @@ class Telegram extends Adapter {
27032684
.catch(err => this.log.error(`Cannot send message to ${this.config.assistantInstance}: ${err}`));
27042685
}
27052686

2706-
this.setState('communicate.requestChatId', { val: msg.chat.id, ack: true });
2707-
this.setState('communicate.requestMessageId', { val: msg.message_id, ack: true });
2708-
this.setState('communicate.requestMessageThreadId', {
2687+
this.setStateSafe('communicate.requestChatId', { val: msg.chat.id, ack: true });
2688+
this.setStateSafe('communicate.requestMessageId', { val: msg.message_id, ack: true });
2689+
this.setStateSafe('communicate.requestMessageThreadId', {
27092690
val: msg.is_topic_message ? msg.message_thread_id : 0,
27102691
ack: true,
27112692
});
2712-
this.setState('communicate.requestUserId', {
2693+
this.setStateSafe('communicate.requestUserId', {
27132694
val: from.id.toString(),
27142695
ack: true,
27152696
});
2716-
this.setState('communicate.request', { val: `[${user}]${msgText}`, ack: true });
2697+
this.setStateSafe('communicate.request', { val: `[${user}]${msgText}`, ack: true });
27172698
}
27182699

27192700
connect(): void {
@@ -2811,7 +2792,7 @@ class Telegram extends Adapter {
28112792
this.connectionState(true);
28122793

28132794
if (this.config.storeRawRequest) {
2814-
this.setState('communicate.requestRaw', { val: JSON.stringify(msg, null, 2), ack: true });
2795+
this.setStateSafe('communicate.requestRaw', { val: JSON.stringify(msg, null, 2), ack: true });
28152796
}
28162797

28172798
this.getMessage(msg);
@@ -2825,6 +2806,33 @@ class Telegram extends Adapter {
28252806
}
28262807
});
28272808

2809+
// Channel posts (in a channel where the bot is an admin) are delivered as a separate event, not
2810+
// as `message`, so they were ignored before. They are anonymous (no `msg.from`), therefore the
2811+
// auth/command pipeline cannot run; instead expose the post text and metadata so scripts can
2812+
// react. See https://github.com/iobroker-community-adapters/ioBroker.telegram/issues/289
2813+
bot.on('channel_post', msg => {
2814+
this.connectionState(true);
2815+
2816+
if (this.config.storeRawRequest) {
2817+
this.setStateSafe('communicate.requestRaw', { val: JSON.stringify(msg, null, 2), ack: true });
2818+
}
2819+
2820+
// stores the channel in communicate.chats and handles/saves any attached media
2821+
this.getMessage(msg);
2822+
2823+
const text = msg.text ?? msg.caption;
2824+
if (text !== undefined) {
2825+
const name = msg.chat.title || String(msg.chat.id);
2826+
this.setStateSafe('communicate.requestChatId', { val: msg.chat.id, ack: true });
2827+
this.setStateSafe('communicate.requestMessageId', { val: msg.message_id, ack: true });
2828+
this.setStateSafe('communicate.requestMessageThreadId', {
2829+
val: msg.is_topic_message ? msg.message_thread_id : 0,
2830+
ack: true,
2831+
});
2832+
this.setStateSafe('communicate.request', { val: `[${name}]${text}`, ack: true });
2833+
}
2834+
});
2835+
28282836
// callback InlineKeyboardButton
28292837
bot.on('callback_query', (callbackQuery: CallbackQuery) => {
28302838
this.connectionState(true);
@@ -2834,38 +2842,24 @@ class Telegram extends Adapter {
28342842
this.callbackQueryId[callbackQuery.from.id] = { id: callbackQuery.id, ts: Date.now() };
28352843

28362844
if (this.config.storeRawRequest) {
2837-
this.setState(
2838-
'communicate.requestRaw',
2839-
JSON.stringify(callbackQuery),
2840-
true,
2841-
err => err && this.log.error(err.message),
2842-
);
2845+
this.setStateSafe('communicate.requestRaw', { val: JSON.stringify(callbackQuery), ack: true });
28432846
}
28442847

2845-
this.setState(
2846-
'communicate.requestMessageId',
2847-
callbackQuery.message!.message_id,
2848-
true,
2849-
err => err && this.log.error(err.message),
2850-
);
2851-
this.setState(
2852-
'communicate.requestChatId',
2853-
callbackQuery.message!.chat.id,
2854-
true,
2855-
err => err && this.log.error(err.message),
2856-
);
2857-
this.setState(
2858-
'communicate.request',
2859-
`[${
2848+
this.setStateSafe('communicate.requestMessageId', {
2849+
val: callbackQuery.message!.message_id,
2850+
ack: true,
2851+
});
2852+
this.setStateSafe('communicate.requestChatId', { val: callbackQuery.message!.chat.id, ack: true });
2853+
this.setStateSafe('communicate.request', {
2854+
val: `[${
28602855
!this.config.useUsername
28612856
? callbackQuery.from.first_name
28622857
: !callbackQuery.from.username
28632858
? callbackQuery.from.first_name
28642859
: callbackQuery.from.username
28652860
}]${callbackQuery.data}`,
2866-
true,
2867-
err => err && this.log.error(err.message),
2868-
);
2861+
ack: true,
2862+
});
28692863

28702864
this.isAnswerForQuestion(callbackQuery);
28712865
});

0 commit comments

Comments
 (0)