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