@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.
Files changed (56) hide show
  1. package/package.json +49 -0
  2. package/src/drivers/line/index.ts +50 -0
  3. package/src/drivers/line/liff_client.ts +126 -0
  4. package/src/drivers/line/liff_sign_in_route.ts +112 -0
  5. package/src/drivers/line/line_beacon.ts +14 -0
  6. package/src/drivers/line/line_config.ts +55 -0
  7. package/src/drivers/line/line_driver.ts +202 -0
  8. package/src/drivers/line/line_flex.ts +117 -0
  9. package/src/drivers/line/line_liff.ts +102 -0
  10. package/src/drivers/line/line_message_mapper.ts +118 -0
  11. package/src/drivers/line/line_provider.ts +33 -0
  12. package/src/drivers/line/line_rich_menu.ts +68 -0
  13. package/src/drivers/line/line_webhook.ts +168 -0
  14. package/src/drivers/messenger/index.ts +16 -0
  15. package/src/drivers/messenger/messenger_config.ts +20 -0
  16. package/src/drivers/messenger/messenger_driver.ts +151 -0
  17. package/src/drivers/messenger/messenger_message_mapper.ts +146 -0
  18. package/src/drivers/messenger/messenger_profile.ts +43 -0
  19. package/src/drivers/messenger/messenger_provider.ts +31 -0
  20. package/src/drivers/messenger/messenger_webhook.ts +165 -0
  21. package/src/drivers/telegram/index.ts +23 -0
  22. package/src/drivers/telegram/telegram_config.ts +19 -0
  23. package/src/drivers/telegram/telegram_driver.ts +121 -0
  24. package/src/drivers/telegram/telegram_message_mapper.ts +147 -0
  25. package/src/drivers/telegram/telegram_provider.ts +32 -0
  26. package/src/drivers/telegram/telegram_web_app.ts +108 -0
  27. package/src/drivers/telegram/telegram_webhook.ts +200 -0
  28. package/src/drivers/whatsapp/flows/index.ts +15 -0
  29. package/src/drivers/whatsapp/flows/whatsapp_flow_builder.ts +55 -0
  30. package/src/drivers/whatsapp/flows/whatsapp_flow_crypto.ts +81 -0
  31. package/src/drivers/whatsapp/index.ts +14 -0
  32. package/src/drivers/whatsapp/whatsapp_config.ts +35 -0
  33. package/src/drivers/whatsapp/whatsapp_driver.ts +111 -0
  34. package/src/drivers/whatsapp/whatsapp_message_mapper.ts +157 -0
  35. package/src/drivers/whatsapp/whatsapp_provider.ts +31 -0
  36. package/src/drivers/whatsapp/whatsapp_webhook.ts +170 -0
  37. package/src/drivers/zalo/index.ts +16 -0
  38. package/src/drivers/zalo/zalo_config.ts +37 -0
  39. package/src/drivers/zalo/zalo_driver.ts +130 -0
  40. package/src/drivers/zalo/zalo_message_mapper.ts +146 -0
  41. package/src/drivers/zalo/zalo_mini_app.ts +17 -0
  42. package/src/drivers/zalo/zalo_provider.ts +31 -0
  43. package/src/drivers/zalo/zalo_webhook.ts +139 -0
  44. package/src/errors.ts +122 -0
  45. package/src/index.ts +52 -0
  46. package/src/instant_capabilities.ts +49 -0
  47. package/src/instant_driver.ts +109 -0
  48. package/src/instant_manager.ts +113 -0
  49. package/src/instant_provider.ts +45 -0
  50. package/src/internal/fetch_json.ts +58 -0
  51. package/src/internal/meta/meta_graph_client.ts +51 -0
  52. package/src/internal/meta/meta_signature.ts +22 -0
  53. package/src/internal/meta/meta_webhook_challenge.ts +19 -0
  54. package/src/message.ts +65 -0
  55. package/src/types.ts +32 -0
  56. 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
+ }