@rikaidev/yomi 0.2.6

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +81 -0
  3. package/PRIVACY.md +39 -0
  4. package/README.md +659 -0
  5. package/dist/auth/credential-store.js +395 -0
  6. package/dist/auth/keychain.js +256 -0
  7. package/dist/cli/login.js +142 -0
  8. package/dist/line/auth/protocol/e2ee-bootstrap.js +87 -0
  9. package/dist/line/auth/protocol/index.js +3 -0
  10. package/dist/line/auth/protocol/login-metadata.js +159 -0
  11. package/dist/line/auth/protocol/pwless-login-flow.js +83 -0
  12. package/dist/line/auth/pwless/index.js +163 -0
  13. package/dist/line/auth/pwless/steps.js +213 -0
  14. package/dist/line/client/auth-service/client.js +240 -0
  15. package/dist/line/client/e2ee-service/client.js +69 -0
  16. package/dist/line/client/e2ee-service/requests.js +92 -0
  17. package/dist/line/client/e2ee-service/shared-key-log.js +24 -0
  18. package/dist/line/client/e2ee-service/shared-key-payload.js +35 -0
  19. package/dist/line/client/index.js +213 -0
  20. package/dist/line/client/obs-media-client.js +140 -0
  21. package/dist/line/client/parsers.js +69 -0
  22. package/dist/line/client/shop-service/client.js +63 -0
  23. package/dist/line/client/sticker-meta.js +46 -0
  24. package/dist/line/client/sync-service/client.js +317 -0
  25. package/dist/line/client/talk-service/chat-query.js +122 -0
  26. package/dist/line/client/talk-service/command-client.js +242 -0
  27. package/dist/line/client/talk-service/contact-query.js +77 -0
  28. package/dist/line/client/talk-service/message-box-query.js +137 -0
  29. package/dist/line/client/talk-service/message-id-query.js +241 -0
  30. package/dist/line/client/talk-service/query-client.js +219 -0
  31. package/dist/line/client/talk-service/requests.js +383 -0
  32. package/dist/line/client/transport.js +138 -0
  33. package/dist/line/core/auth-session-logout.js +55 -0
  34. package/dist/line/core/auth-session-runtime.js +231 -0
  35. package/dist/line/core/auth-session-service.js +85 -0
  36. package/dist/line/core/chat-runtime-service.js +343 -0
  37. package/dist/line/core/client-runtime.js +17 -0
  38. package/dist/line/core/config.js +67 -0
  39. package/dist/line/core/constants.js +56 -0
  40. package/dist/line/core/contact-query-service.js +20 -0
  41. package/dist/line/core/directory-runtime-service.js +83 -0
  42. package/dist/line/core/e2ee/bootstrap/keychain.js +41 -0
  43. package/dist/line/core/e2ee/crypto/crypto-primitives.js +98 -0
  44. package/dist/line/core/e2ee/index.js +6 -0
  45. package/dist/line/core/e2ee/keys/decrypt.js +110 -0
  46. package/dist/line/core/e2ee/keys/encrypt.js +117 -0
  47. package/dist/line/core/e2ee/keys/group-key.js +260 -0
  48. package/dist/line/core/e2ee/keys/key-manager.js +231 -0
  49. package/dist/line/core/e2ee/keys/key-payload.js +130 -0
  50. package/dist/line/core/e2ee/keys/key-types.js +1 -0
  51. package/dist/line/core/e2ee/keys/message-crypto.js +146 -0
  52. package/dist/line/core/e2ee/keys/message-decrypt.js +390 -0
  53. package/dist/line/core/e2ee/keys/peer-key.js +191 -0
  54. package/dist/line/core/e2ee/recovery/bootstrap-keys.js +161 -0
  55. package/dist/line/core/e2ee/types.js +4 -0
  56. package/dist/line/core/group-query-service.js +20 -0
  57. package/dist/line/core/media-decrypt.js +78 -0
  58. package/dist/line/core/media-encrypt.js +97 -0
  59. package/dist/line/core/mention.js +89 -0
  60. package/dist/line/core/message-command-service.js +454 -0
  61. package/dist/line/core/message-query-service.js +161 -0
  62. package/dist/line/core/service.js +134 -0
  63. package/dist/line/core/session-state/index.js +243 -0
  64. package/dist/line/core/session-state/persist.js +192 -0
  65. package/dist/line/core/store.js +8 -0
  66. package/dist/line/core/thrift/binary/binary-reader.js +151 -0
  67. package/dist/line/core/thrift/binary/binary-writer.js +157 -0
  68. package/dist/line/core/thrift/binary/type-codec-dsl.js +42 -0
  69. package/dist/line/core/thrift/compact/compact-reader.js +180 -0
  70. package/dist/line/core/thrift/compact/compact-writer.js +161 -0
  71. package/dist/line/core/thrift/compact/type-codec-dsl.js +66 -0
  72. package/dist/line/core/thrift/fields/builders.js +124 -0
  73. package/dist/line/core/thrift/index.js +12 -0
  74. package/dist/line/core/thrift/message/decoder.js +55 -0
  75. package/dist/line/core/thrift/message/encoder.js +25 -0
  76. package/dist/line/core/thrift/primitives/varint.js +64 -0
  77. package/dist/line/core/thrift/struct/reader.js +172 -0
  78. package/dist/line/core/thrift/struct/writer.js +189 -0
  79. package/dist/line/core/thrift/types.js +23 -0
  80. package/dist/line/core/video-thumbnail.js +104 -0
  81. package/dist/mcp/handlers/contacts.js +166 -0
  82. package/dist/mcp/handlers/conversations.js +193 -0
  83. package/dist/mcp/handlers/groups.js +123 -0
  84. package/dist/mcp/handlers/index.js +15 -0
  85. package/dist/mcp/handlers/login.js +473 -0
  86. package/dist/mcp/handlers/media.js +69 -0
  87. package/dist/mcp/handlers/message-actions.js +62 -0
  88. package/dist/mcp/handlers/scope.js +120 -0
  89. package/dist/mcp/handlers/search.js +196 -0
  90. package/dist/mcp/handlers/send.js +523 -0
  91. package/dist/mcp/handlers/shared.js +84 -0
  92. package/dist/mcp/handlers/stickers.js +87 -0
  93. package/dist/mcp/media.js +228 -0
  94. package/dist/mcp/names.js +133 -0
  95. package/dist/mcp/phi-guard.js +105 -0
  96. package/dist/mcp/policy.js +34 -0
  97. package/dist/mcp/server.js +301 -0
  98. package/dist/mcp/tools.js +761 -0
  99. package/dist/mcp/ui/capability.js +26 -0
  100. package/dist/mcp/ui/login-app-protocol.js +118 -0
  101. package/dist/mcp/ui/login-app-styles.js +142 -0
  102. package/dist/mcp/ui/login-app-view.js +361 -0
  103. package/dist/mcp/ui/login-app.js +72 -0
  104. package/dist/mcp/ui/login-structured-content.js +1 -0
  105. package/dist/mcp/ui/resource.js +35 -0
  106. package/dist/mcp/ui/tools-with-ui.js +40 -0
  107. package/dist/search/bigram.js +86 -0
  108. package/dist/search/capture.js +140 -0
  109. package/dist/search/collector.js +176 -0
  110. package/dist/search/default-embedder.js +101 -0
  111. package/dist/search/embedder.js +13 -0
  112. package/dist/search/scope.js +142 -0
  113. package/dist/search/sqlite.js +141 -0
  114. package/dist/search/store.js +469 -0
  115. package/dist/util/data-dir.js +55 -0
  116. package/dist/util/log.js +86 -0
  117. package/dist/version.js +7 -0
  118. package/package.json +63 -0
  119. package/run.mjs +71 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * LINE Client — TalkService API
3
+ *
4
+ * Core class with transport layer. Domain capabilities are composed
5
+ * from separate modules (DDD bounded contexts):
6
+ * - auth-service/ — AuthService: refresh, logout, bootstrap flows
7
+ * - talk-service/ — TalkService read/write capabilities
8
+ * - sync-service/ — Polling: sync, longPoll, startPolling
9
+ * - e2ee-service/ — E2EE transport capabilities
10
+ * - shop-service/ — ShopService: owned sticker packages
11
+ *
12
+ * Endpoints:
13
+ * /AS4 — AuthService
14
+ * /S4 — TalkService
15
+ * /SYNC4 — SyncService
16
+ * /TSHOP4 — ShopService
17
+ */
18
+ import { EventEmitter } from 'node:events';
19
+ import { createCliLogger } from '../../util/log.js';
20
+ import { LINE_APP_CONFIG } from '../core/config.js';
21
+ import { encodeCallMessage } from '../core/thrift/index.js';
22
+ import { createAuthClient } from './auth-service/client.js';
23
+ import { createE2EEClient } from './e2ee-service/client.js';
24
+ import { parseOperation } from './parsers.js';
25
+ import { createShopClient } from './shop-service/client.js';
26
+ import { createSyncClient } from './sync-service/client.js';
27
+ import { createTalkCommandClient } from './talk-service/command-client.js';
28
+ import { createTalkQueryClient } from './talk-service/query-client.js';
29
+ import { sendRequest } from './transport.js';
30
+ const TALK_EXCEPTION_CODE_NAMES = {
31
+ 5: 'NOT_FOUND',
32
+ };
33
+ const LINE_AUTH_INVALIDATED_PATTERN = /V3_TOKEN_CLIENT_LOGGED_OUT|LOGGED_OUT|DIVESTED/i;
34
+ /**
35
+ * LINE request error aligned with the shape used by linejs InternalError.
36
+ *
37
+ * @param type - Error type/category
38
+ * @param message - Human-readable diagnostic message
39
+ * @param data - Structured exception payload
40
+ */
41
+ export class LineRequestError extends Error {
42
+ data;
43
+ type;
44
+ constructor(type, message, data = {}) {
45
+ super(message);
46
+ this.name = type;
47
+ this.type = type;
48
+ this.data = data;
49
+ }
50
+ }
51
+ /**
52
+ * Detect LINE auth invalidation errors returned after a previously successful login.
53
+ *
54
+ * @param error - Runtime error thrown by LINE TalkService calls.
55
+ * @returns Whether LINE has invalidated the current client token.
56
+ */
57
+ export function isLineAuthInvalidatedError(error) {
58
+ const data = error?.data;
59
+ const exceptionMessage = data?.exception && typeof data.exception === 'object'
60
+ ? String(data.exception['2'] || '')
61
+ : '';
62
+ const message = error instanceof Error ? error.message : String(error || '');
63
+ return LINE_AUTH_INVALIDATED_PATTERN.test(`${message} ${exceptionMessage}`);
64
+ }
65
+ /**
66
+ * Resolve the active LINE client logger, preserving startup indentation.
67
+ *
68
+ * @param client - LINE client instance.
69
+ * @returns Logger for transport and request diagnostics.
70
+ */
71
+ function getLineClientLog(client) {
72
+ if (client?.startupFlowLogger) {
73
+ return client.startupFlowLogger;
74
+ }
75
+ if (client?.logger?.info) {
76
+ return client.logger;
77
+ }
78
+ return client?.logger || createCliLogger('LINE');
79
+ }
80
+ /**
81
+ * Normalize one raw TalkException payload into a linejs-like error shape.
82
+ *
83
+ * @param exception - Decoded thrift exception struct from field 1
84
+ * @returns Normalized code/message/rawCode tuple
85
+ */
86
+ function normalizeTalkException(exception) {
87
+ const rawCode = exception?.[1];
88
+ const numericCode = typeof rawCode === 'number'
89
+ ? rawCode
90
+ : typeof rawCode === 'bigint'
91
+ ? Number(rawCode)
92
+ : null;
93
+ const code = numericCode != null
94
+ ? TALK_EXCEPTION_CODE_NAMES[numericCode] || String(numericCode)
95
+ : typeof rawCode === 'string'
96
+ ? rawCode
97
+ : 'UNKNOWN';
98
+ const message = typeof exception?.[2] === 'string'
99
+ ? exception[2]
100
+ : JSON.stringify(exception);
101
+ return {
102
+ code,
103
+ message,
104
+ rawCode,
105
+ };
106
+ }
107
+ /**
108
+ * LINE protocol client. Communicates with LINE servers via TCompact over HTTPS.
109
+ *
110
+ * @param authToken - The LINE authentication token
111
+ * @param config - Configuration options
112
+ */
113
+ export class LineClient extends EventEmitter {
114
+ authToken;
115
+ host;
116
+ profile;
117
+ revision;
118
+ globalRevision;
119
+ individualRevision;
120
+ polling;
121
+ aborted;
122
+ seq;
123
+ logger;
124
+ startupFlowLogger;
125
+ // The following are mixed in at construction time via Object.assign from
126
+ // the auth/talk/sync/e2ee service factories (see constructor below) — TS
127
+ // cannot see that assignment, so these use definite-assignment assertions.
128
+ openAuthSession;
129
+ issueV3TokenForPrimary;
130
+ restoreE2EEKeyBackup;
131
+ downloadMessageContent;
132
+ downloadMessageContentPreview;
133
+ logoutZ;
134
+ getOwnedStickerPackages;
135
+ updateChatName;
136
+ inviteIntoChat;
137
+ deleteOtherFromChat;
138
+ deleteSelfFromChat;
139
+ createChat;
140
+ react;
141
+ cancelReaction;
142
+ unsendMessage;
143
+ findAndAddContactByMid;
144
+ blockContact;
145
+ unblockContact;
146
+ acceptChatInvitation;
147
+ constructor(authToken, config = {}) {
148
+ super();
149
+ this.authToken = authToken;
150
+ this.host = config.host || LINE_APP_CONFIG.host;
151
+ this.revision = config.revision ?? -1;
152
+ this.globalRevision = config.globalRevision ?? 0;
153
+ this.individualRevision = config.individualRevision ?? 0;
154
+ this.polling = false;
155
+ this.aborted = false;
156
+ this.profile = null;
157
+ this.seq = 1;
158
+ this.logger = config.logger || createCliLogger('LINE');
159
+ this.startupFlowLogger = config.startupFlowLogger || null;
160
+ Object.assign(this, createAuthClient(this), createTalkQueryClient(this), createTalkCommandClient(this), createSyncClient(this), createE2EEClient(this), createShopClient(this));
161
+ }
162
+ /**
163
+ * Send a TCompact Thrift call to an arbitrary LINE service endpoint path.
164
+ * sendTalk is the /S4 (TalkService) special case; other services (e.g. the
165
+ * unified shop at /TSHOP4) share the same envelope, auth header, and
166
+ * token-rotation/exception handling.
167
+ *
168
+ * @param path - Service endpoint path (e.g. /S4, /TSHOP4).
169
+ * @param method - Thrift method name.
170
+ * @param args - Method arguments as Thrift field tuples.
171
+ * @returns The decoded server response.
172
+ */
173
+ async sendCompact(path, method, args) {
174
+ const data = encodeCallMessage(method, this.seq++, args);
175
+ const result = await sendRequest(this.host, path, data, { 'X-Line-Access': this.authToken }, 30000, { logger: getLineClientLog(this) });
176
+ if (result.nextToken) {
177
+ getLineClientLog(this).info('auth.token_rotated', { method });
178
+ this.authToken = result.nextToken;
179
+ this.emit('tokenRotated', result.nextToken);
180
+ }
181
+ if (result.fields?.[1] && typeof result.fields[1] === 'object') {
182
+ const exc = result.fields[1];
183
+ const normalized = normalizeTalkException(exc);
184
+ throw new LineRequestError('RequestError', `Request internal failed, ${method}(${path}) -> ${normalized.message}`, {
185
+ code: normalized.code,
186
+ rawCode: normalized.rawCode,
187
+ method,
188
+ path,
189
+ exception: exc,
190
+ });
191
+ }
192
+ return result;
193
+ }
194
+ /**
195
+ * Send a TalkService (/S4) request to the LINE server.
196
+ *
197
+ * @param method - The TalkService method name
198
+ * @param args - The method arguments as Thrift field tuples
199
+ * @returns The decoded server response
200
+ */
201
+ async sendTalk(method, args) {
202
+ return this.sendCompact(LINE_APP_CONFIG.talkPath, method, args);
203
+ }
204
+ /**
205
+ * Parse a raw Thrift operation struct into a domain object.
206
+ *
207
+ * @param op - Raw operation data
208
+ * @returns Parsed operation or null
209
+ */
210
+ parseOperation(op) {
211
+ return parseOperation(op);
212
+ }
213
+ }
@@ -0,0 +1,140 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { randomUUID } from 'node:crypto';
3
+ import https from 'node:https';
4
+ import { LINE_APP_CONFIG } from '../core/config.js';
5
+ import { TBinaryWriter } from '../core/thrift/binary/binary-writer.js';
6
+ import { THRIFT_TYPE } from '../core/thrift/types.js';
7
+ const OBS_HOST = 'obs.line-apps.com';
8
+ // Uploads do NOT go to the OBS domain — they POST through the LINE gateway
9
+ // (LINE_GW_HOST_DOMAIN). obs.line-apps.com returns 400 for the /oa/ upload
10
+ // route; only the gateway accepts it, and only with an encrypted access token.
11
+ const OBS_UPLOAD_HOST = 'gwz.line.naver.jp';
12
+ /**
13
+ * Build LINE talk metadata required for OBS media downloads.
14
+ *
15
+ * @param messageId - LINE message id.
16
+ * @returns Base64 encoded X-Talk-Meta header.
17
+ */
18
+ function buildTalkMeta(messageId) {
19
+ const writer = new TBinaryWriter();
20
+ writer.writeFieldBegin(4, THRIFT_TYPE.STRING);
21
+ writer.writeString(messageId);
22
+ writer.writeFieldBegin(27, THRIFT_TYPE.LIST);
23
+ writer.writeList(THRIFT_TYPE.STRUCT, []);
24
+ writer.writeFieldStop();
25
+ const message = Buffer.from(writer.getBuffer()).toString('base64');
26
+ return Buffer.from(JSON.stringify({ message })).toString('base64');
27
+ }
28
+ /**
29
+ * Download one LINE OBS media object.
30
+ *
31
+ * @param client - LINE protocol client.
32
+ * @param options - OBS object options.
33
+ * @param options.messageId - LINE message id.
34
+ * @param options.oid - Object id.
35
+ * @param options.sid - Service namespace.
36
+ * @param options.preview - Whether to fetch the preview object.
37
+ * @returns Downloaded encrypted bytes.
38
+ */
39
+ export function downloadLineObsMediaObject(client, options) {
40
+ const oid = options.preview ? `${options.oid}__ud-preview` : options.oid;
41
+ const requestPath = `/r/talk/${options.sid}/${oid}`;
42
+ const headers = {
43
+ 'User-Agent': LINE_APP_CONFIG.userAgent,
44
+ 'X-Line-Access': client.authToken,
45
+ 'X-Line-Application': LINE_APP_CONFIG.lineApp,
46
+ 'X-Talk-Meta': buildTalkMeta(options.messageId),
47
+ accept: 'application/x-thrift',
48
+ 'accept-encoding': 'gzip',
49
+ 'x-lal': 'ja_JP',
50
+ 'x-lhm': 'GET',
51
+ 'x-lpv': '1',
52
+ };
53
+ return new Promise((resolve, reject) => {
54
+ const request = https.request({
55
+ headers,
56
+ hostname: OBS_HOST,
57
+ method: 'GET',
58
+ path: requestPath,
59
+ port: 443,
60
+ timeout: 30000,
61
+ }, (response) => {
62
+ const chunks = [];
63
+ response.on('data', (chunk) => chunks.push(chunk));
64
+ response.on('end', () => {
65
+ const body = Buffer.concat(chunks);
66
+ if (!response.statusCode || response.statusCode >= 400) {
67
+ reject(new Error(`LINE OBS download failed: status=${response.statusCode || 'unknown'} bytes=${body.length}`));
68
+ return;
69
+ }
70
+ resolve(body);
71
+ });
72
+ });
73
+ request.on('timeout', () => {
74
+ request.destroy();
75
+ reject(new Error('LINE OBS download timed out'));
76
+ });
77
+ request.on('error', reject);
78
+ request.end();
79
+ });
80
+ }
81
+ /**
82
+ * Upload one LINE OBS media object (image bytes, or its preview object).
83
+ *
84
+ * Mirrors {@link downloadLineObsMediaObject}'s auth/header scheme, swapped
85
+ * to a POST body upload. Callers are responsible for encrypting `data`
86
+ * (ciphertext‖MAC) before calling this — this function performs no
87
+ * encryption of its own.
88
+ *
89
+ * @param options - OBS object options.
90
+ * @param options.sid - Service namespace (e.g. 'emi' for image).
91
+ * @param options.objectPath - Object path segment (e.g. `reqid-<uuid>`, or `<oid>__ud-preview`).
92
+ * @param options.data - Encrypted bytes to upload as the request body.
93
+ * @param options.mid - Authenticated self MID (sent as X-Line-Mid).
94
+ * @param options.accessToken - Encrypted access token (acquireEncryptedAccessToken, `\x1e`-split index 1); the gateway rejects the plain session JWT.
95
+ * @param options.params - Extra fields merged into the X-Obs-Params header.
96
+ * @returns The server-assigned object id and content hash.
97
+ */
98
+ export async function uploadLineObsMediaObject(options) {
99
+ // Uploads POST to the LINE gateway (OBS_UPLOAD_HOST) at /oa/r/talk/{sid}/{oid}
100
+ // — a different host AND route than downloads (obs.line-apps.com /r/talk/...).
101
+ // The gateway requires the encrypted access token (not the session JWT), an
102
+ // application/octet-stream body, AND HTTP/2: over HTTP/1.1 (node:https) it
103
+ // returns 503, so this uses fetch, which negotiates h2 via ALPN. Verified
104
+ // live against the real gateway (201 Created). The download-only x-lhm/x-lpv
105
+ // are absent.
106
+ const requestUrl = `https://${OBS_UPLOAD_HOST}/oa/r/talk/${options.sid}/${options.objectPath}`;
107
+ // The gateway requires `type` and `name` in X-Obs-Params on EVERY upload,
108
+ // including the preview subresource — a request missing `name` is rejected
109
+ // (400 without name, 503 with neither name nor type). Defaults are supplied
110
+ // here so callers (e.g. the preview upload) need not repeat them; explicit
111
+ // params still override.
112
+ const obsParams = {
113
+ type: 'file',
114
+ ver: '2.0',
115
+ name: randomUUID(),
116
+ ...(options.params ?? {}),
117
+ };
118
+ const headers = {
119
+ 'User-Agent': LINE_APP_CONFIG.userAgent,
120
+ 'X-Line-Access': options.accessToken,
121
+ 'X-Line-Application': LINE_APP_CONFIG.lineApp,
122
+ 'X-Line-Mid': options.mid,
123
+ 'X-Obs-Params': Buffer.from(JSON.stringify(obsParams)).toString('base64'),
124
+ 'Content-Type': 'application/octet-stream',
125
+ 'x-lal': 'ja_JP',
126
+ };
127
+ const response = await fetch(requestUrl, {
128
+ method: 'POST',
129
+ headers,
130
+ body: options.data,
131
+ });
132
+ if (response.status !== 200 && response.status !== 201) {
133
+ throw new Error(`LINE OBS upload failed: status=${response.status}`);
134
+ }
135
+ const oid = response.headers.get('x-obs-oid');
136
+ if (!oid) {
137
+ throw new Error(`LINE OBS upload succeeded but response is missing x-obs-oid header (status=${response.status})`);
138
+ }
139
+ return { oid, hash: response.headers.get('x-obs-hash') };
140
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * LINE Protocol Message & Operation Parsers
3
+ *
4
+ * Transforms raw Thrift structs into domain objects.
5
+ * Pure functions — no side effects, no network calls.
6
+ */
7
+ /**
8
+ * Parse a raw Thrift message struct into a domain object.
9
+ *
10
+ * @param msg - Raw Thrift message struct
11
+ * @returns Parsed message or null if invalid
12
+ */
13
+ export function parseMessage(msg) {
14
+ if (!msg || typeof msg !== 'object') {
15
+ return null;
16
+ }
17
+ return {
18
+ id: msg[4] || null,
19
+ from: msg[1] || null,
20
+ to: msg[2] || null,
21
+ toType: msg[3],
22
+ createdTime: typeof msg[5] === 'bigint' ? Number(msg[5]) : msg[5],
23
+ deliveredTime: typeof msg[6] === 'bigint'
24
+ ? Number(msg[6])
25
+ : typeof msg[7] === 'bigint'
26
+ ? Number(msg[7])
27
+ : (msg[6] ?? msg[7]),
28
+ text: msg[10] || null,
29
+ contentType: msg[15] || 0,
30
+ contentMetadata: msg[18] || {},
31
+ chunks: msg[20] || null,
32
+ };
33
+ }
34
+ /**
35
+ * Parse raw message data into an array of message objects.
36
+ *
37
+ * @param data - Raw message data (array or single struct)
38
+ * @returns Array of parsed message objects
39
+ */
40
+ export function parseMessages(data) {
41
+ if (Array.isArray(data)) {
42
+ return data.map((m) => parseMessage(m)).filter(Boolean);
43
+ }
44
+ if (data && typeof data === 'object') {
45
+ return [parseMessage(data)].filter(Boolean);
46
+ }
47
+ return [];
48
+ }
49
+ /**
50
+ * Parse a raw Thrift operation struct into a domain object.
51
+ *
52
+ * @param op - Raw Thrift operation struct
53
+ * @returns Parsed operation or null if invalid
54
+ */
55
+ export function parseOperation(op) {
56
+ if (!op || typeof op !== 'object') {
57
+ return null;
58
+ }
59
+ return {
60
+ revision: typeof op[1] === 'bigint' ? Number(op[1]) : op[1],
61
+ createdTime: typeof op[2] === 'bigint' ? Number(op[2]) : op[2],
62
+ type: op[3],
63
+ reqSeq: op[4],
64
+ param1: op[10] || null,
65
+ param2: op[11] || null,
66
+ param3: op[12] || null,
67
+ message: op[20] && typeof op[20] === 'object' ? parseMessage(op[20]) : null,
68
+ };
69
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * LINE ShopService capability — the unified shop at /TSHOP4.
3
+ *
4
+ * Only the sticker-ownership read is implemented: getOwnedProductSummaries with
5
+ * shopId "stickershop" returns the sticker PACKAGES the authenticated account
6
+ * owns (and can therefore send — LINE rejects sending a non-owned sticker with
7
+ * "sticker is not owned by the user"). Response summary fields used: 1 =
8
+ * packageId, 11 = title, 21 = version. Field 3 of the return is the total count,
9
+ * used to paginate.
10
+ */
11
+ import { i32Field, stringField, structField } from '../../core/thrift/index.js';
12
+ /** Sticker shop id for the unified shop's getOwnedProductSummaries. */
13
+ const STICKER_SHOP_ID = 'stickershop';
14
+ /** Unified shop endpoint. */
15
+ const SHOP_PATH = '/TSHOP4';
16
+ /**
17
+ * Create the ShopService capability bound to one LINE client runtime.
18
+ *
19
+ * @param runtime - LINE client runtime exposing sendCompact.
20
+ * @returns Shop query methods bound to the runtime.
21
+ */
22
+ export function createShopClient(runtime) {
23
+ return {
24
+ /**
25
+ * List the sticker packages the authenticated account owns, paginating
26
+ * until the server's reported total is reached.
27
+ *
28
+ * @param language - BCP-47-ish language for titles (default 'en').
29
+ * @param country - Country code for the shop locale (default 'TW').
30
+ * @returns Owned sticker packages.
31
+ */
32
+ async getOwnedStickerPackages(language = 'en', country = 'TW') {
33
+ const packages = [];
34
+ const limit = 200;
35
+ let total = Number.POSITIVE_INFINITY;
36
+ while (packages.length < total) {
37
+ const res = await runtime.sendCompact(SHOP_PATH, 'getOwnedProductSummaries', [
38
+ stringField(2, STICKER_SHOP_ID),
39
+ i32Field(3, packages.length),
40
+ i32Field(4, limit),
41
+ structField(5, [stringField(1, language), stringField(2, country)]),
42
+ ]);
43
+ const ret = res?.fields?.[0];
44
+ const summaries = Array.isArray(ret?.[1]) ? ret[1] : [];
45
+ total = Number(ret?.[3] ?? summaries.length);
46
+ for (const s of summaries) {
47
+ if (s?.[1] == null) {
48
+ continue;
49
+ }
50
+ packages.push({
51
+ packageId: String(s[1]),
52
+ title: String(s[11] ?? ''),
53
+ version: String(s[21] ?? '1'),
54
+ });
55
+ }
56
+ if (summaries.length === 0 || summaries.length < limit) {
57
+ break;
58
+ }
59
+ }
60
+ return packages;
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Sticker package metadata from LINE's PUBLIC sticker CDN.
3
+ *
4
+ * getOwnedStickerPackages (ShopService) returns package-level info; to actually
5
+ * send a sticker LINE needs an individual sticker id (STKID). Those live in the
6
+ * package's public productInfo.meta on the sticker CDN — no auth, no account
7
+ * data sent (only the public packageId), so this is safe to fetch for any owned
8
+ * package the caller wants to expand into sendable sticker ids.
9
+ */
10
+ const CDN = 'https://stickershop.line-scdn.net/stickershop/v1/product';
11
+ /**
12
+ * Fetch one sticker package's public metadata (title + individual sticker ids).
13
+ *
14
+ * @param packageId - LINE sticker package id (STKPKGID).
15
+ * @returns Package metadata, or null when the CDN has no such package.
16
+ */
17
+ export async function fetchStickerPackageMeta(packageId) {
18
+ const res = await fetch(`${CDN}/${packageId}/android/productInfo.meta`);
19
+ if (!res.ok) {
20
+ return null;
21
+ }
22
+ const meta = await res.json();
23
+ const stickers = Array.isArray(meta?.stickers) ? meta.stickers : [];
24
+ return {
25
+ packageId: String(meta?.packageId ?? packageId),
26
+ title: meta?.title && typeof meta.title === 'object'
27
+ ? meta.title
28
+ : {},
29
+ stickerIds: stickers
30
+ .map((s) => (s?.id != null ? String(s.id) : null))
31
+ .filter((id) => id !== null),
32
+ };
33
+ }
34
+ /**
35
+ * Fetch one sticker's PNG preview image from the public sticker CDN.
36
+ *
37
+ * @param stickerId - LINE sticker id (STKID).
38
+ * @returns PNG bytes, or null when the CDN has no such sticker.
39
+ */
40
+ export async function fetchStickerImage(stickerId) {
41
+ const res = await fetch(`${CDN.replace('/product', '/sticker')}/${stickerId}/android/sticker.png`);
42
+ if (!res.ok) {
43
+ return null;
44
+ }
45
+ return Buffer.from(await res.arrayBuffer());
46
+ }