mixi2-js 1.2.1 → 1.3.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.
@@ -1,217 +1,315 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_types = require("../types-D9t_NdQ0.cjs");
3
+ //#region src/helpers/address.ts
4
+ /**
5
+ * アクセストークン取得用のエンドポイント URL です。
6
+ */
7
+ const tokenUrl = "https://application-auth.mixi.social/oauth2/token";
8
+ /**
9
+ * API サーバーアドレスです。
10
+ */
11
+ const apiAddress = "application-api.mixi.social";
12
+ /**
13
+ * gRPC ストリーミング接続用のサーバーアドレスです。
14
+ */
15
+ const streamAddress = "application-stream.mixi.social";
16
+ //#endregion
17
+ //#region src/helpers/event-deduplicator.ts
18
+ /**
19
+ * 重複したイベントを検出してスキップするミドルウェア。
20
+ * Webhook 方式のリトライなどで同じイベントが複数回届いた場合に、
21
+ * 内部ハンドラへの二重処理を防ぐ。
22
+ *
23
+ * @example
24
+ * const dedup = new EventDeduplicator(innerHandler);
25
+ * const server = new WebhookServer({ handler: dedup, ... });
26
+ */
27
+ var EventDeduplicator = class {
28
+ inner;
29
+ maxSize;
30
+ maxAge;
31
+ seen = /* @__PURE__ */ new Map();
32
+ constructor(handler, options) {
33
+ this.inner = handler;
34
+ this.maxSize = options?.maxSize ?? 1e3;
35
+ this.maxAge = options?.maxAge ?? 3e5;
36
+ }
37
+ async handle(event) {
38
+ const now = Date.now();
39
+ this.evict(now);
40
+ if (this.seen.has(event.eventId)) return;
41
+ this.seen.set(event.eventId, now);
42
+ if (this.seen.size > this.maxSize) {
43
+ const oldest = this.seen.keys().next().value;
44
+ if (oldest !== void 0) this.seen.delete(oldest);
45
+ }
46
+ await this.inner.handle(event);
47
+ }
48
+ evict(now) {
49
+ for (const [id, ts] of this.seen) if (now - ts > this.maxAge) this.seen.delete(id);
50
+ else break;
51
+ }
9
52
  };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
53
+ //#endregion
54
+ //#region src/helpers/event-logger.ts
55
+ /**
56
+ * 受信したイベントをログ出力するデバッグ用ミドルウェア。
57
+ * 内部ハンドラへの処理はそのまま委譲する。
58
+ *
59
+ * @example
60
+ * const logger = new EventLogger(router);
61
+ * await watcher.watch(logger);
62
+ */
63
+ var EventLogger = class {
64
+ inner;
65
+ logger;
66
+ verbose;
67
+ constructor(handler, options) {
68
+ this.inner = handler;
69
+ this.logger = options?.logger ?? console.log;
70
+ this.verbose = options?.verbose ?? true;
71
+ }
72
+ async handle(event) {
73
+ if (this.verbose) this.logger(`[mixi2] event received: type=${event.eventType} id=${event.eventId}`);
74
+ else this.logger(`[mixi2] event received: type=${event.eventType}`);
75
+ await this.inner.handle(event);
76
+ }
17
77
  };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/helpers/index.ts
21
- var helpers_exports = {};
22
- __export(helpers_exports, {
23
- EventRouter: () => EventRouter,
24
- MediaUploader: () => MediaUploader,
25
- PostBuilder: () => PostBuilder,
26
- ReasonFilter: () => ReasonFilter,
27
- apiAddress: () => apiAddress,
28
- streamAddress: () => streamAddress,
29
- tokenUrl: () => tokenUrl
30
- });
31
- module.exports = __toCommonJS(helpers_exports);
32
-
33
- // src/helpers/address.ts
34
- var tokenUrl = "https://application-auth.mixi.social/oauth2/token";
35
- var apiAddress = "application-api.mixi.social";
36
- var streamAddress = "application-stream.mixi.social";
37
-
38
- // src/helpers/event-router.ts
78
+ //#endregion
79
+ //#region src/helpers/event-router.ts
80
+ /**
81
+ * イベントタイプ別にハンドラを登録できる EventHandler 実装。
82
+ * StreamWatcher.watch() や WebhookServer に直接渡して使用できる。
83
+ */
39
84
  var EventRouter = class {
40
- listeners = /* @__PURE__ */ new Map();
41
- /**
42
- * 指定したイベントタイプのハンドラを登録する。
43
- * 同じイベントタイプに複数のハンドラを登録可能(登録順に実行)。
44
- */
45
- on(eventType, listener) {
46
- const existing = this.listeners.get(eventType);
47
- if (existing) {
48
- existing.push(listener);
49
- } else {
50
- this.listeners.set(eventType, [listener]);
51
- }
52
- return this;
53
- }
54
- /**
55
- * 指定したイベントタイプのハンドラを削除する。
56
- * listener を省略した場合、そのイベントタイプのすべてのハンドラを削除する。
57
- */
58
- off(eventType, listener) {
59
- if (!listener) {
60
- this.listeners.delete(eventType);
61
- return this;
62
- }
63
- const existing = this.listeners.get(eventType);
64
- if (existing) {
65
- const filtered = existing.filter((l) => l !== listener);
66
- if (filtered.length > 0) {
67
- this.listeners.set(eventType, filtered);
68
- } else {
69
- this.listeners.delete(eventType);
70
- }
71
- }
72
- return this;
73
- }
74
- /**
75
- * EventHandler.handle() の実装。
76
- * 登録されたリスナーに対してイベントをルーティングする。
77
- */
78
- async handle(event) {
79
- const listeners = this.listeners.get(event.eventType);
80
- if (!listeners) return;
81
- for (const listener of listeners) {
82
- await listener(event);
83
- }
84
- }
85
+ listeners = /* @__PURE__ */ new Map();
86
+ /**
87
+ * 指定したイベントタイプのハンドラを登録する。
88
+ * 同じイベントタイプに複数のハンドラを登録可能(登録順に実行)。
89
+ */
90
+ on(eventType, listener) {
91
+ const existing = this.listeners.get(eventType);
92
+ if (existing) existing.push(listener);
93
+ else this.listeners.set(eventType, [listener]);
94
+ return this;
95
+ }
96
+ /**
97
+ * 指定したイベントタイプのハンドラを削除する。
98
+ * listener を省略した場合、そのイベントタイプのすべてのハンドラを削除する。
99
+ */
100
+ off(eventType, listener) {
101
+ if (!listener) {
102
+ this.listeners.delete(eventType);
103
+ return this;
104
+ }
105
+ const existing = this.listeners.get(eventType);
106
+ if (existing) {
107
+ const filtered = existing.filter((l) => l !== listener);
108
+ if (filtered.length > 0) this.listeners.set(eventType, filtered);
109
+ else this.listeners.delete(eventType);
110
+ }
111
+ return this;
112
+ }
113
+ /**
114
+ * EventHandler.handle() の実装。
115
+ * 登録されたリスナーに対してイベントをルーティングする。
116
+ */
117
+ async handle(event) {
118
+ const listeners = this.listeners.get(event.eventType);
119
+ if (!listeners) return;
120
+ for (const listener of listeners) await listener(event);
121
+ }
85
122
  };
86
-
87
- // src/helpers/media-uploader.ts
123
+ //#endregion
124
+ //#region src/helpers/media-uploader.ts
125
+ /**
126
+ * メディアアップロードの開始 → データ送信 → 処理完了待機を簡略化するヘルパー。
127
+ *
128
+ * 通常は initiatePostMediaUpload → HTTP POST → getPostMediaStatus のポーリングが必要だが、
129
+ * このクラスで waitForReady() を呼ぶだけで完了まで待機できる。
130
+ */
88
131
  var MediaUploader = class {
89
- client;
90
- pollInterval;
91
- timeout;
92
- constructor(client, options) {
93
- this.client = client;
94
- this.pollInterval = options?.pollInterval ?? 1e3;
95
- this.timeout = options?.timeout ?? 6e4;
96
- }
97
- /**
98
- * メディアアップロードを開始し、uploadUrl と mediaId を返す。
99
- */
100
- async initiate(request) {
101
- const response = await this.client.initiatePostMediaUpload(request);
102
- return {
103
- mediaId: response.mediaId,
104
- uploadUrl: response.uploadUrl
105
- };
106
- }
107
- /**
108
- * メディアの処理が完了するまでポーリングして待機する。
109
- * 完了時に mediaId を返す。失敗時はエラーをスローする。
110
- */
111
- async waitForReady(mediaId) {
112
- const startTime = Date.now();
113
- while (Date.now() - startTime < this.timeout) {
114
- const status = await this.client.getPostMediaStatus(mediaId);
115
- if (status.status === 3 /* COMPLETED */) {
116
- return mediaId;
117
- }
118
- if (status.status === 4 /* FAILED */) {
119
- throw new Error(`Media upload failed: ${mediaId}`);
120
- }
121
- await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
122
- }
123
- throw new Error(
124
- `Media upload timed out after ${this.timeout}ms: ${mediaId}`
125
- );
126
- }
132
+ client;
133
+ pollInterval;
134
+ timeout;
135
+ constructor(client, options) {
136
+ this.client = client;
137
+ this.pollInterval = options?.pollInterval ?? 1e3;
138
+ this.timeout = options?.timeout ?? 6e4;
139
+ }
140
+ /**
141
+ * メディアアップロードを開始し、uploadUrl と mediaId を返す。
142
+ */
143
+ async initiate(request) {
144
+ const response = await this.client.initiatePostMediaUpload(request);
145
+ return {
146
+ mediaId: response.mediaId,
147
+ uploadUrl: response.uploadUrl
148
+ };
149
+ }
150
+ /**
151
+ * メディアの処理が完了するまでポーリングして待機する。
152
+ * 完了時に mediaId を返す。失敗時はエラーをスローする。
153
+ */
154
+ async waitForReady(mediaId) {
155
+ const startTime = Date.now();
156
+ while (Date.now() - startTime < this.timeout) {
157
+ const status = await this.client.getPostMediaStatus(mediaId);
158
+ if (status.status === require_types.MediaUploadStatus.COMPLETED) return mediaId;
159
+ if (status.status === require_types.MediaUploadStatus.FAILED) throw new Error(`Media upload failed: ${mediaId}`);
160
+ await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
161
+ }
162
+ throw new Error(`Media upload timed out after ${this.timeout}ms: ${mediaId}`);
163
+ }
127
164
  };
128
-
129
- // src/helpers/post-builder.ts
165
+ //#endregion
166
+ //#region src/helpers/post-builder.ts
167
+ /**
168
+ * ポスト作成リクエストをメソッドチェーンで組み立てるビルダー。
169
+ *
170
+ * @example
171
+ * const request = new PostBuilder('Hello mixi2!')
172
+ * .reply('post-id')
173
+ * .media(['media-id-1'])
174
+ * .sensitive()
175
+ * .build();
176
+ */
130
177
  var PostBuilder = class {
131
- request;
132
- constructor(text) {
133
- this.request = { text };
134
- }
135
- /** 返信先ポスト ID を設定する。 */
136
- reply(postId) {
137
- this.request.inReplyToPostId = postId;
138
- this.request.quotedPostId = void 0;
139
- return this;
140
- }
141
- /** 引用対象ポスト ID を設定する。 */
142
- quote(postId) {
143
- this.request.quotedPostId = postId;
144
- this.request.inReplyToPostId = void 0;
145
- return this;
146
- }
147
- /** 添付メディア ID を設定する(最大 4 件)。 */
148
- media(mediaIdList) {
149
- this.request.mediaIdList = mediaIdList;
150
- return this;
151
- }
152
- /** センシティブマスクを設定する。 */
153
- sensitive(caption = "") {
154
- this.request.postMask = {
155
- maskType: 1 /* SENSITIVE */,
156
- caption
157
- };
158
- return this;
159
- }
160
- /** ネタバレマスクを設定する。 */
161
- spoiler(caption = "") {
162
- this.request.postMask = {
163
- maskType: 2 /* SPOILER */,
164
- caption
165
- };
166
- return this;
167
- }
168
- /** カスタムマスクを設定する。 */
169
- mask(postMask) {
170
- this.request.postMask = postMask;
171
- return this;
172
- }
173
- /** 配信設定を設定する。 */
174
- publishing(type) {
175
- this.request.publishingType = type;
176
- return this;
177
- }
178
- /** CreatePostRequest オブジェクトを構築する。 */
179
- build() {
180
- return { ...this.request };
181
- }
178
+ request;
179
+ constructor(text) {
180
+ this.request = { text };
181
+ }
182
+ /** 返信先ポスト ID を設定する。 */
183
+ reply(postId) {
184
+ this.request.inReplyToPostId = postId;
185
+ this.request.quotedPostId = void 0;
186
+ return this;
187
+ }
188
+ /** 引用対象ポスト ID を設定する。 */
189
+ quote(postId) {
190
+ this.request.quotedPostId = postId;
191
+ this.request.inReplyToPostId = void 0;
192
+ return this;
193
+ }
194
+ /** 添付メディア ID を設定する(最大 4 件)。 */
195
+ media(mediaIdList) {
196
+ this.request.mediaIdList = mediaIdList;
197
+ return this;
198
+ }
199
+ /** センシティブマスクを設定する。 */
200
+ sensitive(caption = "") {
201
+ this.request.postMask = {
202
+ maskType: require_types.PostMaskType.SENSITIVE,
203
+ caption
204
+ };
205
+ return this;
206
+ }
207
+ /** ネタバレマスクを設定する。 */
208
+ spoiler(caption = "") {
209
+ this.request.postMask = {
210
+ maskType: require_types.PostMaskType.SPOILER,
211
+ caption
212
+ };
213
+ return this;
214
+ }
215
+ /** カスタムマスクを設定する。 */
216
+ mask(postMask) {
217
+ this.request.postMask = postMask;
218
+ return this;
219
+ }
220
+ /** 配信設定を設定する。 */
221
+ publishing(type) {
222
+ this.request.publishingType = type;
223
+ return this;
224
+ }
225
+ /** CreatePostRequest オブジェクトを構築する。 */
226
+ build() {
227
+ return { ...this.request };
228
+ }
182
229
  };
183
-
184
- // src/helpers/reason-filter.ts
230
+ //#endregion
231
+ //#region src/helpers/reason-filter.ts
232
+ /**
233
+ * EventReason に基づいてイベントをフィルタリングするミドルウェア。
234
+ * 指定した理由に一致するイベントのみを内部のハンドラに渡す。
235
+ *
236
+ * @example
237
+ * const filter = new ReasonFilter(innerHandler, [
238
+ * EventReason.POST_REPLY,
239
+ * EventReason.POST_MENTIONED,
240
+ * ]);
241
+ * await watcher.watch(filter);
242
+ */
185
243
  var ReasonFilter = class {
186
- inner;
187
- allowedReasons;
188
- constructor(handler, reasons) {
189
- this.inner = handler;
190
- this.allowedReasons = new Set(reasons);
191
- }
192
- async handle(event) {
193
- const reasons = this.getReasons(event);
194
- if (reasons.length === 0 || reasons.some((r) => this.allowedReasons.has(r))) {
195
- await this.inner.handle(event);
196
- }
197
- }
198
- getReasons(event) {
199
- if (event.postCreatedEvent) {
200
- return event.postCreatedEvent.eventReasonList || [];
201
- }
202
- if (event.chatMessageReceivedEvent) {
203
- return event.chatMessageReceivedEvent.eventReasonList || [];
204
- }
205
- return [];
206
- }
244
+ inner;
245
+ allowedReasons;
246
+ constructor(handler, reasons) {
247
+ this.inner = handler;
248
+ this.allowedReasons = new Set(reasons);
249
+ }
250
+ async handle(event) {
251
+ const reasons = this.getReasons(event);
252
+ if (reasons.length === 0 || reasons.some((r) => this.allowedReasons.has(r))) await this.inner.handle(event);
253
+ }
254
+ getReasons(event) {
255
+ if (event.postCreatedEvent) return event.postCreatedEvent.eventReasonList || [];
256
+ if (event.chatMessageReceivedEvent) return event.chatMessageReceivedEvent.eventReasonList || [];
257
+ return [];
258
+ }
207
259
  };
208
- // Annotate the CommonJS export names for ESM import in node:
209
- 0 && (module.exports = {
210
- EventRouter,
211
- MediaUploader,
212
- PostBuilder,
213
- ReasonFilter,
214
- apiAddress,
215
- streamAddress,
216
- tokenUrl
217
- });
260
+ //#endregion
261
+ //#region src/helpers/text-splitter.ts
262
+ /** mixi2 の 1 ポストあたりの最大文字数 */
263
+ const maxPostLength = 149;
264
+ /**
265
+ * 長いテキストを mixi2 の文字数制限内に収まる複数チャンクに分割するヘルパー。
266
+ * デフォルトは 149 文字制限(mixi2 のポスト本文上限)に準拠。
267
+ *
268
+ * @example
269
+ * const splitter = new TextSplitter();
270
+ * const chunks = splitter.split('長いテキスト...');
271
+ * for (const chunk of chunks) {
272
+ * await client.createPost({ text: chunk });
273
+ * }
274
+ */
275
+ var TextSplitter = class {
276
+ maxLength;
277
+ splitOnWord;
278
+ constructor(options) {
279
+ this.maxLength = options?.maxLength ?? 149;
280
+ this.splitOnWord = options?.splitOnWord ?? true;
281
+ }
282
+ /**
283
+ * テキストを maxLength 以内の複数チャンクに分割して返す。
284
+ * テキストが maxLength 以内の場合は 1 要素の配列を返す。
285
+ */
286
+ split(text) {
287
+ if (text.length <= this.maxLength) return [text];
288
+ const chunks = [];
289
+ let remaining = text;
290
+ while (remaining.length > this.maxLength) {
291
+ let splitAt = this.maxLength;
292
+ if (this.splitOnWord) {
293
+ const candidate = remaining.slice(0, this.maxLength);
294
+ const lastBreak = Math.max(candidate.lastIndexOf(" "), candidate.lastIndexOf(" "), candidate.lastIndexOf("、"), candidate.lastIndexOf("。"), candidate.lastIndexOf("!"), candidate.lastIndexOf("?"), candidate.lastIndexOf("!"), candidate.lastIndexOf("?"), candidate.lastIndexOf("\n"));
295
+ if (lastBreak > 0) splitAt = lastBreak + 1;
296
+ }
297
+ chunks.push(remaining.slice(0, splitAt).trimEnd());
298
+ remaining = remaining.slice(splitAt).trimStart();
299
+ }
300
+ if (remaining.length > 0) chunks.push(remaining);
301
+ return chunks;
302
+ }
303
+ };
304
+ //#endregion
305
+ exports.EventDeduplicator = EventDeduplicator;
306
+ exports.EventLogger = EventLogger;
307
+ exports.EventRouter = EventRouter;
308
+ exports.MediaUploader = MediaUploader;
309
+ exports.PostBuilder = PostBuilder;
310
+ exports.ReasonFilter = ReasonFilter;
311
+ exports.TextSplitter = TextSplitter;
312
+ exports.apiAddress = apiAddress;
313
+ exports.maxPostLength = maxPostLength;
314
+ exports.streamAddress = streamAddress;
315
+ exports.tokenUrl = tokenUrl;