@kyyinfinite/lumina 1.0.0

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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +629 -0
  3. package/examples/ai-rich.js +84 -0
  4. package/examples/button.js +57 -0
  5. package/examples/carousel.js +51 -0
  6. package/examples/interactive.js +102 -0
  7. package/examples/media.js +66 -0
  8. package/examples/simple-bot.js +56 -0
  9. package/package.json +86 -0
  10. package/src/builders/ai-rich.js +644 -0
  11. package/src/builders/base.js +109 -0
  12. package/src/builders/button-v2.js +159 -0
  13. package/src/builders/button.js +398 -0
  14. package/src/builders/card.js +168 -0
  15. package/src/builders/carousel.js +122 -0
  16. package/src/builders/index.d.ts +1 -0
  17. package/src/builders/index.js +13 -0
  18. package/src/client/bot.js +192 -0
  19. package/src/client/connection.js +180 -0
  20. package/src/errors.js +88 -0
  21. package/src/index.d.ts +458 -0
  22. package/src/index.js +152 -0
  23. package/src/media/fetch.js +67 -0
  24. package/src/media/image.js +86 -0
  25. package/src/media/index.d.ts +1 -0
  26. package/src/media/index.js +12 -0
  27. package/src/media/resolver.js +115 -0
  28. package/src/media/uploader.js +65 -0
  29. package/src/media/video.js +195 -0
  30. package/src/parsers/code-tokenizer-keywords.js +128 -0
  31. package/src/parsers/code-tokenizer.js +191 -0
  32. package/src/parsers/index.d.ts +1 -0
  33. package/src/parsers/index.js +11 -0
  34. package/src/parsers/inline-entity.js +231 -0
  35. package/src/parsers/table-metadata.js +69 -0
  36. package/src/proto/enums.js +170 -0
  37. package/src/proto/index.d.ts +1 -0
  38. package/src/proto/index.js +13 -0
  39. package/src/proto/layouts.js +89 -0
  40. package/src/proto/primitives.js +181 -0
  41. package/src/proto/relay-nodes.js +55 -0
  42. package/src/proto/rich-response.js +144 -0
  43. package/src/proto/updater.js +318 -0
  44. package/src/services/index.d.ts +1 -0
  45. package/src/services/index.js +10 -0
  46. package/src/services/media-service.js +184 -0
  47. package/src/services/message-service.js +288 -0
  48. package/src/services/proto-service.js +90 -0
  49. package/src/utils/id.js +42 -0
  50. package/src/utils/logger.js +65 -0
  51. package/src/utils/mime.js +104 -0
  52. package/src/utils/promise.js +52 -0
  53. package/src/utils/validator.js +129 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @file layouts.js
3
+ * @module lumina/proto/layouts
4
+ *
5
+ * Layout view-model factories. Each layout wraps one or more UX primitives
6
+ * inside `view_model`, with the proper `GenAI{Kind}LayoutViewModel` typename.
7
+ *
8
+ * Replaces the implicit `Array.isArray(data) ? 'primitives' : 'primitive'`
9
+ * branching in the legacy `AIRich.newLayout` static method — explicit
10
+ * single/hscroll/action-row variants instead.
11
+ */
12
+
13
+ import { LayoutKind, TYPENAME } from './enums.js'
14
+
15
+ /**
16
+ * Wrap a single primitive in a "Single" layout view-model.
17
+ *
18
+ * @param {object} primitive
19
+ * @param {object} [extra] Additional fields merged at top-level.
20
+ * @returns {object}
21
+ */
22
+ export function singleLayout(primitive, extra = {}) {
23
+ return {
24
+ ...extra,
25
+ view_model: {
26
+ primitive,
27
+ __typename: TYPENAME.layout(LayoutKind.SINGLE),
28
+ },
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Wrap multiple primitives in an "HScroll" (horizontal scroll) layout.
34
+ *
35
+ * @param {object[]} primitives
36
+ * @param {object} [extra]
37
+ * @returns {object}
38
+ */
39
+ export function hscrollLayout(primitives, extra = {}) {
40
+ return {
41
+ ...extra,
42
+ view_model: {
43
+ primitives,
44
+ __typename: TYPENAME.layout(LayoutKind.HSCROLL),
45
+ },
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Wrap multiple primitives in an "ActionRow" layout (used for follow-up
51
+ * suggestion pills that should appear as a horizontal row of buttons).
52
+ *
53
+ * @param {object[]} primitives
54
+ * @param {object} [extra]
55
+ * @returns {object}
56
+ */
57
+ export function actionRowLayout(primitives, extra = {}) {
58
+ return {
59
+ ...extra,
60
+ view_model: {
61
+ primitives,
62
+ __typename: TYPENAME.layout(LayoutKind.ACTION_ROW),
63
+ },
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Auto-pick helper. Picks `singleLayout` for a single primitive and
69
+ * `hscrollLayout` for arrays — useful for API surfaces that accept both.
70
+ *
71
+ * @param {LayoutKind} kind Forced kind; overrides auto-detection.
72
+ * @param {object|object[]} data
73
+ * @param {object} [extra]
74
+ * @returns {object}
75
+ */
76
+ export function layoutFor(kind, data, extra = {}) {
77
+ switch (kind) {
78
+ case LayoutKind.SINGLE:
79
+ return singleLayout(Array.isArray(data) ? data[0] : data, extra)
80
+ case LayoutKind.HSCROLL:
81
+ return hscrollLayout(Array.isArray(data) ? data : [data], extra)
82
+ case LayoutKind.ACTION_ROW:
83
+ return actionRowLayout(Array.isArray(data) ? data : [data], extra)
84
+ default:
85
+ throw new TypeError(`Unknown layout kind: ${kind}`)
86
+ }
87
+ }
88
+
89
+ export default { singleLayout, hscrollLayout, actionRowLayout, layoutFor }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * @file primitives.js
3
+ * @module lumina/proto/primitives
4
+ *
5
+ * Pure factory functions for every GenAI "UX primitive" used by the AI Rich
6
+ * Response envelope. Each factory returns a plain object ready to be wrapped
7
+ * by a layout view-model (see {@link module:proto/layouts}).
8
+ *
9
+ * All `__typename` strings are sourced from {@link module:proto/enums.TYPENAME}
10
+ * — never hardcode them here.
11
+ */
12
+
13
+ import { TYPENAME, ImagineType, SourceType, PromptType } from './enums.js'
14
+
15
+ /**
16
+ * Markdown-text primitive. May carry inline entities extracted by
17
+ * `extractInlineEntities`.
18
+ *
19
+ * @param {string} text
20
+ * @param {Array<object>} [inlineEntities]
21
+ */
22
+ export function markdownTextPrimitive(text, inlineEntities) {
23
+ const primitive = { text, __typename: TYPENAME.MARKDOWN_TEXT }
24
+ if (inlineEntities?.length) primitive.inline_entities = inlineEntities
25
+ return primitive
26
+ }
27
+
28
+ /**
29
+ * Code primitive.
30
+ *
31
+ * @param {string} language
32
+ * @param {Array<{ content: string, type: string }>} unifiedBlocks
33
+ */
34
+ export function codePrimitive(language, unifiedBlocks) {
35
+ return {
36
+ language,
37
+ code_blocks: unifiedBlocks,
38
+ __typename: TYPENAME.CODE,
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Table primitive.
44
+ *
45
+ * @param {Array<object>} unifiedRows Output of `toTableMetadata().unifiedRows`.
46
+ */
47
+ export function tablePrimitive(unifiedRows) {
48
+ return {
49
+ rows: unifiedRows,
50
+ __typename: TYPENAME.TABLE,
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Search-result (sources) primitive.
56
+ *
57
+ * @param {Array<object>} sources Pre-shaped source objects.
58
+ */
59
+ export function searchResultPrimitive(sources) {
60
+ return {
61
+ sources,
62
+ __typename: TYPENAME.SEARCH_RESULT,
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Reel primitive (one per reel item).
68
+ *
69
+ * @param {object} item Pre-shaped reel descriptor.
70
+ */
71
+ export function reelPrimitive(item) {
72
+ return { ...item, __typename: TYPENAME.REEL }
73
+ }
74
+
75
+ /**
76
+ * Imagine primitive — used for both images and videos (animate).
77
+ *
78
+ * @param {{ url: string, mime_type: string, file_length?: number, duration?: number }} media
79
+ * @param {'IMAGE'|'ANIMATE'} kind
80
+ * @param {string} [status='READY']
81
+ * @param {string} [thumbnailBase64] Base64 JPEG thumbnail (video only).
82
+ */
83
+ export function imaginePrimitive(media, kind, status = 'READY', thumbnailBase64) {
84
+ const primitive = {
85
+ media,
86
+ imagine_type: kind,
87
+ status: { status },
88
+ __typename: TYPENAME.IMAGINE,
89
+ }
90
+ if (thumbnailBase64) primitive.thumbnail = { raw_media: thumbnailBase64 }
91
+ return primitive
92
+ }
93
+
94
+ /**
95
+ * Product-card primitive.
96
+ *
97
+ * @param {object} item Pre-shaped product descriptor (already media-resolved).
98
+ */
99
+ export function productCardPrimitive(item) {
100
+ return { ...item, __typename: TYPENAME.PRODUCT_CARD }
101
+ }
102
+
103
+ /**
104
+ * Post primitive.
105
+ *
106
+ * @param {object} p Pre-shaped post descriptor (already media-resolved).
107
+ * @param {boolean} isCarousel
108
+ */
109
+ export function postPrimitive(p, isCarousel) {
110
+ return { ...p, is_carousel: isCarousel, __typename: TYPENAME.POST }
111
+ }
112
+
113
+ /**
114
+ * Metadata-text primitive (used for footer / tip).
115
+ *
116
+ * @param {string} text
117
+ */
118
+ export function metadataTextPrimitive(text) {
119
+ return { text, __typename: TYPENAME.METADATA_TEXT }
120
+ }
121
+
122
+ /**
123
+ * Follow-up suggestion pill primitive.
124
+ *
125
+ * @param {string} text
126
+ */
127
+ export function followUpSuggestionPillPrimitive(text) {
128
+ return {
129
+ prompt_text: text,
130
+ prompt_type: PromptType.SUGGESTED_PROMPT,
131
+ __typename: TYPENAME.FOLLOW_UP_PILL,
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Helper: shape a source entry for `searchResultPrimitive`.
137
+ *
138
+ * @param {{ iconUrl?: string, url?: string, text?: string, subtitle?: string }} src
139
+ */
140
+ export function shapeSourceEntry({ iconUrl = '', url = '', text = '', subtitle = 'AI' } = {}) {
141
+ return {
142
+ source_type: SourceType.THIRD_PARTY,
143
+ source_display_name: text,
144
+ source_subtitle: subtitle,
145
+ source_url: url,
146
+ favicon: { url: iconUrl, mime_type: 'image/jpeg', width: 16, height: 16 },
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Helper: shape a single reel item for `reelPrimitive`.
152
+ */
153
+ export function shapeReelEntry(item) {
154
+ return {
155
+ reels_url: item.videoUrl ?? item.url ?? '',
156
+ thumbnail_url: item.thumbnail ?? '',
157
+ creator: item.username ?? item.title ?? '',
158
+ avatar_url: item.avatar ?? '',
159
+ reels_title: item.reelsTitle ?? item.title ?? '',
160
+ likes_count: item.likes ?? item.like ?? 0,
161
+ shares_count: item.shares ?? item.share ?? 0,
162
+ view_count: item.views ?? item.view ?? 0,
163
+ reel_source: item.source ?? 'IG',
164
+ is_verified: !!(item.isVerified ?? item.verified),
165
+ }
166
+ }
167
+
168
+ export default {
169
+ markdownTextPrimitive,
170
+ codePrimitive,
171
+ tablePrimitive,
172
+ searchResultPrimitive,
173
+ reelPrimitive,
174
+ imaginePrimitive,
175
+ productCardPrimitive,
176
+ postPrimitive,
177
+ metadataTextPrimitive,
178
+ followUpSuggestionPillPrimitive,
179
+ shapeSourceEntry,
180
+ shapeReelEntry,
181
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @file relay-nodes.js
3
+ * @module lumina/proto/relay-nodes
4
+ *
5
+ * Single source of truth for the `additionalNodes` block that Baileys
6
+ * `relayMessage` expects when sending interactive (native-flow) messages.
7
+ *
8
+ * The legacy code duplicated this 12-line block three times (Button.send,
9
+ * ButtonV2.send, Carousel.send). Lumina centralises it here.
10
+ */
11
+
12
+ import { NativeFlow } from './enums.js'
13
+
14
+ /**
15
+ * Build the standard biz/interactive/native_flow additional-nodes block.
16
+ *
17
+ * @param {object} [opts]
18
+ * @param {string} [opts.outerVersion=NativeFlow.OUTER_VERSION]
19
+ * @param {string} [opts.flowVersion=NativeFlow.FLOW_VERSION]
20
+ * @param {string} [opts.flowName=NativeFlow.FLOW_NAME_MIXED]
21
+ * @param {boolean} [opts.includeBiz=true] Whether to wrap in the `<biz>` node.
22
+ * @returns {Array<object>} Array suitable for `relayMessage`'s `additionalNodes`.
23
+ */
24
+ export function createInteractiveNodes(opts = {}) {
25
+ const outerVersion = opts.outerVersion ?? NativeFlow.OUTER_VERSION
26
+ const flowVersion = opts.flowVersion ?? NativeFlow.FLOW_VERSION
27
+ const flowName = opts.flowName ?? NativeFlow.FLOW_NAME_MIXED
28
+ const includeBiz = opts.includeBiz ?? true
29
+
30
+ const interactive = {
31
+ tag: NativeFlow.INTERACTIVE_TAG,
32
+ attrs: { type: NativeFlow.TYPE, v: outerVersion },
33
+ content: [{ tag: NativeFlow.TYPE, attrs: { v: flowVersion, name: flowName } }],
34
+ }
35
+
36
+ if (!includeBiz) return [interactive]
37
+
38
+ return [
39
+ {
40
+ tag: NativeFlow.BIZ_TAG,
41
+ attrs: {},
42
+ content: [interactive],
43
+ },
44
+ ]
45
+ }
46
+
47
+ /**
48
+ * Convenience: returns the same block but with `name: 'native_flow'` only
49
+ * (no biz wrapper). Useful for non-business accounts.
50
+ */
51
+ export function createBareInteractiveNodes(opts = {}) {
52
+ return createInteractiveNodes({ ...opts, includeBiz: false })
53
+ }
54
+
55
+ export default { createInteractiveNodes, createBareInteractiveNodes }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @file rich-response.js
3
+ * @module lumina/proto/rich-response
4
+ *
5
+ * Assembles the `botForwardedMessage.richResponseMessage` envelope used by
6
+ * the AI Rich Response feature. Pure function — no socket access. The
7
+ * {@link AIRichBuilder} calls this from its `build()` method.
8
+ *
9
+ * Replaces the legacy `AIRich.build()` method (1436–1503) which:
10
+ * - Hardcoded `botJid: '0@bot'`
11
+ * - Hardcoded `forwardOrigin: 4`
12
+ * - Hardcoded `forwardingScore: 1`
13
+ * - Hardcoded personal leftover `disclaimerText: '~ Ahmad tumbuh kembang'`
14
+ * All four are now sourced from {@link module:proto/enums} / parameters.
15
+ */
16
+
17
+ import crypto from 'node:crypto'
18
+
19
+ import { BOT_JID, ForwardOrigin } from './enums.js'
20
+
21
+ /**
22
+ * @typedef {object} RichResponseOptions
23
+ * @property {string} [title] Sets `botMetadata.messageDisclaimerText`.
24
+ * @property {Array<object>} sections Pre-resolved layout sections.
25
+ * @property {Array<object>} [submessages=[]] Pre-resolved submessage objects.
26
+ * @property {Array<object>} [richResponseSources=[]]
27
+ * @property {object} [contextInfo={}]
28
+ * @property {string} [footer] Appended as a metadata-text section.
29
+ * @property {boolean} [forwarded=true] Attach forward metadata.
30
+ * @property {boolean|object} [notification=false] Attach session-transparency metadata.
31
+ * @property {boolean} [includesUnifiedResponse=true]
32
+ * @property {boolean} [includesSubmessages=true]
33
+ * @property {object} [quoted] Quoted-message descriptor.
34
+ * @property {string} [quotedParticipant]
35
+ * @property {object} [extraPayload] Merged at top-level.
36
+ */
37
+
38
+ /**
39
+ * Assemble a `botForwardedMessage` envelope.
40
+ *
41
+ * @param {RichResponseOptions} opts
42
+ * @returns {Promise<object>} The envelope, ready to pass to `relayMessage`.
43
+ */
44
+ export async function assembleRichResponse({
45
+ title,
46
+ sections,
47
+ submessages = [],
48
+ richResponseSources = [],
49
+ contextInfo = {},
50
+ footer,
51
+ forwarded = true,
52
+ notification = false,
53
+ includesUnifiedResponse = true,
54
+ includesSubmessages = true,
55
+ quoted,
56
+ quotedParticipant,
57
+ extraPayload = {},
58
+ } = {}) {
59
+ const forwardMeta = forwarded
60
+ ? {
61
+ forwardingScore: 1,
62
+ isForwarded: true,
63
+ forwardedAiBotMessageInfo: { botJid: BOT_JID },
64
+ forwardOrigin: ForwardOrigin.BOT,
65
+ }
66
+ : {}
67
+
68
+ const notificationMeta = notification
69
+ ? {
70
+ sessionTransparencyMetadata: {
71
+ // User-supplied disclaimer (no personal leftovers).
72
+ disclaimerText: typeof notification === 'object' ? notification.disclaimerText ?? '' : '',
73
+ hcaId: typeof notification === 'object' && notification.hcaId
74
+ ? notification.hcaId
75
+ : `hca_${crypto.randomUUID()}`,
76
+ sessionTransparencyType: notification.sessionTransparencyType ?? 1,
77
+ },
78
+ }
79
+ : {}
80
+
81
+ const quotedMeta = quoted
82
+ ? {
83
+ stanzaId: quoted?.key?.id ?? quoted?.id,
84
+ participant: quotedParticipant ?? quoted?.key?.participant ?? quoted?.key?.remoteJid,
85
+ quotedType: 0,
86
+ quotedMessage:
87
+ typeof quoted === 'object' && quoted !== null ? quoted.message ?? quoted : undefined,
88
+ }
89
+ : {}
90
+
91
+ const finalSections = footer
92
+ ? [
93
+ ...sections,
94
+ // Footer is appended as a metadata-text primitive, wrapped in a
95
+ // Single layout view-model. Built inline to avoid a circular import
96
+ // with primitives/layouts — those are pure helpers, but this envelope
97
+ // assembler must remain dependency-light for testability.
98
+ {
99
+ view_model: {
100
+ primitive: { text: footer, __typename: 'GenAIMetadataTextPrimitive' },
101
+ __typename: 'GenAISingleLayoutViewModel',
102
+ },
103
+ },
104
+ ]
105
+ : sections
106
+
107
+ return {
108
+ messageContextInfo: {
109
+ deviceListMetadata: {},
110
+ deviceListMetadataVersion: 2,
111
+ botMetadata: {
112
+ messageDisclaimerText: title,
113
+ richResponseSourcesMetadata: { sources: richResponseSources },
114
+ ...notificationMeta,
115
+ },
116
+ },
117
+ ...extraPayload,
118
+ botForwardedMessage: {
119
+ message: {
120
+ richResponseMessage: {
121
+ messageType: 1,
122
+ submessages: includesSubmessages ? submessages : [],
123
+ unifiedResponse: {
124
+ data: includesUnifiedResponse
125
+ ? Buffer.from(
126
+ JSON.stringify({
127
+ response_id: crypto.randomUUID(),
128
+ sections: finalSections,
129
+ }),
130
+ ).toString('base64')
131
+ : '',
132
+ },
133
+ contextInfo: {
134
+ ...forwardMeta,
135
+ ...quotedMeta,
136
+ ...contextInfo,
137
+ },
138
+ },
139
+ },
140
+ },
141
+ }
142
+ }
143
+
144
+ export default assembleRichResponse