@diegoaltoworks/chatter 0.26.0 → 0.27.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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-agnostic reply gates — pure and testable, zero imports from the
|
|
3
|
+
* rest of chatter. A transport (WhatsApp, or any future channel) resolves
|
|
4
|
+
* its own wire format into a {@link ChannelMessage} and calls
|
|
5
|
+
* {@link decideChannelAction} to learn whether to answer, stay quiet, or
|
|
6
|
+
* flip a mute switch. Nothing here knows about jids, Baileys, or any
|
|
7
|
+
* specific bot's name — mention/reply-to detection is the transport's job;
|
|
8
|
+
* this module only combines the already-resolved booleans with allowlist,
|
|
9
|
+
* mute, and rate-limit policy.
|
|
10
|
+
*/
|
|
11
|
+
/** The slice of an inbound message every channel can resolve into this shape. */
|
|
12
|
+
export interface ChannelMessage {
|
|
13
|
+
/** The conversation this message belongs to — a DM thread or a group. */
|
|
14
|
+
chatId: string;
|
|
15
|
+
senderId: string;
|
|
16
|
+
text: string;
|
|
17
|
+
/** True for a 1:1 conversation; false for a group/channel with other members. */
|
|
18
|
+
isDirectMessage: boolean;
|
|
19
|
+
/** Resolved by the caller: does this message @-mention the bot? */
|
|
20
|
+
mentionsBot: boolean;
|
|
21
|
+
/** Resolved by the caller: is this message a reply to one of the bot's own messages? */
|
|
22
|
+
isReplyToBot: boolean;
|
|
23
|
+
/** True when the bot itself sent this message (including via another session — see {@link isEffectivelyFromSelf}). */
|
|
24
|
+
fromBot: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type ChannelAction = "reply" | "ignore" | "mute" | "unmute";
|
|
27
|
+
export interface ChannelGateConfig {
|
|
28
|
+
/** Chats eligible for a reply. Empty = every chat is eligible (the master allowlist switch). */
|
|
29
|
+
allowedChats: string[];
|
|
30
|
+
/** Group chats currently muted — addressed messages there are ignored until unmuted. Has no effect on DMs, which always reply. */
|
|
31
|
+
mutedChats: Set<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Caller-supplied trigger for "go quiet in this group chat". No default:
|
|
34
|
+
* this module ships no bot name or in-character phrasing, so mute/unmute
|
|
35
|
+
* are inert unless a host configures a pattern. Never applied to DMs.
|
|
36
|
+
*/
|
|
37
|
+
muteRegex?: RegExp;
|
|
38
|
+
unmuteRegex?: RegExp;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* DMs always reply. In groups, only an allowlisted, unmuted chat gets a
|
|
42
|
+
* reply, and only when addressed (mentioned or replying to the bot) —
|
|
43
|
+
* merely mentioning the bot's name in third person is not an invitation.
|
|
44
|
+
* The unmute check runs before the muted-chat gate so an unmute phrase can
|
|
45
|
+
* un-silence the bot in the same message, and both run after the allowlist
|
|
46
|
+
* so a chat the bot isn't eligible for can't toggle its mute state.
|
|
47
|
+
*/
|
|
48
|
+
export declare function decideChannelAction(msg: ChannelMessage, config: ChannelGateConfig): ChannelAction;
|
|
49
|
+
/**
|
|
50
|
+
* Sliding-window rate limiter (pure, injectable clock): true = allowed and
|
|
51
|
+
* counted. One instance guards one budget — callers wanting separate DM and
|
|
52
|
+
* group budgets create two (see {@link underReplyRateLimit}).
|
|
53
|
+
*/
|
|
54
|
+
export declare function createSlidingWindowRateLimiter(maxPerWindow: number, windowMs: number, now?: () => number): (key: string) => boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Routes a chat to its DM or group rate limiter so a flood in one can't
|
|
57
|
+
* spend the other's budget — a DM flood used to be entirely unthrottled
|
|
58
|
+
* before this split existed. Each limiter is expected to be its own
|
|
59
|
+
* {@link createSlidingWindowRateLimiter} instance.
|
|
60
|
+
*/
|
|
61
|
+
export declare function underReplyRateLimit(msg: Pick<ChannelMessage, "chatId" | "isDirectMessage">, limiters: {
|
|
62
|
+
dm: (chatId: string) => boolean;
|
|
63
|
+
group: (chatId: string) => boolean;
|
|
64
|
+
}): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Every session's own identities ever seen, keyed by session id. A
|
|
67
|
+
* multi-session deployment (e.g. two linked numbers in one process) gives
|
|
68
|
+
* each session only its own identity from its transport — a message that
|
|
69
|
+
* actually came from a *different* session's identity looks like a
|
|
70
|
+
* stranger, not "us", and the two sessions can ping-pong replies at each
|
|
71
|
+
* other indefinitely. Callers populate this directly (`registry.set(sessionId, ids)`)
|
|
72
|
+
* as each session's identities become known, typically on connect and kept
|
|
73
|
+
* in sync as they change.
|
|
74
|
+
*
|
|
75
|
+
* Entries are NOT expected to be removed when a session disconnects: a
|
|
76
|
+
* session's own identity is still "us" while it reconnects, and losing that
|
|
77
|
+
* during exactly the disconnect/reconnect window this guard exists for
|
|
78
|
+
* would defeat it.
|
|
79
|
+
*/
|
|
80
|
+
export type SessionIdentityRegistry = Map<string, string[]>;
|
|
81
|
+
/** True when `id` matches an identity of ANY session ever registered, not just the one that received the message. */
|
|
82
|
+
export declare function isFromAnySession(id: string | undefined, registry: SessionIdentityRegistry): boolean;
|
|
83
|
+
/**
|
|
84
|
+
* The one predicate transports need before deciding fromBot: a message is
|
|
85
|
+
* effectively from the bot itself if the transport already flagged it so,
|
|
86
|
+
* or if its sender id matches an identity registered by any session (the
|
|
87
|
+
* loop-guard case a single session's own flag can't catch).
|
|
88
|
+
*/
|
|
89
|
+
export declare function isEffectivelyFromSelf(msg: Pick<ChannelMessage, "fromBot" | "senderId">, registry: SessionIdentityRegistry): boolean;
|
|
90
|
+
//# sourceMappingURL=gates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../../src/channels/gates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,eAAe,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,wFAAwF;IACxF,YAAY,EAAE,OAAO,CAAC;IACtB,sHAAsH;IACtH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEnE,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,kIAAkI;IAClI,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,GAAG,aAAa,CAYjG;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAM,MAAiB,SAGD,MAAM,KAAG,OAAO,CAY5C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,iBAAiB,CAAC,EACvD,QAAQ,EAAE;IAAE,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAAC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,GAChF,OAAO,CAET;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,uBAAuB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAE5D,qHAAqH;AACrH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAMT;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,SAAS,GAAG,UAAU,CAAC,EACjD,QAAQ,EAAE,uBAAuB,GAChC,OAAO,CAET"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gates.test.d.ts","sourceRoot":"","sources":["../../src/channels/gates.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED