@effect-uai/slack 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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/Slack.d.mts +43 -0
- package/dist/Slack.d.mts.map +1 -0
- package/dist/Slack.mjs +215 -0
- package/dist/Slack.mjs.map +1 -0
- package/dist/api-BBgliKo7.d.mts +51 -0
- package/dist/api-BBgliKo7.d.mts.map +1 -0
- package/dist/events-Cj5RRjK1.d.mts +317 -0
- package/dist/events-Cj5RRjK1.d.mts.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/internal/api.d.mts +2 -0
- package/dist/internal/api.mjs +79 -0
- package/dist/internal/api.mjs.map +1 -0
- package/dist/internal/events.d.mts +2 -0
- package/dist/internal/events.mjs +184 -0
- package/dist/internal/events.mjs.map +1 -0
- package/dist/internal/events.test.d.mts +1 -0
- package/dist/internal/events.test.mjs +174 -0
- package/dist/internal/events.test.mjs.map +1 -0
- package/dist/internal/socket.d.mts +2 -0
- package/dist/internal/socket.mjs +109 -0
- package/dist/internal/socket.mjs.map +1 -0
- package/dist/internal/socket.test.d.mts +1 -0
- package/dist/internal/socket.test.mjs +25 -0
- package/dist/internal/socket.test.mjs.map +1 -0
- package/dist/magic-string.es-CUAnjKYC.mjs +1014 -0
- package/dist/magic-string.es-CUAnjKYC.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/socket-DJgdt_ka.d.mts +54 -0
- package/dist/socket-DJgdt_ka.d.mts.map +1 -0
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs +13658 -0
- package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
- package/package.json +59 -0
- package/src/Slack.ts +419 -0
- package/src/index.ts +1 -0
- package/src/internal/api.ts +171 -0
- package/src/internal/events.test.ts +121 -0
- package/src/internal/events.ts +330 -0
- package/src/internal/socket.test.ts +25 -0
- package/src/internal/socket.ts +238 -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,51 @@
|
|
|
1
|
+
# @effect-uai/slack
|
|
2
|
+
|
|
3
|
+
Slack provider for [`@effect-uai/core`](https://www.npmjs.com/package/@effect-uai/core).
|
|
4
|
+
|
|
5
|
+
Implements the `Messenger` contract over Socket Mode and the Web API, so your
|
|
6
|
+
agent runs as a Slack bot: mentions, DMs, slash commands and button presses
|
|
7
|
+
in, streamed markdown replies in a thread, files and reactions out. One
|
|
8
|
+
websocket, so no request URL, and no Bolt.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add @effect-uai/slack @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 slackLayer } from "@effect-uai/slack/Slack"
|
|
24
|
+
|
|
25
|
+
const provider = slackLayer({
|
|
26
|
+
botToken: Redacted.make(process.env.SLACK_BOT_TOKEN!),
|
|
27
|
+
appToken: Redacted.make(process.env.SLACK_APP_TOKEN!),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
await Effect.runPromise(
|
|
31
|
+
program.pipe(Effect.scoped, Effect.provide(provider), Effect.provide(NodeHttpClient.layerUndici)),
|
|
32
|
+
)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The layer registers both the provider-typed `Slack` tag and the generic
|
|
36
|
+
`Messenger` tag. The app needs Socket Mode, an `xapp-` app token and an
|
|
37
|
+
`xoxb-` bot token; the docs page has a manifest with the scopes and events.
|
|
38
|
+
|
|
39
|
+
## Threads
|
|
40
|
+
|
|
41
|
+
By default a top-level mention is answered in a thread under it, and
|
|
42
|
+
follow-ups inside that thread are the same conversation. `replyIn: "channel"`
|
|
43
|
+
answers in the channel instead.
|
|
44
|
+
|
|
45
|
+
## Docs
|
|
46
|
+
|
|
47
|
+
<https://effect-uai.betalyra.com/messenger/providers/slack/>
|
|
48
|
+
|
|
49
|
+
## License
|
|
50
|
+
|
|
51
|
+
MIT
|
package/dist/Slack.d.mts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { r as Config$1 } from "./api-BBgliKo7.mjs";
|
|
2
|
+
import { m as ReplyIn, r as BotIdentity } from "./events-Cj5RRjK1.mjs";
|
|
3
|
+
import { n as DisconnectAction, s as classifyDisconnect } from "./socket-DJgdt_ka.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 Slack_d_exports {
|
|
10
|
+
export { BotIdentity, Config, DisconnectAction, ReplyIn, Slack, SlackService, classifyDisconnect, layer, limits, make };
|
|
11
|
+
}
|
|
12
|
+
type Config = Config$1 & {
|
|
13
|
+
/**
|
|
14
|
+
* Where a top-level mention is answered. Default `"thread"`, which keeps a
|
|
15
|
+
* conversation out of the channel and lets follow-ups inside it map back.
|
|
16
|
+
*/
|
|
17
|
+
readonly replyIn?: ReplyIn;
|
|
18
|
+
/** Tuning for `stream`, which is post-then-edit on Slack. */
|
|
19
|
+
readonly stream?: StreamViaEditsOptions;
|
|
20
|
+
};
|
|
21
|
+
type SlackService = MessengerService & {
|
|
22
|
+
/** From `auth.test` at layer build: the identity behind `addressed`. */
|
|
23
|
+
readonly bot: BotIdentity;
|
|
24
|
+
};
|
|
25
|
+
declare const Slack_base: Context.ServiceClass<Slack, "@betalyra/effect-uai/providers/slack/Slack", SlackService>;
|
|
26
|
+
/**
|
|
27
|
+
* Provider-typed tag. Yield this for the bot identity; yield the generic
|
|
28
|
+
* `Messenger` tag for provider-portable code. Both are registered by {@link layer}.
|
|
29
|
+
*/
|
|
30
|
+
declare class Slack extends Slack_base {}
|
|
31
|
+
/** `markdown_text` allows far more, but a chat message past this is unreadable. */
|
|
32
|
+
declare const limits: MessengerLimits;
|
|
33
|
+
declare const make: (cfg: Config) => Effect.Effect<SlackService, MessengerError.MessengerConnectFailed, HttpClient.HttpClient | Scope.Scope>;
|
|
34
|
+
/**
|
|
35
|
+
* One Socket Mode connection, registered under both the `Slack` and
|
|
36
|
+
* `Messenger` tags. Building the layer authenticates and waits for `hello`, so
|
|
37
|
+
* a rejected token fails here; the connection lives until the scope closes and
|
|
38
|
+
* `events` ends with it.
|
|
39
|
+
*/
|
|
40
|
+
declare const layer: (cfg: Config) => Layer.Layer<Slack | Messenger, MessengerError.MessengerConnectFailed, HttpClient.HttpClient>;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { type BotIdentity, Config, type DisconnectAction, type ReplyIn, Slack, SlackService, classifyDisconnect, layer, limits, make, Slack_d_exports as t };
|
|
43
|
+
//# sourceMappingURL=Slack.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slack.d.mts","names":[],"sources":["../src/Slack.ts"],"mappings":";;;;;;;;;;;KA4CY,SAAS;;;;;WAKV,UAAU;;WAEV,SAAS;;KAGR,eAAe;;WAEhB,KAAK;;;;;;;cAOH,cAAc;;cAKd,QAAQ;cA8GR,OAAI,KACV,WACJ,OAAO,OACR,cACA,eAAe,wBACf,WAAW,aAAa,MAAM;;;;;;;cAoOnB,QAAK,KACX,WACJ,MAAM,MAAM,QAAQ,WAAW,eAAe,wBAAwB,WAAW"}
|
package/dist/Slack.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import { call, callAsApp, decoded, provider, query, upload } from "./internal/api.mjs";
|
|
3
|
+
import { toEvents } from "./internal/events.mjs";
|
|
4
|
+
import { classifyDisconnect, connect } from "./internal/socket.mjs";
|
|
5
|
+
import { Array, Context, Effect, Encoding, Layer, Match, Option, Queue, 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/Slack.ts
|
|
11
|
+
var Slack_exports = /* @__PURE__ */ __exportAll({
|
|
12
|
+
Slack: () => Slack,
|
|
13
|
+
classifyDisconnect: () => classifyDisconnect,
|
|
14
|
+
layer: () => layer,
|
|
15
|
+
limits: () => limits,
|
|
16
|
+
make: () => make
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Provider-typed tag. Yield this for the bot identity; yield the generic
|
|
20
|
+
* `Messenger` tag for provider-portable code. Both are registered by {@link layer}.
|
|
21
|
+
*/
|
|
22
|
+
var Slack = class extends Context.Service()("@betalyra/effect-uai/providers/slack/Slack") {};
|
|
23
|
+
/** `markdown_text` allows far more, but a chat message past this is unreadable. */
|
|
24
|
+
const limits = {
|
|
25
|
+
maxText: 4e3,
|
|
26
|
+
maxCaption: 4e3
|
|
27
|
+
};
|
|
28
|
+
const defaultStream = { every: "1200 millis" };
|
|
29
|
+
const AuthTest = Schema.Struct({
|
|
30
|
+
user_id: Schema.String,
|
|
31
|
+
bot_id: Schema.String,
|
|
32
|
+
team_id: Schema.String
|
|
33
|
+
});
|
|
34
|
+
const Opened = Schema.Struct({ url: Schema.String });
|
|
35
|
+
const Posted = Schema.Struct({ ts: Schema.String });
|
|
36
|
+
const UploadUrl = Schema.Struct({
|
|
37
|
+
upload_url: Schema.String,
|
|
38
|
+
file_id: Schema.String
|
|
39
|
+
});
|
|
40
|
+
const Shares = Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Array(Schema.Struct({ ts: Schema.optional(Schema.String) }))));
|
|
41
|
+
const Completed = Schema.Struct({ files: Schema.Array(Schema.Struct({
|
|
42
|
+
id: Schema.String,
|
|
43
|
+
shares: Schema.optional(Shares)
|
|
44
|
+
})) });
|
|
45
|
+
const RawCall = Schema.Struct({
|
|
46
|
+
method: Schema.String,
|
|
47
|
+
params: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
|
|
48
|
+
});
|
|
49
|
+
/** The message a file was shared as, when Slack reports one. */
|
|
50
|
+
const shareTs = (uploaded) => Array.head(uploaded.files).pipe(Option.flatMap((file) => Array.findFirst(Object.values(file.shares ?? {}).flatMap((byChannel) => Object.values(byChannel).flat()), (share) => share.ts !== void 0)), Option.flatMap((share) => Option.fromNullishOr(share.ts)));
|
|
51
|
+
const isApiFailure = (e) => e._tag === "SlackApiFailure";
|
|
52
|
+
const fails = (needle) => (e) => isApiFailure(e) && e.error === needle;
|
|
53
|
+
const reasonOf = (e) => isApiFailure(e) ? e.error : "rate limited";
|
|
54
|
+
/** An API rejection becomes the verb's `MessengerRequestFailed`; typed errors pass through. */
|
|
55
|
+
const requestFailed = (operation) => (e) => Match.value(e).pipe(Match.tag("SlackApiFailure", (failure) => new MessengerError.MessengerRequestFailed({
|
|
56
|
+
provider,
|
|
57
|
+
operation,
|
|
58
|
+
reason: failure.error,
|
|
59
|
+
raw: failure.raw
|
|
60
|
+
})), Match.orElse((typed) => typed));
|
|
61
|
+
const unsupported = (capability, reason) => new MessengerError.MessengerUnsupported({
|
|
62
|
+
provider,
|
|
63
|
+
capability,
|
|
64
|
+
reason
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* Slack's "reply" is posting in the thread, so a `replyTo` outside one opens a
|
|
68
|
+
* thread on that message and inside one is already named by the conversation.
|
|
69
|
+
*/
|
|
70
|
+
const target = (at, msg) => {
|
|
71
|
+
const thread = at.thread ?? msg.replyTo;
|
|
72
|
+
return {
|
|
73
|
+
channel: at.channel,
|
|
74
|
+
...thread !== void 0 && { thread_ts: thread }
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const defaultFilename = (media) => `file.${media.mimeType?.split("/")[1] ?? "bin"}`;
|
|
78
|
+
const invalidMedia = (raw) => new MessengerError.MessengerRequestFailed({
|
|
79
|
+
provider,
|
|
80
|
+
operation: "post",
|
|
81
|
+
reason: "invalid base64 media",
|
|
82
|
+
raw
|
|
83
|
+
});
|
|
84
|
+
const shortcode = (emoji) => emoji.replace(/^:|:$/g, "");
|
|
85
|
+
const make = (cfg) => Effect.gen(function* () {
|
|
86
|
+
const client = yield* HttpClient.HttpClient;
|
|
87
|
+
const withClient = Effect.provideService(HttpClient.HttpClient, client);
|
|
88
|
+
const call$1 = (method, params) => withClient(call(cfg)(method, params));
|
|
89
|
+
const query$1 = (method, params) => withClient(query(cfg)(method, params));
|
|
90
|
+
const connectFailed = (e) => new MessengerError.MessengerConnectFailed({
|
|
91
|
+
provider,
|
|
92
|
+
reason: reasonOf(e),
|
|
93
|
+
raw: isApiFailure(e) ? e.raw : e
|
|
94
|
+
});
|
|
95
|
+
const identity = yield* call$1("auth.test").pipe(decoded("auth.test", AuthTest), Effect.mapError(connectFailed));
|
|
96
|
+
const bot = {
|
|
97
|
+
userId: identity.user_id,
|
|
98
|
+
botId: identity.bot_id,
|
|
99
|
+
teamId: identity.team_id
|
|
100
|
+
};
|
|
101
|
+
const open = withClient(callAsApp(cfg)("apps.connections.open")).pipe(decoded("apps.connections.open", Opened), Effect.map(({ url }) => url), Effect.mapError((e) => new MessengerError.MessengerConnectFailed({
|
|
102
|
+
provider,
|
|
103
|
+
reason: reasonOf(e),
|
|
104
|
+
raw: isApiFailure(e) ? e.raw : e
|
|
105
|
+
})));
|
|
106
|
+
const session = yield* connect({ open });
|
|
107
|
+
const toEvents$1 = toEvents({
|
|
108
|
+
bot,
|
|
109
|
+
replyIn: cfg.replyIn ?? "thread"
|
|
110
|
+
});
|
|
111
|
+
const inbox = yield* Queue.unbounded();
|
|
112
|
+
yield* Stream.fromQueue(session.envelopes).pipe(Stream.runForEach(({ envelope, raw }) => Queue.offerAll(inbox, toEvents$1(envelope, raw))), Effect.catch((closed) => Queue.fail(inbox, closed)), Effect.ensuring(Queue.end(inbox)), Effect.forkScoped);
|
|
113
|
+
const sendRaw = (payload) => Effect.gen(function* () {
|
|
114
|
+
const { method, params } = yield* Schema.decodeUnknownEffect(RawCall)(payload).pipe(Effect.mapError(() => new MessengerError.MessengerRequestFailed({
|
|
115
|
+
provider,
|
|
116
|
+
operation: "post",
|
|
117
|
+
reason: "raw payload must be { method, params? }",
|
|
118
|
+
raw: payload
|
|
119
|
+
})));
|
|
120
|
+
return yield* call$1(method, params);
|
|
121
|
+
});
|
|
122
|
+
const sendUrl = (at, msg, body, url) => call$1("chat.postMessage", {
|
|
123
|
+
...target(at, msg),
|
|
124
|
+
markdown_text: Array.getSomes([Option.fromNullishOr(body.caption), Option.some(url)]).join("\n")
|
|
125
|
+
}).pipe(decoded("chat.postMessage", Posted), Effect.map(({ ts }) => ts));
|
|
126
|
+
const sendBytes = (at, msg, body, bytes, mimeType) => Effect.gen(function* () {
|
|
127
|
+
const filename = body.filename ?? defaultFilename(body.media);
|
|
128
|
+
const reserved = yield* query$1("files.getUploadURLExternal", {
|
|
129
|
+
filename,
|
|
130
|
+
length: bytes.length
|
|
131
|
+
}).pipe(decoded("files.getUploadURLExternal", UploadUrl));
|
|
132
|
+
yield* withClient(upload(reserved.upload_url, {
|
|
133
|
+
bytes,
|
|
134
|
+
filename,
|
|
135
|
+
mimeType
|
|
136
|
+
}));
|
|
137
|
+
const uploaded = yield* call$1("files.completeUploadExternal", {
|
|
138
|
+
files: [{
|
|
139
|
+
id: reserved.file_id,
|
|
140
|
+
title: filename
|
|
141
|
+
}],
|
|
142
|
+
channel_id: at.channel,
|
|
143
|
+
...target(at, msg),
|
|
144
|
+
...body.caption !== void 0 && { initial_comment: body.caption }
|
|
145
|
+
}).pipe(decoded("files.completeUploadExternal", Completed));
|
|
146
|
+
return Option.getOrElse(shareTs(uploaded), () => reserved.file_id);
|
|
147
|
+
});
|
|
148
|
+
const sendMedia = (at, msg, body) => Match.value(body.media).pipe(Match.tag("url", ({ url }) => sendUrl(at, msg, body, url)), Match.tag("bytes", ({ bytes, mimeType }) => sendBytes(at, msg, body, bytes, mimeType)), Match.tag("base64", ({ base64, mimeType }) => Effect.fromResult(Encoding.decodeBase64(base64)).pipe(Effect.mapError(invalidMedia), Effect.flatMap((bytes) => sendBytes(at, msg, body, bytes, mimeType)))), Match.exhaustive);
|
|
149
|
+
const sendChunks = (at, msg, body) => Effect.gen(function* () {
|
|
150
|
+
const [first, ...rest] = splitForLimit(body, limits.maxText);
|
|
151
|
+
const send = (chunk) => call$1("chat.postMessage", {
|
|
152
|
+
...target(at, msg),
|
|
153
|
+
markdown_text: chunk
|
|
154
|
+
}).pipe(decoded("chat.postMessage", Posted), Effect.map(({ ts }) => ts));
|
|
155
|
+
const head = yield* send(first);
|
|
156
|
+
const tail = yield* Effect.forEach(rest, send);
|
|
157
|
+
return Option.getOrElse(Array.last(tail), () => head);
|
|
158
|
+
});
|
|
159
|
+
const post = (msg) => Effect.gen(function* () {
|
|
160
|
+
const at = yield* CurrentConversation;
|
|
161
|
+
const ts = yield* Match.value(msg.body).pipe(Match.tag("Text", ({ text }) => sendChunks(at, msg, text)), Match.tag("Media", (body) => sendMedia(at, msg, body)), Match.tag("Raw", ({ payload }) => sendRaw(payload).pipe(decoded("raw", Posted), Effect.map(({ ts }) => ts))), Match.exhaustive);
|
|
162
|
+
return MessageId(ts);
|
|
163
|
+
}).pipe(Effect.mapError(requestFailed("post")));
|
|
164
|
+
const edit = (ref, next) => Match.value(next.body).pipe(Match.tag("Text", ({ text }) => call$1("chat.update", {
|
|
165
|
+
channel: ref.conversation.channel,
|
|
166
|
+
ts: ref.id,
|
|
167
|
+
markdown_text: text
|
|
168
|
+
}).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);
|
|
169
|
+
const react = (ref, emoji) => call$1("reactions.add", {
|
|
170
|
+
channel: ref.conversation.channel,
|
|
171
|
+
timestamp: ref.id,
|
|
172
|
+
name: shortcode(emoji)
|
|
173
|
+
}).pipe(Effect.asVoid, Effect.catchIf(fails("invalid_name"), () => Effect.fail(unsupported("reaction", `${emoji} is not an emoji this workspace has`))), Effect.mapError(requestFailed("react")));
|
|
174
|
+
/**
|
|
175
|
+
* Best effort: Slack shows a session status on its agent surfaces only, so
|
|
176
|
+
* a rejection is logged and swallowed rather than taking a turn down.
|
|
177
|
+
*/
|
|
178
|
+
const typing = Effect.gen(function* () {
|
|
179
|
+
const at = yield* CurrentConversation;
|
|
180
|
+
const status = (state) => call$1("agents.sessions.setStatus", {
|
|
181
|
+
channel_id: at.channel,
|
|
182
|
+
...at.thread !== void 0 && { thread_ts: at.thread },
|
|
183
|
+
status: state
|
|
184
|
+
}).pipe(Effect.asVoid, Effect.catch((e) => Effect.logDebug("slack status not set", { reason: reasonOf(e) })));
|
|
185
|
+
yield* Effect.acquireRelease(status("processing"), () => status("active"));
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
bot,
|
|
189
|
+
events: Stream.fromQueue(inbox),
|
|
190
|
+
post,
|
|
191
|
+
edit,
|
|
192
|
+
react,
|
|
193
|
+
typing,
|
|
194
|
+
stream: streamViaEdits({
|
|
195
|
+
post,
|
|
196
|
+
edit,
|
|
197
|
+
limits
|
|
198
|
+
}, {
|
|
199
|
+
...defaultStream,
|
|
200
|
+
...cfg.stream
|
|
201
|
+
}),
|
|
202
|
+
limits
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
/**
|
|
206
|
+
* One Socket Mode connection, registered under both the `Slack` and
|
|
207
|
+
* `Messenger` tags. Building the layer authenticates and waits for `hello`, so
|
|
208
|
+
* a rejected token fails here; the connection lives until the scope closes and
|
|
209
|
+
* `events` ends with it.
|
|
210
|
+
*/
|
|
211
|
+
const layer = (cfg) => Layer.effectContext(Effect.map(make(cfg), (service) => Context.make(Slack, service).pipe(Context.add(Messenger, service))));
|
|
212
|
+
//#endregion
|
|
213
|
+
export { Slack, classifyDisconnect, layer, limits, make, Slack_exports as t };
|
|
214
|
+
|
|
215
|
+
//# sourceMappingURL=Slack.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Slack.mjs","names":["Arr","Api.provider","call","Api.call","query","Api.query","Api.decoded","Api.callAsApp","SocketMode.connect","toEvents","Events.toEvents","Api.upload"],"sources":["../src/Slack.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 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 Api from \"./internal/api.js\"\nimport * as Events from \"./internal/events.js\"\nimport * as SocketMode from \"./internal/socket.js\"\n\nexport type { BotIdentity, ReplyIn } from \"./internal/events.js\"\nexport { classifyDisconnect, type DisconnectAction } from \"./internal/socket.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport type Config = Api.Config & {\n /**\n * Where a top-level mention is answered. Default `\"thread\"`, which keeps a\n * conversation out of the channel and lets follow-ups inside it map back.\n */\n readonly replyIn?: Events.ReplyIn\n /** Tuning for `stream`, which is post-then-edit on Slack. */\n readonly stream?: StreamViaEditsOptions\n}\n\nexport type SlackService = MessengerService & {\n /** From `auth.test` 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 Slack extends Context.Service<Slack, SlackService>()(\n \"@betalyra/effect-uai/providers/slack/Slack\",\n) {}\n\n/** `markdown_text` allows far more, but a chat message past this is unreadable. */\nexport const limits: MessengerLimits = { maxText: 4000, maxCaption: 4000 }\n\n// `chat.update` is Tier 3, so an edit a second is comfortably inside it.\nconst defaultStream: StreamViaEditsOptions = { every: \"1200 millis\" }\n\n// ---------------------------------------------------------------------------\n// Wire results\n// ---------------------------------------------------------------------------\n\nconst AuthTest = Schema.Struct({\n user_id: Schema.String,\n bot_id: Schema.String,\n team_id: Schema.String,\n})\nconst Opened = Schema.Struct({ url: Schema.String })\nconst Posted = Schema.Struct({ ts: Schema.String })\nconst UploadUrl = Schema.Struct({ upload_url: Schema.String, file_id: Schema.String })\n\n// `shares` nests visibility, then channel, then the messages the file appears in.\nconst Shares = Schema.Record(\n Schema.String,\n Schema.Record(Schema.String, Schema.Array(Schema.Struct({ ts: Schema.optional(Schema.String) }))),\n)\nconst Completed = Schema.Struct({\n files: Schema.Array(Schema.Struct({ id: Schema.String, shares: Schema.optional(Shares) })),\n})\ntype Completed = typeof Completed.Type\n\nconst RawCall = Schema.Struct({\n method: Schema.String,\n params: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),\n})\n\n/** The message a file was shared as, when Slack reports one. */\nconst shareTs = (uploaded: Completed): Option.Option<string> =>\n Arr.head(uploaded.files).pipe(\n Option.flatMap((file) =>\n Arr.findFirst(\n Object.values(file.shares ?? {}).flatMap((byChannel) => Object.values(byChannel).flat()),\n (share) => share.ts !== undefined,\n ),\n ),\n Option.flatMap((share) => Option.fromNullishOr(share.ts)),\n )\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\nconst isApiFailure = (e: Api.ApiError): e is Api.ApiFailure => e._tag === \"SlackApiFailure\"\n\nconst fails = (needle: string) => (e: Api.ApiError) => isApiFailure(e) && e.error === needle\n\nconst reasonOf = (e: Api.ApiError): string => (isApiFailure(e) ? e.error : \"rate limited\")\n\n/** An API rejection becomes the verb's `MessengerRequestFailed`; typed errors pass through. */\nconst requestFailed =\n (operation: MessengerError.MessengerOperation) =>\n (e: Api.ApiError | MessengerError.MessengerError): MessengerError.MessengerError =>\n Match.value(e).pipe(\n Match.tag(\n \"SlackApiFailure\",\n (failure) =>\n new MessengerError.MessengerRequestFailed({\n provider: Api.provider,\n operation,\n reason: failure.error,\n raw: failure.raw,\n }),\n ),\n Match.orElse((typed) => typed),\n )\n\nconst unsupported = (capability: string, reason: string) =>\n new MessengerError.MessengerUnsupported({ provider: Api.provider, capability, reason })\n\n// ---------------------------------------------------------------------------\n// Outbound params\n// ---------------------------------------------------------------------------\n\ntype MediaFields = Extract<OutboundBody, { _tag: \"Media\" }>\n\n/**\n * Slack's \"reply\" is posting in the thread, so a `replyTo` outside one opens a\n * thread on that message and inside one is already named by the conversation.\n */\nconst target = (at: ConversationRef, msg: Outbound): Api.Params => {\n const thread = at.thread ?? msg.replyTo\n return { channel: at.channel, ...(thread !== undefined && { thread_ts: thread }) }\n}\n\nconst defaultFilename = (media: MediaSource): string =>\n `file.${media.mimeType?.split(\"/\")[1] ?? \"bin\"}`\n\nconst invalidMedia = (raw: unknown) =>\n new MessengerError.MessengerRequestFailed({\n provider: Api.provider,\n operation: \"post\",\n reason: \"invalid base64 media\",\n raw,\n })\n\n// `:eyes:` and `eyes` name the same reaction; Slack wants it bare. Only the\n// outer colons go, so a skin tone modifier survives.\nconst shortcode = (emoji: string): string => emoji.replace(/^:|:$/g, \"\")\n\n// ---------------------------------------------------------------------------\n// Service\n// ---------------------------------------------------------------------------\n\nexport const make = (\n cfg: Config,\n): Effect.Effect<\n SlackService,\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: string, params?: Api.Params) => withClient(Api.call(cfg)(method, params))\n const query = (method: string, params: Api.Params) => withClient(Api.query(cfg)(method, params))\n\n const connectFailed = (e: Api.ApiError) =>\n new MessengerError.MessengerConnectFailed({\n provider: Api.provider,\n reason: reasonOf(e),\n raw: isApiFailure(e) ? e.raw : e,\n })\n\n // A bad bot token fails here rather than as a socket close a moment later.\n const identity = yield* call(\"auth.test\").pipe(\n Api.decoded(\"auth.test\", AuthTest),\n Effect.mapError(connectFailed),\n )\n const bot: Events.BotIdentity = {\n userId: identity.user_id,\n botId: identity.bot_id,\n teamId: identity.team_id,\n }\n\n // -- inbound -----------------------------------------------------------\n\n // Each connection needs its own single-use URL, so this runs again on\n // every reconnect. The app token is used nowhere else.\n const open = withClient(Api.callAsApp(cfg)(\"apps.connections.open\")).pipe(\n Api.decoded(\"apps.connections.open\", Opened),\n Effect.map(({ url }) => url),\n Effect.mapError(\n (e): MessengerError.MessengerError =>\n new MessengerError.MessengerConnectFailed({\n provider: Api.provider,\n reason: reasonOf(e),\n raw: isApiFailure(e) ? e.raw : e,\n }),\n ),\n )\n\n const session = yield* SocketMode.connect({ open })\n const toEvents = Events.toEvents({ bot, replyIn: cfg.replyIn ?? \"thread\" })\n const inbox = yield* Queue.unbounded<InboundEvent, MessengerError.MessengerError | Cause.Done>()\n\n yield* Stream.fromQueue(session.envelopes).pipe(\n Stream.runForEach(({ envelope, raw }) => Queue.offerAll(inbox, toEvents(envelope, raw))),\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, params } = yield* Schema.decodeUnknownEffect(RawCall)(payload).pipe(\n Effect.mapError(\n () =>\n new MessengerError.MessengerRequestFailed({\n provider: Api.provider,\n operation: \"post\",\n reason: \"raw payload must be { method, params? }\",\n raw: payload,\n }),\n ),\n )\n return yield* call(method, params)\n })\n\n // A URL is not a Slack upload, so it goes as the link and Slack unfurls it.\n const sendUrl = (at: ConversationRef, msg: Outbound, body: MediaFields, url: string) =>\n call(\"chat.postMessage\", {\n ...target(at, msg),\n markdown_text: Arr.getSomes([Option.fromNullishOr(body.caption), Option.some(url)]).join(\n \"\\n\",\n ),\n }).pipe(\n Api.decoded(\"chat.postMessage\", Posted),\n Effect.map(({ ts }) => ts),\n )\n\n // Three steps: reserve a URL, put the bytes there, then share the file into\n // the conversation with the caption as its comment.\n const sendBytes = (\n at: ConversationRef,\n msg: Outbound,\n body: MediaFields,\n bytes: Uint8Array,\n mimeType: string,\n ) =>\n Effect.gen(function* () {\n const filename = body.filename ?? defaultFilename(body.media)\n const reserved = yield* query(\"files.getUploadURLExternal\", {\n filename,\n length: bytes.length,\n }).pipe(Api.decoded(\"files.getUploadURLExternal\", UploadUrl))\n yield* withClient(Api.upload(reserved.upload_url, { bytes, filename, mimeType }))\n const uploaded = yield* call(\"files.completeUploadExternal\", {\n files: [{ id: reserved.file_id, title: filename }],\n channel_id: at.channel,\n ...target(at, msg),\n ...(body.caption !== undefined && { initial_comment: body.caption }),\n }).pipe(Api.decoded(\"files.completeUploadExternal\", Completed))\n // A share reports the message it became; without one the file id is\n // the only handle there is, and a file message cannot be edited anyway.\n return Option.getOrElse(shareTs(uploaded), () => reserved.file_id)\n })\n\n const sendMedia = (at: ConversationRef, msg: Outbound, body: MediaFields) =>\n Match.value(body.media).pipe(\n Match.tag(\"url\", ({ url }) => sendUrl(at, msg, body, url)),\n Match.tag(\"bytes\", ({ bytes, mimeType }) => sendBytes(at, msg, body, bytes, mimeType)),\n Match.tag(\"base64\", ({ base64, mimeType }) =>\n Effect.fromResult(Encoding.decodeBase64(base64)).pipe(\n Effect.mapError(invalidMedia),\n Effect.flatMap((bytes) => sendBytes(at, msg, body, bytes, mimeType)),\n ),\n ),\n Match.exhaustive,\n )\n\n // Every chunk lands in the same thread, so only the id of the last matters.\n const sendChunks = (at: ConversationRef, msg: Outbound, body: string) =>\n Effect.gen(function* () {\n const [first, ...rest] = splitForLimit(body, limits.maxText)\n const send = (chunk: string) =>\n call(\"chat.postMessage\", { ...target(at, msg), markdown_text: chunk }).pipe(\n Api.decoded(\"chat.postMessage\", Posted),\n Effect.map(({ ts }) => ts),\n )\n const head = yield* send(first)\n const tail = yield* Effect.forEach(rest, send)\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 ts = yield* Match.value(msg.body).pipe(\n Match.tag(\"Text\", ({ text }) => sendChunks(at, msg, text)),\n Match.tag(\"Media\", (body) => sendMedia(at, msg, body)),\n Match.tag(\"Raw\", ({ payload }) =>\n sendRaw(payload).pipe(\n Api.decoded(\"raw\", Posted),\n Effect.map(({ ts }) => ts),\n ),\n ),\n Match.exhaustive,\n )\n return MessageId(ts)\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(\"chat.update\", {\n channel: ref.conversation.channel,\n ts: ref.id,\n markdown_text: 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 // The shortcode is the same spelling a `Reaction` event delivers.\n const react: MessengerService[\"react\"] = (ref, emoji) =>\n call(\"reactions.add\", {\n channel: ref.conversation.channel,\n timestamp: ref.id,\n name: shortcode(emoji),\n }).pipe(\n Effect.asVoid,\n Effect.catchIf(fails(\"invalid_name\"), () =>\n Effect.fail(unsupported(\"reaction\", `${emoji} is not an emoji this workspace has`)),\n ),\n Effect.mapError(requestFailed(\"react\")),\n )\n\n /**\n * Best effort: Slack shows a session status on its agent surfaces only, so\n * a rejection is logged and swallowed rather than taking a turn down.\n */\n const typing: MessengerService[\"typing\"] = Effect.gen(function* () {\n const at = yield* CurrentConversation\n const status = (state: string) =>\n call(\"agents.sessions.setStatus\", {\n channel_id: at.channel,\n ...(at.thread !== undefined && { thread_ts: at.thread }),\n status: state,\n }).pipe(\n Effect.asVoid,\n Effect.catch((e) => Effect.logDebug(\"slack status not set\", { reason: reasonOf(e) })),\n )\n yield* Effect.acquireRelease(status(\"processing\"), () => status(\"active\"))\n })\n\n return {\n 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 Socket Mode connection, registered under both the `Slack` and\n * `Messenger` tags. Building the layer authenticates and waits for `hello`, so\n * a rejected token fails here; the connection lives until the scope closes and\n * `events` ends with it.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<Slack | Messenger, MessengerError.MessengerConnectFailed, HttpClient.HttpClient> =>\n Layer.effectContext(\n Effect.map(make(cfg), (service) =>\n Context.make(Slack, service).pipe(Context.add(Messenger, service)),\n ),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Effect, Redacted, Schema } from "effect";
|
|
2
|
+
import { HttpClient } from "effect/unstable/http";
|
|
3
|
+
import * as MessengerError from "@effect-uai/core/MessengerError";
|
|
4
|
+
//#region src/internal/api.d.ts
|
|
5
|
+
declare const provider = "slack";
|
|
6
|
+
type Config = {
|
|
7
|
+
/** `xoxb-`, for every Web API call. */
|
|
8
|
+
readonly botToken: Redacted.Redacted;
|
|
9
|
+
/** `xapp-`, for `apps.connections.open` and nothing else. */
|
|
10
|
+
readonly appToken: Redacted.Redacted;
|
|
11
|
+
/** Defaults to `https://slack.com/api`. */
|
|
12
|
+
readonly baseUrl?: string;
|
|
13
|
+
};
|
|
14
|
+
declare const ApiFailure_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
15
|
+
readonly _tag: "SlackApiFailure";
|
|
16
|
+
} & Readonly<A>;
|
|
17
|
+
/**
|
|
18
|
+
* A Web API call that came back `ok: false` or never came back at all.
|
|
19
|
+
* `error` is Slack's own error string (`channel_not_found`, `invalid_auth`);
|
|
20
|
+
* each verb maps this onto its own `MessengerError`.
|
|
21
|
+
*/
|
|
22
|
+
declare class ApiFailure extends ApiFailure_base<{
|
|
23
|
+
readonly method: string;
|
|
24
|
+
readonly status?: number;
|
|
25
|
+
readonly error: string;
|
|
26
|
+
readonly raw: unknown;
|
|
27
|
+
}> {}
|
|
28
|
+
type ApiError = ApiFailure | MessengerError.MessengerRateLimited;
|
|
29
|
+
type Params = Readonly<Record<string, unknown>>;
|
|
30
|
+
/** The bytes behind a `files.getUploadURLExternal` URL. */
|
|
31
|
+
type Upload = {
|
|
32
|
+
readonly bytes: Uint8Array;
|
|
33
|
+
readonly filename: string;
|
|
34
|
+
readonly mimeType: string;
|
|
35
|
+
};
|
|
36
|
+
/** One JSON method call with the bot token. The body comes back undecoded. */
|
|
37
|
+
declare const call: (cfg: Config) => (method: string, params?: Params) => Effect.Effect<unknown, ApiError, HttpClient.HttpClient>;
|
|
38
|
+
/** `apps.connections.open`, the only call the app token is for. */
|
|
39
|
+
declare const callAsApp: (cfg: Config) => (method: string) => Effect.Effect<unknown, ApiError, HttpClient.HttpClient>;
|
|
40
|
+
/** One method called with query parameters, for the few that take no JSON body. */
|
|
41
|
+
declare const query: (cfg: Config) => (method: string, params: Params) => Effect.Effect<unknown, ApiError, HttpClient.HttpClient>;
|
|
42
|
+
/**
|
|
43
|
+
* Raw bytes to the URL `files.getUploadURLExternal` handed out. Not a Web API
|
|
44
|
+
* call: no token, no `ok` envelope, and the response body is plain text.
|
|
45
|
+
*/
|
|
46
|
+
declare const upload: (url: string, file: Upload) => Effect.Effect<void, ApiFailure, HttpClient.HttpClient>;
|
|
47
|
+
/** Decode a response body; a shape mismatch is a malformed response, not a defect. */
|
|
48
|
+
declare const decoded: <A>(method: string, schema: Schema.Decoder<A>) => <E, R>(result: Effect.Effect<unknown, E, R>) => Effect.Effect<A, E | ApiFailure, R>;
|
|
49
|
+
//#endregion
|
|
50
|
+
export { Upload as a, decoded as c, upload as d, Params as i, provider as l, ApiFailure as n, call as o, Config as r, callAsApp as s, ApiError as t, query as u };
|
|
51
|
+
//# sourceMappingURL=api-BBgliKo7.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-BBgliKo7.d.mts","names":[],"sources":["../src/internal/api.ts"],"mappings":";;;;cASa;KAED;;WAED,UAAU,SAAS;;WAEnB,UAAU,SAAS;;WAEnB;;;;;;;;;;cAQE,mBAAmB;WACrB;WACA;WACA;WACA;;KAGC,WAAW,aAAa,eAAe;KAQvC,SAAS,SAAS;;KAGlB;WACD,OAAO;WACP;WACA;;;cA4DE,OAAI,KACT,YAAM,gBACG,SAAW,WAAS,OAAO,gBAAgB,UAAU,WAAW;;cAIpE,YAAS,KACd,YAAM,mBACM,OAAO,gBAAgB,UAAU,WAAW;;cAInD,QAAK,KACV,YAAM,gBACG,QAAU,WAAS,OAAO,gBAAgB,UAAU,WAAW;;;;;cAyBnE,SAAM,aACN,MACL,WACL,OAAO,aAAa,YAAY,WAAW;;cAcjC,UACV,GAAC,gBAAgB,QAAU,OAAO,QAAQ,QAC1C,GAAG,GAAC,QAAU,OAAO,gBAAgB,GAAG,OAAK,OAAO,OAAO,GAAG,IAAI,YAAY"}
|