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.
- package/package.json +1 -1
- package/src/ids/generators.js +14 -0
- package/src/ids/prefixes.js +6 -0
- package/src/index.js +1 -0
- package/src/instant-messages/im-platform.js +18 -0
- package/src/instant-messages/index.js +4 -0
- package/src/instant-messages/message-type.js +153 -0
- package/src/instant-messages/message-types.js +127 -0
- package/src/instant-messages/message-unified-mapper.js +251 -0
- package/tests/ids/prefixes.unit.test.js +3 -1
- package/tests/instant-messages/applications.unit.test.js +27 -0
- package/tests/instant-messages/message-type.unit.test.js +193 -0
- package/tests/instant-messages/message-types.unit.test.js +93 -0
- package/tests/instant-messages/message-unified-mapper-telegram.unit.test.js +66 -0
- package/tests/instant-messages/message-unified-mapper-united.unit.test.js +94 -0
- package/tests/instant-messages/message-unified-mapper-whatsapp.unit.test.js +65 -0
- package/tests/instant-messages/mock-messages/telegram/contact.json +27 -0
- package/tests/instant-messages/mock-messages/telegram/document.json +43 -0
- package/tests/instant-messages/mock-messages/telegram/location.json +26 -0
- package/tests/instant-messages/mock-messages/telegram/photo.json +52 -0
- package/tests/instant-messages/mock-messages/telegram/poll.json +45 -0
- package/tests/instant-messages/mock-messages/telegram/text.json +63 -0
- package/tests/instant-messages/mock-messages/telegram/video.json +46 -0
- package/tests/instant-messages/mock-messages/telegram/video_note.json +43 -0
- package/tests/instant-messages/mock-messages/telegram/voice.json +29 -0
- package/tests/instant-messages/mock-messages/whatsapp/audio.json +42 -0
- package/tests/instant-messages/mock-messages/whatsapp/contacts.json +50 -0
- package/tests/instant-messages/mock-messages/whatsapp/document.json +42 -0
- package/tests/instant-messages/mock-messages/whatsapp/image.json +41 -0
- package/tests/instant-messages/mock-messages/whatsapp/location.json +40 -0
- package/tests/instant-messages/mock-messages/whatsapp/reaction.json +40 -0
- package/tests/instant-messages/mock-messages/whatsapp/sticker.json +42 -0
- package/tests/instant-messages/mock-messages/whatsapp/text.json +39 -0
- package/tests/instant-messages/mock-messages/whatsapp/video.json +41 -0
- package/types/ids/generators.d.ts +1 -0
- package/types/ids/prefixes.d.ts +2 -0
- package/types/index.d.ts +1 -0
- package/types/instant-messages/im-platform.d.ts +13 -0
- package/types/instant-messages/index.d.ts +4 -0
- package/types/instant-messages/message-type.d.ts +28 -0
- package/types/instant-messages/message-types.d.ts +62 -0
- package/types/instant-messages/message-unified-mapper.d.ts +380 -0
package/package.json
CHANGED
package/src/ids/generators.js
CHANGED
|
@@ -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)
|
package/src/ids/prefixes.js
CHANGED
package/src/index.js
CHANGED
|
@@ -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,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
|
+
}
|
|
@@ -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
|
+
})
|