@effect-uai/discord 0.14.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/Discord.d.mts +55 -0
  4. package/dist/Discord.d.mts.map +1 -0
  5. package/dist/Discord.mjs +196 -0
  6. package/dist/Discord.mjs.map +1 -0
  7. package/dist/events-Du1u-kVV.d.mts +172 -0
  8. package/dist/events-Du1u-kVV.d.mts.map +1 -0
  9. package/dist/gateway-C_Ae1HYB.d.mts +57 -0
  10. package/dist/gateway-C_Ae1HYB.d.mts.map +1 -0
  11. package/dist/index.d.mts +2 -0
  12. package/dist/index.mjs +2 -0
  13. package/dist/internal/events.d.mts +2 -0
  14. package/dist/internal/events.mjs +109 -0
  15. package/dist/internal/events.mjs.map +1 -0
  16. package/dist/internal/events.test.d.mts +1 -0
  17. package/dist/internal/events.test.mjs +130 -0
  18. package/dist/internal/events.test.mjs.map +1 -0
  19. package/dist/internal/gateway.d.mts +2 -0
  20. package/dist/internal/gateway.mjs +216 -0
  21. package/dist/internal/gateway.mjs.map +1 -0
  22. package/dist/internal/gateway.test.d.mts +1 -0
  23. package/dist/internal/gateway.test.mjs +40 -0
  24. package/dist/internal/gateway.test.mjs.map +1 -0
  25. package/dist/internal/rest.d.mts +2 -0
  26. package/dist/internal/rest.mjs +89 -0
  27. package/dist/internal/rest.mjs.map +1 -0
  28. package/dist/magic-string.es-CUAnjKYC.mjs +1014 -0
  29. package/dist/magic-string.es-CUAnjKYC.mjs.map +1 -0
  30. package/dist/rest-CZSW-VJK.d.mts +44 -0
  31. package/dist/rest-CZSW-VJK.d.mts.map +1 -0
  32. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  33. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs +13658 -0
  34. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
  35. package/package.json +59 -0
  36. package/src/Discord.ts +393 -0
  37. package/src/index.ts +1 -0
  38. package/src/internal/events.test.ts +89 -0
  39. package/src/internal/events.ts +197 -0
  40. package/src/internal/gateway.test.ts +27 -0
  41. package/src/internal/gateway.ts +368 -0
  42. package/src/internal/rest.ts +164 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Betalyra Sociedade Unipessoal Lda.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @effect-uai/discord
2
+
3
+ Discord provider for [`@effect-uai/core`](https://www.npmjs.com/package/@effect-uai/core).
4
+
5
+ Implements the `Messenger` contract over the gateway and the v10 REST API, so
6
+ your agent runs as a Discord bot: DMs, channel mentions, threads and button
7
+ presses in, streamed markdown replies, media and reactions out. One gateway
8
+ websocket, so no public URL, and no discord.js.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pnpm add @effect-uai/discord @effect-uai/core effect
14
+ ```
15
+
16
+ ESM-only. Requires `effect@4.x` and `@effect-uai/core` as peers.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { Effect, Redacted } from "effect"
22
+ import { NodeHttpClient } from "@effect/platform-node"
23
+ import { layer as discordLayer } from "@effect-uai/discord/Discord"
24
+
25
+ const provider = discordLayer({ token: Redacted.make(process.env.DISCORD_BOT_TOKEN!) })
26
+
27
+ await Effect.runPromise(
28
+ program.pipe(Effect.scoped, Effect.provide(provider), Effect.provide(NodeHttpClient.layerUndici)),
29
+ )
30
+ ```
31
+
32
+ The layer registers both the provider-typed `Discord` tag and the generic
33
+ `Messenger` tag. Building it waits for Discord's `READY`, so a bad token
34
+ fails at wiring time; the session stays live until the scope closes.
35
+
36
+ ## Intents
37
+
38
+ The default intents cover DMs and mentions. Reading every channel message
39
+ needs the privileged Message Content intent:
40
+ `intents: Discord.defaultIntents | Discord.Intents.MessageContent`.
41
+
42
+ ## Docs
43
+
44
+ <https://effect-uai.betalyra.com/messenger/providers/discord/>
45
+
46
+ ## License
47
+
48
+ MIT
@@ -0,0 +1,55 @@
1
+ import { t as BotIdentity } from "./events-Du1u-kVV.mjs";
2
+ import { r as Config$1 } from "./rest-CZSW-VJK.mjs";
3
+ import { s as classifyClose, t as CloseAction } from "./gateway-C_Ae1HYB.mjs";
4
+ import { Context, Effect, Layer, Scope } from "effect";
5
+ import { HttpClient } from "effect/unstable/http";
6
+ import { Messenger, MessengerLimits, MessengerService } from "@effect-uai/core/Messenger";
7
+ import { StreamViaEditsOptions } from "@effect-uai/core/MessengerAdapter";
8
+ import * as MessengerError from "@effect-uai/core/MessengerError";
9
+ declare namespace Discord_d_exports {
10
+ export { BotIdentity, CloseAction, Config, Discord, DiscordService, Intents, classifyClose, defaultIntents, layer, limits, make };
11
+ }
12
+ /** Gateway intent bits. `MessageContent` is the one Discord gates behind review. */
13
+ declare const Intents: {
14
+ readonly Guilds: number;
15
+ readonly GuildMessages: number;
16
+ readonly GuildMessageReactions: number;
17
+ readonly DirectMessages: number;
18
+ readonly DirectMessageReactions: number;
19
+ readonly MessageContent: number;
20
+ };
21
+ /**
22
+ * What a mention-or-DM bot needs. Even without the privileged
23
+ * `MessageContent` intent, `content` arrives for DMs and for messages that
24
+ * mention the bot, which is exactly the set `addressed` covers.
25
+ */
26
+ declare const defaultIntents: number;
27
+ type Config = Config$1 & {
28
+ /** Gateway intents mask. Defaults to {@link defaultIntents}. */
29
+ readonly intents?: number;
30
+ /** Tuning for `stream`, which is post-then-edit on Discord. */
31
+ readonly stream?: StreamViaEditsOptions;
32
+ };
33
+ type DiscordService = MessengerService & {
34
+ /** From `READY` at layer build: the identity behind `addressed`. */
35
+ readonly bot: BotIdentity;
36
+ };
37
+ declare const Discord_base: Context.ServiceClass<Discord, "@betalyra/effect-uai/providers/discord/Discord", DiscordService>;
38
+ /**
39
+ * Provider-typed tag. Yield this for the bot identity; yield the generic
40
+ * `Messenger` tag for provider-portable code. Both are registered by {@link layer}.
41
+ */
42
+ declare class Discord extends Discord_base {}
43
+ declare const limits: MessengerLimits;
44
+ declare const make: (cfg: Config) => Effect.Effect<DiscordService, MessengerError.MessengerConnectFailed, HttpClient.HttpClient | Scope.Scope>;
45
+ /**
46
+ * One gateway session, registered under both the `Discord` and `Messenger`
47
+ * tags. Building the layer identifies and waits for `READY`, so a bad token
48
+ * or an intent the portal has not granted fails here; the connection lives
49
+ * until the scope closes and `events` ends with it. Run one instance per
50
+ * bot token.
51
+ */
52
+ declare const layer: (cfg: Config) => Layer.Layer<Discord | Messenger, MessengerError.MessengerConnectFailed, HttpClient.HttpClient>;
53
+ //#endregion
54
+ export { type BotIdentity, type CloseAction, Config, Discord, DiscordService, Intents, classifyClose, defaultIntents, layer, limits, make, Discord_d_exports as t };
55
+ //# sourceMappingURL=Discord.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Discord.d.mts","names":[],"sources":["../src/Discord.ts"],"mappings":";;;;;;;;;;;;cA8Ca;WACX;WACA;WACA;WACA;WACA;WACA;;;;;;;cAQW;KAOD,SAAS;;WAEV;;WAEA,SAAS;;KAGR,iBAAiB;;WAElB,KAAK;;;;;;;cAOH,gBAAgB;cAIhB,QAAQ;cAwFR,OAAI,KACV,WACJ,OAAO,OACR,gBACA,eAAe,wBACf,WAAW,aAAa,MAAM;;;;;;;;cA6MnB,QAAK,KACX,WACJ,MAAM,MAAM,UAAU,WAAW,eAAe,wBAAwB,WAAW"}
@@ -0,0 +1,196 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { toEvents } from "./internal/events.mjs";
3
+ import { call, decoded, provider, upload } from "./internal/rest.mjs";
4
+ import { GatewayInfo, classifyClose, connect } from "./internal/gateway.mjs";
5
+ import { Array, Context, Effect, Encoding, Layer, Match, Option, Queue, Schedule, Schema, Stream } from "effect";
6
+ import { HttpClient } from "effect/unstable/http";
7
+ import { CurrentConversation, MessageId, Messenger } from "@effect-uai/core/Messenger";
8
+ import { splitForLimit, streamViaEdits } from "@effect-uai/core/MessengerAdapter";
9
+ import * as MessengerError from "@effect-uai/core/MessengerError";
10
+ //#region src/Discord.ts
11
+ var Discord_exports = /* @__PURE__ */ __exportAll({
12
+ Discord: () => Discord,
13
+ Intents: () => Intents,
14
+ classifyClose: () => classifyClose,
15
+ defaultIntents: () => defaultIntents,
16
+ layer: () => layer,
17
+ limits: () => limits,
18
+ make: () => make
19
+ });
20
+ /** Gateway intent bits. `MessageContent` is the one Discord gates behind review. */
21
+ const Intents = {
22
+ Guilds: 1,
23
+ GuildMessages: 512,
24
+ GuildMessageReactions: 1024,
25
+ DirectMessages: 4096,
26
+ DirectMessageReactions: 8192,
27
+ MessageContent: 32768
28
+ };
29
+ /**
30
+ * What a mention-or-DM bot needs. Even without the privileged
31
+ * `MessageContent` intent, `content` arrives for DMs and for messages that
32
+ * mention the bot, which is exactly the set `addressed` covers.
33
+ */
34
+ const defaultIntents = Intents.Guilds | Intents.GuildMessages | Intents.GuildMessageReactions | Intents.DirectMessages | Intents.DirectMessageReactions;
35
+ /**
36
+ * Provider-typed tag. Yield this for the bot identity; yield the generic
37
+ * `Messenger` tag for provider-portable code. Both are registered by {@link layer}.
38
+ */
39
+ var Discord = class extends Context.Service()("@betalyra/effect-uai/providers/discord/Discord") {};
40
+ const limits = {
41
+ maxText: 2e3,
42
+ maxCaption: 2e3
43
+ };
44
+ const defaultStream = { every: "1200 millis" };
45
+ const DEFERRED_UPDATE = 6;
46
+ const Me = Schema.Struct({
47
+ id: Schema.String,
48
+ username: Schema.String
49
+ });
50
+ const Sent = Schema.Struct({ id: Schema.String });
51
+ const RawCall = Schema.Struct({
52
+ method: Schema.Literals([
53
+ "GET",
54
+ "POST",
55
+ "PATCH",
56
+ "PUT",
57
+ "DELETE"
58
+ ]),
59
+ path: Schema.String,
60
+ body: Schema.optional(Schema.Unknown)
61
+ });
62
+ const isApiFailure = (e) => e._tag === "DiscordApiFailure";
63
+ const describes = (needle) => (e) => isApiFailure(e) && e.message.includes(needle);
64
+ const reasonOf = (e) => isApiFailure(e) ? e.message : "rate limited";
65
+ /** An API rejection becomes the verb's `MessengerRequestFailed`; typed errors pass through. */
66
+ const requestFailed = (operation) => (e) => Match.value(e).pipe(Match.tag("DiscordApiFailure", (failure) => new MessengerError.MessengerRequestFailed({
67
+ provider,
68
+ operation,
69
+ reason: failure.message,
70
+ raw: failure.raw
71
+ })), Match.orElse((typed) => typed));
72
+ const unsupported = (capability, reason) => new MessengerError.MessengerUnsupported({
73
+ provider,
74
+ capability,
75
+ reason
76
+ });
77
+ const noMentions = { allowed_mentions: { parse: [] } };
78
+ const replyFields = (msg) => msg.replyTo === void 0 ? {} : { message_reference: {
79
+ message_id: msg.replyTo,
80
+ fail_if_not_exists: false
81
+ } };
82
+ const messages = (at) => `/channels/${at.channel}/messages`;
83
+ const captionField = (caption) => caption === void 0 ? {} : { content: caption };
84
+ const defaultFilename = (media) => `file.${media.mimeType?.split("/")[1] ?? "bin"}`;
85
+ const invalidMedia = (raw) => new MessengerError.MessengerRequestFailed({
86
+ provider,
87
+ operation: "post",
88
+ reason: "invalid base64 media",
89
+ raw
90
+ });
91
+ const make = (cfg) => Effect.gen(function* () {
92
+ const client = yield* HttpClient.HttpClient;
93
+ const withClient = Effect.provideService(HttpClient.HttpClient, client);
94
+ const call$1 = (method, path, body) => withClient(call(cfg)(method, path, body));
95
+ const upload$1 = (path, payload, file) => withClient(upload(cfg)(path, payload, file));
96
+ const connectFailed = (e) => new MessengerError.MessengerConnectFailed({
97
+ provider,
98
+ reason: reasonOf(e),
99
+ raw: isApiFailure(e) ? e.raw : e
100
+ });
101
+ yield* call$1("GET", "/users/@me").pipe(decoded("GET /users/@me", Me), Effect.mapError(connectFailed));
102
+ const { url } = yield* call$1("GET", "/gateway/bot").pipe(decoded("GET /gateway/bot", GatewayInfo), Effect.mapError(connectFailed));
103
+ const session = yield* connect({
104
+ token: cfg.token,
105
+ intents: cfg.intents ?? defaultIntents,
106
+ url
107
+ });
108
+ const toEvents$1 = toEvents(session.bot);
109
+ const inbox = yield* Queue.unbounded();
110
+ const acknowledge = (dispatch) => Match.value(dispatch).pipe(Match.when({ t: "INTERACTION_CREATE" }, ({ d }) => d.type === 3 ? call$1("POST", `/interactions/${d.id}/${d.token}/callback`, { type: DEFERRED_UPDATE }).pipe(Effect.ignore) : Effect.void), Match.orElse(() => Effect.void));
111
+ yield* Stream.fromQueue(session.dispatches).pipe(Stream.runForEach(({ dispatch, raw }) => acknowledge(dispatch).pipe(Effect.andThen(Queue.offerAll(inbox, toEvents$1(dispatch, raw))))), Effect.catch((closed) => Queue.fail(inbox, closed)), Effect.ensuring(Queue.end(inbox)), Effect.forkScoped);
112
+ const sendRaw = (payload) => Effect.gen(function* () {
113
+ const { method, path, body } = yield* Schema.decodeUnknownEffect(RawCall)(payload).pipe(Effect.mapError(() => new MessengerError.MessengerRequestFailed({
114
+ provider,
115
+ operation: "post",
116
+ reason: "raw payload must be { method, path, body? }",
117
+ raw: payload
118
+ })));
119
+ return yield* call$1(method, path, body);
120
+ });
121
+ const sendUrl = (at, fields, body, url) => call$1("POST", messages(at), {
122
+ ...fields,
123
+ content: Array.getSomes([Option.fromNullishOr(body.caption), Option.some(url)]).join("\n")
124
+ });
125
+ const sendMedia = (at, msg, body) => {
126
+ const fields = {
127
+ ...noMentions,
128
+ ...replyFields(msg)
129
+ };
130
+ const send = (bytes, mimeType) => upload$1(messages(at), {
131
+ ...fields,
132
+ ...captionField(body.caption)
133
+ }, {
134
+ bytes,
135
+ filename: body.filename ?? defaultFilename(body.media),
136
+ mimeType
137
+ });
138
+ return Match.value(body.media).pipe(Match.tag("url", ({ url }) => sendUrl(at, fields, body, url)), Match.tag("bytes", ({ bytes, mimeType }) => send(bytes, mimeType)), Match.tag("base64", ({ base64, mimeType }) => Effect.fromResult(Encoding.decodeBase64(base64)).pipe(Effect.mapError(invalidMedia), Effect.flatMap((bytes) => send(bytes, mimeType)))), Match.exhaustive);
139
+ };
140
+ const sendChunks = (at, msg, text) => Effect.gen(function* () {
141
+ const [first, ...rest] = splitForLimit(text, limits.maxText);
142
+ const send = (chunk, extra) => call$1("POST", messages(at), {
143
+ ...noMentions,
144
+ ...extra,
145
+ content: chunk
146
+ }).pipe(decoded("POST message", Sent));
147
+ const head = yield* send(first, replyFields(msg));
148
+ const tail = yield* Effect.forEach(rest, (chunk) => send(chunk, {}));
149
+ return Option.getOrElse(Array.last(tail), () => head);
150
+ });
151
+ const post = (msg) => Effect.gen(function* () {
152
+ const at = yield* CurrentConversation;
153
+ const sent = yield* Match.value(msg.body).pipe(Match.tag("Text", ({ text }) => sendChunks(at, msg, text)), Match.tag("Media", (body) => sendMedia(at, msg, body).pipe(decoded("POST media", Sent))), Match.tag("Raw", ({ payload }) => sendRaw(payload).pipe(decoded("raw", Sent))), Match.exhaustive);
154
+ return MessageId(sent.id);
155
+ }).pipe(Effect.mapError(requestFailed("post")));
156
+ const edit = (ref, next) => Match.value(next.body).pipe(Match.tag("Text", ({ text }) => call$1("PATCH", `${messages(ref.conversation)}/${ref.id}`, {
157
+ ...noMentions,
158
+ content: text
159
+ }).pipe(Effect.asVoid, Effect.mapError(requestFailed("edit")))), Match.tag("Raw", ({ payload }) => sendRaw(payload).pipe(Effect.asVoid, Effect.mapError(requestFailed("edit")))), Match.tag("Media", () => Effect.fail(unsupported("media edits", "send a new message instead"))), Match.exhaustive);
160
+ const react = (ref, emoji) => call$1("PUT", `${messages(ref.conversation)}/${ref.id}/reactions/${encodeURIComponent(emoji)}/@me`).pipe(Effect.asVoid, Effect.catchIf(describes("Unknown Emoji"), () => Effect.fail(unsupported("reaction", `${emoji} is not an emoji Discord knows`))), Effect.mapError(requestFailed("react")));
161
+ const typing = Effect.gen(function* () {
162
+ const at = yield* CurrentConversation;
163
+ const once = call$1("POST", `/channels/${at.channel}/typing`);
164
+ yield* once.pipe(Effect.mapError(requestFailed("typing")));
165
+ yield* once.pipe(Effect.ignore, Effect.schedule(Schedule.spaced("8 seconds")), Effect.forkScoped);
166
+ });
167
+ return {
168
+ bot: session.bot,
169
+ events: Stream.fromQueue(inbox),
170
+ post,
171
+ edit,
172
+ react,
173
+ typing,
174
+ stream: streamViaEdits({
175
+ post,
176
+ edit,
177
+ limits
178
+ }, {
179
+ ...defaultStream,
180
+ ...cfg.stream
181
+ }),
182
+ limits
183
+ };
184
+ });
185
+ /**
186
+ * One gateway session, registered under both the `Discord` and `Messenger`
187
+ * tags. Building the layer identifies and waits for `READY`, so a bad token
188
+ * or an intent the portal has not granted fails here; the connection lives
189
+ * until the scope closes and `events` ends with it. Run one instance per
190
+ * bot token.
191
+ */
192
+ const layer = (cfg) => Layer.effectContext(Effect.map(make(cfg), (service) => Context.make(Discord, service).pipe(Context.add(Messenger, service))));
193
+ //#endregion
194
+ export { Discord, Intents, classifyClose, defaultIntents, layer, limits, make, Discord_exports as t };
195
+
196
+ //# sourceMappingURL=Discord.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Discord.mjs","names":["Rest.provider","call","Rest.call","upload","Rest.upload","Rest.decoded","Gateway.GatewayInfo","Gateway.connect","toEvents","Events.toEvents","Arr"],"sources":["../src/Discord.ts"],"sourcesContent":["import {\n Array as Arr,\n type Cause,\n Context,\n Effect,\n Encoding,\n Layer,\n Match,\n Option,\n Queue,\n Schedule,\n Schema,\n type Scope,\n Stream,\n} from \"effect\"\nimport { HttpClient } from \"effect/unstable/http\"\nimport type { MediaSource } from \"@effect-uai/core/Media\"\nimport {\n CurrentConversation,\n type ConversationRef,\n type InboundEvent,\n MessageId,\n Messenger,\n type MessengerLimits,\n type MessengerService,\n type Outbound,\n type OutboundBody,\n} from \"@effect-uai/core/Messenger\"\nimport {\n type StreamViaEditsOptions,\n splitForLimit,\n streamViaEdits,\n} from \"@effect-uai/core/MessengerAdapter\"\nimport * as MessengerError from \"@effect-uai/core/MessengerError\"\nimport * as Events from \"./internal/events.js\"\nimport * as Gateway from \"./internal/gateway.js\"\nimport * as Rest from \"./internal/rest.js\"\n\nexport type { BotIdentity } from \"./internal/events.js\"\nexport { classifyClose, type CloseAction } from \"./internal/gateway.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** Gateway intent bits. `MessageContent` is the one Discord gates behind review. */\nexport const Intents = {\n Guilds: 1 << 0,\n GuildMessages: 1 << 9,\n GuildMessageReactions: 1 << 10,\n DirectMessages: 1 << 12,\n DirectMessageReactions: 1 << 13,\n MessageContent: 1 << 15,\n} as const\n\n/**\n * What a mention-or-DM bot needs. Even without the privileged\n * `MessageContent` intent, `content` arrives for DMs and for messages that\n * mention the bot, which is exactly the set `addressed` covers.\n */\nexport const defaultIntents: number =\n Intents.Guilds |\n Intents.GuildMessages |\n Intents.GuildMessageReactions |\n Intents.DirectMessages |\n Intents.DirectMessageReactions\n\nexport type Config = Rest.Config & {\n /** Gateway intents mask. Defaults to {@link defaultIntents}. */\n readonly intents?: number\n /** Tuning for `stream`, which is post-then-edit on Discord. */\n readonly stream?: StreamViaEditsOptions\n}\n\nexport type DiscordService = MessengerService & {\n /** From `READY` at layer build: the identity behind `addressed`. */\n readonly bot: Events.BotIdentity\n}\n\n/**\n * Provider-typed tag. Yield this for the bot identity; yield the generic\n * `Messenger` tag for provider-portable code. Both are registered by {@link layer}.\n */\nexport class Discord extends Context.Service<Discord, DiscordService>()(\n \"@betalyra/effect-uai/providers/discord/Discord\",\n) {}\n\nexport const limits: MessengerLimits = { maxText: 2000, maxCaption: 2000 }\n\n// Under the observed five-edits-per-five-seconds-per-channel bucket.\nconst defaultStream: StreamViaEditsOptions = { every: \"1200 millis\" }\n\n// A deferred update: the interaction is answered, nothing changes on screen.\nconst DEFERRED_UPDATE = 6\n\n// ---------------------------------------------------------------------------\n// Wire results\n// ---------------------------------------------------------------------------\n\nconst Me = Schema.Struct({ id: Schema.String, username: Schema.String })\nconst Sent = Schema.Struct({ id: Schema.String })\nconst RawCall = Schema.Struct({\n method: Schema.Literals([\"GET\", \"POST\", \"PATCH\", \"PUT\", \"DELETE\"]),\n path: Schema.String,\n body: Schema.optional(Schema.Unknown),\n})\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\nconst isApiFailure = (e: Rest.ApiError): e is Rest.ApiFailure => e._tag === \"DiscordApiFailure\"\n\nconst describes = (needle: string) => (e: Rest.ApiError) =>\n isApiFailure(e) && e.message.includes(needle)\n\nconst reasonOf = (e: Rest.ApiError): string => (isApiFailure(e) ? e.message : \"rate limited\")\n\n/** An API rejection becomes the verb's `MessengerRequestFailed`; typed errors pass through. */\nconst requestFailed =\n (operation: MessengerError.MessengerOperation) =>\n (e: Rest.ApiError | MessengerError.MessengerError): MessengerError.MessengerError =>\n Match.value(e).pipe(\n Match.tag(\n \"DiscordApiFailure\",\n (failure) =>\n new MessengerError.MessengerRequestFailed({\n provider: Rest.provider,\n operation,\n reason: failure.message,\n raw: failure.raw,\n }),\n ),\n Match.orElse((typed) => typed),\n )\n\nconst unsupported = (capability: string, reason: string) =>\n new MessengerError.MessengerUnsupported({ provider: Rest.provider, capability, reason })\n\n// ---------------------------------------------------------------------------\n// Outbound params\n// ---------------------------------------------------------------------------\n\ntype Fields = Rest.Fields\n\ntype MediaFields = Extract<OutboundBody, { _tag: \"Media\" }>\n\n// Model output must never be able to ping anyone, so nothing is ever parsed.\nconst noMentions: Fields = { allowed_mentions: { parse: [] } }\n\nconst replyFields = (msg: Outbound): Fields =>\n msg.replyTo === undefined\n ? {}\n : { message_reference: { message_id: msg.replyTo, fail_if_not_exists: false } }\n\nconst messages = (at: ConversationRef): string => `/channels/${at.channel}/messages`\n\nconst captionField = (caption: string | undefined): Fields =>\n caption === undefined ? {} : { content: caption }\n\nconst defaultFilename = (media: MediaSource): string =>\n `file.${media.mimeType?.split(\"/\")[1] ?? \"bin\"}`\n\nconst invalidMedia = (raw: unknown) =>\n new MessengerError.MessengerRequestFailed({\n provider: Rest.provider,\n operation: \"post\",\n reason: \"invalid base64 media\",\n raw,\n })\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<\n DiscordService,\n MessengerError.MessengerConnectFailed,\n HttpClient.HttpClient | Scope.Scope\n> =>\n Effect.gen(function* () {\n // Captured once so the verbs need no `HttpClient` of their own.\n const client = yield* HttpClient.HttpClient\n const withClient = Effect.provideService(HttpClient.HttpClient, client)\n const call = (method: Rest.Method, path: string, body?: unknown) =>\n withClient(Rest.call(cfg)(method, path, body))\n const upload = (path: string, payload: Fields, file: Rest.Upload) =>\n withClient(Rest.upload(cfg)(path, payload, file))\n\n const connectFailed = (e: Rest.ApiError) =>\n new MessengerError.MessengerConnectFailed({\n provider: Rest.provider,\n reason: reasonOf(e),\n raw: isApiFailure(e) ? e.raw : e,\n })\n\n // A bad token fails here rather than as a gateway close a moment later.\n yield* call(\"GET\", \"/users/@me\").pipe(\n Rest.decoded(\"GET /users/@me\", Me),\n Effect.mapError(connectFailed),\n )\n const { url } = yield* call(\"GET\", \"/gateway/bot\").pipe(\n Rest.decoded(\"GET /gateway/bot\", Gateway.GatewayInfo),\n Effect.mapError(connectFailed),\n )\n\n // -- inbound -----------------------------------------------------------\n\n const session = yield* Gateway.connect({\n token: cfg.token,\n intents: cfg.intents ?? defaultIntents,\n url,\n })\n const toEvents = Events.toEvents(session.bot)\n const inbox = yield* Queue.unbounded<InboundEvent, MessengerError.MessengerError | Cause.Done>()\n\n // Discord drops a component interaction that goes unanswered for three\n // seconds, so it is acknowledged in the reader fiber rather than by the\n // recipe, the twin of Telegram's `answerCallbackQuery`.\n const acknowledge = (dispatch: Events.Dispatch) =>\n Match.value(dispatch).pipe(\n Match.when({ t: \"INTERACTION_CREATE\" }, ({ d }) =>\n d.type === Events.MESSAGE_COMPONENT\n ? call(\"POST\", `/interactions/${d.id}/${d.token}/callback`, {\n type: DEFERRED_UPDATE,\n }).pipe(Effect.ignore)\n : Effect.void,\n ),\n Match.orElse(() => Effect.void),\n )\n\n yield* Stream.fromQueue(session.dispatches).pipe(\n Stream.runForEach(({ dispatch, raw }) =>\n acknowledge(dispatch).pipe(Effect.andThen(Queue.offerAll(inbox, toEvents(dispatch, raw)))),\n ),\n Effect.catch((closed) => Queue.fail(inbox, closed)),\n Effect.ensuring(Queue.end(inbox)),\n Effect.forkScoped,\n )\n\n // -- outbound ----------------------------------------------------------\n\n const sendRaw = (payload: unknown) =>\n Effect.gen(function* () {\n const { method, path, body } = yield* Schema.decodeUnknownEffect(RawCall)(payload).pipe(\n Effect.mapError(\n () =>\n new MessengerError.MessengerRequestFailed({\n provider: Rest.provider,\n operation: \"post\",\n reason: \"raw payload must be { method, path, body? }\",\n raw: payload,\n }),\n ),\n )\n return yield* call(method, path, body)\n })\n\n // Discord has no attachment-by-URL, so a URL goes as content and Discord\n // unfurls it. Not as an embed: embeds need the Embed Links permission and\n // are silently stripped without it, which Discord then rejects as an\n // empty message.\n const sendUrl = (at: ConversationRef, fields: Fields, body: MediaFields, url: string) =>\n call(\"POST\", messages(at), {\n ...fields,\n content: Arr.getSomes([Option.fromNullishOr(body.caption), Option.some(url)]).join(\"\\n\"),\n })\n\n const sendMedia = (at: ConversationRef, msg: Outbound, body: MediaFields) => {\n const fields = { ...noMentions, ...replyFields(msg) }\n const send = (bytes: Uint8Array, mimeType: string) =>\n upload(\n messages(at),\n { ...fields, ...captionField(body.caption) },\n { bytes, filename: body.filename ?? defaultFilename(body.media), mimeType },\n )\n return Match.value(body.media).pipe(\n Match.tag(\"url\", ({ url }) => sendUrl(at, fields, body, url)),\n Match.tag(\"bytes\", ({ bytes, mimeType }) => send(bytes, mimeType)),\n Match.tag(\"base64\", ({ base64, mimeType }) =>\n Effect.fromResult(Encoding.decodeBase64(base64)).pipe(\n Effect.mapError(invalidMedia),\n Effect.flatMap((bytes) => send(bytes, mimeType)),\n ),\n ),\n Match.exhaustive,\n )\n }\n\n // The reply lands on the first chunk; the id is the last one's.\n const sendChunks = (at: ConversationRef, msg: Outbound, text: string) =>\n Effect.gen(function* () {\n const [first, ...rest] = splitForLimit(text, limits.maxText)\n const send = (chunk: string, extra: Fields) =>\n call(\"POST\", messages(at), { ...noMentions, ...extra, content: chunk }).pipe(\n Rest.decoded(\"POST message\", Sent),\n )\n const head = yield* send(first, replyFields(msg))\n const tail = yield* Effect.forEach(rest, (chunk) => send(chunk, {}))\n return Option.getOrElse(Arr.last(tail), () => head)\n })\n\n const post: MessengerService[\"post\"] = (msg) =>\n Effect.gen(function* () {\n const at = yield* CurrentConversation\n const sent = yield* Match.value(msg.body).pipe(\n Match.tag(\"Text\", ({ text }) => sendChunks(at, msg, text)),\n Match.tag(\"Media\", (body) =>\n sendMedia(at, msg, body).pipe(Rest.decoded(\"POST media\", Sent)),\n ),\n Match.tag(\"Raw\", ({ payload }) => sendRaw(payload).pipe(Rest.decoded(\"raw\", Sent))),\n Match.exhaustive,\n )\n return MessageId(sent.id)\n }).pipe(Effect.mapError(requestFailed(\"post\")))\n\n const edit: MessengerService[\"edit\"] = (ref, next) =>\n Match.value(next.body).pipe(\n Match.tag(\"Text\", ({ text }) =>\n call(\"PATCH\", `${messages(ref.conversation)}/${ref.id}`, {\n ...noMentions,\n content: text,\n }).pipe(Effect.asVoid, Effect.mapError(requestFailed(\"edit\"))),\n ),\n Match.tag(\"Raw\", ({ payload }) =>\n sendRaw(payload).pipe(Effect.asVoid, Effect.mapError(requestFailed(\"edit\"))),\n ),\n Match.tag(\"Media\", () =>\n Effect.fail(unsupported(\"media edits\", \"send a new message instead\")),\n ),\n Match.exhaustive,\n )\n\n // Unicode emoji go URL-encoded and custom ones as `name:id`, which is the\n // same spelling a `Reaction` event delivers.\n const react: MessengerService[\"react\"] = (ref, emoji) =>\n call(\n \"PUT\",\n `${messages(ref.conversation)}/${ref.id}/reactions/${encodeURIComponent(emoji)}/@me`,\n ).pipe(\n Effect.asVoid,\n Effect.catchIf(describes(\"Unknown Emoji\"), () =>\n Effect.fail(unsupported(\"reaction\", `${emoji} is not an emoji Discord knows`)),\n ),\n Effect.mapError(requestFailed(\"react\")),\n )\n\n // The indicator lasts ten seconds, so it is re-sent every eight until the\n // scope closes. The first send is awaited so a bad channel fails here.\n const typing: MessengerService[\"typing\"] = Effect.gen(function* () {\n const at = yield* CurrentConversation\n const once = call(\"POST\", `/channels/${at.channel}/typing`)\n yield* once.pipe(Effect.mapError(requestFailed(\"typing\")))\n yield* once.pipe(\n Effect.ignore,\n Effect.schedule(Schedule.spaced(\"8 seconds\")),\n Effect.forkScoped,\n )\n })\n\n return {\n bot: session.bot,\n events: Stream.fromQueue(inbox),\n post,\n edit,\n react,\n typing,\n stream: streamViaEdits({ post, edit, limits }, { ...defaultStream, ...cfg.stream }),\n limits,\n }\n })\n\n// ---------------------------------------------------------------------------\n// Layer\n// ---------------------------------------------------------------------------\n\n/**\n * One gateway session, registered under both the `Discord` and `Messenger`\n * tags. Building the layer identifies and waits for `READY`, so a bad token\n * or an intent the portal has not granted fails here; the connection lives\n * until the scope closes and `events` ends with it. Run one instance per\n * bot token.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Discord | Messenger, MessengerError.MessengerConnectFailed, HttpClient.HttpClient> =>\n Layer.effectContext(\n Effect.map(make(cfg), (service) =>\n Context.make(Discord, service).pipe(Context.add(Messenger, service)),\n ),\n )\n"],"mappings":"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"}
@@ -0,0 +1,172 @@
1
+ import { Schema } from "effect";
2
+ import { InboundEvent } from "@effect-uai/core/Messenger";
3
+ //#region src/internal/events.d.ts
4
+ declare const User: Schema.Struct<{
5
+ readonly id: Schema.String;
6
+ readonly username: Schema.optional<Schema.String>;
7
+ readonly bot: Schema.optional<Schema.Boolean>;
8
+ }>;
9
+ type User = typeof User.Type;
10
+ /** Unicode reactions carry a `name` and a null `id`; custom ones carry both. */
11
+ declare const Emoji: Schema.Struct<{
12
+ readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
13
+ readonly name: Schema.optional<Schema.NullOr<Schema.String>>;
14
+ }>;
15
+ type Emoji = typeof Emoji.Type;
16
+ declare const ReplyTarget: Schema.Struct<{
17
+ readonly id: Schema.String;
18
+ readonly author: Schema.optional<Schema.Struct<{
19
+ readonly id: Schema.String;
20
+ readonly username: Schema.optional<Schema.String>;
21
+ readonly bot: Schema.optional<Schema.Boolean>;
22
+ }>>;
23
+ }>;
24
+ type ReplyTarget = typeof ReplyTarget.Type;
25
+ declare const MessageCreate: Schema.Struct<{
26
+ readonly id: Schema.String;
27
+ readonly channel_id: Schema.String;
28
+ readonly guild_id: Schema.optional<Schema.String>;
29
+ readonly author: Schema.Struct<{
30
+ readonly id: Schema.String;
31
+ readonly username: Schema.optional<Schema.String>;
32
+ readonly bot: Schema.optional<Schema.Boolean>;
33
+ }>;
34
+ readonly content: Schema.optional<Schema.String>;
35
+ readonly mentions: Schema.optional<Schema.$Array<Schema.Struct<{
36
+ readonly id: Schema.String;
37
+ readonly username: Schema.optional<Schema.String>;
38
+ readonly bot: Schema.optional<Schema.Boolean>;
39
+ }>>>;
40
+ readonly referenced_message: Schema.optional<Schema.NullOr<Schema.Struct<{
41
+ readonly id: Schema.String;
42
+ readonly author: Schema.optional<Schema.Struct<{
43
+ readonly id: Schema.String;
44
+ readonly username: Schema.optional<Schema.String>;
45
+ readonly bot: Schema.optional<Schema.Boolean>;
46
+ }>>;
47
+ }>>>;
48
+ }>;
49
+ type MessageCreate = typeof MessageCreate.Type;
50
+ declare const ReactionAdd: Schema.Struct<{
51
+ readonly user_id: Schema.String;
52
+ readonly channel_id: Schema.String;
53
+ readonly message_id: Schema.String;
54
+ readonly emoji: Schema.Struct<{
55
+ readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
56
+ readonly name: Schema.optional<Schema.NullOr<Schema.String>>;
57
+ }>;
58
+ }>;
59
+ type ReactionAdd = typeof ReactionAdd.Type;
60
+ declare const InteractionData: Schema.Struct<{
61
+ readonly custom_id: Schema.optional<Schema.String>;
62
+ readonly values: Schema.optional<Schema.$Array<Schema.String>>;
63
+ }>;
64
+ type InteractionData = typeof InteractionData.Type;
65
+ declare const InteractionCreate: Schema.Struct<{
66
+ readonly id: Schema.String;
67
+ readonly token: Schema.String;
68
+ /** 1 ping, 2 slash command, 3 message component, 4 autocomplete, 5 modal. */
69
+ readonly type: Schema.Number;
70
+ readonly channel_id: Schema.optional<Schema.String>;
71
+ readonly user: Schema.optional<Schema.Struct<{
72
+ readonly id: Schema.String;
73
+ readonly username: Schema.optional<Schema.String>;
74
+ readonly bot: Schema.optional<Schema.Boolean>;
75
+ }>>;
76
+ readonly member: Schema.optional<Schema.Struct<{
77
+ readonly user: Schema.optional<Schema.Struct<{
78
+ readonly id: Schema.String;
79
+ readonly username: Schema.optional<Schema.String>;
80
+ readonly bot: Schema.optional<Schema.Boolean>;
81
+ }>>;
82
+ }>>;
83
+ readonly data: Schema.optional<Schema.Struct<{
84
+ readonly custom_id: Schema.optional<Schema.String>;
85
+ readonly values: Schema.optional<Schema.$Array<Schema.String>>;
86
+ }>>;
87
+ }>;
88
+ type InteractionCreate = typeof InteractionCreate.Type;
89
+ /** The component interaction type; every other one is ignored in v1. */
90
+ declare const MESSAGE_COMPONENT = 3;
91
+ /**
92
+ * The dispatches this adapter reads, discriminated by the gateway's `t`.
93
+ * Anything else fails to decode and is dropped by the session.
94
+ */
95
+ declare const Dispatch: Schema.Union<readonly [Schema.Struct<{
96
+ readonly t: Schema.Literal<"MESSAGE_CREATE">;
97
+ readonly d: Schema.Struct<{
98
+ readonly id: Schema.String;
99
+ readonly channel_id: Schema.String;
100
+ readonly guild_id: Schema.optional<Schema.String>;
101
+ readonly author: Schema.Struct<{
102
+ readonly id: Schema.String;
103
+ readonly username: Schema.optional<Schema.String>;
104
+ readonly bot: Schema.optional<Schema.Boolean>;
105
+ }>;
106
+ readonly content: Schema.optional<Schema.String>;
107
+ readonly mentions: Schema.optional<Schema.$Array<Schema.Struct<{
108
+ readonly id: Schema.String;
109
+ readonly username: Schema.optional<Schema.String>;
110
+ readonly bot: Schema.optional<Schema.Boolean>;
111
+ }>>>;
112
+ readonly referenced_message: Schema.optional<Schema.NullOr<Schema.Struct<{
113
+ readonly id: Schema.String;
114
+ readonly author: Schema.optional<Schema.Struct<{
115
+ readonly id: Schema.String;
116
+ readonly username: Schema.optional<Schema.String>;
117
+ readonly bot: Schema.optional<Schema.Boolean>;
118
+ }>>;
119
+ }>>>;
120
+ }>;
121
+ }>, Schema.Struct<{
122
+ readonly t: Schema.Literal<"MESSAGE_REACTION_ADD">;
123
+ readonly d: Schema.Struct<{
124
+ readonly user_id: Schema.String;
125
+ readonly channel_id: Schema.String;
126
+ readonly message_id: Schema.String;
127
+ readonly emoji: Schema.Struct<{
128
+ readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
129
+ readonly name: Schema.optional<Schema.NullOr<Schema.String>>;
130
+ }>;
131
+ }>;
132
+ }>, Schema.Struct<{
133
+ readonly t: Schema.Literal<"INTERACTION_CREATE">;
134
+ readonly d: Schema.Struct<{
135
+ readonly id: Schema.String;
136
+ readonly token: Schema.String;
137
+ /** 1 ping, 2 slash command, 3 message component, 4 autocomplete, 5 modal. */
138
+ readonly type: Schema.Number;
139
+ readonly channel_id: Schema.optional<Schema.String>;
140
+ readonly user: Schema.optional<Schema.Struct<{
141
+ readonly id: Schema.String;
142
+ readonly username: Schema.optional<Schema.String>;
143
+ readonly bot: Schema.optional<Schema.Boolean>;
144
+ }>>;
145
+ readonly member: Schema.optional<Schema.Struct<{
146
+ readonly user: Schema.optional<Schema.Struct<{
147
+ readonly id: Schema.String;
148
+ readonly username: Schema.optional<Schema.String>;
149
+ readonly bot: Schema.optional<Schema.Boolean>;
150
+ }>>;
151
+ }>>;
152
+ readonly data: Schema.optional<Schema.Struct<{
153
+ readonly custom_id: Schema.optional<Schema.String>;
154
+ readonly values: Schema.optional<Schema.$Array<Schema.String>>;
155
+ }>>;
156
+ }>;
157
+ }>]>;
158
+ type Dispatch = typeof Dispatch.Type;
159
+ type BotIdentity = {
160
+ readonly id: string;
161
+ readonly username: string;
162
+ };
163
+ /**
164
+ * Everything one dispatch says to the agent. `raw` is the payload as it came
165
+ * off the wire, since decoding keeps only the fields above. Discord has no
166
+ * text-command convention, so nothing here becomes a `Command`: slash
167
+ * commands are a follow-up and need an interaction response of their own.
168
+ */
169
+ declare const toEvents: (bot: BotIdentity) => (dispatch: Dispatch, raw: unknown) => ReadonlyArray<InboundEvent>;
170
+ //#endregion
171
+ export { InteractionData as a, ReactionAdd as c, toEvents as d, InteractionCreate as i, ReplyTarget as l, Dispatch as n, MESSAGE_COMPONENT as o, Emoji as r, MessageCreate as s, BotIdentity as t, User as u };
172
+ //# sourceMappingURL=events-Du1u-kVV.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-Du1u-kVV.d.mts","names":[],"sources":["../src/internal/events.ts"],"mappings":";;;cAaa,MAAI,OAAA;;;;;KAKL,cAAc,KAAK;;cAGlB,OAAK,OAAA;;;;KAIN,eAAe,MAAM;cAEpB,aAAW,OAAA;;;;;;;;KAIZ,qBAAqB,YAAY;cAEhC,eAAa,OAAA;;;;;;;;;;;;;;;;;;;;;;;;KAUd,uBAAuB,cAAc;cAEpC,aAAW,OAAA;;;;;;;;;KAMZ,qBAAqB,YAAY;cAEhC,iBAAe,OAAA;;;;KAIhB,yBAAyB,gBAAgB;cAExC,mBAAiB,OAAA;;;;;;;;;;;;;;;;;;;;;;;KAUlB,2BAA2B,kBAAkB;;cAG5C;;;;;cAMA,UAAQ,OAAA,gBAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAKT,kBAAkB,SAAS;KAE3B;WACD;WACA;;;;;;;;cAmGE,WAAQ,KACb,iBAAW,UACN,UAAQ,iBAAiB,cAAc"}
@@ -0,0 +1,57 @@
1
+ import { n as Dispatch, t as BotIdentity } from "./events-Du1u-kVV.mjs";
2
+ import { Cause, Effect, Queue, Redacted, Schema, Scope } from "effect";
3
+ import * as MessengerError from "@effect-uai/core/MessengerError";
4
+ //#region src/internal/gateway.d.ts
5
+ declare const Ready: Schema.Struct<{
6
+ readonly user: Schema.Struct<{
7
+ readonly id: Schema.String;
8
+ readonly username: Schema.String;
9
+ }>;
10
+ readonly session_id: Schema.String;
11
+ readonly resume_gateway_url: Schema.String;
12
+ }>;
13
+ type Ready = typeof Ready.Type;
14
+ /** `GET /gateway/bot`, the only REST call the session needs. */
15
+ declare const GatewayInfo: Schema.Struct<{
16
+ readonly url: Schema.String;
17
+ }>;
18
+ /** What the session does once a connection is gone. */
19
+ type CloseAction = "resume" | "reidentify" | "fatal";
20
+ /**
21
+ * Everything else, transport drops included, resumes: Discord only names the
22
+ * codes that must not be retried, so an unknown code is a reconnect.
23
+ */
24
+ declare const classifyClose: (code: number) => CloseAction;
25
+ /** Discord's own words for a fatal code, since its close frames carry no reason. */
26
+ declare const closeReason: (code: number, reason?: string) => string;
27
+ type Config = {
28
+ readonly token: Redacted.Redacted;
29
+ readonly intents: number;
30
+ /** From `GET /gateway/bot`. Resumes go to `resume_gateway_url` instead. */
31
+ readonly url: string;
32
+ };
33
+ /** A decoded dispatch beside the payload it came from, which `raw` carries. */
34
+ type Incoming = {
35
+ readonly dispatch: Dispatch;
36
+ readonly raw: unknown;
37
+ };
38
+ type Session = {
39
+ readonly bot: BotIdentity;
40
+ /**
41
+ * Dispatches in arrival order, ended when the scope closes and failed with
42
+ * `MessengerTransportClosed` on a fatal gateway close.
43
+ */
44
+ readonly dispatches: Queue.Queue<Incoming, MessengerError.MessengerError | Cause.Done>;
45
+ };
46
+ /**
47
+ * One gateway session: connect, identify, heartbeat, resume, reconnect.
48
+ *
49
+ * Returns once `READY` has arrived, so a bad token or a disallowed intent is
50
+ * a `MessengerConnectFailed` at layer build rather than a stream that dies a
51
+ * moment later. From then on reconnects are silent and only a fatal close
52
+ * ends `dispatches`.
53
+ */
54
+ declare const connect: (cfg: Config) => Effect.Effect<Session, MessengerError.MessengerConnectFailed, Scope.Scope>;
55
+ //#endregion
56
+ export { Ready as a, closeReason as c, Incoming as i, connect as l, Config as n, Session as o, GatewayInfo as r, classifyClose as s, CloseAction as t };
57
+ //# sourceMappingURL=gateway-C_Ae1HYB.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-C_Ae1HYB.d.mts","names":[],"sources":["../src/internal/gateway.ts"],"mappings":";;;;cAmCa,OAAK,OAAA;;;;;;;;KAKN,eAAe,MAAM;;cAGpB,aAAW,OAAA;gBAAX,OAAA;;;KAoBD;;;;;cAmBC,gBAAa,iBAAmB;;cAchC,cAAW,cAAgB;KAY5B;WACD,OAAO,SAAS;WAChB;;WAEA;;;KAIC;WACD,UAAU;WACV;;KAGC;WACD,KAAK;;;;;WAKL,YAAY,MAAM,MAAM,UAAU,eAAe,iBAAiB,MAAM;;;;;;;;;;cAgCtE,UAAO,KACb,WACJ,OAAO,OAAO,SAAS,eAAe,wBAAwB,MAAM"}
@@ -0,0 +1,2 @@
1
+ import { t as Discord_d_exports } from "./Discord.mjs";
2
+ export { Discord_d_exports as Discord };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as Discord_exports } from "./Discord.mjs";
2
+ export { Discord_exports as Discord };
@@ -0,0 +1,2 @@
1
+ import { a as InteractionData, c as ReactionAdd, d as toEvents, i as InteractionCreate, l as ReplyTarget, n as Dispatch, o as MESSAGE_COMPONENT, r as Emoji, s as MessageCreate, t as BotIdentity, u as User } from "../events-Du1u-kVV.mjs";
2
+ export { BotIdentity, Dispatch, Emoji, InteractionCreate, InteractionData, MESSAGE_COMPONENT, MessageCreate, ReactionAdd, ReplyTarget, User, toEvents };