cygnet 0.1.0 → 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,1350 @@
1
+ // src/core/error.ts
2
+ class BotError extends Error {
3
+ error;
4
+ ctx;
5
+ constructor(error, ctx) {
6
+ super(`Bot error caused by update (${getUpdateType(ctx.update.envelope)}): ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? { cause: error } : undefined);
7
+ this.error = error;
8
+ this.ctx = ctx;
9
+ this.name = "BotError";
10
+ }
11
+ }
12
+
13
+ class SignalError extends Error {
14
+ statusCode;
15
+ description;
16
+ constructor(statusCode, description) {
17
+ super(`Signal API error ${statusCode}: ${description}`);
18
+ this.statusCode = statusCode;
19
+ this.description = description;
20
+ this.name = "SignalError";
21
+ }
22
+ }
23
+ function getUpdateType(envelope) {
24
+ if (envelope.dataMessage)
25
+ return "dataMessage";
26
+ if (envelope.syncMessage)
27
+ return "syncMessage";
28
+ if (envelope.editMessage)
29
+ return "editMessage";
30
+ if (envelope.deleteMessage)
31
+ return "deleteMessage";
32
+ if (envelope.receiptMessage)
33
+ return "receiptMessage";
34
+ if (envelope.typingMessage)
35
+ return "typingMessage";
36
+ if (envelope.callMessage)
37
+ return "callMessage";
38
+ return "unknown";
39
+ }
40
+
41
+ // src/filter.ts
42
+ function matchFilter(ctx, query) {
43
+ const env = ctx.update.envelope;
44
+ switch (query) {
45
+ case "message":
46
+ return env.dataMessage != null && !env.dataMessage.reaction && env.dataMessage.groupInfo?.type !== "UPDATE";
47
+ case "message:text":
48
+ return env.dataMessage != null && !env.dataMessage.reaction && typeof env.dataMessage.message === "string" && env.dataMessage.message.length > 0;
49
+ case "message:attachments":
50
+ return env.dataMessage != null && !env.dataMessage.reaction && (env.dataMessage.attachments?.length ?? 0) > 0;
51
+ case "message:quote":
52
+ return env.dataMessage != null && !env.dataMessage.reaction && env.dataMessage.quote != null;
53
+ case "message:reaction":
54
+ return env.dataMessage != null && env.dataMessage.reaction != null;
55
+ case "message:group":
56
+ return env.dataMessage != null && !env.dataMessage.reaction && env.dataMessage.groupInfo != null && env.dataMessage.groupInfo.type !== "UPDATE";
57
+ case "message:private":
58
+ return env.dataMessage != null && !env.dataMessage.reaction && env.dataMessage.groupInfo == null;
59
+ case "group_update":
60
+ return env.dataMessage != null && env.dataMessage.groupInfo?.type === "UPDATE";
61
+ case "message:sticker":
62
+ return env.dataMessage != null && !env.dataMessage.reaction && env.dataMessage.sticker != null;
63
+ case "edit_message":
64
+ return env.editMessage != null;
65
+ case "delete_message":
66
+ return env.deleteMessage != null;
67
+ case "receipt":
68
+ return env.receiptMessage != null;
69
+ case "typing":
70
+ return env.typingMessage != null;
71
+ case "call":
72
+ return env.callMessage != null;
73
+ case "sync_message":
74
+ return env.syncMessage != null;
75
+ default:
76
+ return false;
77
+ }
78
+ }
79
+
80
+ // src/composer.ts
81
+ function flatten(mw) {
82
+ return typeof mw === "function" ? mw : (ctx, next) => mw.middleware()(ctx, next);
83
+ }
84
+ function concat(first, then) {
85
+ return async (ctx, next) => {
86
+ let called = false;
87
+ await first(ctx, async () => {
88
+ if (called)
89
+ throw new Error("next() called multiple times");
90
+ called = true;
91
+ await then(ctx, next);
92
+ });
93
+ };
94
+ }
95
+ function run(mw, ctx) {
96
+ return new Promise((resolve, reject) => {
97
+ Promise.resolve(mw(ctx, async () => {})).then(() => resolve(), reject);
98
+ });
99
+ }
100
+ function compose(middlewares) {
101
+ const fns = middlewares.map(flatten);
102
+ return fns.reduce(concat, (_ctx, next) => next());
103
+ }
104
+
105
+ class Composer {
106
+ #handler;
107
+ _onForkError = (err) => console.error("[cygnet] Error in forked middleware:", err);
108
+ constructor(...middleware) {
109
+ this.#handler = middleware.length > 0 ? compose(middleware) : (_ctx, next) => next();
110
+ }
111
+ middleware() {
112
+ return this.#handler;
113
+ }
114
+ use(...middleware) {
115
+ const mw = compose(middleware);
116
+ this.#handler = concat(this.#handler, mw);
117
+ return this;
118
+ }
119
+ on(filter, ...middleware) {
120
+ const filters = Array.isArray(filter) ? filter : [filter];
121
+ return this.filter((ctx) => filters.some((q) => matchFilter(ctx, q)), ...middleware);
122
+ }
123
+ chatType(type, ...middleware) {
124
+ return this.filter((ctx) => type === "group" ? ctx.isGroup : !ctx.isGroup, ...middleware);
125
+ }
126
+ hears(trigger, ...middleware) {
127
+ const triggers = Array.isArray(trigger) ? trigger : [trigger];
128
+ return this.filter((ctx) => {
129
+ const text = ctx.text;
130
+ if (!text)
131
+ return false;
132
+ for (const t of triggers) {
133
+ if (typeof t === "string") {
134
+ if (text.includes(t)) {
135
+ ctx.match = t;
136
+ return true;
137
+ }
138
+ } else {
139
+ const m = t.exec(text);
140
+ if (m) {
141
+ ctx.match = m;
142
+ return true;
143
+ }
144
+ }
145
+ }
146
+ return false;
147
+ }, ...middleware);
148
+ }
149
+ command(command, ...middleware) {
150
+ const commands = Array.isArray(command) ? command : [command];
151
+ const normalized = commands.map((c) => c.replace(/^\//, "").toLowerCase());
152
+ return this.filter((ctx) => {
153
+ const text = ctx.text;
154
+ if (!text?.startsWith("/"))
155
+ return false;
156
+ const match = /^\/([a-z0-9_]+)(@\S+)?(?:\s(.*))?$/i.exec(text);
157
+ if (!match)
158
+ return false;
159
+ const cmd = (match[1] ?? "").toLowerCase();
160
+ if (!normalized.includes(cmd))
161
+ return false;
162
+ ctx.match = (match[3] ?? "").trim();
163
+ return true;
164
+ }, ...middleware);
165
+ }
166
+ filter(predicate, ...middleware) {
167
+ const mw = compose(middleware);
168
+ return this.use(async (ctx, next) => {
169
+ if (await predicate(ctx)) {
170
+ await mw(ctx, next);
171
+ } else {
172
+ await next();
173
+ }
174
+ });
175
+ }
176
+ drop(predicate) {
177
+ return this.use(async (ctx, next) => {
178
+ if (!await predicate(ctx))
179
+ await next();
180
+ });
181
+ }
182
+ branch(predicate, trueMiddleware, falseMiddleware) {
183
+ return this.use(async (ctx, next) => {
184
+ if (await predicate(ctx)) {
185
+ await flatten(trueMiddleware)(ctx, next);
186
+ } else {
187
+ await flatten(falseMiddleware)(ctx, next);
188
+ }
189
+ });
190
+ }
191
+ fork(...middleware) {
192
+ const mw = compose(middleware);
193
+ return this.use((ctx, next) => {
194
+ run(mw, ctx).catch((err) => this._onForkError(err, ctx));
195
+ return next();
196
+ });
197
+ }
198
+ lazy(factory) {
199
+ return this.use(async (ctx, next) => {
200
+ const result = await factory(ctx);
201
+ const middlewares = Array.isArray(result) ? result : [result];
202
+ const mw = compose(middlewares);
203
+ await mw(ctx, next);
204
+ });
205
+ }
206
+ errorBoundary(errorHandler, ...middleware) {
207
+ const mw = compose(middleware);
208
+ return this.use(async (ctx, next) => {
209
+ try {
210
+ await run(mw, ctx);
211
+ } catch (err) {
212
+ await errorHandler(new BotError(err, ctx), next);
213
+ return;
214
+ }
215
+ await next();
216
+ });
217
+ }
218
+ }
219
+
220
+ // src/context.ts
221
+ var groupStateCaches = new Map;
222
+ function groupHasMember(group, memberId) {
223
+ if (group.isMember !== undefined)
224
+ return group.isMember;
225
+ const members = group.members;
226
+ if (!Array.isArray(members))
227
+ return false;
228
+ return members.some((member) => typeof member === "string" ? member === memberId : member.number === memberId || member.uuid === memberId);
229
+ }
230
+ function getGroupStateCache(botId) {
231
+ let cache = groupStateCaches.get(botId);
232
+ if (!cache) {
233
+ cache = new Map;
234
+ groupStateCaches.set(botId, cache);
235
+ }
236
+ return cache;
237
+ }
238
+ function primeGroupStateCache(botId, groups) {
239
+ const cache = getGroupStateCache(botId);
240
+ const seen = new Set;
241
+ for (const group of groups) {
242
+ seen.add(group.id);
243
+ const previous = cache.get(group.id);
244
+ cache.set(group.id, {
245
+ name: group.name,
246
+ isMember: groupHasMember(group, botId),
247
+ revision: group.revision ?? previous?.revision
248
+ });
249
+ }
250
+ for (const [groupId, previous] of cache) {
251
+ if (!seen.has(groupId) && previous.isMember) {
252
+ cache.set(groupId, {
253
+ ...previous,
254
+ isMember: false
255
+ });
256
+ }
257
+ }
258
+ }
259
+ function restoreGroupStateCache(botId, snapshot) {
260
+ const cache = getGroupStateCache(botId);
261
+ for (const [groupId, state] of Object.entries(snapshot)) {
262
+ cache.set(groupId, {
263
+ name: state.name,
264
+ isMember: state.isMember,
265
+ revision: state.revision
266
+ });
267
+ }
268
+ }
269
+ function snapshotGroupStateCache(botId) {
270
+ const cache = getGroupStateCache(botId);
271
+ const snapshot = {};
272
+ for (const [groupId, state] of cache) {
273
+ snapshot[groupId] = {
274
+ name: state.name,
275
+ isMember: state.isMember,
276
+ revision: state.revision
277
+ };
278
+ }
279
+ return snapshot;
280
+ }
281
+
282
+ class Context {
283
+ update;
284
+ api;
285
+ me;
286
+ match;
287
+ constructor(update, api, me) {
288
+ this.update = update;
289
+ this.api = api;
290
+ this.me = me;
291
+ }
292
+ get dataMessage() {
293
+ return this.update.envelope.dataMessage;
294
+ }
295
+ get syncMessage() {
296
+ return this.update.envelope.syncMessage;
297
+ }
298
+ get editMessage() {
299
+ return this.update.envelope.editMessage;
300
+ }
301
+ get deleteMessage() {
302
+ return this.update.envelope.deleteMessage;
303
+ }
304
+ get receipt() {
305
+ return this.update.envelope.receiptMessage;
306
+ }
307
+ get typingMessage() {
308
+ return this.update.envelope.typingMessage;
309
+ }
310
+ get callMessage() {
311
+ return this.update.envelope.callMessage;
312
+ }
313
+ get message() {
314
+ const dm = this.update.envelope.dataMessage;
315
+ return dm && !dm.reaction && dm.groupInfo?.type !== "UPDATE" ? dm : undefined;
316
+ }
317
+ get reaction() {
318
+ const dm = this.update.envelope.dataMessage;
319
+ return dm?.reaction ?? undefined;
320
+ }
321
+ get groupUpdate() {
322
+ const dm = this.update.envelope.dataMessage;
323
+ return dm?.groupInfo?.type === "UPDATE" ? dm : undefined;
324
+ }
325
+ get from() {
326
+ return this.update.envelope.sourceNumber || undefined;
327
+ }
328
+ get fromUuid() {
329
+ return this.update.envelope.sourceUuid || this.update.envelope.source || undefined;
330
+ }
331
+ get fromName() {
332
+ return this.update.envelope.sourceName || undefined;
333
+ }
334
+ get sender() {
335
+ return this.update.envelope.sourceNumber || this.update.envelope.sourceUuid || this.update.envelope.source;
336
+ }
337
+ get chat() {
338
+ const dm = this.update.envelope.dataMessage;
339
+ if (dm?.groupInfo?.groupId)
340
+ return `group.${btoa(dm.groupInfo.groupId)}`;
341
+ const em = this.update.envelope.editMessage;
342
+ if (em?.message?.groupInfo?.groupId)
343
+ return `group.${btoa(em.message.groupInfo.groupId)}`;
344
+ const typing = this.update.envelope.typingMessage;
345
+ if (typing?.groupId)
346
+ return `group.${btoa(typing.groupId)}`;
347
+ return this.update.envelope.sourceNumber || this.update.envelope.source;
348
+ }
349
+ get isGroup() {
350
+ return this.update.envelope.dataMessage?.groupInfo != null || this.update.envelope.editMessage?.message?.groupInfo != null;
351
+ }
352
+ get text() {
353
+ const dm = this.update.envelope.dataMessage;
354
+ if (dm && !dm.reaction && typeof dm.message === "string") {
355
+ return dm.message;
356
+ }
357
+ const em = this.update.envelope.editMessage;
358
+ if (em && typeof em.message.message === "string") {
359
+ return em.message.message;
360
+ }
361
+ return "";
362
+ }
363
+ get msgTimestamp() {
364
+ return this.update.envelope.dataMessage?.timestamp ?? this.update.envelope.editMessage?.message?.timestamp ?? this.update.envelope.timestamp;
365
+ }
366
+ async reply(text, options = {}) {
367
+ await this.api.send(this.chat, text, options);
368
+ }
369
+ async quote(text, options = {}) {
370
+ const ts = this.msgTimestamp;
371
+ const author = this.sender;
372
+ if (!ts || !author) {
373
+ return this.reply(text, options);
374
+ }
375
+ await this.api.send(this.chat, text, {
376
+ ...options,
377
+ quote: {
378
+ timestamp: ts,
379
+ author,
380
+ text: this.text || undefined
381
+ }
382
+ });
383
+ }
384
+ async react(emoji) {
385
+ const ts = this.msgTimestamp;
386
+ const author = this.sender;
387
+ if (!ts || !author)
388
+ throw new Error("Cannot react: no message timestamp or author");
389
+ await this.api.react(this.chat, {
390
+ reaction: emoji,
391
+ targetAuthor: author,
392
+ targetTimestamp: ts
393
+ });
394
+ }
395
+ async unreact(emoji) {
396
+ const ts = this.msgTimestamp;
397
+ const author = this.sender;
398
+ if (!ts || !author)
399
+ throw new Error("Cannot unreact: no message timestamp or author");
400
+ await this.api.react(this.chat, {
401
+ reaction: emoji,
402
+ targetAuthor: author,
403
+ targetTimestamp: ts,
404
+ isRemove: true
405
+ });
406
+ }
407
+ async typing(stop = false) {
408
+ await this.api.typing(this.chat, stop);
409
+ }
410
+ async deleteMsg(timestamp) {
411
+ const ts = timestamp ?? this.msgTimestamp;
412
+ if (!ts)
413
+ throw new Error("Cannot delete: no timestamp");
414
+ await this.api.deleteMessage(this.chat, ts);
415
+ }
416
+ has(query) {
417
+ return matchFilter(this, query);
418
+ }
419
+ hasText(trigger) {
420
+ const text = this.text;
421
+ if (!text)
422
+ return false;
423
+ if (trigger === undefined)
424
+ return true;
425
+ if (typeof trigger === "string") {
426
+ if (text.includes(trigger)) {
427
+ this.match = trigger;
428
+ return true;
429
+ }
430
+ return false;
431
+ }
432
+ const m = trigger.exec(text);
433
+ if (m) {
434
+ this.match = m;
435
+ return true;
436
+ }
437
+ return false;
438
+ }
439
+ hasChatType(type) {
440
+ return type === "group" ? this.isGroup : !this.isGroup;
441
+ }
442
+ async inspectGroupUpdate() {
443
+ const update = this.groupUpdate;
444
+ const info = update?.groupInfo;
445
+ if (!update || !info?.groupId)
446
+ return;
447
+ const groupId = `group.${btoa(info.groupId)}`;
448
+ const cache = getGroupStateCache(this.me);
449
+ const previous = cache.get(groupId);
450
+ const incomingRevision = info.revision;
451
+ const previousRevision = previous?.revision;
452
+ if (previousRevision !== undefined && incomingRevision !== undefined && incomingRevision <= previousRevision) {
453
+ return {
454
+ kind: "stale",
455
+ groupId,
456
+ groupName: info.groupName ?? previous?.name,
457
+ revision: incomingRevision,
458
+ previousName: previous?.name,
459
+ previousRevision
460
+ };
461
+ }
462
+ const hasGap = previousRevision !== undefined && incomingRevision !== undefined && incomingRevision > previousRevision + 1;
463
+ if (hasGap) {
464
+ console.warn(`[cygnet] Group update gap for ${groupId}: expected revision ${previousRevision + 1}, got ${incomingRevision}`);
465
+ }
466
+ const nameChanged = previous?.name !== undefined && info.groupName !== undefined && previous.name !== info.groupName;
467
+ const needsReconcile = hasGap || !previous || !previous.isMember || !nameChanged;
468
+ let currentGroup;
469
+ if (needsReconcile) {
470
+ try {
471
+ const groups = await this.api.getGroups();
472
+ currentGroup = groups.find((group) => group.id === groupId);
473
+ } catch {}
474
+ }
475
+ let kind = "unknown";
476
+ let nextState;
477
+ if (!needsReconcile && previous) {
478
+ kind = "renamed";
479
+ nextState = {
480
+ name: info.groupName ?? previous.name,
481
+ isMember: true,
482
+ revision: incomingRevision ?? previousRevision
483
+ };
484
+ } else if (currentGroup) {
485
+ const isMember = groupHasMember(currentGroup, this.me);
486
+ if (!isMember) {
487
+ kind = previous?.isMember ? "left" : "unknown";
488
+ nextState = {
489
+ name: currentGroup.name || (info.groupName ?? previous?.name),
490
+ isMember: false,
491
+ revision: incomingRevision ?? currentGroup.revision ?? previousRevision
492
+ };
493
+ } else if (!previous || !previous.isMember) {
494
+ kind = "joined";
495
+ nextState = {
496
+ name: currentGroup.name || info.groupName,
497
+ isMember: true,
498
+ revision: incomingRevision ?? currentGroup.revision ?? previousRevision
499
+ };
500
+ } else if (nameChanged) {
501
+ kind = "renamed";
502
+ nextState = {
503
+ name: currentGroup.name || info.groupName,
504
+ isMember: true,
505
+ revision: incomingRevision ?? currentGroup.revision ?? previousRevision
506
+ };
507
+ } else {
508
+ kind = "updated";
509
+ nextState = {
510
+ name: currentGroup.name || (info.groupName ?? previous.name),
511
+ isMember: true,
512
+ revision: incomingRevision ?? currentGroup.revision ?? previousRevision
513
+ };
514
+ }
515
+ } else if (!previous) {
516
+ nextState = {
517
+ name: info.groupName,
518
+ isMember: false,
519
+ revision: incomingRevision
520
+ };
521
+ } else if (!previous.isMember) {
522
+ nextState = {
523
+ name: info.groupName ?? previous.name,
524
+ isMember: false,
525
+ revision: incomingRevision ?? previousRevision
526
+ };
527
+ } else if (nameChanged) {
528
+ kind = "renamed";
529
+ nextState = {
530
+ name: info.groupName ?? previous.name,
531
+ isMember: true,
532
+ revision: incomingRevision ?? previousRevision
533
+ };
534
+ } else {
535
+ kind = "updated";
536
+ nextState = {
537
+ name: info.groupName ?? previous.name,
538
+ isMember: true,
539
+ revision: incomingRevision ?? previousRevision
540
+ };
541
+ }
542
+ cache.set(groupId, nextState);
543
+ const missedRevisions = hasGap && previousRevision !== undefined && incomingRevision !== undefined ? incomingRevision - previousRevision - 1 : undefined;
544
+ return {
545
+ kind,
546
+ groupId,
547
+ groupName: currentGroup?.name || nextState.name,
548
+ revision: info.revision,
549
+ previousName: previous?.name,
550
+ previousRevision,
551
+ missedRevisions,
552
+ currentGroup
553
+ };
554
+ }
555
+ }
556
+
557
+ // src/core/client.ts
558
+ class HttpClient {
559
+ baseUrl;
560
+ phoneNumber;
561
+ constructor(config) {
562
+ let url = config.baseUrl;
563
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
564
+ url = "http://" + url;
565
+ }
566
+ this.baseUrl = url.replace(/\/$/, "");
567
+ this.phoneNumber = config.phoneNumber;
568
+ }
569
+ async get(path) {
570
+ const res = await fetch(this.baseUrl + path, {
571
+ method: "GET",
572
+ headers: { "Content-Type": "application/json" }
573
+ });
574
+ return this.#handleResponse(res);
575
+ }
576
+ async post(path, body) {
577
+ const res = await fetch(this.baseUrl + path, {
578
+ method: "POST",
579
+ headers: { "Content-Type": "application/json" },
580
+ body: body !== undefined ? JSON.stringify(body) : undefined
581
+ });
582
+ return this.#handleResponse(res);
583
+ }
584
+ async#handleResponse(res) {
585
+ if (res.ok) {
586
+ const text = await res.text();
587
+ if (!text)
588
+ return;
589
+ try {
590
+ return JSON.parse(text);
591
+ } catch {
592
+ return text;
593
+ }
594
+ }
595
+ let description;
596
+ try {
597
+ const body = await res.json();
598
+ description = body.error ?? body.message ?? res.statusText;
599
+ } catch {
600
+ description = res.statusText;
601
+ }
602
+ throw new SignalError(res.status, description);
603
+ }
604
+ wsReceiveUrl() {
605
+ const wsBase = this.baseUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
606
+ return `${wsBase}/v1/receive/${encodeURIComponent(this.phoneNumber)}`;
607
+ }
608
+ }
609
+
610
+ // src/core/api.ts
611
+ class SignalAPI {
612
+ #client;
613
+ constructor(config) {
614
+ this.#client = new HttpClient(config);
615
+ }
616
+ get phoneNumber() {
617
+ return this.#client.phoneNumber;
618
+ }
619
+ async checkHealth() {
620
+ try {
621
+ await this.#client.get("/v1/health");
622
+ return true;
623
+ } catch {
624
+ return false;
625
+ }
626
+ }
627
+ async send(recipient, text, options = {}) {
628
+ const payload = {
629
+ number: this.#client.phoneNumber,
630
+ message: text,
631
+ recipients: [recipient]
632
+ };
633
+ if (options.base64Attachments?.length) {
634
+ payload.base64_attachments = options.base64Attachments;
635
+ }
636
+ if (options.quote) {
637
+ payload.quote_timestamp = options.quote.timestamp;
638
+ payload.quote_author = options.quote.author;
639
+ if (options.quote.text !== undefined)
640
+ payload.quote_message = options.quote.text;
641
+ if (options.quote.mentions?.length)
642
+ payload.quote_mentions = options.quote.mentions;
643
+ }
644
+ if (options.mentions?.length) {
645
+ payload.mentions = options.mentions;
646
+ }
647
+ if (options.textMode) {
648
+ payload.text_mode = options.textMode;
649
+ }
650
+ if (options.viewOnce) {
651
+ payload.view_once = options.viewOnce;
652
+ }
653
+ if (options.editTimestamp !== undefined) {
654
+ payload.edit_timestamp = options.editTimestamp;
655
+ }
656
+ return this.#client.post("/v2/send", payload);
657
+ }
658
+ async react(recipient, reaction) {
659
+ const payload = {
660
+ recipient,
661
+ reaction: reaction.reaction,
662
+ target_author: reaction.targetAuthor,
663
+ timestamp: reaction.targetTimestamp,
664
+ remove: reaction.isRemove ?? false
665
+ };
666
+ await this.#client.post(`/v1/${encodeURIComponent(this.#client.phoneNumber)}/reaction`, payload);
667
+ }
668
+ async typing(recipient, stop = false) {
669
+ const payload = {
670
+ account: this.#client.phoneNumber,
671
+ recipient,
672
+ stop
673
+ };
674
+ await this.#client.post("/v1/typing", payload);
675
+ }
676
+ async deleteMessage(recipient, timestamp) {
677
+ const payload = {
678
+ recipient,
679
+ timestamp
680
+ };
681
+ await this.#client.post(`/v1/${encodeURIComponent(this.#client.phoneNumber)}/delete-message`, payload);
682
+ }
683
+ async editMessage(recipient, targetTimestamp, newText, options = {}) {
684
+ return this.send(recipient, newText, {
685
+ ...options,
686
+ editTimestamp: targetTimestamp
687
+ });
688
+ }
689
+ async getGroups() {
690
+ return this.#client.get(`/v1/groups/${encodeURIComponent(this.#client.phoneNumber)}`);
691
+ }
692
+ get httpClient() {
693
+ return this.#client;
694
+ }
695
+ }
696
+
697
+ // src/core/polling.ts
698
+ class PollingListener {
699
+ #client;
700
+ #receivePath;
701
+ #interval;
702
+ #stopped = false;
703
+ constructor(client, options = {}) {
704
+ this.#client = client;
705
+ this.#receivePath = `/v1/receive/${encodeURIComponent(client.phoneNumber)}`;
706
+ this.#interval = options.interval ?? 1000;
707
+ }
708
+ stop() {
709
+ this.#stopped = true;
710
+ }
711
+ async* [Symbol.asyncIterator]() {
712
+ while (!this.#stopped) {
713
+ try {
714
+ const updates = await this.#client.get(this.#receivePath);
715
+ if (Array.isArray(updates)) {
716
+ for (const update of updates) {
717
+ if (this.#stopped)
718
+ return;
719
+ yield update;
720
+ }
721
+ }
722
+ } catch (err) {
723
+ if (this.#stopped)
724
+ return;
725
+ console.error("[cygnet] Polling error:", err);
726
+ }
727
+ if (!this.#stopped) {
728
+ await sleep(this.#interval);
729
+ }
730
+ }
731
+ }
732
+ }
733
+ function sleep(ms) {
734
+ return new Promise((res) => setTimeout(res, ms));
735
+ }
736
+
737
+ // src/core/websocket.ts
738
+ class WebSocketListener {
739
+ #url;
740
+ #maxDelay;
741
+ #initialDelay;
742
+ #stopped = false;
743
+ #ws = null;
744
+ constructor(wsUrl, options = {}) {
745
+ this.#url = wsUrl;
746
+ this.#maxDelay = options.maxReconnectDelay ?? 30000;
747
+ this.#initialDelay = options.initialReconnectDelay ?? 1000;
748
+ }
749
+ stop() {
750
+ this.#stopped = true;
751
+ this.#ws?.close();
752
+ }
753
+ async* [Symbol.asyncIterator]() {
754
+ let delay = this.#initialDelay;
755
+ while (!this.#stopped) {
756
+ try {
757
+ yield* this.#connect();
758
+ delay = this.#initialDelay;
759
+ } catch (err) {
760
+ if (this.#stopped)
761
+ break;
762
+ console.error(`[cygnet] WebSocket error, reconnecting in ${delay}ms:`, err);
763
+ await sleep2(delay);
764
+ delay = Math.min(delay * 2, this.#maxDelay);
765
+ }
766
+ }
767
+ }
768
+ async* #connect() {
769
+ const ws = new WebSocket(this.#url);
770
+ this.#ws = ws;
771
+ const queue = [];
772
+ let resolve = null;
773
+ let closed = false;
774
+ let closeError = null;
775
+ ws.addEventListener("message", (event) => {
776
+ try {
777
+ const data = JSON.parse(event.data);
778
+ queue.push(data);
779
+ resolve?.();
780
+ resolve = null;
781
+ } catch (err) {
782
+ console.error("[cygnet] Failed to parse WebSocket message:", err);
783
+ }
784
+ });
785
+ ws.addEventListener("close", (event) => {
786
+ closed = true;
787
+ if (!event.wasClean && event.code !== 1000) {
788
+ closeError = new Error(`WebSocket closed with code ${event.code}: ${event.reason}`);
789
+ }
790
+ resolve?.();
791
+ resolve = null;
792
+ });
793
+ ws.addEventListener("error", (event) => {
794
+ closeError = new Error(`WebSocket error: ${event.message ?? "unknown"}`);
795
+ resolve?.();
796
+ resolve = null;
797
+ });
798
+ await new Promise((res, rej) => {
799
+ ws.addEventListener("open", () => res());
800
+ ws.addEventListener("error", (e) => rej(new Error(`Failed to connect: ${e.message ?? "unknown"}`)));
801
+ });
802
+ try {
803
+ while (!this.#stopped) {
804
+ while (queue.length > 0) {
805
+ yield queue.shift();
806
+ }
807
+ if (closed)
808
+ break;
809
+ await new Promise((res) => {
810
+ resolve = res;
811
+ if (queue.length > 0 || closed) {
812
+ resolve = null;
813
+ res();
814
+ }
815
+ });
816
+ }
817
+ } finally {
818
+ ws.close();
819
+ this.#ws = null;
820
+ }
821
+ if (closeError)
822
+ throw closeError;
823
+ }
824
+ }
825
+ function sleep2(ms) {
826
+ return new Promise((res) => setTimeout(res, ms));
827
+ }
828
+
829
+ // src/bot.ts
830
+ class Bot extends Composer {
831
+ api;
832
+ config;
833
+ #me = "";
834
+ #stopped = false;
835
+ #listener = null;
836
+ #errorHandler = defaultErrorHandler;
837
+ constructor(config) {
838
+ super();
839
+ this.config = config;
840
+ this.api = new SignalAPI({
841
+ baseUrl: config.signalService,
842
+ phoneNumber: config.phoneNumber
843
+ });
844
+ this._onForkError = (err, ctx) => {
845
+ const botError = new BotError(err, ctx);
846
+ Promise.resolve(this.#errorHandler(botError)).catch((handlerErr) => {
847
+ console.error("[cygnet] Error in error handler:", handlerErr);
848
+ });
849
+ };
850
+ }
851
+ catch(handler) {
852
+ this.#errorHandler = handler;
853
+ return this;
854
+ }
855
+ async init() {
856
+ const healthy = await this.api.checkHealth();
857
+ if (!healthy) {
858
+ throw new Error(`[cygnet] Cannot reach signal-cli-rest-api at ${this.config.signalService}. Is it running?`);
859
+ }
860
+ this.#me = this.config.phoneNumber;
861
+ await this.#restoreGroupStateCache();
862
+ try {
863
+ const groups = await this.api.getGroups();
864
+ primeGroupStateCache(this.#me, groups);
865
+ await this.#persistGroupStateCache();
866
+ } catch (err) {
867
+ console.warn("[cygnet] Failed to prime group state cache:", err);
868
+ }
869
+ }
870
+ async start() {
871
+ this.#stopped = false;
872
+ await this.init();
873
+ const transport = this.config.transport ?? "websocket";
874
+ if (transport === "polling") {
875
+ this.#listener = new PollingListener(this.api.httpClient, {
876
+ interval: this.config.pollingInterval
877
+ });
878
+ console.log(`[cygnet] Bot started as ${this.#me} (polling)`);
879
+ } else {
880
+ const wsUrl = this.api.httpClient.wsReceiveUrl();
881
+ this.#listener = new WebSocketListener(wsUrl);
882
+ console.log(`[cygnet] Bot started as ${this.#me} (websocket)`);
883
+ }
884
+ for await (const update of this.#listener) {
885
+ if (this.#stopped)
886
+ break;
887
+ await this.handleUpdate(update);
888
+ }
889
+ }
890
+ stop() {
891
+ this.#stopped = true;
892
+ this.#listener?.stop();
893
+ console.log("[cygnet] Bot stopped.");
894
+ }
895
+ async handleUpdate(update) {
896
+ const ContextClass = this.config.ContextConstructor ?? Context;
897
+ const ctx = new ContextClass(update, this.api, this.#me);
898
+ try {
899
+ await run(this.middleware(), ctx);
900
+ } catch (err) {
901
+ const botError = new BotError(err, ctx);
902
+ try {
903
+ await this.#errorHandler(botError);
904
+ } catch (handlerErr) {
905
+ console.error("[cygnet] Error in error handler:", handlerErr);
906
+ }
907
+ }
908
+ if (update.envelope.dataMessage?.groupInfo?.type === "UPDATE") {
909
+ await this.#persistGroupStateCache();
910
+ }
911
+ }
912
+ async#restoreGroupStateCache() {
913
+ const storage = this.config.groupStateStorage;
914
+ if (!storage)
915
+ return;
916
+ try {
917
+ const snapshot = await storage.read(this.#groupStateKey());
918
+ if (!snapshot)
919
+ return;
920
+ if (typeof snapshot !== "object" || Array.isArray(snapshot)) {
921
+ console.warn("[cygnet] Ignoring invalid group state snapshot.");
922
+ return;
923
+ }
924
+ restoreGroupStateCache(this.#me, snapshot);
925
+ } catch (err) {
926
+ console.warn("[cygnet] Failed to restore group state cache:", err);
927
+ }
928
+ }
929
+ async#persistGroupStateCache() {
930
+ const storage = this.config.groupStateStorage;
931
+ if (!storage)
932
+ return;
933
+ try {
934
+ const snapshot = snapshotGroupStateCache(this.#me);
935
+ await storage.write(this.#groupStateKey(), snapshot);
936
+ } catch (err) {
937
+ console.warn("[cygnet] Failed to persist group state cache:", err);
938
+ }
939
+ }
940
+ #groupStateKey() {
941
+ return this.config.groupStateKey ?? this.#me;
942
+ }
943
+ }
944
+ function defaultErrorHandler(err) {
945
+ console.error("[cygnet] Unhandled error:", err.error);
946
+ console.error("[cygnet] Set bot.catch() to handle errors");
947
+ }
948
+ // src/convenience/session.ts
949
+ import { readFile, writeFile } from "fs/promises";
950
+ var directStorageBrand = Symbol("cygnet.directStorage");
951
+ function session(options = {}) {
952
+ const storage = options.storage ?? new MemoryStorage;
953
+ const getKey = options.getSessionKey ?? ((ctx) => ctx.chat);
954
+ const initial = options.initial;
955
+ const prefix = options.keyPrefix ?? "";
956
+ return async (ctx, next) => {
957
+ const rawKey = await getKey(ctx);
958
+ const key = rawKey === undefined ? undefined : prefix + rawKey;
959
+ if (key === undefined) {
960
+ return next();
961
+ }
962
+ let data = await storage.read(key);
963
+ if (data === undefined && initial) {
964
+ data = initial();
965
+ }
966
+ ctx.session = data;
967
+ await next();
968
+ const updated = ctx.session;
969
+ if (updated === null || updated === undefined) {
970
+ await storage.delete(key);
971
+ } else {
972
+ await storage.write(key, updated);
973
+ }
974
+ };
975
+ }
976
+
977
+ class MemoryStorage {
978
+ [directStorageBrand] = true;
979
+ #store = new Map;
980
+ read(key) {
981
+ return this.#store.get(key);
982
+ }
983
+ write(key, value) {
984
+ this.#store.set(key, value);
985
+ }
986
+ delete(key) {
987
+ this.#store.delete(key);
988
+ }
989
+ keys() {
990
+ return this.#store.keys();
991
+ }
992
+ }
993
+
994
+ class FileStorage {
995
+ [directStorageBrand] = true;
996
+ #path;
997
+ constructor(path) {
998
+ this.#path = path;
999
+ }
1000
+ async read(key) {
1001
+ const store = await this.#readStore();
1002
+ return store[key];
1003
+ }
1004
+ async write(key, value) {
1005
+ const store = await this.#readStore();
1006
+ store[key] = value;
1007
+ await this.#writeStore(store);
1008
+ }
1009
+ async delete(key) {
1010
+ const store = await this.#readStore();
1011
+ if (!(key in store))
1012
+ return;
1013
+ delete store[key];
1014
+ await this.#writeStore(store);
1015
+ }
1016
+ async#readStore() {
1017
+ try {
1018
+ const text = await readFile(this.#path, "utf8");
1019
+ if (!text)
1020
+ return {};
1021
+ const parsed = JSON.parse(text);
1022
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1023
+ return {};
1024
+ }
1025
+ return parsed;
1026
+ } catch (err) {
1027
+ const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : "";
1028
+ if (code === "ENOENT")
1029
+ return {};
1030
+ throw err;
1031
+ }
1032
+ }
1033
+ async#writeStore(store) {
1034
+ await writeFile(this.#path, JSON.stringify(store), "utf8");
1035
+ }
1036
+ }
1037
+ function enhanceStorage(options) {
1038
+ const { storage, ttl } = options;
1039
+ return {
1040
+ async read(key) {
1041
+ const entry = await storage.read(key);
1042
+ if (!entry)
1043
+ return;
1044
+ if (entry.expires !== undefined && Date.now() > entry.expires) {
1045
+ await storage.delete(key);
1046
+ return;
1047
+ }
1048
+ return entry.value;
1049
+ },
1050
+ async write(key, value) {
1051
+ await storage.write(key, {
1052
+ value,
1053
+ expires: Date.now() + ttl
1054
+ });
1055
+ },
1056
+ async delete(key) {
1057
+ await storage.delete(key);
1058
+ }
1059
+ };
1060
+ }
1061
+ // src/convenience/scenes/base.ts
1062
+ class BaseScene extends Composer {
1063
+ id;
1064
+ #ttl;
1065
+ #enterHandlers = [];
1066
+ #leaveHandlers = [];
1067
+ constructor(id, ttl) {
1068
+ super();
1069
+ this.id = id;
1070
+ this.#ttl = ttl;
1071
+ }
1072
+ get ttl() {
1073
+ return this.#ttl;
1074
+ }
1075
+ enter(...middleware) {
1076
+ this.#enterHandlers.push(...middleware);
1077
+ return this;
1078
+ }
1079
+ leave(...middleware) {
1080
+ this.#leaveHandlers.push(...middleware);
1081
+ return this;
1082
+ }
1083
+ enterMiddleware() {
1084
+ if (this.#enterHandlers.length === 0)
1085
+ return (_ctx, next) => next();
1086
+ const handlers = this.#enterHandlers;
1087
+ return (ctx, next) => {
1088
+ let i = 0;
1089
+ const dispatch = async () => {
1090
+ if (i >= handlers.length)
1091
+ return next();
1092
+ const mw = handlers[i++];
1093
+ if (!mw)
1094
+ return next();
1095
+ if (typeof mw === "function")
1096
+ await mw(ctx, dispatch);
1097
+ else
1098
+ await mw.middleware()(ctx, dispatch);
1099
+ };
1100
+ return dispatch();
1101
+ };
1102
+ }
1103
+ leaveMiddleware() {
1104
+ if (this.#leaveHandlers.length === 0)
1105
+ return (_ctx, next) => next();
1106
+ const handlers = this.#leaveHandlers;
1107
+ return (ctx, next) => {
1108
+ let i = 0;
1109
+ const dispatch = async () => {
1110
+ if (i >= handlers.length)
1111
+ return next();
1112
+ const mw = handlers[i++];
1113
+ if (!mw)
1114
+ return next();
1115
+ if (typeof mw === "function")
1116
+ await mw(ctx, dispatch);
1117
+ else
1118
+ await mw.middleware()(ctx, dispatch);
1119
+ };
1120
+ return dispatch();
1121
+ };
1122
+ }
1123
+ }
1124
+ // src/convenience/scenes/stage.ts
1125
+ class Stage extends Composer {
1126
+ #scenes;
1127
+ constructor(scenes) {
1128
+ super();
1129
+ this.#scenes = new Map(scenes.map((s) => [s.id, s]));
1130
+ }
1131
+ middleware() {
1132
+ const scenes = this.#scenes;
1133
+ return async (ctx, next) => {
1134
+ const sceneCtrl = getOrCreateSceneController(ctx, scenes);
1135
+ const current = sceneCtrl.current;
1136
+ if (current) {
1137
+ const expires = ctx.session.__scenes?.expires;
1138
+ if (expires && Date.now() > expires) {
1139
+ await sceneCtrl.leave();
1140
+ return next();
1141
+ }
1142
+ await run(flatten(current), ctx);
1143
+ return;
1144
+ }
1145
+ await next();
1146
+ };
1147
+ }
1148
+ enter(sceneId) {
1149
+ return (ctx) => getOrCreateSceneController(ctx, this.#scenes).enter(sceneId);
1150
+ }
1151
+ leave() {
1152
+ return (ctx) => getOrCreateSceneController(ctx, this.#scenes).leave();
1153
+ }
1154
+ reenter() {
1155
+ return (ctx) => getOrCreateSceneController(ctx, this.#scenes).reenter();
1156
+ }
1157
+ static enter(sceneId) {
1158
+ return (ctx) => {
1159
+ const scene = ctx.scene;
1160
+ if (!scene) {
1161
+ throw new Error(`[cygnet] ctx.scene is not attached. Use a Stage instance helper (stage.enter("${sceneId}")) or register bot.use(stage) before this middleware.`);
1162
+ }
1163
+ return scene.enter(sceneId);
1164
+ };
1165
+ }
1166
+ static leave() {
1167
+ return (ctx) => {
1168
+ const scene = ctx.scene;
1169
+ if (!scene) {
1170
+ throw new Error("[cygnet] ctx.scene is not attached. Use a Stage instance helper (stage.leave()) or register bot.use(stage) before this middleware.");
1171
+ }
1172
+ return scene.leave();
1173
+ };
1174
+ }
1175
+ static reenter() {
1176
+ return (ctx) => {
1177
+ const scene = ctx.scene;
1178
+ if (!scene) {
1179
+ throw new Error("[cygnet] ctx.scene is not attached. Use a Stage instance helper (stage.reenter()) or register bot.use(stage) before this middleware.");
1180
+ }
1181
+ return scene.reenter();
1182
+ };
1183
+ }
1184
+ }
1185
+ function getOrCreateSceneController(ctx, scenes) {
1186
+ const flavored = ctx;
1187
+ if (flavored.scene)
1188
+ return flavored.scene;
1189
+ const sceneCtrl = createSceneController(ctx, scenes);
1190
+ flavored.scene = sceneCtrl;
1191
+ return sceneCtrl;
1192
+ }
1193
+ function createSceneController(ctx, scenes) {
1194
+ const ctrl = {
1195
+ get id() {
1196
+ return ctx.session.__scenes?.current;
1197
+ },
1198
+ get current() {
1199
+ const id = ctx.session.__scenes?.current;
1200
+ return id ? scenes.get(id) : undefined;
1201
+ },
1202
+ get state() {
1203
+ return ctx.session.__scenes?.state ?? {};
1204
+ },
1205
+ set state(val) {
1206
+ if (!ctx.session.__scenes)
1207
+ ctx.session.__scenes = {};
1208
+ ctx.session.__scenes.state = val;
1209
+ },
1210
+ async enter(sceneId, initialState) {
1211
+ const current = scenes.get(ctx.session.__scenes?.current ?? "");
1212
+ if (current) {
1213
+ await run(flatten(current.leaveMiddleware()), ctx);
1214
+ }
1215
+ const next = scenes.get(sceneId);
1216
+ if (!next)
1217
+ throw new Error(`[cygnet] Scene "${sceneId}" not found`);
1218
+ const ttl = next.ttl;
1219
+ ctx.session.__scenes = {
1220
+ current: sceneId,
1221
+ state: initialState ?? {},
1222
+ cursor: 0,
1223
+ expires: ttl ? Date.now() + ttl : undefined
1224
+ };
1225
+ await run(flatten(next.enterMiddleware()), ctx);
1226
+ },
1227
+ async leave() {
1228
+ const id = ctx.session.__scenes?.current;
1229
+ if (!id)
1230
+ return;
1231
+ const scene = scenes.get(id);
1232
+ if (scene) {
1233
+ await run(flatten(scene.leaveMiddleware()), ctx);
1234
+ }
1235
+ ctx.session.__scenes = undefined;
1236
+ },
1237
+ async reenter() {
1238
+ const id = ctx.session.__scenes?.current;
1239
+ if (!id)
1240
+ return;
1241
+ await ctrl.enter(id, ctrl.state);
1242
+ }
1243
+ };
1244
+ return ctrl;
1245
+ }
1246
+ // src/convenience/scenes/wizard.ts
1247
+ class WizardScene extends BaseScene {
1248
+ #steps;
1249
+ constructor(id, ...steps) {
1250
+ super(id);
1251
+ this.#steps = steps;
1252
+ this.use((ctx, next) => {
1253
+ const step = this.#currentStep(ctx);
1254
+ if (!step)
1255
+ return next();
1256
+ this.#attachWizard(ctx);
1257
+ if (typeof step === "function")
1258
+ return step(ctx, next);
1259
+ return step.middleware()(ctx, next);
1260
+ });
1261
+ }
1262
+ enterMiddleware() {
1263
+ const enter = super.enterMiddleware();
1264
+ return async (ctx, next) => {
1265
+ const runFirstStep = async () => {
1266
+ const step = this.#steps[0];
1267
+ if (!step) {
1268
+ await next();
1269
+ return;
1270
+ }
1271
+ this.#attachWizard(ctx);
1272
+ if (typeof step === "function") {
1273
+ await step(ctx, next);
1274
+ return;
1275
+ }
1276
+ await step.middleware()(ctx, next);
1277
+ };
1278
+ if (typeof enter === "function") {
1279
+ await enter(ctx, runFirstStep);
1280
+ return;
1281
+ }
1282
+ await enter.middleware()(ctx, runFirstStep);
1283
+ };
1284
+ }
1285
+ get stepCount() {
1286
+ return this.#steps.length;
1287
+ }
1288
+ #currentStep(ctx) {
1289
+ const cursor = ctx.scene.current?.id === this.id ? ctx.session.__scenes?.cursor ?? 0 : 0;
1290
+ return this.#steps[cursor];
1291
+ }
1292
+ #attachWizard(ctx) {
1293
+ const self = this;
1294
+ const wizard = {
1295
+ get cursor() {
1296
+ return ctx.session.__scenes?.cursor ?? 0;
1297
+ },
1298
+ selectStep(index) {
1299
+ if (!ctx.session.__scenes)
1300
+ ctx.session.__scenes = {};
1301
+ ctx.session.__scenes.cursor = Math.max(0, Math.min(index, self.#steps.length - 1));
1302
+ },
1303
+ async advance() {
1304
+ wizard.selectStep(wizard.cursor + 1);
1305
+ },
1306
+ async retreat() {
1307
+ wizard.selectStep(wizard.cursor - 1);
1308
+ },
1309
+ get state() {
1310
+ return ctx.session.__scenes?.state ?? {};
1311
+ },
1312
+ set state(val) {
1313
+ if (!ctx.session.__scenes)
1314
+ ctx.session.__scenes = {};
1315
+ ctx.session.__scenes.state = val;
1316
+ }
1317
+ };
1318
+ ctx.wizard = wizard;
1319
+ return wizard;
1320
+ }
1321
+ }
1322
+ export {
1323
+ snapshotGroupStateCache,
1324
+ session,
1325
+ run,
1326
+ restoreGroupStateCache,
1327
+ primeGroupStateCache,
1328
+ matchFilter,
1329
+ flatten,
1330
+ enhanceStorage,
1331
+ directStorageBrand,
1332
+ concat,
1333
+ WizardScene,
1334
+ WebSocketListener,
1335
+ Stage,
1336
+ SignalError,
1337
+ SignalAPI,
1338
+ PollingListener,
1339
+ MemoryStorage,
1340
+ HttpClient,
1341
+ FileStorage,
1342
+ Context,
1343
+ Composer,
1344
+ BotError,
1345
+ Bot,
1346
+ BaseScene
1347
+ };
1348
+
1349
+ //# debugId=5A36B56B282D103864756E2164756E21
1350
+ //# sourceMappingURL=index.js.map