@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.
- package/LICENSE +21 -0
- package/README.md +629 -0
- package/examples/ai-rich.js +84 -0
- package/examples/button.js +57 -0
- package/examples/carousel.js +51 -0
- package/examples/interactive.js +102 -0
- package/examples/media.js +66 -0
- package/examples/simple-bot.js +56 -0
- package/package.json +86 -0
- package/src/builders/ai-rich.js +644 -0
- package/src/builders/base.js +109 -0
- package/src/builders/button-v2.js +159 -0
- package/src/builders/button.js +398 -0
- package/src/builders/card.js +168 -0
- package/src/builders/carousel.js +122 -0
- package/src/builders/index.d.ts +1 -0
- package/src/builders/index.js +13 -0
- package/src/client/bot.js +192 -0
- package/src/client/connection.js +180 -0
- package/src/errors.js +88 -0
- package/src/index.d.ts +458 -0
- package/src/index.js +152 -0
- package/src/media/fetch.js +67 -0
- package/src/media/image.js +86 -0
- package/src/media/index.d.ts +1 -0
- package/src/media/index.js +12 -0
- package/src/media/resolver.js +115 -0
- package/src/media/uploader.js +65 -0
- package/src/media/video.js +195 -0
- package/src/parsers/code-tokenizer-keywords.js +128 -0
- package/src/parsers/code-tokenizer.js +191 -0
- package/src/parsers/index.d.ts +1 -0
- package/src/parsers/index.js +11 -0
- package/src/parsers/inline-entity.js +231 -0
- package/src/parsers/table-metadata.js +69 -0
- package/src/proto/enums.js +170 -0
- package/src/proto/index.d.ts +1 -0
- package/src/proto/index.js +13 -0
- package/src/proto/layouts.js +89 -0
- package/src/proto/primitives.js +181 -0
- package/src/proto/relay-nodes.js +55 -0
- package/src/proto/rich-response.js +144 -0
- package/src/proto/updater.js +318 -0
- package/src/services/index.d.ts +1 -0
- package/src/services/index.js +10 -0
- package/src/services/media-service.js +184 -0
- package/src/services/message-service.js +288 -0
- package/src/services/proto-service.js +90 -0
- package/src/utils/id.js +42 -0
- package/src/utils/logger.js +65 -0
- package/src/utils/mime.js +104 -0
- package/src/utils/promise.js +52 -0
- package/src/utils/validator.js +129 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file code-tokenizer.js
|
|
3
|
+
* @module lumina/parsers/code-tokenizer
|
|
4
|
+
*
|
|
5
|
+
* Lightweight hand-rolled lexer for code-block syntax highlighting inside
|
|
6
|
+
* AI Rich Responses. Returns two parallel representations:
|
|
7
|
+
*
|
|
8
|
+
* - `codeBlock` — array of `{ codeContent, highlightType }` where
|
|
9
|
+
* `highlightType` is the numeric 0-5 enum used by WAProto.
|
|
10
|
+
* - `unifiedBlocks` — array of `{ content, type }` with the string label
|
|
11
|
+
* (`'DEFAULT' | 'KEYWORD' | 'METHOD' | 'STR' | 'NUMBER' | 'COMMENT'`).
|
|
12
|
+
*
|
|
13
|
+
* The legacy `AIRich.tokenizer` rebuilt its keyword Set on every call and
|
|
14
|
+
* inlined all 660 lines of language maps. Lumina splits the catalog into
|
|
15
|
+
* `code-tokenizer-keywords.js` (built once) and keeps this file focused on
|
|
16
|
+
* the lexer itself (~120 lines).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { HighlightType, HighlightLabel } from '../proto/enums.js'
|
|
20
|
+
import { KEYWORDS, SLASH_COMMENT_LANGS, HASH_COMMENT_LANGS, BLOCK_COMMENT_LANGS } from './code-tokenizer-keywords.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} TokenizedCode
|
|
24
|
+
* @property {Array<{ codeContent: string, highlightType: number }>} codeBlock
|
|
25
|
+
* @property {Array<{ content: string, type: string }>} unifiedBlocks
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Identifier character class per language (CSS & HTML allow `-` / `:`). */
|
|
29
|
+
function identifierChar(lang) {
|
|
30
|
+
switch (lang) {
|
|
31
|
+
case 'css':
|
|
32
|
+
return /[a-zA-Z0-9_$-]/
|
|
33
|
+
case 'html':
|
|
34
|
+
return /[a-zA-Z0-9_$:-]/
|
|
35
|
+
default:
|
|
36
|
+
return /[a-zA-Z0-9_$]/
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tokenize a code string for syntax highlighting.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} code
|
|
44
|
+
* @param {string} [lang='javascript'] Lower-case language id. Unknown ids degrade gracefully to plain text.
|
|
45
|
+
* @returns {TokenizedCode}
|
|
46
|
+
*/
|
|
47
|
+
export function tokenizeCode(code, lang = 'javascript') {
|
|
48
|
+
if (typeof code !== 'string' || code.length === 0) {
|
|
49
|
+
return { codeBlock: [], unifiedBlocks: [] }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Plain-text fast-path.
|
|
53
|
+
if (!lang || lang === 'txt' || lang === 'text' || lang === 'plaintext') {
|
|
54
|
+
return {
|
|
55
|
+
codeBlock: [{ codeContent: code, highlightType: HighlightType.DEFAULT }],
|
|
56
|
+
unifiedBlocks: [{ content: code, type: HighlightLabel[HighlightType.DEFAULT] }],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lower = lang.toLowerCase()
|
|
61
|
+
const keywords = KEYWORDS[lower] ?? new Set()
|
|
62
|
+
const isIdent = identifierChar(lower)
|
|
63
|
+
const supportsSlashComments = SLASH_COMMENT_LANGS.has(lower)
|
|
64
|
+
const supportsHashComments = HASH_COMMENT_LANGS.has(lower)
|
|
65
|
+
const supportsBlockComments = BLOCK_COMMENT_LANGS.has(lower)
|
|
66
|
+
|
|
67
|
+
/** @type {Array<{ codeContent: string, highlightType: number }>} */
|
|
68
|
+
const tokens = []
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Push a token, merging with the previous one if both share the same type.
|
|
72
|
+
*/
|
|
73
|
+
const push = (content, type) => {
|
|
74
|
+
if (!content) return
|
|
75
|
+
const last = tokens[tokens.length - 1]
|
|
76
|
+
if (last && last.highlightType === type) last.codeContent += content
|
|
77
|
+
else tokens.push({ codeContent: content, highlightType: type })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let i = 0
|
|
81
|
+
while (i < code.length) {
|
|
82
|
+
const c = code[i]
|
|
83
|
+
|
|
84
|
+
// Whitespace.
|
|
85
|
+
if (/\s/.test(c)) {
|
|
86
|
+
const start = i
|
|
87
|
+
while (i < code.length && /\s/.test(code[i])) i++
|
|
88
|
+
push(code.slice(start, i), HighlightType.DEFAULT)
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Block comment: /* ... */
|
|
93
|
+
if (supportsBlockComments && c === '/' && code[i + 1] === '*') {
|
|
94
|
+
const start = i
|
|
95
|
+
i += 2
|
|
96
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) i++
|
|
97
|
+
i = Math.min(i + 2, code.length)
|
|
98
|
+
push(code.slice(start, i), HighlightType.COMMENT)
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Slash line comment: //
|
|
103
|
+
if (supportsSlashComments && c === '/' && code[i + 1] === '/') {
|
|
104
|
+
const start = i
|
|
105
|
+
while (i < code.length && code[i] !== '\n') i++
|
|
106
|
+
push(code.slice(start, i), HighlightType.COMMENT)
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Hash line comment: # (python, bash, php, rust)
|
|
111
|
+
if (supportsHashComments && c === '#') {
|
|
112
|
+
const start = i
|
|
113
|
+
while (i < code.length && code[i] !== '\n') i++
|
|
114
|
+
push(code.slice(start, i), HighlightType.COMMENT)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// String literals: ', ", `
|
|
119
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
120
|
+
const start = i
|
|
121
|
+
const quote = c
|
|
122
|
+
i++
|
|
123
|
+
while (i < code.length) {
|
|
124
|
+
if (code[i] === '\\' && i + 1 < code.length) i += 2
|
|
125
|
+
else if (code[i] === quote) {
|
|
126
|
+
i++
|
|
127
|
+
break
|
|
128
|
+
} else i++
|
|
129
|
+
}
|
|
130
|
+
push(code.slice(start, i), HighlightType.STRING)
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Numbers.
|
|
135
|
+
if (/[0-9]/.test(c)) {
|
|
136
|
+
const start = i
|
|
137
|
+
while (i < code.length && /[0-9._a-fxA-fX]/.test(code[i])) i++
|
|
138
|
+
push(code.slice(start, i), HighlightType.NUMBER)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Identifiers & keywords.
|
|
143
|
+
if (/[a-zA-Z_$]/.test(c)) {
|
|
144
|
+
const start = i
|
|
145
|
+
while (i < code.length && isIdent.test(code[i])) i++
|
|
146
|
+
const word = code.slice(start, i)
|
|
147
|
+
|
|
148
|
+
let type = HighlightType.DEFAULT
|
|
149
|
+
|
|
150
|
+
if (keywords.has(word)) {
|
|
151
|
+
type = HighlightType.KEYWORD
|
|
152
|
+
} else if (lower === 'css') {
|
|
153
|
+
// Property name? next non-space char is ':'.
|
|
154
|
+
let j = i
|
|
155
|
+
while (j < code.length && /\s/.test(code[j])) j++
|
|
156
|
+
if (code[j] === ':') type = HighlightType.KEYWORD
|
|
157
|
+
} else if (lower === 'html') {
|
|
158
|
+
// Tag name? previous non-space char is '<' or '</'.
|
|
159
|
+
let p = start - 1
|
|
160
|
+
while (p >= 0 && /\s/.test(code[p])) p--
|
|
161
|
+
if (code[p] === '<' || (code[p] === '/' && code[p - 1] === '<')) {
|
|
162
|
+
type = HighlightType.KEYWORD
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Function call? Next non-space char is '('.
|
|
167
|
+
if (type === HighlightType.DEFAULT) {
|
|
168
|
+
let j = i
|
|
169
|
+
while (j < code.length && /\s/.test(code[j])) j++
|
|
170
|
+
if (code[j] === '(') type = HighlightType.METHOD
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
push(word, type)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Everything else: punctuation.
|
|
178
|
+
push(c, HighlightType.DEFAULT)
|
|
179
|
+
i++
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
codeBlock: tokens,
|
|
184
|
+
unifiedBlocks: tokens.map((t) => ({
|
|
185
|
+
content: t.codeContent,
|
|
186
|
+
type: HighlightLabel[t.highlightType],
|
|
187
|
+
})),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export default tokenizeCode
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../index.d.ts'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file parsers/index.js
|
|
3
|
+
* @module lumina/parsers
|
|
4
|
+
*
|
|
5
|
+
* Barrel re-export for the parsers layer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { extractInlineEntities } from './inline-entity.js'
|
|
9
|
+
export { tokenizeCode } from './code-tokenizer.js'
|
|
10
|
+
export { toTableMetadata } from './table-metadata.js'
|
|
11
|
+
export { KEYWORDS } from './code-tokenizer-keywords.js'
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file inline-entity.js
|
|
3
|
+
* @module lumina/parsers/inline-entity
|
|
4
|
+
*
|
|
5
|
+
* extractInlineEntities — parses three markdown-like inline syntaxes inside
|
|
6
|
+
* AI-rich text content and returns the rewritten text plus proto-ready
|
|
7
|
+
* inline-entity metadata.
|
|
8
|
+
*
|
|
9
|
+
* Supported syntaxes:
|
|
10
|
+
*
|
|
11
|
+
* [text](url) → hyperlink (untrusted if `url` starts with `!`)
|
|
12
|
+
* [](url) → citation (renders as a numeric superscript)
|
|
13
|
+
* [text]<url|w|h|fh|p> → latex (math image with dimensions)
|
|
14
|
+
*
|
|
15
|
+
* The rewritten text replaces each `[...]` span with `{{KEY}}...{{/KEY}}`
|
|
16
|
+
* placeholders that WhatsApp's rich-response renderer substitutes back with
|
|
17
|
+
* the entity's interactive UI element.
|
|
18
|
+
*
|
|
19
|
+
* This is the modernised, de-obfuscated successor to the legacy `extractIE`
|
|
20
|
+
* function in `_build-m.js`. Key changes:
|
|
21
|
+
*
|
|
22
|
+
* 1. The `NIXEL_` prefix (obfuscated as `\u004E\u0049\u0058\u0045\u004C_`)
|
|
23
|
+
* is replaced by the configurable `LUMINA_` prefix.
|
|
24
|
+
* 2. The off-by-one bug on citation keys is fixed: every counter is now
|
|
25
|
+
* zero-based and keys match `reference_id` consistently.
|
|
26
|
+
* 3. Magic numbers (font_height, padding, default width/height) are pulled
|
|
27
|
+
* into named constants.
|
|
28
|
+
* 4. The `createIE` switch-by-if is replaced by a typed lookup table.
|
|
29
|
+
* 5. Pure function, no side-effects — easily unit-testable.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { entityKey } from '../utils/id.js'
|
|
33
|
+
|
|
34
|
+
/** @typedef {'hyperlink'|'citation'|'latex'} EntityType */
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default LaTeX rendering parameters. Sourced from the legacy implementation.
|
|
38
|
+
* Adjust if WhatsApp updates its rendering defaults.
|
|
39
|
+
*/
|
|
40
|
+
const LATEX_DEFAULTS = Object.freeze({
|
|
41
|
+
width: 100,
|
|
42
|
+
height: 100,
|
|
43
|
+
fontHeight: 83.333333333333,
|
|
44
|
+
padding: 15,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Per-type metadata shaper. Replaces the legacy `createIE` switch.
|
|
49
|
+
*
|
|
50
|
+
* @type {Record<EntityType, (item: any) => object>}
|
|
51
|
+
*/
|
|
52
|
+
const SHAPERS = {
|
|
53
|
+
hyperlink: (item) => ({
|
|
54
|
+
key: item.key,
|
|
55
|
+
metadata: {
|
|
56
|
+
display_name: item.text,
|
|
57
|
+
is_trusted: item.isTrusted,
|
|
58
|
+
url: item.url,
|
|
59
|
+
__typename: 'GenAIInlineLinkItem',
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
citation: (item) => ({
|
|
63
|
+
key: item.key,
|
|
64
|
+
metadata: {
|
|
65
|
+
reference_id: item.referenceId,
|
|
66
|
+
reference_url: item.url,
|
|
67
|
+
reference_title: item.url,
|
|
68
|
+
reference_display_name: item.url,
|
|
69
|
+
sources: [],
|
|
70
|
+
__typename: 'GenAISearchCitationItem',
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
latex: (item) => ({
|
|
74
|
+
key: item.key,
|
|
75
|
+
metadata: {
|
|
76
|
+
latex_expression: item.text,
|
|
77
|
+
latex_image: {
|
|
78
|
+
url: item.url,
|
|
79
|
+
width: Number(item.width) || LATEX_DEFAULTS.width,
|
|
80
|
+
height: Number(item.height) || LATEX_DEFAULTS.height,
|
|
81
|
+
},
|
|
82
|
+
font_height: Number(item.fontHeight) || LATEX_DEFAULTS.fontHeight,
|
|
83
|
+
padding: Number(item.padding) || LATEX_DEFAULTS.padding,
|
|
84
|
+
__typename: 'GenAILatexItem',
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} ExtractOptions
|
|
91
|
+
* @property {boolean} [extract=true] Master switch — when false, returns the text unchanged.
|
|
92
|
+
* @property {boolean} [hyperlink=true] Parse `[text](url)` syntax.
|
|
93
|
+
* @property {boolean} [citation=true] Parse `[](url)` syntax.
|
|
94
|
+
* @property {boolean} [latex=true] Parse `[text]<url|w|h|fh|p>` syntax.
|
|
95
|
+
* @property {string} [prefix='LUMINA'] Override the entity-key prefix.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {object} ExtractResult
|
|
100
|
+
* @property {string} text Rewritten text with `{{KEY}}…{{/KEY}}` placeholders.
|
|
101
|
+
* @property {Array<{ type: EntityType, key: string, [k: string]: any }>} entities
|
|
102
|
+
* Raw per-entity descriptors (one per matched span).
|
|
103
|
+
* @property {Array<{ key: string, metadata: object }>} metadata
|
|
104
|
+
* Proto-ready inline-entity metadata, ready to attach to a primitive.
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse inline entities from text.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} text
|
|
111
|
+
* @param {ExtractOptions} [opts]
|
|
112
|
+
* @returns {ExtractResult}
|
|
113
|
+
*/
|
|
114
|
+
export function extractInlineEntities(text, opts = {}) {
|
|
115
|
+
const {
|
|
116
|
+
extract = true,
|
|
117
|
+
hyperlink = true,
|
|
118
|
+
citation = true,
|
|
119
|
+
latex = true,
|
|
120
|
+
prefix = 'LUMINA',
|
|
121
|
+
} = opts
|
|
122
|
+
|
|
123
|
+
if (!extract || typeof text !== 'string' || text.length === 0) {
|
|
124
|
+
return { text: text ?? '', entities: [], metadata: [] }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** @type {Array<{ type: EntityType, key: string, [k: string]: any }>} */
|
|
128
|
+
const entities = []
|
|
129
|
+
/** @type {Array<{ key: string, metadata: object }>} */
|
|
130
|
+
const metadata = []
|
|
131
|
+
let rewritten = ''
|
|
132
|
+
let last = 0
|
|
133
|
+
let citationCount = 0
|
|
134
|
+
let hyperlinkCount = 0
|
|
135
|
+
let latexCount = 0
|
|
136
|
+
|
|
137
|
+
/** Bracket stack — supports nested brackets inside the link text. */
|
|
138
|
+
const stack = []
|
|
139
|
+
|
|
140
|
+
for (let i = 0; i < text.length; i++) {
|
|
141
|
+
const ch = text[i]
|
|
142
|
+
|
|
143
|
+
// Push `[` (escaped `\[` ignored).
|
|
144
|
+
if (ch === '[' && text[i - 1] !== '\\') {
|
|
145
|
+
stack.push(i)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// `]` only triggers when followed by `(` (link) or `<` (latex).
|
|
150
|
+
if (ch === ']' && (text[i + 1] === '(' || text[i + 1] === '<')) {
|
|
151
|
+
const start = stack.pop()
|
|
152
|
+
if (start == null) continue
|
|
153
|
+
|
|
154
|
+
const open = text[i + 1]
|
|
155
|
+
const close = open === '(' ? ')' : '>'
|
|
156
|
+
const type = /** @type {EntityType} */ (open === '(' ? 'hyperlink-or-citation' : 'latex')
|
|
157
|
+
|
|
158
|
+
// Walk to the matching close, respecting backslash escapes & nesting.
|
|
159
|
+
let end = i + 2
|
|
160
|
+
let depth = 1
|
|
161
|
+
while (end < text.length && depth) {
|
|
162
|
+
if (text[end] === open && text[end - 1] !== '\\') depth++
|
|
163
|
+
else if (text[end] === close && text[end - 1] !== '\\') depth--
|
|
164
|
+
end++
|
|
165
|
+
}
|
|
166
|
+
if (depth) continue // unbalanced — skip.
|
|
167
|
+
|
|
168
|
+
const raw = text.slice(start + 1, i).trim()
|
|
169
|
+
const url = text.slice(i + 2, end - 1).trim()
|
|
170
|
+
|
|
171
|
+
/** @type {{ type: EntityType, key: string, [k: string]: any } | null} */
|
|
172
|
+
let entry = null
|
|
173
|
+
|
|
174
|
+
if (type === 'latex') {
|
|
175
|
+
if (!latex) continue
|
|
176
|
+
const [txt = '', width = null, height = null, fontHeight = null, padding = null] =
|
|
177
|
+
raw.split('|')
|
|
178
|
+
const key = entityKey('LATEX', latexCount++)
|
|
179
|
+
entry = {
|
|
180
|
+
type: 'latex',
|
|
181
|
+
key,
|
|
182
|
+
text: txt,
|
|
183
|
+
url,
|
|
184
|
+
width,
|
|
185
|
+
height,
|
|
186
|
+
fontHeight,
|
|
187
|
+
padding,
|
|
188
|
+
}
|
|
189
|
+
rewritten += text.slice(last, start) + `{{${key}}}${txt || 'image'}{{/${key}}}`
|
|
190
|
+
} else if (raw) {
|
|
191
|
+
if (!hyperlink) continue
|
|
192
|
+
// Untrusted link → URL prefixed with `!`.
|
|
193
|
+
const trusted = !url.startsWith('!')
|
|
194
|
+
const cleanUrl = trusted ? url : url.slice(1)
|
|
195
|
+
const key = entityKey('HYPERLINK', hyperlinkCount++)
|
|
196
|
+
entry = {
|
|
197
|
+
type: 'hyperlink',
|
|
198
|
+
key,
|
|
199
|
+
text: raw,
|
|
200
|
+
url: cleanUrl,
|
|
201
|
+
isTrusted: trusted,
|
|
202
|
+
}
|
|
203
|
+
rewritten += text.slice(last, start) + `{{${key}}}${cleanUrl}{{/${key}}}`
|
|
204
|
+
} else {
|
|
205
|
+
if (!citation) continue
|
|
206
|
+
const key = entityKey('CITATION', citationCount)
|
|
207
|
+
citationCount++
|
|
208
|
+
entry = {
|
|
209
|
+
type: 'citation',
|
|
210
|
+
key,
|
|
211
|
+
referenceId: citationCount, // 1-based, matches key index (zero-based +1)
|
|
212
|
+
text: '',
|
|
213
|
+
url,
|
|
214
|
+
}
|
|
215
|
+
rewritten += text.slice(last, start) + `{{${key}}}${url}{{/${key}}}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
last = end
|
|
219
|
+
i = end - 1
|
|
220
|
+
|
|
221
|
+
entities.push(entry)
|
|
222
|
+
const shaped = SHAPERS[entry.type](entry)
|
|
223
|
+
if (shaped) metadata.push(shaped)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
rewritten += text.slice(last)
|
|
228
|
+
return { text: rewritten, entities, metadata }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default extractInlineEntities
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file table-metadata.js
|
|
3
|
+
* @module lumina/parsers/table-metadata
|
|
4
|
+
*
|
|
5
|
+
* Convert a 2-D array of strings into the metadata shape WhatsApp expects
|
|
6
|
+
* inside a `richResponseMessage.submessages[].tableMetadata`.
|
|
7
|
+
*
|
|
8
|
+
* The legacy `AIRich.toTableMetadata` always returned `title: ''` with no
|
|
9
|
+
* way to set it. Lumina accepts a `title` option.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { extractInlineEntities } from './inline-entity.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} TableMetadata
|
|
16
|
+
* @property {string} title
|
|
17
|
+
* @property {Array<{ items: string[], isHeading?: boolean }>} rows Submessage metadata.
|
|
18
|
+
* @property {Array<{ is_header: boolean, cells: string[], markdown_cells?: Array<{ text: string, inline_entities?: object[] }> }>} unifiedRows Section primitive rows.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string[][]} table 2-D array of strings. First row is the header.
|
|
23
|
+
* @param {object} [opts]
|
|
24
|
+
* @param {string} [opts.title='']
|
|
25
|
+
* @param {boolean} [opts.hyperlink=true]
|
|
26
|
+
* @param {boolean} [opts.citation=true]
|
|
27
|
+
* @param {boolean} [opts.latex=true]
|
|
28
|
+
* @returns {TableMetadata}
|
|
29
|
+
*/
|
|
30
|
+
export function toTableMetadata(table, opts = {}) {
|
|
31
|
+
if (!Array.isArray(table) || !table.every((row) => Array.isArray(row) && row.every((c) => typeof c === 'string'))) {
|
|
32
|
+
throw new TypeError('Table must be a 2-D array of strings')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { title = '', hyperlink = true, citation = true, latex = true } = opts
|
|
36
|
+
|
|
37
|
+
const [header, ...rows] = table
|
|
38
|
+
const maxLen = Math.max(header.length, ...rows.map((r) => r.length))
|
|
39
|
+
|
|
40
|
+
/** Pad a row to the table's max column count. */
|
|
41
|
+
const normalize = (r) => [...r, ...Array(Math.max(0, maxLen - r.length)).fill('')]
|
|
42
|
+
|
|
43
|
+
const unifiedRows = [
|
|
44
|
+
{ is_header: true, cells: normalize(header) },
|
|
45
|
+
...rows.map((r) => ({ is_header: false, cells: normalize(r) })),
|
|
46
|
+
].map((row) => {
|
|
47
|
+
const markdownCells = row.cells.map((cell) => {
|
|
48
|
+
const extracted = extractInlineEntities(cell, { hyperlink, citation, latex })
|
|
49
|
+
const out = { text: extracted.text }
|
|
50
|
+
if (extracted.metadata.length) out.inline_entities = extracted.metadata
|
|
51
|
+
return out
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const hasInline = markdownCells.some((c) => c.inline_entities?.length)
|
|
55
|
+
return {
|
|
56
|
+
...row,
|
|
57
|
+
...(hasInline ? { markdown_cells: markdownCells } : {}),
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const rowsMeta = unifiedRows.map((r) => ({
|
|
62
|
+
items: r.cells,
|
|
63
|
+
...(r.is_header ? { isHeading: true } : {}),
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
return { title, rows: rowsMeta, unifiedRows }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default toTableMetadata
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file enums.js
|
|
3
|
+
* @module lumina/proto/enums
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for every magic number / magic string the WhatsApp
|
|
6
|
+
* protocol expects inside interactive messages, rich responses, and relay
|
|
7
|
+
* nodes. Update the wire format here once, and the whole framework follows.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the 20+ scattered magic values documented in Tahap-1 analysis:
|
|
10
|
+
* - messageType: 1, 2, 4, 5, 9
|
|
11
|
+
* - forwardOrigin: 4
|
|
12
|
+
* - headerType: 0,1,3,4,5,6
|
|
13
|
+
* - native_flow v: '9'
|
|
14
|
+
* - botJid: '0@bot'
|
|
15
|
+
* - 12x __typename string literals
|
|
16
|
+
* - highlightType: 0-5
|
|
17
|
+
* - imagine_type: 'IMAGE' | 'ANIMATE'
|
|
18
|
+
* - etc.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rich-response submessage type. Stored in `richResponseMessage.submessages[].messageType`.
|
|
23
|
+
*/
|
|
24
|
+
export const MessageType = Object.freeze({
|
|
25
|
+
/** Grid-image rich response. */
|
|
26
|
+
RICH_RESPONSE: 1,
|
|
27
|
+
/** Plain text submessage (also used as fallback for video/product/post). */
|
|
28
|
+
TEXT: 2,
|
|
29
|
+
/** Table metadata submessage. */
|
|
30
|
+
TABLE: 4,
|
|
31
|
+
/** Code-block metadata submessage. */
|
|
32
|
+
CODE: 5,
|
|
33
|
+
/** Reels / contentItems metadata submessage. */
|
|
34
|
+
REELS: 9,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `contextInfo.forwardOrigin` enum used by WhatsApp for AI bot messages.
|
|
39
|
+
*/
|
|
40
|
+
export const ForwardOrigin = Object.freeze({
|
|
41
|
+
BOT: 4,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* ButtonsMessage headerType enum. The legacy ButtonV2 fallback used 6
|
|
46
|
+
* (LOCATION_THUMBNAIL) without documenting it — now explicit.
|
|
47
|
+
*/
|
|
48
|
+
export const HeaderType = Object.freeze({
|
|
49
|
+
EMPTY: 0,
|
|
50
|
+
TEXT: 1,
|
|
51
|
+
IMAGE: 3,
|
|
52
|
+
VIDEO: 4,
|
|
53
|
+
DOCUMENT: 5,
|
|
54
|
+
/** Pre-existing hack: location message with a JPEG thumbnail. */
|
|
55
|
+
LOCATION_THUMBNAIL: 6,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Native-flow biz/interactive node versions.
|
|
60
|
+
*/
|
|
61
|
+
export const NativeFlow = Object.freeze({
|
|
62
|
+
/** Outer `<interactive v="1">` version. */
|
|
63
|
+
OUTER_VERSION: '1',
|
|
64
|
+
/** Inner `<native_flow v="9">` version. */
|
|
65
|
+
FLOW_VERSION: '9',
|
|
66
|
+
/** Default flow name when mixing button types. */
|
|
67
|
+
FLOW_NAME_MIXED: 'mixed',
|
|
68
|
+
TYPE: 'native_flow',
|
|
69
|
+
BIZ_TAG: 'biz',
|
|
70
|
+
INTERACTIVE_TAG: 'interactive',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Hardcoded bot JID that WhatsApp expects inside `forwardedAiBotMessageInfo`.
|
|
75
|
+
*/
|
|
76
|
+
export const BOT_JID = '0@bot'
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Layout view-model kinds. Each becomes `GenAI{Kind}LayoutViewModel`.
|
|
80
|
+
*/
|
|
81
|
+
export const LayoutKind = Object.freeze({
|
|
82
|
+
SINGLE: 'Single',
|
|
83
|
+
HSCROLL: 'HScroll',
|
|
84
|
+
ACTION_ROW: 'ActionRow',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Code-tokenizer highlight type numbers → string labels. Matches the
|
|
89
|
+
* `TYPE_MAP` constant in the legacy AIRich.tokenizer.
|
|
90
|
+
*/
|
|
91
|
+
export const HighlightType = Object.freeze({
|
|
92
|
+
DEFAULT: 0,
|
|
93
|
+
KEYWORD: 1,
|
|
94
|
+
METHOD: 2,
|
|
95
|
+
STRING: 3,
|
|
96
|
+
NUMBER: 4,
|
|
97
|
+
COMMENT: 5,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
/** String labels corresponding to {@link HighlightType}. */
|
|
101
|
+
export const HighlightLabel = Object.freeze({
|
|
102
|
+
0: 'DEFAULT',
|
|
103
|
+
1: 'KEYWORD',
|
|
104
|
+
2: 'METHOD',
|
|
105
|
+
3: 'STR',
|
|
106
|
+
4: 'NUMBER',
|
|
107
|
+
5: 'COMMENT',
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
/** `imagine_type` field on GenAIImaginePrimitive. */
|
|
111
|
+
export const ImagineType = Object.freeze({
|
|
112
|
+
IMAGE: 'IMAGE',
|
|
113
|
+
ANIMATE: 'ANIMATE',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/** `source_type` field on GenAISearchResultPrimitive. */
|
|
117
|
+
export const SourceType = Object.freeze({
|
|
118
|
+
THIRD_PARTY: 'THIRD_PARTY',
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/** `prompt_type` field on GenAIFollowUpSuggestionPillPrimitive. */
|
|
122
|
+
export const PromptType = Object.freeze({
|
|
123
|
+
SUGGESTED_PROMPT: 'SUGGESTED_PROMPT',
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
/** `sessionTransparencyType` field on botMetadata. */
|
|
127
|
+
export const SessionTransparencyType = Object.freeze({
|
|
128
|
+
DEFAULT: 1,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* All GenAI `__typename` strings. Single source of truth — typo-proof.
|
|
133
|
+
*
|
|
134
|
+
* NOTE: legacy code had `GenATableUXPrimitive` (missing the second 'I').
|
|
135
|
+
* Verified against current WhatsApp Web wire format → the correct spelling
|
|
136
|
+
* is `GenAITableUXPrimitive`. Lumina corrects this.
|
|
137
|
+
*/
|
|
138
|
+
export const TYPENAME = Object.freeze({
|
|
139
|
+
MARKDOWN_TEXT: 'GenAIMarkdownTextUXPrimitive',
|
|
140
|
+
CODE: 'GenAICodeUXPrimitive',
|
|
141
|
+
TABLE: 'GenAITableUXPrimitive',
|
|
142
|
+
SEARCH_RESULT: 'GenAISearchResultPrimitive',
|
|
143
|
+
REEL: 'GenAIReelPrimitive',
|
|
144
|
+
IMAGINE: 'GenAIImaginePrimitive',
|
|
145
|
+
PRODUCT_CARD: 'GenAIProductItemCardPrimitive',
|
|
146
|
+
POST: 'GenAIPostPrimitive',
|
|
147
|
+
METADATA_TEXT: 'GenAIMetadataTextPrimitive',
|
|
148
|
+
FOLLOW_UP_PILL: 'GenAIFollowUpSuggestionPillPrimitive',
|
|
149
|
+
UNIFIED_SECTION: 'GenAIUnifiedResponseSection',
|
|
150
|
+
/** Layout view-models are suffixed `GenAI{Kind}LayoutViewModel`. */
|
|
151
|
+
layout(kind) {
|
|
152
|
+
return `GenAI${kind}LayoutViewModel`
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
export default {
|
|
157
|
+
MessageType,
|
|
158
|
+
ForwardOrigin,
|
|
159
|
+
HeaderType,
|
|
160
|
+
NativeFlow,
|
|
161
|
+
BOT_JID,
|
|
162
|
+
LayoutKind,
|
|
163
|
+
HighlightType,
|
|
164
|
+
HighlightLabel,
|
|
165
|
+
ImagineType,
|
|
166
|
+
SourceType,
|
|
167
|
+
PromptType,
|
|
168
|
+
SessionTransparencyType,
|
|
169
|
+
TYPENAME,
|
|
170
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../index.d.ts'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file proto/index.js
|
|
3
|
+
* @module lumina/proto
|
|
4
|
+
*
|
|
5
|
+
* Barrel re-export for the entire proto catalog. Subpath `@kyyinfinite/lumina/proto`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './enums.js'
|
|
9
|
+
export * from './primitives.js'
|
|
10
|
+
export * from './layouts.js'
|
|
11
|
+
export * from './relay-nodes.js'
|
|
12
|
+
export * from './rich-response.js'
|
|
13
|
+
export { ProtoUpdater, transformToESM, applyKnownFixes } from './updater.js'
|