core-services-sdk 1.3.44 → 1.3.46

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.
Files changed (42) hide show
  1. package/package.json +1 -1
  2. package/src/ids/generators.js +14 -0
  3. package/src/ids/prefixes.js +6 -0
  4. package/src/index.js +1 -0
  5. package/src/instant-messages/im-platform.js +18 -0
  6. package/src/instant-messages/index.js +4 -0
  7. package/src/instant-messages/message-type.js +153 -0
  8. package/src/instant-messages/message-types.js +127 -0
  9. package/src/instant-messages/message-unified-mapper.js +251 -0
  10. package/tests/ids/prefixes.unit.test.js +3 -1
  11. package/tests/instant-messages/applications.unit.test.js +27 -0
  12. package/tests/instant-messages/message-type.unit.test.js +193 -0
  13. package/tests/instant-messages/message-types.unit.test.js +93 -0
  14. package/tests/instant-messages/message-unified-mapper-telegram.unit.test.js +66 -0
  15. package/tests/instant-messages/message-unified-mapper-united.unit.test.js +94 -0
  16. package/tests/instant-messages/message-unified-mapper-whatsapp.unit.test.js +65 -0
  17. package/tests/instant-messages/mock-messages/telegram/contact.json +27 -0
  18. package/tests/instant-messages/mock-messages/telegram/document.json +43 -0
  19. package/tests/instant-messages/mock-messages/telegram/location.json +26 -0
  20. package/tests/instant-messages/mock-messages/telegram/photo.json +52 -0
  21. package/tests/instant-messages/mock-messages/telegram/poll.json +45 -0
  22. package/tests/instant-messages/mock-messages/telegram/text.json +63 -0
  23. package/tests/instant-messages/mock-messages/telegram/video.json +46 -0
  24. package/tests/instant-messages/mock-messages/telegram/video_note.json +43 -0
  25. package/tests/instant-messages/mock-messages/telegram/voice.json +29 -0
  26. package/tests/instant-messages/mock-messages/whatsapp/audio.json +42 -0
  27. package/tests/instant-messages/mock-messages/whatsapp/contacts.json +50 -0
  28. package/tests/instant-messages/mock-messages/whatsapp/document.json +42 -0
  29. package/tests/instant-messages/mock-messages/whatsapp/image.json +41 -0
  30. package/tests/instant-messages/mock-messages/whatsapp/location.json +40 -0
  31. package/tests/instant-messages/mock-messages/whatsapp/reaction.json +40 -0
  32. package/tests/instant-messages/mock-messages/whatsapp/sticker.json +42 -0
  33. package/tests/instant-messages/mock-messages/whatsapp/text.json +39 -0
  34. package/tests/instant-messages/mock-messages/whatsapp/video.json +41 -0
  35. package/types/ids/generators.d.ts +1 -0
  36. package/types/ids/prefixes.d.ts +2 -0
  37. package/types/index.d.ts +1 -0
  38. package/types/instant-messages/im-platform.d.ts +13 -0
  39. package/types/instant-messages/index.d.ts +4 -0
  40. package/types/instant-messages/message-type.d.ts +28 -0
  41. package/types/instant-messages/message-types.d.ts +62 -0
  42. package/types/instant-messages/message-unified-mapper.d.ts +380 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core-services-sdk",
3
- "version": "1.3.44",
3
+ "version": "1.3.46",
4
4
  "main": "src/index.js",
5
5
  "type": "module",
6
6
  "types": "types/index.d.ts",
@@ -223,3 +223,17 @@ export const generateResourceId = () => generatePrefixedId(ID_PREFIXES.RESOURCE)
223
223
  */
224
224
  export const generateIncomingEmailId = () =>
225
225
  generatePrefixedId(ID_PREFIXES.INCOMING_EMAIL)
226
+
227
+ /**
228
+ * Generates a resource ID with a `eml_` prefix.
229
+ *
230
+ * @returns {string} An Email ID.
231
+ */
232
+ export const generateEmailId = () => generatePrefixedId(ID_PREFIXES.EMAIL)
233
+
234
+ /**
235
+ * Generates a resource ID with a `im_` prefix.
236
+ *
237
+ * @returns {string} An Instant Message ID.
238
+ */
239
+ export const generateImId = () => generatePrefixedId(ID_PREFIXES.IM)
@@ -88,4 +88,10 @@ export const ID_PREFIXES = Object.freeze({
88
88
 
89
89
  /** Incoming email ID prefix */
90
90
  INCOMING_EMAIL: 'ieml',
91
+
92
+ /** Email ID prefix */
93
+ EMAIL: 'eml',
94
+
95
+ /** Instant Message ID prefix */
96
+ IM: 'im',
91
97
  })
package/src/index.js CHANGED
@@ -9,3 +9,4 @@ export * from './mailer/index.js'
9
9
  export * from './rabbit-mq/index.js'
10
10
  export * from './templates/index.js'
11
11
  export * from './util/index.js'
12
+ export * from './instant-messages/index.js'
@@ -0,0 +1,18 @@
1
+ /**
2
+
3
+ Supported instant-messaging application identifiers.
4
+
5
+ Used across the system to determine which message-mapper,
6
+
7
+ parser, and processing logic to apply (Telegram vs WhatsApp).
8
+
9
+ @enum {string}
10
+
11
+ @property {"telegram"} TELEGRAM - Telegram Bot API messages
12
+
13
+ @property {"whatsapp"} WHATSAPP - WhatsApp Business API messages
14
+ */
15
+ export const IM_PLATFORM = {
16
+ TELEGRAM: 'telegram',
17
+ WHATSAPP: 'whatsapp',
18
+ }
@@ -0,0 +1,4 @@
1
+ export * from './im-platform.js'
2
+ export * from './message-type.js'
3
+ export * from './message-types.js'
4
+ export * from './message-unified-mapper.js'
@@ -0,0 +1,153 @@
1
+ import { MESSAGE_MEDIA_TYPE, MESSAGE_TYPE } from './message-types.js'
2
+
3
+ /**
4
+ * Creates a predicate that checks whether a given media type exists
5
+ * inside the platform-original message object.
6
+ *
7
+ * @param {string} mediaType - One of MESSAGE_MEDIA_TYPE.*
8
+ * @returns {(params: { originalMessage: any }) => boolean}
9
+ */
10
+ export const isItMediaType =
11
+ (mediaType) =>
12
+ ({ originalMessage }) => {
13
+ const message = originalMessage?.message
14
+ if (!message || typeof message !== 'object') {
15
+ return false
16
+ }
17
+ return mediaType in message
18
+ }
19
+
20
+ /**
21
+ * Creates a predicate that checks whether the normalized `type`
22
+ * of the incoming platform message equals a given expected type.
23
+ *
24
+ * @function isMessageTypeof
25
+ * @param {string} typeOfMessage - One of the MESSAGE_TYPE.* values.
26
+ * @returns {(params: { originalMessage: any }) => boolean}
27
+ * A function that accepts an object containing `originalMessage`
28
+ * and returns true if its `type` matches the expected type.
29
+ */
30
+ export const isMessageTypeof =
31
+ (typeOfMessage) =>
32
+ ({ originalMessage }) => {
33
+ const type = originalMessage?.type
34
+ return type === typeOfMessage
35
+ }
36
+
37
+ /**
38
+ * Detects Telegram interactive "callback_query" messages.
39
+ *
40
+ * These represent button clicks on inline keyboards.
41
+ *
42
+ * @function isCallbackQuery
43
+ * @param {Object} params
44
+ * @param {Object} params.originalMessage - Raw Telegram update
45
+ * @returns {boolean}
46
+ */
47
+ export const isCallbackQuery = ({ originalMessage }) => {
48
+ return 'callback_query' in originalMessage
49
+ }
50
+
51
+ // Media-type detectors for each supported message detail section.
52
+ // Telegram/WhatsApp provide different fields; these predicate utilities
53
+ // allow consistent detection used inside getTelegramMessageType().
54
+
55
+ export const isItPoll = isItMediaType(MESSAGE_MEDIA_TYPE.POLL)
56
+ export const isItMessage = isMessageTypeof(MESSAGE_TYPE.MESSAGE)
57
+ export const isItVoice = isItMediaType(MESSAGE_MEDIA_TYPE.VOICE)
58
+ export const isItVideo = isItMediaType(MESSAGE_MEDIA_TYPE.VIDEO)
59
+ export const isItPhoto = isItMediaType(MESSAGE_MEDIA_TYPE.PHOTO)
60
+ export const isItFreeText = isItMediaType(MESSAGE_MEDIA_TYPE.TEXT)
61
+ export const isItSticker = isItMediaType(MESSAGE_MEDIA_TYPE.STICKER)
62
+ export const isItContact = isItMediaType(MESSAGE_MEDIA_TYPE.CONTACT)
63
+ export const isItLocation = isItMediaType(MESSAGE_MEDIA_TYPE.LOCATION)
64
+ export const isItDocument = isItMediaType(MESSAGE_MEDIA_TYPE.DOCUMENT)
65
+ export const isItVideoNote = isItMediaType(MESSAGE_MEDIA_TYPE.VIDEO_NOTE)
66
+ export const isItButtonClick = isMessageTypeof(MESSAGE_TYPE.BUTTON_CLICK)
67
+
68
+ /**
69
+ * Determines a normalized unified message type based on the raw platform update
70
+ * structure received from Telegram or WhatsApp.
71
+ *
72
+ * The resolution order:
73
+ * 1. Callback button click (Telegram inline keyboard)
74
+ * 2. Standard media checks (text, photo, video, etc.)
75
+ * 3. Special formats (polls, video notes, contacts, forwarded messages)
76
+ * 4. Fallback to UNKNOWN_MESSAGE_TYPE
77
+ *
78
+ * @function getTelegramMessageType
79
+ * @param {Object} params
80
+ * @param {Object} params.originalMessage - Raw update object from Telegram or WhatsApp.
81
+ * For Telegram:
82
+ * - May contain: message, callback_query, poll, etc.
83
+ * For WhatsApp:
84
+ * - Normalized structure after base extraction: { type, message, ... }
85
+ * @returns {string}
86
+ * Returns one of:
87
+ * - MESSAGE_MEDIA_TYPE.* (text, photo, video, document, ...)
88
+ * - MESSAGE_TYPE.BUTTON_CLICK
89
+ * - MESSAGE_TYPE.UNKNOWN_MESSAGE_TYPE
90
+ *
91
+ * @example
92
+ * getTelegramMessageType({ originalMessage: telegramUpdate })
93
+ * // → "text"
94
+ *
95
+ * @example
96
+ * getTelegramMessageType({ originalMessage: whatsappPayload })
97
+ * // → "image"
98
+ */
99
+ export const getTelegramMessageType = ({ originalMessage }) => {
100
+ switch (true) {
101
+ case isCallbackQuery({ originalMessage }): {
102
+ return MESSAGE_TYPE.BUTTON_CLICK
103
+ }
104
+
105
+ case isItFreeText({ originalMessage }): {
106
+ return MESSAGE_MEDIA_TYPE.TEXT
107
+ }
108
+
109
+ case isItVideo({ originalMessage }): {
110
+ return MESSAGE_MEDIA_TYPE.VIDEO
111
+ }
112
+
113
+ case isItPhoto({ originalMessage }): {
114
+ return MESSAGE_MEDIA_TYPE.PHOTO
115
+ }
116
+
117
+ case isItDocument({ originalMessage }): {
118
+ return MESSAGE_MEDIA_TYPE.DOCUMENT
119
+ }
120
+
121
+ case isItLocation({ originalMessage }): {
122
+ return MESSAGE_MEDIA_TYPE.LOCATION
123
+ }
124
+
125
+ case isItVoice({ originalMessage }): {
126
+ return MESSAGE_MEDIA_TYPE.VOICE
127
+ }
128
+
129
+ case isItVideoNote({ originalMessage }): {
130
+ return MESSAGE_MEDIA_TYPE.VIDEO_NOTE
131
+ }
132
+
133
+ case isItPoll({ originalMessage }): {
134
+ return MESSAGE_MEDIA_TYPE.POLL
135
+ }
136
+
137
+ case isItSticker({ originalMessage }): {
138
+ return MESSAGE_MEDIA_TYPE.STICKER
139
+ }
140
+
141
+ case isItMessage({ originalMessage }): {
142
+ return MESSAGE_MEDIA_TYPE.MESSAGE
143
+ }
144
+
145
+ case isItContact({ originalMessage }): {
146
+ return MESSAGE_MEDIA_TYPE.CONTACT
147
+ }
148
+
149
+ default: {
150
+ return MESSAGE_TYPE.UNKNOWN_MESSAGE_TYPE
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Enumerates all supported incoming media/content types
3
+ * across messaging platforms (Telegram, WhatsApp, etc).
4
+ *
5
+ * This is the unified taxonomy used inside the system
6
+ * after normalization of the raw message payload.
7
+ *
8
+ * @readonly
9
+ * @enum {string}
10
+ *
11
+ * @property {"text"} TEXT
12
+ * Represents plain text content.
13
+ *
14
+ * @property {"poll"} POLL
15
+ * Represents a Telegram poll (multiple-choice question).
16
+ *
17
+ * @property {"video"} VIDEO
18
+ * Represents a standard video file.
19
+ *
20
+ * @property {"photo"} PHOTO
21
+ * Represents a Telegram “photo” array (before mapping to IMAGE).
22
+ *
23
+ * @property {"image"} IMAGE
24
+ * Represents an image file (after normalization).
25
+ *
26
+ * @property {"voice"} VOICE
27
+ * Represents Telegram's "voice" messages (OGG encoded voice notes).
28
+ *
29
+ * @property {"audio"} AUDIO
30
+ * Represents general audio files (WhatsApp voice notes, audio uploads).
31
+ *
32
+ * @property {"sticker"} STICKER
33
+ * Represents sticker messages (Telegram or WhatsApp).
34
+ *
35
+ * @property {"contact"} CONTACT
36
+ * Represents a shared contact card.
37
+ *
38
+ * @property {"reaction"} REACTION
39
+ * Represents WhatsApp/Telegram reactions (emojis on messages).
40
+ *
41
+ * @property {"document"} DOCUMENT
42
+ * Represents generic uploaded files, including PDFs.
43
+ *
44
+ * @property {"location"} LOCATION
45
+ * Represents geographic coordinates.
46
+ *
47
+ * @property {"contacts"} CONTACTS
48
+ * Represents WhatsApp contacts array (before mapping to CONTACT).
49
+ *
50
+ * @property {"video_note"} VIDEO_NOTE
51
+ * Represents Telegram's circular "video note".
52
+ *
53
+ * @property {"button_click"} BUTTON_CLICK
54
+ * Represents a button press (interactive replies).
55
+ *
56
+ * @property {"button_click_multiple"} BUTTON_CLICK_MULTIPLE
57
+ * Represents list/menu selection (e.g., WhatsApp list_reply).
58
+ */
59
+ export const MESSAGE_MEDIA_TYPE = {
60
+ TEXT: 'text',
61
+ POLL: 'poll',
62
+ VIDEO: 'video',
63
+ PHOTO: 'photo',
64
+ IMAGE: 'image',
65
+ VOICE: 'voice',
66
+ AUDIO: 'audio',
67
+ STICKER: 'sticker',
68
+ CONTACT: 'contact',
69
+ MESSAGE: 'message',
70
+ REACTION: 'reaction',
71
+ DOCUMENT: 'document',
72
+ LOCATION: 'location',
73
+ CONTACTS: 'contacts',
74
+ VIDEO_NOTE: 'video_note',
75
+ BUTTON_CLICK: 'button_click',
76
+ BUTTON_CLICK_MULTIPLE: 'button_click_multiple',
77
+ }
78
+
79
+ /**
80
+ * Additional high-level message categories.
81
+ *
82
+ * These represent logical groupings rather than raw media types.
83
+ *
84
+ * @readonly
85
+ * @enum {string}
86
+ *
87
+ * @property {"message"} MESSAGE
88
+ * Regular message container (base type in some providers).
89
+ *
90
+ * @property {"button_click"} BUTTON_CLICK
91
+ * A click on a single interactive button.
92
+ *
93
+ * @property {"button_click_multiple"} BUTTON_CLICK_MULTIPLE
94
+ * A selection from a list of interactive reply choices.
95
+ *
96
+ * @property {"unknown_message_type"} UNKNOWN_MESSAGE_TYPE
97
+ * Used when the system cannot identify or normalize the message type.
98
+ */
99
+ export const MESSAGE_TYPE = {
100
+ MESSAGE: 'message',
101
+ BUTTON_CLICK: MESSAGE_MEDIA_TYPE.BUTTON_CLICK,
102
+ BUTTON_CLICK_MULTIPLE: MESSAGE_MEDIA_TYPE.BUTTON_CLICK_MULTIPLE,
103
+ UNKNOWN_MESSAGE_TYPE: 'unknown_message_type',
104
+ }
105
+
106
+ /**
107
+ * Maps platform-specific message types into the unified equivalents.
108
+ *
109
+ * This is used to convert raw provider terminology into internal naming.
110
+ *
111
+ * @readonly
112
+ * @enum {string}
113
+ *
114
+ * @property {"audio"} VOICE
115
+ * Telegram's "voice" is normalized into the system's AUDIO type.
116
+ *
117
+ * @property {"image"} PHOTO
118
+ * Telegram's "photo" array is normalized into IMAGE.
119
+ *
120
+ * @property {"contact"} CONTACTS
121
+ * WhatsApp's "contacts" array is normalized into CONTACT.
122
+ */
123
+ export const MESSAGE_MEDIA_TYPE_MAPPER = {
124
+ [MESSAGE_MEDIA_TYPE.VOICE]: MESSAGE_MEDIA_TYPE.AUDIO,
125
+ [MESSAGE_MEDIA_TYPE.PHOTO]: MESSAGE_MEDIA_TYPE.IMAGE,
126
+ [MESSAGE_MEDIA_TYPE.CONTACTS]: MESSAGE_MEDIA_TYPE.CONTACT,
127
+ }
@@ -0,0 +1,251 @@
1
+ import { IM_PLATFORM } from './im-platform.js'
2
+ import { getTelegramMessageType } from './message-type.js'
3
+ import {
4
+ MESSAGE_TYPE,
5
+ MESSAGE_MEDIA_TYPE,
6
+ MESSAGE_MEDIA_TYPE_MAPPER,
7
+ } from './message-types.js'
8
+
9
+ const INTERACTIVE_MAPPER = {
10
+ button_reply: MESSAGE_TYPE.BUTTON_CLICK,
11
+ list_reply: MESSAGE_TYPE.BUTTON_CLICK_MULTIPLE,
12
+ }
13
+ /**
14
+ * Universal message type resolver.
15
+ *
16
+ * Detects whether the message is Telegram or WhatsApp automatically,
17
+ * and delegates to the correct internal resolver.
18
+ *
19
+ * @param {Object} params
20
+ * @param {Object} params.originalMessage - Raw message payload
21
+ * @returns {string} Unified message type
22
+ */
23
+ export const getMessageType = ({ originalMessage }) => {
24
+ if (!originalMessage || typeof originalMessage !== 'object') {
25
+ return MESSAGE_TYPE.UNKNOWN_MESSAGE_TYPE
26
+ }
27
+
28
+ // Telegram format
29
+ if (
30
+ 'update_id' in originalMessage ||
31
+ 'message' in originalMessage ||
32
+ 'callback_query' in originalMessage
33
+ ) {
34
+ return getTelegramMessageType({ originalMessage })
35
+ }
36
+
37
+ // WhatsApp format
38
+ const entry = originalMessage?.entry?.[0]
39
+ const change = entry?.changes?.[0]
40
+ const message = change?.value?.messages?.[0]
41
+
42
+ if (message) {
43
+ return getWhatsAppMessageType({ message })
44
+ }
45
+
46
+ return MESSAGE_TYPE.UNKNOWN_MESSAGE_TYPE
47
+ }
48
+
49
+ export const mapMessageTelegramBase = ({ originalMessage }) => {
50
+ const { callback_query, message, update_id } = originalMessage
51
+ const messageData = callback_query?.message || message
52
+ const { chat, date, from, message_id } = messageData
53
+ const type = getTelegramMessageType({ originalMessage })
54
+ const typeMapped = MESSAGE_MEDIA_TYPE_MAPPER[type] || type
55
+ const { forward_date, forward_from } = messageData
56
+ const itIsForward = !!(forward_date && forward_from)
57
+
58
+ const messageBase = {
59
+ id: update_id,
60
+ imExtraInfo: {
61
+ tmId: message_id,
62
+ },
63
+ chatId: chat.id,
64
+ type: typeMapped,
65
+ chatter: {
66
+ ...from,
67
+ id: chat.id,
68
+ name: `${chat.first_name} ${chat.last_name}`,
69
+ username: chat.username,
70
+ },
71
+ itIsForward,
72
+ ...(itIsForward ? { forwardInfo: { forward_date, forward_from } } : null),
73
+ timestamp: `${date}`,
74
+ }
75
+ return { messageBase, message: messageData, type }
76
+ }
77
+
78
+ export const mapMessageWhatsAppContent = ({ message, type }) => {
79
+ switch (type) {
80
+ case MESSAGE_MEDIA_TYPE.TEXT:
81
+ return {
82
+ text: message.text?.body,
83
+ }
84
+ case MESSAGE_MEDIA_TYPE.IMAGE:
85
+ case MESSAGE_MEDIA_TYPE.VIDEO:
86
+ case MESSAGE_MEDIA_TYPE.AUDIO:
87
+ case MESSAGE_MEDIA_TYPE.STICKER:
88
+ case MESSAGE_MEDIA_TYPE.LOCATION:
89
+ case MESSAGE_MEDIA_TYPE.REACTION:
90
+ return {
91
+ [type]: message[type],
92
+ }
93
+ case MESSAGE_MEDIA_TYPE.DOCUMENT:
94
+ return {
95
+ [type]: message[type],
96
+ }
97
+ case MESSAGE_MEDIA_TYPE.CONTACTS:
98
+ return {
99
+ [MESSAGE_MEDIA_TYPE.CONTACT]: message[type],
100
+ }
101
+ case MESSAGE_MEDIA_TYPE.BUTTON_CLICK:
102
+ const { interactive } = message
103
+ const { button_reply } = interactive
104
+ return {
105
+ reply: button_reply,
106
+ }
107
+ case MESSAGE_MEDIA_TYPE.BUTTON_CLICK_MULTIPLE:
108
+ const { interactive: interactiveMultiple } = message
109
+ const { list_reply } = interactiveMultiple
110
+ return {
111
+ reply: list_reply,
112
+ }
113
+ default:
114
+ return {}
115
+ }
116
+ }
117
+
118
+ export const mapMessageTelegram = ({ originalMessage }) => {
119
+ const { messageBase, type, message } = mapMessageTelegramBase({
120
+ originalMessage,
121
+ })
122
+ const messageContent = mapMessageTelegramContent({
123
+ type,
124
+ message,
125
+ originalMessage,
126
+ })
127
+ const messageMapped = { ...messageBase, ...messageContent }
128
+ return messageMapped
129
+ }
130
+
131
+ export const getWhatsAppMessageType = ({ message }) => {
132
+ const { type } = message
133
+ switch (type) {
134
+ case 'interactive': {
135
+ const { interactive } = message
136
+ return INTERACTIVE_MAPPER[interactive.type] || interactive.type
137
+ }
138
+ default:
139
+ return type
140
+ }
141
+ }
142
+
143
+ export const extractReply = ({ originalMessage }) => {
144
+ const { callback_query } = originalMessage
145
+ const { data: id, message } = callback_query
146
+ const {
147
+ reply_markup: { inline_keyboard },
148
+ } = message
149
+ const buttonsFlat = inline_keyboard.reduce((buttons, button) => {
150
+ return buttons.concat(button.flat())
151
+ }, [])
152
+ const { text: title } = buttonsFlat.find(
153
+ (button) => button.callback_data === id,
154
+ )
155
+
156
+ return { id, title }
157
+ }
158
+ export const whatsappBaseExtraction = ({ originalMessage }) => {
159
+ const {
160
+ entry: [{ changes, id }],
161
+ } = originalMessage
162
+ const [change] = changes
163
+ const { field, value } = change
164
+ return { field, value, wbaid: id }
165
+ }
166
+
167
+ export const mapMessageWhatsAppBase = ({ originalMessage }) => {
168
+ const { field, value, wbaid } = whatsappBaseExtraction({ originalMessage })
169
+ const { [field]: messages, contacts } = value
170
+ const [message] = messages
171
+ const [contact] = contacts
172
+ const { id, from, timestamp, context } = message
173
+ const type = getWhatsAppMessageType({ message })
174
+ const messageBase = {
175
+ id: id,
176
+ chatId: from,
177
+ imExtraInfo: {
178
+ wbaid,
179
+ },
180
+ type,
181
+ chatter: {
182
+ id: from,
183
+ name: contacts[0].profile.name,
184
+ username: contact.wa_id,
185
+ },
186
+ itIsForward: !!context?.forwarded,
187
+ timestamp: timestamp,
188
+ }
189
+
190
+ return { messageBase, message, contact, context }
191
+ }
192
+
193
+ export const mapMessageTelegramContent = ({
194
+ type,
195
+ message,
196
+ originalMessage,
197
+ }) => {
198
+ switch (type) {
199
+ case MESSAGE_MEDIA_TYPE.TEXT:
200
+ return {
201
+ text: message.text,
202
+ }
203
+ case MESSAGE_MEDIA_TYPE.PHOTO:
204
+ case MESSAGE_MEDIA_TYPE.VOICE:
205
+ return {
206
+ [MESSAGE_MEDIA_TYPE_MAPPER[type] || type]: message[type],
207
+ }
208
+ case MESSAGE_MEDIA_TYPE.POLL:
209
+ case MESSAGE_MEDIA_TYPE.VIDEO:
210
+ case MESSAGE_MEDIA_TYPE.STICKER:
211
+ case MESSAGE_MEDIA_TYPE.CONTACT:
212
+ case MESSAGE_MEDIA_TYPE.LOCATION:
213
+ case MESSAGE_MEDIA_TYPE.VIDEO_NOTE:
214
+ return {
215
+ [type]: message[type],
216
+ }
217
+ case MESSAGE_MEDIA_TYPE.DOCUMENT:
218
+ const { animation } = message
219
+ return {
220
+ [type]: message[type],
221
+ ...(animation ? { animation } : null),
222
+ ...(animation ? { attachment: 'animation' } : null),
223
+ }
224
+ case MESSAGE_MEDIA_TYPE.BUTTON_CLICK:
225
+ const reply = extractReply({ originalMessage })
226
+ return {
227
+ reply,
228
+ }
229
+ default:
230
+ return {}
231
+ }
232
+ }
233
+
234
+ export const mapMessageWhatsApp = ({ originalMessage }) => {
235
+ const { messageBase, message, context } = mapMessageWhatsAppBase({
236
+ originalMessage,
237
+ })
238
+ const { type } = messageBase
239
+ const messageContent = mapMessageWhatsAppContent({
240
+ type,
241
+ message,
242
+ context,
243
+ })
244
+
245
+ return { ...messageBase, ...messageContent }
246
+ }
247
+
248
+ export const messageUnifiedMapper = {
249
+ [IM_PLATFORM.TELEGRAM]: mapMessageTelegram,
250
+ [IM_PLATFORM.WHATSAPP]: mapMessageWhatsApp,
251
+ }
@@ -31,7 +31,9 @@ describe('ID_PREFIXES', () => {
31
31
  DEVICE: 'dev',
32
32
  ALERT: 'alr',
33
33
  RESOURCE: 'res',
34
- INCOMING_EMAIL: 'ieml', // ✅ added
34
+ INCOMING_EMAIL: 'ieml',
35
+ EMAIL: 'eml',
36
+ IM: 'im',
35
37
  })
36
38
  })
37
39
 
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { IM_PLATFORM } from '../../src/instant-messages/im-platform.js'
3
+
4
+ describe('IM_PLATFORM enum', () => {
5
+ it('should expose the expected keys', () => {
6
+ expect(IM_PLATFORM).toHaveProperty('TELEGRAM')
7
+ expect(IM_PLATFORM).toHaveProperty('WHATSAPP')
8
+ })
9
+
10
+ it('should map keys to correct string values', () => {
11
+ expect(IM_PLATFORM.TELEGRAM).toBe('telegram')
12
+ expect(IM_PLATFORM.WHATSAPP).toBe('whatsapp')
13
+ })
14
+
15
+ it('should contain exactly two entries and nothing else', () => {
16
+ expect(Object.keys(IM_PLATFORM).length).toBe(2)
17
+ expect(Object.values(IM_PLATFORM)).toContain('telegram')
18
+ expect(Object.values(IM_PLATFORM)).toContain('whatsapp')
19
+ })
20
+
21
+ it('should not allow undefined application identifiers', () => {
22
+ const allowed = Object.values(IM_PLATFORM)
23
+ expect(allowed.includes('signal')).toBe(false)
24
+ expect(allowed.includes('viber')).toBe(false)
25
+ expect(allowed.includes('sms')).toBe(false)
26
+ })
27
+ })