@strav/instant 1.0.0-alpha.42
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 +49 -0
- package/src/drivers/line/index.ts +50 -0
- package/src/drivers/line/liff_client.ts +126 -0
- package/src/drivers/line/liff_sign_in_route.ts +112 -0
- package/src/drivers/line/line_beacon.ts +14 -0
- package/src/drivers/line/line_config.ts +55 -0
- package/src/drivers/line/line_driver.ts +202 -0
- package/src/drivers/line/line_flex.ts +117 -0
- package/src/drivers/line/line_liff.ts +102 -0
- package/src/drivers/line/line_message_mapper.ts +118 -0
- package/src/drivers/line/line_provider.ts +33 -0
- package/src/drivers/line/line_rich_menu.ts +68 -0
- package/src/drivers/line/line_webhook.ts +168 -0
- package/src/drivers/messenger/index.ts +16 -0
- package/src/drivers/messenger/messenger_config.ts +20 -0
- package/src/drivers/messenger/messenger_driver.ts +151 -0
- package/src/drivers/messenger/messenger_message_mapper.ts +146 -0
- package/src/drivers/messenger/messenger_profile.ts +43 -0
- package/src/drivers/messenger/messenger_provider.ts +31 -0
- package/src/drivers/messenger/messenger_webhook.ts +165 -0
- package/src/drivers/telegram/index.ts +23 -0
- package/src/drivers/telegram/telegram_config.ts +19 -0
- package/src/drivers/telegram/telegram_driver.ts +121 -0
- package/src/drivers/telegram/telegram_message_mapper.ts +147 -0
- package/src/drivers/telegram/telegram_provider.ts +32 -0
- package/src/drivers/telegram/telegram_web_app.ts +108 -0
- package/src/drivers/telegram/telegram_webhook.ts +200 -0
- package/src/drivers/whatsapp/flows/index.ts +15 -0
- package/src/drivers/whatsapp/flows/whatsapp_flow_builder.ts +55 -0
- package/src/drivers/whatsapp/flows/whatsapp_flow_crypto.ts +81 -0
- package/src/drivers/whatsapp/index.ts +14 -0
- package/src/drivers/whatsapp/whatsapp_config.ts +35 -0
- package/src/drivers/whatsapp/whatsapp_driver.ts +111 -0
- package/src/drivers/whatsapp/whatsapp_message_mapper.ts +157 -0
- package/src/drivers/whatsapp/whatsapp_provider.ts +31 -0
- package/src/drivers/whatsapp/whatsapp_webhook.ts +170 -0
- package/src/drivers/zalo/index.ts +16 -0
- package/src/drivers/zalo/zalo_config.ts +37 -0
- package/src/drivers/zalo/zalo_driver.ts +130 -0
- package/src/drivers/zalo/zalo_message_mapper.ts +146 -0
- package/src/drivers/zalo/zalo_mini_app.ts +17 -0
- package/src/drivers/zalo/zalo_provider.ts +31 -0
- package/src/drivers/zalo/zalo_webhook.ts +139 -0
- package/src/errors.ts +122 -0
- package/src/index.ts +52 -0
- package/src/instant_capabilities.ts +49 -0
- package/src/instant_driver.ts +109 -0
- package/src/instant_manager.ts +113 -0
- package/src/instant_provider.ts +45 -0
- package/src/internal/fetch_json.ts +58 -0
- package/src/internal/meta/meta_graph_client.ts +51 -0
- package/src/internal/meta/meta_signature.ts +22 -0
- package/src/internal/meta/meta_webhook_challenge.ts +19 -0
- package/src/message.ts +65 -0
- package/src/types.ts +32 -0
- package/src/webhook_event.ts +93 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MessengerDriver` — `InstantDriver` for Meta's Send API.
|
|
3
|
+
*
|
|
4
|
+
* `send`/`push` POST to `/{pageId}/messages` (page-scoped). No
|
|
5
|
+
* native `multicast`; `broadcast` requires the two-step
|
|
6
|
+
* Broadcast API (`message_creatives` → `broadcast_messages`)
|
|
7
|
+
* and only works with a `MESSAGE_TAG` set.
|
|
8
|
+
*
|
|
9
|
+
* `profile(userId)` calls `/{userId}?fields=name,profile_pic`
|
|
10
|
+
* for a Page-scoped PSID.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { InstantProviderError, ProviderUnsupportedError } from '../../errors.ts'
|
|
14
|
+
import type { InstantCapability } from '../../instant_capabilities.ts'
|
|
15
|
+
import type { InstantDriver, UserProfile, WebhookOps } from '../../instant_driver.ts'
|
|
16
|
+
import type { OutgoingMessage, SendResult } from '../../message.ts'
|
|
17
|
+
import { MetaGraphClient } from '../../internal/meta/meta_graph_client.ts'
|
|
18
|
+
import type { MessengerProviderConfig } from './messenger_config.ts'
|
|
19
|
+
import { toMessengerPayload } from './messenger_message_mapper.ts'
|
|
20
|
+
import { MessengerBotProfile } from './messenger_profile.ts'
|
|
21
|
+
import {
|
|
22
|
+
parseMessengerWebhook,
|
|
23
|
+
verifyMessengerChallenge,
|
|
24
|
+
verifyMessengerSignature,
|
|
25
|
+
} from './messenger_webhook.ts'
|
|
26
|
+
|
|
27
|
+
const DEFAULT_CAPABILITIES: ReadonlySet<InstantCapability> = new Set<InstantCapability>([
|
|
28
|
+
'send.text',
|
|
29
|
+
'send.image',
|
|
30
|
+
'send.video',
|
|
31
|
+
'send.audio',
|
|
32
|
+
'send.file',
|
|
33
|
+
'send.location',
|
|
34
|
+
'send.quickReplies',
|
|
35
|
+
'send.interactive',
|
|
36
|
+
'send.template',
|
|
37
|
+
'push',
|
|
38
|
+
'broadcast',
|
|
39
|
+
'profile',
|
|
40
|
+
'persistentMenu',
|
|
41
|
+
'miniApp',
|
|
42
|
+
'webhook.signature',
|
|
43
|
+
'webhook.parse',
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
export interface MessengerDriverOptions {
|
|
47
|
+
instanceName: string
|
|
48
|
+
config: MessengerProviderConfig
|
|
49
|
+
client?: MetaGraphClient
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class MessengerDriver implements InstantDriver {
|
|
53
|
+
readonly name = 'messenger'
|
|
54
|
+
readonly instanceName: string
|
|
55
|
+
readonly capabilities = DEFAULT_CAPABILITIES
|
|
56
|
+
readonly webhook: WebhookOps
|
|
57
|
+
private readonly client: MetaGraphClient
|
|
58
|
+
private readonly pageId: string
|
|
59
|
+
private _botProfile?: MessengerBotProfile
|
|
60
|
+
|
|
61
|
+
constructor(options: MessengerDriverOptions) {
|
|
62
|
+
const { instanceName, config } = options
|
|
63
|
+
for (const field of ['pageId', 'pageAccessToken', 'appSecret', 'verifyToken'] as const) {
|
|
64
|
+
if (!config[field]) {
|
|
65
|
+
throw new InstantProviderError(
|
|
66
|
+
`MessengerDriver: \`${field}\` is required for provider "${instanceName}".`,
|
|
67
|
+
{ provider: 'messenger', operation: 'init', status: 500 },
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.instanceName = instanceName
|
|
72
|
+
this.pageId = config.pageId
|
|
73
|
+
this.client =
|
|
74
|
+
options.client ??
|
|
75
|
+
new MetaGraphClient({
|
|
76
|
+
accessToken: config.pageAccessToken,
|
|
77
|
+
...(config.apiVersion ? { apiVersion: config.apiVersion } : {}),
|
|
78
|
+
provider: 'messenger',
|
|
79
|
+
})
|
|
80
|
+
const appSecret = config.appSecret
|
|
81
|
+
const verifyToken = config.verifyToken
|
|
82
|
+
this.webhook = {
|
|
83
|
+
verifySignature: (rawBody, header) => verifyMessengerSignature(rawBody, header, appSecret),
|
|
84
|
+
parse: (rawBody) => parseMessengerWebhook(rawBody),
|
|
85
|
+
verifyChallenge: (params) => verifyMessengerChallenge(params, verifyToken),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async send(to: string, message: OutgoingMessage): Promise<SendResult> {
|
|
90
|
+
return this.push(to, message)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async push(to: string, message: OutgoingMessage): Promise<SendResult> {
|
|
94
|
+
const body = toMessengerPayload(to, message)
|
|
95
|
+
const response = await this.client.post<{ message_id?: string; recipient_id?: string }>(
|
|
96
|
+
`/${this.pageId}/messages`,
|
|
97
|
+
body,
|
|
98
|
+
'send',
|
|
99
|
+
)
|
|
100
|
+
return {
|
|
101
|
+
provider: 'messenger',
|
|
102
|
+
accepted: true,
|
|
103
|
+
...(response.message_id ? { messageId: response.message_id } : {}),
|
|
104
|
+
raw: response,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async broadcast(message: OutgoingMessage): Promise<SendResult> {
|
|
109
|
+
const raw = (message.raw ?? {}) as { messagingType?: string; tag?: string }
|
|
110
|
+
if (raw.messagingType !== 'MESSAGE_TAG' || !raw.tag) {
|
|
111
|
+
throw new ProviderUnsupportedError('messenger', 'broadcast', {
|
|
112
|
+
reason: 'Broadcast requires `raw.messagingType="MESSAGE_TAG"` and `raw.tag` set to an approved tag.',
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
const messageBody = (toMessengerPayload('PLACEHOLDER', message) as { message: unknown }).message
|
|
116
|
+
const creative = await this.client.post<{ message_creative_id: string }>(
|
|
117
|
+
'/me/message_creatives',
|
|
118
|
+
{ messages: [messageBody] },
|
|
119
|
+
'broadcast.creative',
|
|
120
|
+
)
|
|
121
|
+
const result = await this.client.post<{ broadcast_id: string }>(
|
|
122
|
+
'/me/broadcast_messages',
|
|
123
|
+
{ message_creative_id: creative.message_creative_id, messaging_type: 'MESSAGE_TAG', tag: raw.tag },
|
|
124
|
+
'broadcast.send',
|
|
125
|
+
)
|
|
126
|
+
return { provider: 'messenger', accepted: true, messageId: result.broadcast_id, raw: result }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async profile(userId: string): Promise<UserProfile> {
|
|
130
|
+
const response = await this.client.get<{
|
|
131
|
+
id?: string
|
|
132
|
+
name?: string
|
|
133
|
+
first_name?: string
|
|
134
|
+
last_name?: string
|
|
135
|
+
profile_pic?: string
|
|
136
|
+
}>(`/${userId}?fields=name,first_name,last_name,profile_pic`, 'profile')
|
|
137
|
+
const fullName = [response.first_name, response.last_name].filter(Boolean).join(' ')
|
|
138
|
+
const displayName = response.name ?? (fullName.length > 0 ? fullName : undefined)
|
|
139
|
+
return {
|
|
140
|
+
userId,
|
|
141
|
+
...(displayName ? { displayName } : {}),
|
|
142
|
+
...(response.profile_pic ? { pictureUrl: response.profile_pic } : {}),
|
|
143
|
+
raw: response,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
get botProfile(): MessengerBotProfile {
|
|
148
|
+
if (!this._botProfile) this._botProfile = new MessengerBotProfile(this.client)
|
|
149
|
+
return this._botProfile
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map `OutgoingMessage` → Messenger Send API body.
|
|
3
|
+
*
|
|
4
|
+
* The Send API takes one message per call: `{ recipient, message,
|
|
5
|
+
* messaging_type }`. Quick replies attach to `message.quick_replies`.
|
|
6
|
+
* Rich content (generic / button / list / media templates) flows
|
|
7
|
+
* through `raw.template`.
|
|
8
|
+
*
|
|
9
|
+
* Provider-native escape hatches via `raw`:
|
|
10
|
+
* - `raw.messagingType?: 'RESPONSE' | 'UPDATE' | 'MESSAGE_TAG'` (default RESPONSE)
|
|
11
|
+
* - `raw.tag?: string` — required when messagingType === 'MESSAGE_TAG'
|
|
12
|
+
* - `raw.template?: { type: 'generic'|'button'|'list'|'media', payload }`
|
|
13
|
+
* - `raw.persona_id?: string`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { InstantProviderError } from '../../errors.ts'
|
|
17
|
+
import type { Attachment, OutgoingMessage, QuickReply } from '../../message.ts'
|
|
18
|
+
|
|
19
|
+
interface MessengerRawExtras {
|
|
20
|
+
messagingType?: 'RESPONSE' | 'UPDATE' | 'MESSAGE_TAG'
|
|
21
|
+
tag?: string
|
|
22
|
+
template?: { type: 'generic' | 'button' | 'list' | 'media'; payload: unknown }
|
|
23
|
+
persona_id?: string
|
|
24
|
+
notification_type?: 'REGULAR' | 'SILENT_PUSH' | 'NO_PUSH'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function toMessengerPayload(to: string, message: OutgoingMessage): Record<string, unknown> {
|
|
28
|
+
const raw = (message.raw ?? {}) as MessengerRawExtras
|
|
29
|
+
const messagingType = raw.messagingType ?? 'RESPONSE'
|
|
30
|
+
if (messagingType === 'MESSAGE_TAG' && !raw.tag) {
|
|
31
|
+
throw new InstantProviderError(
|
|
32
|
+
'Messenger MESSAGE_TAG sends require `raw.tag` (the approved message tag).',
|
|
33
|
+
{ provider: 'messenger', operation: 'send', status: 400 },
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const body = buildMessage(message, raw)
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
recipient: { id: to },
|
|
41
|
+
messaging_type: messagingType,
|
|
42
|
+
...(raw.tag ? { tag: raw.tag } : {}),
|
|
43
|
+
...(raw.notification_type ? { notification_type: raw.notification_type } : {}),
|
|
44
|
+
...(raw.persona_id ? { persona_id: raw.persona_id } : {}),
|
|
45
|
+
message: body,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildMessage(
|
|
50
|
+
message: OutgoingMessage,
|
|
51
|
+
raw: MessengerRawExtras,
|
|
52
|
+
): Record<string, unknown> {
|
|
53
|
+
const body: Record<string, unknown> = {}
|
|
54
|
+
const quickReplies = mapQuickReplies(message.quickReplies)
|
|
55
|
+
if (quickReplies) body['quick_replies'] = quickReplies
|
|
56
|
+
|
|
57
|
+
if (raw.template) {
|
|
58
|
+
body['attachment'] = {
|
|
59
|
+
type: 'template',
|
|
60
|
+
payload: { template_type: raw.template.type, ...(raw.template.payload as object) },
|
|
61
|
+
}
|
|
62
|
+
if (message.text && !('text' in body)) body['text'] = message.text
|
|
63
|
+
return body
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const attachment = message.attachments?.[0]
|
|
67
|
+
if (attachment) {
|
|
68
|
+
body['attachment'] = attachmentPayload(attachment)
|
|
69
|
+
return body
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (message.text) {
|
|
73
|
+
body['text'] = message.text
|
|
74
|
+
return body
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!quickReplies) {
|
|
78
|
+
throw new InstantProviderError(
|
|
79
|
+
'Messenger send requires `text`, an attachment, or `raw.template`.',
|
|
80
|
+
{ provider: 'messenger', operation: 'send', status: 400 },
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
return body
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mapQuickReplies(quickReplies: QuickReply[] | undefined): unknown[] | undefined {
|
|
87
|
+
if (!quickReplies || quickReplies.length === 0) return undefined
|
|
88
|
+
if (quickReplies.length > 13) {
|
|
89
|
+
throw new InstantProviderError(
|
|
90
|
+
'Messenger supports at most 13 quick replies per message.',
|
|
91
|
+
{ provider: 'messenger', operation: 'send', status: 400 },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
return quickReplies.map((qr) => {
|
|
95
|
+
const base: Record<string, unknown> = { content_type: 'text', title: qr.label.slice(0, 20) }
|
|
96
|
+
switch (qr.action.type) {
|
|
97
|
+
case 'postback':
|
|
98
|
+
base['payload'] = qr.action.data
|
|
99
|
+
break
|
|
100
|
+
case 'message':
|
|
101
|
+
base['payload'] = qr.action.text
|
|
102
|
+
break
|
|
103
|
+
case 'uri':
|
|
104
|
+
// No native URL quick reply — emit as text and let the
|
|
105
|
+
// bot field it as a payload click.
|
|
106
|
+
base['payload'] = qr.action.uri
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
if (qr.iconUrl) base['image_url'] = qr.iconUrl
|
|
110
|
+
return base
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function attachmentPayload(a: Attachment): Record<string, unknown> {
|
|
115
|
+
switch (a.type) {
|
|
116
|
+
case 'image':
|
|
117
|
+
return { type: 'image', payload: { url: a.url, is_reusable: false } }
|
|
118
|
+
case 'video':
|
|
119
|
+
return { type: 'video', payload: { url: a.url, is_reusable: false } }
|
|
120
|
+
case 'audio':
|
|
121
|
+
return { type: 'audio', payload: { url: a.url, is_reusable: false } }
|
|
122
|
+
case 'file':
|
|
123
|
+
return { type: 'file', payload: { url: a.url, is_reusable: false } }
|
|
124
|
+
case 'location':
|
|
125
|
+
// Messenger doesn't accept outbound location attachments;
|
|
126
|
+
// surface via a generic template anchor instead.
|
|
127
|
+
return {
|
|
128
|
+
type: 'template',
|
|
129
|
+
payload: {
|
|
130
|
+
template_type: 'generic',
|
|
131
|
+
elements: [
|
|
132
|
+
{
|
|
133
|
+
title: a.title ?? 'Location',
|
|
134
|
+
subtitle: a.address,
|
|
135
|
+
default_action: {
|
|
136
|
+
type: 'web_url',
|
|
137
|
+
url: `https://maps.google.com/?q=${a.latitude},${a.longitude}`,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
case 'sticker':
|
|
144
|
+
return { type: 'image', payload: { url: a.stickerId, is_reusable: false } }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messenger Profile API helpers — sets the bot's get-started
|
|
3
|
+
* button, persistent menu, and greeting on the Page-scoped
|
|
4
|
+
* profile. Used by apps onboarding a new bot deployment.
|
|
5
|
+
*
|
|
6
|
+
* Exposed via `MessengerDriver.profile()` is the *user* profile
|
|
7
|
+
* lookup (Page-scoped id → public name). These helpers configure
|
|
8
|
+
* the *bot's own* profile and are reached via
|
|
9
|
+
* `driver.botProfile`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { MetaGraphClient } from '../../internal/meta/meta_graph_client.ts'
|
|
13
|
+
|
|
14
|
+
export interface PersistentMenuEntry {
|
|
15
|
+
locale: 'default' | string
|
|
16
|
+
composer_input_disabled?: boolean
|
|
17
|
+
call_to_actions: PersistentMenuItem[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type PersistentMenuItem =
|
|
21
|
+
| { type: 'postback'; title: string; payload: string }
|
|
22
|
+
| { type: 'web_url'; title: string; url: string; webview_height_ratio?: 'compact' | 'tall' | 'full' }
|
|
23
|
+
| { type: 'nested'; title: string; call_to_actions: PersistentMenuItem[] }
|
|
24
|
+
|
|
25
|
+
export class MessengerBotProfile {
|
|
26
|
+
constructor(private readonly client: MetaGraphClient) {}
|
|
27
|
+
|
|
28
|
+
setGetStarted(payload: string): Promise<unknown> {
|
|
29
|
+
return this.client.post('/me/messenger_profile', { get_started: { payload } }, 'setGetStarted')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setGreeting(greetings: Array<{ locale: string; text: string }>): Promise<unknown> {
|
|
33
|
+
return this.client.post('/me/messenger_profile', { greeting: greetings }, 'setGreeting')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setPersistentMenu(menu: PersistentMenuEntry[]): Promise<unknown> {
|
|
37
|
+
return this.client.post('/me/messenger_profile', { persistent_menu: menu }, 'setPersistentMenu')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deletePersistentMenu(): Promise<unknown> {
|
|
41
|
+
return this.client.post('/me/messenger_profile', { fields: ['persistent_menu'] }, 'deletePersistentMenu')
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MessengerInstantProvider` — `ServiceProvider` that registers
|
|
3
|
+
* the Messenger Send API driver factory on the `InstantManager`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type Application, ServiceProvider } from '@strav/kernel'
|
|
7
|
+
import { InstantConfigError } from '../../errors.ts'
|
|
8
|
+
import { InstantManager } from '../../instant_manager.ts'
|
|
9
|
+
import type { MessengerProviderConfig } from './messenger_config.ts'
|
|
10
|
+
import { MessengerDriver } from './messenger_driver.ts'
|
|
11
|
+
|
|
12
|
+
export class MessengerInstantProvider extends ServiceProvider {
|
|
13
|
+
override readonly name = 'instant-messenger'
|
|
14
|
+
override readonly dependencies = ['instant']
|
|
15
|
+
|
|
16
|
+
override register(app: Application): void {
|
|
17
|
+
const manager = app.resolve(InstantManager)
|
|
18
|
+
manager.extend('messenger', ({ instanceName, config }) => {
|
|
19
|
+
const cfg = config as MessengerProviderConfig
|
|
20
|
+
for (const field of ['pageId', 'pageAccessToken', 'appSecret', 'verifyToken'] as const) {
|
|
21
|
+
if (!cfg[field]) {
|
|
22
|
+
throw new InstantConfigError(
|
|
23
|
+
`MessengerInstantProvider: \`${field}\` is required for provider "${instanceName}".`,
|
|
24
|
+
{ context: { instanceName, field } },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return new MessengerDriver({ instanceName, config: cfg })
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messenger webhook — signature delegates to the shared Meta
|
|
3
|
+
* verifier; parser walks `entry[].messaging[]`.
|
|
4
|
+
*
|
|
5
|
+
* Mapping:
|
|
6
|
+
* - `message.text` → `message.text`
|
|
7
|
+
* - `message.attachments[].type === 'location'` → `message.location`
|
|
8
|
+
* - other `attachments[]` → `message.image|video|audio|file`
|
|
9
|
+
* - `message.quick_reply` → `postback` with `data = payload`
|
|
10
|
+
* - `postback` → `postback`
|
|
11
|
+
* - `referral`, `optin` → `follow` (synthetic — these signal
|
|
12
|
+
* a new conversation entry-point)
|
|
13
|
+
* - `reaction`, `read`, `delivery`, `account_linking` → `unknown`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { WebhookSignatureError } from '../../errors.ts'
|
|
17
|
+
import { verifyMetaChallenge } from '../../internal/meta/meta_webhook_challenge.ts'
|
|
18
|
+
import { verifyMetaSignature } from '../../internal/meta/meta_signature.ts'
|
|
19
|
+
import type {
|
|
20
|
+
FollowEvent,
|
|
21
|
+
LocationMessageEvent,
|
|
22
|
+
MediaMessageEvent,
|
|
23
|
+
PostbackEvent,
|
|
24
|
+
TextMessageEvent,
|
|
25
|
+
UnknownEvent,
|
|
26
|
+
WebhookEvent,
|
|
27
|
+
WebhookEventBase,
|
|
28
|
+
} from '../../webhook_event.ts'
|
|
29
|
+
|
|
30
|
+
export function verifyMessengerSignature(
|
|
31
|
+
rawBody: string,
|
|
32
|
+
header: string | null | undefined,
|
|
33
|
+
appSecret: string,
|
|
34
|
+
): boolean {
|
|
35
|
+
return verifyMetaSignature(rawBody, header, appSecret)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function verifyMessengerChallenge(
|
|
39
|
+
params: Record<string, string | undefined>,
|
|
40
|
+
verifyToken: string,
|
|
41
|
+
): string | null {
|
|
42
|
+
return verifyMetaChallenge(params, verifyToken)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface MessengerPayload {
|
|
46
|
+
object?: string
|
|
47
|
+
entry?: MessengerEntry[]
|
|
48
|
+
}
|
|
49
|
+
interface MessengerEntry {
|
|
50
|
+
id: string
|
|
51
|
+
time?: number
|
|
52
|
+
messaging?: MessengerEvent[]
|
|
53
|
+
}
|
|
54
|
+
interface MessengerEvent {
|
|
55
|
+
sender?: { id: string }
|
|
56
|
+
recipient?: { id: string }
|
|
57
|
+
timestamp: number
|
|
58
|
+
message?: MessengerMessage
|
|
59
|
+
postback?: { payload: string; title?: string }
|
|
60
|
+
referral?: { ref?: string; source?: string; type?: string }
|
|
61
|
+
optin?: { ref?: string }
|
|
62
|
+
reaction?: unknown
|
|
63
|
+
read?: unknown
|
|
64
|
+
delivery?: unknown
|
|
65
|
+
account_linking?: unknown
|
|
66
|
+
}
|
|
67
|
+
interface MessengerMessage {
|
|
68
|
+
mid: string
|
|
69
|
+
text?: string
|
|
70
|
+
attachments?: Array<{
|
|
71
|
+
type: 'image' | 'video' | 'audio' | 'file' | 'location' | 'fallback' | 'template'
|
|
72
|
+
payload: {
|
|
73
|
+
url?: string
|
|
74
|
+
coordinates?: { lat: number; long: number }
|
|
75
|
+
title?: string
|
|
76
|
+
}
|
|
77
|
+
}>
|
|
78
|
+
quick_reply?: { payload: string }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseMessengerWebhook(rawBody: string): WebhookEvent[] {
|
|
82
|
+
let payload: MessengerPayload
|
|
83
|
+
try {
|
|
84
|
+
payload = JSON.parse(rawBody) as MessengerPayload
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
throw new WebhookSignatureError('Messenger webhook body is not valid JSON.', { cause })
|
|
87
|
+
}
|
|
88
|
+
const events: WebhookEvent[] = []
|
|
89
|
+
for (const entry of payload.entry ?? []) {
|
|
90
|
+
for (const m of entry.messaging ?? []) {
|
|
91
|
+
const event = mapEvent(m)
|
|
92
|
+
if (event) events.push(event)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return events
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function mapEvent(e: MessengerEvent): WebhookEvent | undefined {
|
|
99
|
+
if (!e.sender) return undefined
|
|
100
|
+
const base: WebhookEventBase = {
|
|
101
|
+
provider: 'messenger',
|
|
102
|
+
userId: e.sender.id,
|
|
103
|
+
timestamp: new Date(e.timestamp),
|
|
104
|
+
source: 'user',
|
|
105
|
+
raw: e,
|
|
106
|
+
}
|
|
107
|
+
if (e.message?.quick_reply) {
|
|
108
|
+
const event: PostbackEvent = { ...base, type: 'postback', data: e.message.quick_reply.payload }
|
|
109
|
+
return event
|
|
110
|
+
}
|
|
111
|
+
if (e.message?.text) {
|
|
112
|
+
const event: TextMessageEvent = { ...base, type: 'message.text', text: e.message.text, messageId: e.message.mid }
|
|
113
|
+
return event
|
|
114
|
+
}
|
|
115
|
+
const attachment = e.message?.attachments?.[0]
|
|
116
|
+
if (attachment) {
|
|
117
|
+
if (attachment.type === 'location' && attachment.payload.coordinates) {
|
|
118
|
+
const event: LocationMessageEvent = {
|
|
119
|
+
...base,
|
|
120
|
+
type: 'message.location',
|
|
121
|
+
messageId: e.message?.mid ?? '',
|
|
122
|
+
latitude: attachment.payload.coordinates.lat,
|
|
123
|
+
longitude: attachment.payload.coordinates.long,
|
|
124
|
+
...(attachment.payload.title ? { title: attachment.payload.title } : {}),
|
|
125
|
+
}
|
|
126
|
+
return event
|
|
127
|
+
}
|
|
128
|
+
const mediaType = attachmentToEventType(attachment.type)
|
|
129
|
+
if (mediaType) {
|
|
130
|
+
const event: MediaMessageEvent = {
|
|
131
|
+
...base,
|
|
132
|
+
type: mediaType,
|
|
133
|
+
messageId: e.message?.mid ?? '',
|
|
134
|
+
...(attachment.payload.url ? { contentUrl: attachment.payload.url } : {}),
|
|
135
|
+
}
|
|
136
|
+
return event
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (e.postback) {
|
|
140
|
+
const event: PostbackEvent = { ...base, type: 'postback', data: e.postback.payload }
|
|
141
|
+
return event
|
|
142
|
+
}
|
|
143
|
+
if (e.referral || e.optin) {
|
|
144
|
+
const event: FollowEvent = { ...base, type: 'follow' }
|
|
145
|
+
return event
|
|
146
|
+
}
|
|
147
|
+
const fallback: UnknownEvent = { ...base, type: 'unknown' }
|
|
148
|
+
return fallback
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function attachmentToEventType(
|
|
152
|
+
type: string,
|
|
153
|
+
): MediaMessageEvent['type'] | undefined {
|
|
154
|
+
switch (type) {
|
|
155
|
+
case 'image':
|
|
156
|
+
return 'message.image'
|
|
157
|
+
case 'video':
|
|
158
|
+
return 'message.video'
|
|
159
|
+
case 'audio':
|
|
160
|
+
return 'message.audio'
|
|
161
|
+
case 'file':
|
|
162
|
+
return 'message.file'
|
|
163
|
+
}
|
|
164
|
+
return undefined
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Public API of `@strav/instant/telegram`.
|
|
2
|
+
//
|
|
3
|
+
// Subpath barrel for the Telegram driver. Apps register the
|
|
4
|
+
// ServiceProvider in `bootstrap/providers.ts`:
|
|
5
|
+
//
|
|
6
|
+
// ```ts
|
|
7
|
+
// import { InstantProvider } from '@strav/instant'
|
|
8
|
+
// import { TelegramInstantProvider } from '@strav/instant/telegram'
|
|
9
|
+
//
|
|
10
|
+
// export default [InstantProvider, TelegramInstantProvider, ...]
|
|
11
|
+
// ```
|
|
12
|
+
|
|
13
|
+
export type { TelegramProviderConfig } from './telegram_config.ts'
|
|
14
|
+
export { TelegramDriver, type TelegramDriverOptions } from './telegram_driver.ts'
|
|
15
|
+
export { type TelegramCall, toTelegramCalls } from './telegram_message_mapper.ts'
|
|
16
|
+
export { TelegramInstantProvider } from './telegram_provider.ts'
|
|
17
|
+
export {
|
|
18
|
+
type VerifiedWebAppInitData,
|
|
19
|
+
verifyTelegramWebAppInitData,
|
|
20
|
+
webAppButton,
|
|
21
|
+
type WebAppUser,
|
|
22
|
+
} from './telegram_web_app.ts'
|
|
23
|
+
export { parseTelegramWebhook, verifyTelegramSignature } from './telegram_webhook.ts'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TelegramProviderConfig` — config shape consumed by
|
|
3
|
+
* `TelegramInstantProvider`.
|
|
4
|
+
*
|
|
5
|
+
* `botToken` is the token from `@BotFather`.
|
|
6
|
+
* `webhookSecretToken`, when set, is sent back by Telegram as
|
|
7
|
+
* the `x-telegram-bot-api-secret-token` header on every
|
|
8
|
+
* webhook POST. Setting it is strongly recommended.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ProviderConfig } from '../../types.ts'
|
|
12
|
+
|
|
13
|
+
export interface TelegramProviderConfig extends ProviderConfig {
|
|
14
|
+
driver: 'telegram'
|
|
15
|
+
botToken: string
|
|
16
|
+
webhookSecretToken?: string
|
|
17
|
+
/** Override Bot API base (defaults to `https://api.telegram.org`). */
|
|
18
|
+
apiBaseURL?: string
|
|
19
|
+
}
|