node-red-contrib-knx-ultimate 6.3.28 → 6.3.30

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.
@@ -128,15 +128,47 @@ const resolveTelegramVoiceAllowedOrigin = (message) => {
128
128
  }
129
129
 
130
130
  const applyKnxAiTelegramVoiceInputPresetFallback = ({ preset, message } = {}) => {
131
- if (String(preset || '') !== 'windkh-telegrambot' || !message || typeof message !== 'object') return null
131
+ const normalizedPreset = String(preset || '')
132
+ if (!['windkh-telegrambot', 'redbot-telegram'].includes(normalizedPreset) || !message || typeof message !== 'object') return null
132
133
  const telegram = message.payload && typeof message.payload === 'object' ? message.payload : null
133
- if (!telegram || telegram.type !== 'voice') return null
134
+ if (!telegram) return null
134
135
  const chatId = telegram.chatId
135
136
  if (chatId === undefined || chatId === null || chatId === '') return null
136
137
  const original = message.originalMessage && typeof message.originalMessage === 'object'
137
138
  ? message.originalMessage
138
139
  : {}
139
140
  const voice = findTelegramVoiceMetadata(message)
141
+
142
+ if (normalizedPreset === 'redbot-telegram') {
143
+ const redbotType = String(telegram.type || '').trim().toLowerCase()
144
+ if ((redbotType !== 'audio' && redbotType !== 'voice') || !Buffer.isBuffer(telegram.content)) return null
145
+ const mediaType = normalizeVoiceMediaType(voice.mime_type || telegram.mimeType)
146
+ message.sessionId = String(chatId)
147
+ message.language = (original.from && original.from.language_code) ||
148
+ (original.message && original.message.from && original.message.from.language_code) ||
149
+ telegram.language ||
150
+ message.language ||
151
+ ''
152
+ message.topic = 'ask'
153
+ delete message.prompt
154
+ message.knxAi = Object.assign({}, message.knxAi, {
155
+ sessionId: String(chatId),
156
+ voiceInput: {
157
+ source: 'telegram',
158
+ originalType: 'voice',
159
+ transport: 'redbot-buffer',
160
+ data: telegram.content,
161
+ fileId: String(voice.file_id || '').trim(),
162
+ mediaType,
163
+ filename: sanitizeVoiceFilename({ filename: telegram.filename || voice.file_name, mediaType }),
164
+ durationSeconds: Math.max(0, Number(voice.duration || telegram.duration) || 0),
165
+ fileSize: Math.max(0, Number(voice.file_size || telegram.fileSize) || telegram.content.length)
166
+ }
167
+ })
168
+ return message
169
+ }
170
+
171
+ if (telegram.type !== 'voice') return null
140
172
  const mediaType = normalizeVoiceMediaType(voice.mime_type || telegram.contentType || telegram.mimeType)
141
173
  const fileId = String(telegram.content || voice.file_id || '').trim()
142
174
  const weblink = String(telegram.weblink || message.weblink || '').trim()
@@ -175,6 +207,7 @@ const redactKnxAiTelegramVoiceLocations = (message) => {
175
207
  if (!message || typeof message !== 'object') return message
176
208
  if (message.payload && typeof message.payload === 'object') {
177
209
  message.payload = Object.assign({}, message.payload)
210
+ if (Buffer.isBuffer(message.payload.content)) message.payload.content = ''
178
211
  delete message.payload.weblink
179
212
  delete message.payload.path
180
213
  }
@@ -182,6 +215,7 @@ const redactKnxAiTelegramVoiceLocations = (message) => {
182
215
  delete message.path
183
216
  if (message.knxAi && message.knxAi.voiceInput && typeof message.knxAi.voiceInput === 'object') {
184
217
  const voiceInput = Object.assign({}, message.knxAi.voiceInput)
218
+ delete voiceInput.data
185
219
  delete voiceInput.weblink
186
220
  delete voiceInput.path
187
221
  message.knxAi = Object.assign({}, message.knxAi, { voiceInput })
@@ -258,6 +292,16 @@ const fetchKnxAiTelegramVoice = async ({
258
292
  }
259
293
  const mediaType = normalizeVoiceMediaType(source.mediaType)
260
294
  const filename = sanitizeVoiceFilename({ filename: source.filename, mediaType })
295
+ if (Buffer.isBuffer(source.data)) {
296
+ if (!source.data.length) throw new Error('Telegram returned an empty voice message')
297
+ if (source.data.length > maxBytes) throw new Error(`Voice audio exceeds the ${Math.round(maxBytes / (1024 * 1024))} MB limit`)
298
+ return {
299
+ data: Buffer.from(source.data),
300
+ mediaType,
301
+ filename,
302
+ source: String(source.transport || 'telegram-buffer')
303
+ }
304
+ }
261
305
  const weblink = String(source.weblink || '').trim()
262
306
  if (weblink) {
263
307
  let parsed
@@ -409,11 +453,15 @@ const postKnxAiVoiceSpeech = async ({
409
453
  }
410
454
 
411
455
  const applyKnxAiTelegramVoiceOutputPresetFallback = ({ preset, message, inputMessage } = {}) => {
412
- if (String(preset || '') !== 'windkh-telegrambot' || !message || typeof message !== 'object') return message
456
+ const normalizedPreset = String(preset || '')
457
+ if (!['windkh-telegrambot', 'redbot-telegram'].includes(normalizedPreset) || !message || typeof message !== 'object') return message
413
458
  const audio = message.knxAi && message.knxAi.audio
414
459
  if (!audio || !Buffer.isBuffer(audio.data) || !audio.data.length) return message
415
460
  const currentPayload = message.payload && typeof message.payload === 'object' ? message.payload : null
416
- if (currentPayload && (currentPayload.type === 'voice' || currentPayload.type === 'photo')) return message
461
+ if (currentPayload && (currentPayload.type === 'voice' || currentPayload.type === 'audio' || currentPayload.type === 'photo')) return message
462
+ if (normalizedPreset === 'redbot-telegram' && currentPayload && currentPayload.type === 'inline-buttons') return message
463
+ const confirmation = message.knxAi && message.knxAi.confirmationRequest
464
+ if (normalizedPreset === 'redbot-telegram' && confirmation && confirmation.required === true) return message
417
465
  const source = inputMessage && typeof inputMessage === 'object'
418
466
  ? inputMessage
419
467
  : message.inputMessage && typeof message.inputMessage === 'object'
@@ -435,6 +483,28 @@ const applyKnxAiTelegramVoiceOutputPresetFallback = ({ preset, message, inputMes
435
483
  .filter(Boolean)
436
484
  .join('\n')
437
485
  .slice(0, 1024)
486
+
487
+ if (normalizedPreset === 'redbot-telegram') {
488
+ const transport = currentPayload && currentPayload.transport
489
+ ? currentPayload.transport
490
+ : sourcePayload.transport || 'telegram'
491
+ const userId = currentPayload && currentPayload.userId !== undefined
492
+ ? currentPayload.userId
493
+ : sourcePayload.userId
494
+ message.payload = {
495
+ transport,
496
+ chatId,
497
+ type: 'audio',
498
+ inbound: false,
499
+ content: audio.data,
500
+ filename: sanitizeVoiceFilename({ filename: audio.filename, mediaType: audio.mediaType, fallback: 'knx-ai-reply' }),
501
+ mimeType: normalizeVoiceMediaType(audio.mediaType),
502
+ caption
503
+ }
504
+ if (userId !== undefined) message.payload.userId = userId
505
+ return message
506
+ }
507
+
438
508
  const options = Object.assign({}, currentPayload && currentPayload.options ? currentPayload.options : {})
439
509
  if (caption) options.caption = caption
440
510
  message.payload = {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "6.3.28",
6
+ "version": "6.3.30",
7
7
  "description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT and Modbus adapters, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
8
8
  "files": [
9
9
  "nodes/",
@@ -193,15 +193,13 @@ return msg;`
193
193
  id: 'redbot-telegram',
194
194
  title: 'RedBot / node-red-contrib-chatbot (Telegram)',
195
195
  inputCode: `// RedBot Telegram Receiver -> KNX AI
196
- // RedBot normalizes Telegram text and inline-button postbacks in msg.payload.
196
+ // RedBot normalizes Telegram text, voice audio and inline-button postbacks in msg.payload.
197
197
  const redbot = msg.payload;
198
198
  if (!redbot || typeof redbot !== 'object') return;
199
199
  if (redbot.transport && redbot.transport !== 'telegram') return;
200
200
 
201
201
  const chatId = redbot.chatId;
202
- const content = typeof redbot.content === 'string' ? redbot.content.trim() : '';
203
- if (chatId === undefined || chatId === null || content === '') return;
204
- if (redbot.type !== 'message') return;
202
+ if (chatId === undefined || chatId === null) return;
205
203
 
206
204
  msg.sessionId = String(chatId);
207
205
  msg.language =
@@ -210,6 +208,37 @@ msg.language =
210
208
  msg.language ||
211
209
  '';
212
210
 
211
+ const redbotType = String(redbot.type || '').trim().toLowerCase();
212
+ if ((redbotType === 'audio' || redbotType === 'voice') && Buffer.isBuffer(redbot.content)) {
213
+ const original = msg.originalMessage && typeof msg.originalMessage === 'object'
214
+ ? msg.originalMessage
215
+ : {};
216
+ const voice = original.voice && typeof original.voice === 'object'
217
+ ? original.voice
218
+ : original.message && original.message.voice && typeof original.message.voice === 'object'
219
+ ? original.message.voice
220
+ : {};
221
+ msg.topic = 'ask';
222
+ msg.knxAi = Object.assign({}, msg.knxAi, {
223
+ sessionId: String(chatId),
224
+ voiceInput: {
225
+ source: 'telegram',
226
+ originalType: 'voice',
227
+ transport: 'redbot-buffer',
228
+ data: redbot.content,
229
+ fileId: String(voice.file_id || '').trim(),
230
+ mediaType: String(voice.mime_type || redbot.mimeType || 'audio/ogg'),
231
+ filename: String(voice.file_name || redbot.filename || 'telegram-voice.ogg'),
232
+ durationSeconds: Math.max(0, Number(voice.duration || redbot.duration) || 0),
233
+ fileSize: Math.max(0, Number(voice.file_size || redbot.fileSize) || redbot.content.length)
234
+ }
235
+ });
236
+ return msg;
237
+ }
238
+
239
+ const content = typeof redbot.content === 'string' ? redbot.content.trim() : '';
240
+ if (redbotType !== 'message' || content === '') return;
241
+
213
242
  const action = content.toLowerCase();
214
243
  if (action === 'confirm' || action === 'cancel') {
215
244
  msg.topic = action;
@@ -300,6 +329,33 @@ if (image && Buffer.isBuffer(image.data)) {
300
329
  return msg;
301
330
  }
302
331
 
332
+ const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
333
+ const audio = msg.knxAi && msg.knxAi.audio;
334
+ if (audio && Buffer.isBuffer(audio.data) && !(confirmation && confirmation.required === true)) {
335
+ const voiceLanguage = String(msg.knxAi.language || (source && source.language) || '').trim().toLowerCase().split(/[-_]/)[0];
336
+ const disclosureLabels = {
337
+ de: 'KI-generierte Stimme',
338
+ en: 'AI-generated voice',
339
+ es: 'Voz generada por IA',
340
+ fr: 'Voix générée par l’IA',
341
+ it: 'Voce generata dall’IA',
342
+ zh: 'AI 生成的语音'
343
+ };
344
+ const disclosure = disclosureLabels[voiceLanguage] || disclosureLabels.en;
345
+ msg.payload = {
346
+ transport: sourcePayload.transport || 'telegram',
347
+ chatId: chatId,
348
+ type: 'audio',
349
+ inbound: false,
350
+ content: audio.data,
351
+ filename: audio.filename || 'knx-ai-reply.ogg',
352
+ mimeType: audio.mediaType || 'audio/ogg',
353
+ caption: [disclosure, content].filter(Boolean).join('\\n').slice(0, 1024)
354
+ };
355
+ if (sourcePayload.userId !== undefined) msg.payload.userId = sourcePayload.userId;
356
+ return msg;
357
+ }
358
+
303
359
  const redbotPayload = {
304
360
  transport: sourcePayload.transport || 'telegram',
305
361
  chatId: chatId,
@@ -309,7 +365,6 @@ const redbotPayload = {
309
365
  };
310
366
  if (sourcePayload.userId !== undefined) redbotPayload.userId = sourcePayload.userId;
311
367
 
312
- const confirmation = msg.knxAi && msg.knxAi.confirmationRequest;
313
368
  if (confirmation && confirmation.required === true && Array.isArray(confirmation.actions)) {
314
369
  redbotPayload.type = 'inline-buttons';
315
370
  redbotPayload.buttons = confirmation.actions.map(function (action) {