@amykit/plugin-slack 0.5.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/README.md +61 -0
- package/dist/SlackApi.d.ts +67 -0
- package/dist/SlackApi.d.ts.map +1 -0
- package/dist/SlackApi.js +143 -0
- package/dist/SlackApi.js.map +1 -0
- package/dist/SlackConversation.d.ts +111 -0
- package/dist/SlackConversation.d.ts.map +1 -0
- package/dist/SlackConversation.js +504 -0
- package/dist/SlackConversation.js.map +1 -0
- package/dist/ThreadMap.d.ts +150 -0
- package/dist/ThreadMap.d.ts.map +1 -0
- package/dist/ThreadMap.js +444 -0
- package/dist/ThreadMap.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +44 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +43 -0
- package/dist/plugin.js.map +1 -0
- package/dist/secret.d.ts +13 -0
- package/dist/secret.d.ts.map +1 -0
- package/dist/secret.js +56 -0
- package/dist/secret.js.map +1 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<!-- Generated by `npm run docs:generate` from the package itself. Do not edit by hand. -->
|
|
2
|
+
|
|
3
|
+
# @amykit/plugin-slack
|
|
4
|
+
|
|
5
|
+
A Slack thread per piece of work, over the Web API, as the conversation port.
|
|
6
|
+
|
|
7
|
+
A plugin for [amy](https://github.com/nicolasmelo1/amy). It provides the `conversation` port.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# Add @amykit/plugin-slack to amy.yaml, then install configured plugins:
|
|
13
|
+
amy init --install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then name it in `~/.amy/config.yaml`:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
workflows:
|
|
20
|
+
your-profile:
|
|
21
|
+
plugins:
|
|
22
|
+
- "@amykit/plugin-slack"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
plugins:
|
|
29
|
+
"@amykit/plugin-slack":
|
|
30
|
+
channel: <string>
|
|
31
|
+
defaultRetryAfterSeconds: 60
|
|
32
|
+
directory: "slack"
|
|
33
|
+
maxRateLimitRetries: 5
|
|
34
|
+
operator: <string>
|
|
35
|
+
retentionDays: 30
|
|
36
|
+
token: <string>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Setting | Type | Required | Default | What it is |
|
|
40
|
+
| :-- | :-- | :-- | :-- | :-- |
|
|
41
|
+
| `channel` | `string` | **yes** | | the channel id every work thread opens in, such as C0XXXXXXX — an id, not a name |
|
|
42
|
+
| `defaultRetryAfterSeconds` | `number` | no | `60` | how long to wait after a 429 that carries no Retry-After; one names its own wait, and that wins |
|
|
43
|
+
| `directory` | `string` | no | `slack` | where each work item's thread is remembered, relative to the state directory |
|
|
44
|
+
| `maxRateLimitRetries` | `number` | no | `5` | how many times a call Slack rate limits (HTTP 429) is waited out and tried again before it fails |
|
|
45
|
+
| `operator` | `string` | **yes** | | the user id, such as U0XXXXXXX, whose replies are answers; everybody else's are ignored |
|
|
46
|
+
| `retentionDays` | `number` | no | `30` | days a downloaded file or a thread's memory stays unused before it is pruned, 0 to keep everything. A pruned file is fetched again when its reply is read; a pruned thread means the next question opens a new one |
|
|
47
|
+
| `token` | `string` | **yes** | | where the bot token lives: `env:SLACK_BOT_TOKEN` or `file:<path>#<KEY>`, never the token itself |
|
|
48
|
+
|
|
49
|
+
Every field is checked at boot against the schema this package declares. A key that is
|
|
50
|
+
not one of the above is a refusal naming the plugin and the key, not a setting that
|
|
51
|
+
silently never applied.
|
|
52
|
+
|
|
53
|
+
## Where the reasoning is
|
|
54
|
+
|
|
55
|
+
- [Writing a plugin](https://github.com/nicolasmelo1/amy/tree/main/docs/build/write-a-plugin.md)
|
|
56
|
+
- [Reference: plugins](https://github.com/nicolasmelo1/amy/tree/main/docs/reference/plugins.md)
|
|
57
|
+
- [Ports and the registry](https://github.com/nicolasmelo1/amy/tree/main/docs/concepts/ports.md)
|
|
58
|
+
|
|
59
|
+
## Licence
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** One HTTP exchange, small enough to script in a test. */
|
|
2
|
+
export interface HttpRequest {
|
|
3
|
+
url: string;
|
|
4
|
+
method: "GET" | "POST";
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body?: string;
|
|
7
|
+
/** Refuse the answer once its body passes this many bytes, while it arrives. */
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface HttpResponse {
|
|
11
|
+
status: number;
|
|
12
|
+
/** Lower-cased names. */
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
body: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
export type HttpTransport = (request: HttpRequest) => Promise<HttpResponse>;
|
|
17
|
+
/** An answer larger than the request allowed, refused before it was held whole. */
|
|
18
|
+
export declare class TooLarge extends Error {
|
|
19
|
+
readonly limit: number;
|
|
20
|
+
constructor(limit: number);
|
|
21
|
+
}
|
|
22
|
+
export declare const fetchTransport: HttpTransport;
|
|
23
|
+
export declare const SLACK_API = "https://slack.com/api";
|
|
24
|
+
/** What one Web API method answered, with the headers that carry the scopes. */
|
|
25
|
+
export interface SlackAnswer {
|
|
26
|
+
body: Record<string, unknown>;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
}
|
|
29
|
+
export interface SlackApiOptions {
|
|
30
|
+
transport?: HttpTransport;
|
|
31
|
+
sleep?: (ms: number) => Promise<void>;
|
|
32
|
+
/** How many times a `429` is waited out before the call fails. */
|
|
33
|
+
maxRateLimitRetries?: number;
|
|
34
|
+
/** How long a `429` that names no `Retry-After` is waited out. */
|
|
35
|
+
defaultRetryAfterSeconds?: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Slack's answer when it waits without saying for how long: the one-a-minute
|
|
39
|
+
* limit on `conversations.replies` is the one this plugin is likeliest to hit.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_RETRY_AFTER_S = 60;
|
|
42
|
+
export declare const DEFAULT_RATE_LIMIT_RETRIES = 5;
|
|
43
|
+
/**
|
|
44
|
+
* The Web API, and only the Web API.
|
|
45
|
+
*
|
|
46
|
+
* No Socket Mode: Slack hands each event to one of an app's connections, so
|
|
47
|
+
* a connection opened here would take events from whatever else is connected.
|
|
48
|
+
*/
|
|
49
|
+
export declare class SlackApi {
|
|
50
|
+
private readonly token;
|
|
51
|
+
private readonly transport;
|
|
52
|
+
private readonly sleep;
|
|
53
|
+
private readonly maxRetries;
|
|
54
|
+
private readonly defaultRetryAfterS;
|
|
55
|
+
constructor(token: string, options?: SlackApiOptions);
|
|
56
|
+
/** Calls a method form-encoded, which every method accepts, and refuses `ok: false`. */
|
|
57
|
+
call(method: string, args?: Record<string, string | undefined>): Promise<SlackAnswer>;
|
|
58
|
+
/**
|
|
59
|
+
* A file's bytes, with the bot token.
|
|
60
|
+
*
|
|
61
|
+
* Only ever to Slack's own hosts over https: the URL comes out of a message,
|
|
62
|
+
* and the token must not follow one anywhere else.
|
|
63
|
+
*/
|
|
64
|
+
download(url: string, maxBytes?: number): Promise<Uint8Array>;
|
|
65
|
+
private send;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=SlackApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackApi.d.ts","sourceRoot":"","sources":["../src/SlackApi.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAE5E,mFAAmF;AACnF,qBAAa,QAAS,SAAQ,KAAK;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAGnC;AAED,eAAO,MAAM,cAAc,EAAE,aAW5B,CAAC;AAkCF,eAAO,MAAM,SAAS,0BAA0B,CAAC;AAEjD,gFAAgF;AAChF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,kEAAkE;IAClE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,eAAO,MAAM,0BAA0B,IAAI,CAAC;AAE5C;;;;;GAKG;AACH,qBAAa,QAAQ;IAOjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IANxB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;gBAGzB,KAAK,EAAE,MAAM,EAC9B,OAAO,GAAE,eAAoB;IAQ/B,wFAAwF;IAClF,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAwB/F;;;;;OAKG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;YAYrD,IAAI;CAUnB"}
|
package/dist/SlackApi.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** An answer larger than the request allowed, refused before it was held whole. */
|
|
2
|
+
export class TooLarge extends Error {
|
|
3
|
+
limit;
|
|
4
|
+
constructor(limit) {
|
|
5
|
+
super(`the answer is larger than ${limit} bytes`);
|
|
6
|
+
this.limit = limit;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export const fetchTransport = async (request) => {
|
|
10
|
+
const response = await fetch(request.url, {
|
|
11
|
+
method: request.method,
|
|
12
|
+
headers: request.headers,
|
|
13
|
+
body: request.body,
|
|
14
|
+
});
|
|
15
|
+
const headers = {};
|
|
16
|
+
response.headers.forEach((value, name) => {
|
|
17
|
+
headers[name.toLowerCase()] = value;
|
|
18
|
+
});
|
|
19
|
+
return { status: response.status, headers, body: await readBody(response, request.maxBytes) };
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The body, counted as it arrives: a declared length over the limit is
|
|
23
|
+
* refused unread, and a missing or wrong one is caught by the count.
|
|
24
|
+
*/
|
|
25
|
+
async function readBody(response, maxBytes) {
|
|
26
|
+
if (maxBytes === undefined || !response.body)
|
|
27
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
28
|
+
if (Number(response.headers.get("content-length") ?? 0) > maxBytes) {
|
|
29
|
+
await response.body.cancel();
|
|
30
|
+
throw new TooLarge(maxBytes);
|
|
31
|
+
}
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let total = 0;
|
|
34
|
+
const reader = response.body.getReader();
|
|
35
|
+
for (;;) {
|
|
36
|
+
const { done, value } = await reader.read();
|
|
37
|
+
if (done)
|
|
38
|
+
break;
|
|
39
|
+
total += value.byteLength;
|
|
40
|
+
if (total > maxBytes) {
|
|
41
|
+
await reader.cancel();
|
|
42
|
+
throw new TooLarge(maxBytes);
|
|
43
|
+
}
|
|
44
|
+
chunks.push(value);
|
|
45
|
+
}
|
|
46
|
+
const body = new Uint8Array(total);
|
|
47
|
+
let offset = 0;
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
body.set(chunk, offset);
|
|
50
|
+
offset += chunk.byteLength;
|
|
51
|
+
}
|
|
52
|
+
return body;
|
|
53
|
+
}
|
|
54
|
+
export const SLACK_API = "https://slack.com/api";
|
|
55
|
+
/**
|
|
56
|
+
* Slack's answer when it waits without saying for how long: the one-a-minute
|
|
57
|
+
* limit on `conversations.replies` is the one this plugin is likeliest to hit.
|
|
58
|
+
*/
|
|
59
|
+
export const DEFAULT_RETRY_AFTER_S = 60;
|
|
60
|
+
export const DEFAULT_RATE_LIMIT_RETRIES = 5;
|
|
61
|
+
/**
|
|
62
|
+
* The Web API, and only the Web API.
|
|
63
|
+
*
|
|
64
|
+
* No Socket Mode: Slack hands each event to one of an app's connections, so
|
|
65
|
+
* a connection opened here would take events from whatever else is connected.
|
|
66
|
+
*/
|
|
67
|
+
export class SlackApi {
|
|
68
|
+
token;
|
|
69
|
+
transport;
|
|
70
|
+
sleep;
|
|
71
|
+
maxRetries;
|
|
72
|
+
defaultRetryAfterS;
|
|
73
|
+
constructor(token, options = {}) {
|
|
74
|
+
this.token = token;
|
|
75
|
+
this.transport = options.transport ?? fetchTransport;
|
|
76
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
77
|
+
this.maxRetries = options.maxRateLimitRetries ?? DEFAULT_RATE_LIMIT_RETRIES;
|
|
78
|
+
this.defaultRetryAfterS = options.defaultRetryAfterSeconds ?? DEFAULT_RETRY_AFTER_S;
|
|
79
|
+
}
|
|
80
|
+
/** Calls a method form-encoded, which every method accepts, and refuses `ok: false`. */
|
|
81
|
+
async call(method, args = {}) {
|
|
82
|
+
const form = new URLSearchParams();
|
|
83
|
+
for (const [key, value] of Object.entries(args))
|
|
84
|
+
if (value !== undefined)
|
|
85
|
+
form.set(key, value);
|
|
86
|
+
const response = await this.send({
|
|
87
|
+
url: `${SLACK_API}/${method}`,
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
Authorization: `Bearer ${this.token}`,
|
|
91
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
92
|
+
},
|
|
93
|
+
body: form.toString(),
|
|
94
|
+
});
|
|
95
|
+
if (response.status !== 200)
|
|
96
|
+
throw new Error(`slack ${method} answered HTTP ${response.status}`);
|
|
97
|
+
const body = JSON.parse(new TextDecoder().decode(response.body));
|
|
98
|
+
if (body.ok !== true) {
|
|
99
|
+
// `missing_scope` carries the scope it wanted, which is the fix.
|
|
100
|
+
const needed = typeof body.needed === "string" ? ` (needs ${body.needed})` : "";
|
|
101
|
+
throw new Error(`slack ${method} refused: ${String(body.error ?? "unknown error")}${needed}`);
|
|
102
|
+
}
|
|
103
|
+
return { body, headers: response.headers };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A file's bytes, with the bot token.
|
|
107
|
+
*
|
|
108
|
+
* Only ever to Slack's own hosts over https: the URL comes out of a message,
|
|
109
|
+
* and the token must not follow one anywhere else.
|
|
110
|
+
*/
|
|
111
|
+
async download(url, maxBytes) {
|
|
112
|
+
const parsed = new URL(url);
|
|
113
|
+
if (parsed.protocol !== "https:" || !isSlackHost(parsed.hostname)) {
|
|
114
|
+
throw new Error(`refusing to send the slack token to ${parsed.origin}`);
|
|
115
|
+
}
|
|
116
|
+
const response = await this.send({ url, method: "GET", headers: { Authorization: `Bearer ${this.token}` }, maxBytes });
|
|
117
|
+
if (response.status !== 200)
|
|
118
|
+
throw new Error(`slack file ${parsed.pathname} answered HTTP ${response.status}`);
|
|
119
|
+
// Checked again here for a transport that does not honour the limit.
|
|
120
|
+
if (maxBytes !== undefined && response.body.byteLength > maxBytes)
|
|
121
|
+
throw new TooLarge(maxBytes);
|
|
122
|
+
return response.body;
|
|
123
|
+
}
|
|
124
|
+
async send(request) {
|
|
125
|
+
for (let attempt = 0;; attempt++) {
|
|
126
|
+
const response = await this.transport(request);
|
|
127
|
+
if (response.status !== 429)
|
|
128
|
+
return response;
|
|
129
|
+
if (attempt >= this.maxRetries) {
|
|
130
|
+
throw new Error(`slack kept rate limiting ${request.url} after ${attempt + 1} tries`);
|
|
131
|
+
}
|
|
132
|
+
await this.sleep(retryAfterMs(response.headers["retry-after"], this.defaultRetryAfterS));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function retryAfterMs(header, fallbackS) {
|
|
137
|
+
const seconds = header?.trim() ? Number(header) : Number.NaN;
|
|
138
|
+
return (Number.isFinite(seconds) && seconds >= 0 ? seconds : fallbackS) * 1000;
|
|
139
|
+
}
|
|
140
|
+
function isSlackHost(hostname) {
|
|
141
|
+
return hostname === "slack.com" || hostname.endsWith(".slack.com");
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=SlackApi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackApi.js","sourceRoot":"","sources":["../src/SlackApi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Conversation, Reply, ThreadRef } from "@amykit/core";
|
|
2
|
+
import { SlackApi } from "./SlackApi.js";
|
|
3
|
+
export interface SlackConversationOptions {
|
|
4
|
+
/** A channel id, not a name. */
|
|
5
|
+
channel: string;
|
|
6
|
+
/** The only user whose replies are answers. */
|
|
7
|
+
operator: string;
|
|
8
|
+
/** Where the thread map and downloaded files live. */
|
|
9
|
+
directory: string;
|
|
10
|
+
/** How long to wait for another process opening a thread. */
|
|
11
|
+
lockWaitMs?: number;
|
|
12
|
+
/** How long a claim on a thread holds without being renewed. */
|
|
13
|
+
leaseMs?: number;
|
|
14
|
+
/** The largest attachment downloaded; a larger one is named in the reply instead. */
|
|
15
|
+
maxFileBytes?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Days a downloaded file or a thread's memory stays unused before it is
|
|
18
|
+
* pruned; `0` keeps everything.
|
|
19
|
+
*/
|
|
20
|
+
retentionDays?: number;
|
|
21
|
+
now?: () => Date;
|
|
22
|
+
}
|
|
23
|
+
/** What `amy doctor` prints for this plugin, one line per question. */
|
|
24
|
+
export interface SlackCheck {
|
|
25
|
+
label: string;
|
|
26
|
+
ok: boolean;
|
|
27
|
+
detail?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Long enough for an operator's holiday, short enough that screenshots do not pile up. */
|
|
30
|
+
export declare const DEFAULT_RETENTION_DAYS = 30;
|
|
31
|
+
export declare class SlackConversation implements Conversation {
|
|
32
|
+
private readonly api;
|
|
33
|
+
private readonly options;
|
|
34
|
+
private readonly threads;
|
|
35
|
+
private readonly filesDirectory;
|
|
36
|
+
private readonly opening;
|
|
37
|
+
private botUser?;
|
|
38
|
+
private prunedAt;
|
|
39
|
+
constructor(api: SlackApi, options: SlackConversationOptions);
|
|
40
|
+
open(workId: string, title: string): Promise<ThreadRef>;
|
|
41
|
+
post(thread: ThreadRef, message: {
|
|
42
|
+
text: string;
|
|
43
|
+
files?: string[];
|
|
44
|
+
}): Promise<string>;
|
|
45
|
+
replies(thread: ThreadRef, since: string): Promise<Reply[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the token can do what this plugin does, asked of Slack itself.
|
|
48
|
+
*
|
|
49
|
+
* `auth.test` for the token, its `x-oauth-scopes` header for the scopes,
|
|
50
|
+
* and `conversations.info` for whether the channel is one the bot can see
|
|
51
|
+
* — which also says whether the history scope it needs is the public or
|
|
52
|
+
* the private one.
|
|
53
|
+
*/
|
|
54
|
+
checks(): Promise<SlackCheck[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Forgets one piece of work: its thread's memory and every file downloaded
|
|
57
|
+
* from it. The thread stays in Slack; the next `open` starts a new one.
|
|
58
|
+
*/
|
|
59
|
+
forget(workId: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Removes what nobody used within the retention: a downloaded file, which
|
|
62
|
+
* the next read of its reply fetches again, and a thread's memory, whose
|
|
63
|
+
* files go with it. Returns how many of each went.
|
|
64
|
+
*/
|
|
65
|
+
prune(now?: Date): {
|
|
66
|
+
threads: number;
|
|
67
|
+
files: number;
|
|
68
|
+
};
|
|
69
|
+
private clock;
|
|
70
|
+
/** Where one thread's downloads live, so forgetting it takes them too. */
|
|
71
|
+
private threadFiles;
|
|
72
|
+
/** Beside the thread's files, not in them, so removing those leaves it. */
|
|
73
|
+
private forgottenMarker;
|
|
74
|
+
/**
|
|
75
|
+
* Marks the thread forgotten, then removes its files. The marker is what a
|
|
76
|
+
* download still in flight — in this process or another — reads after it
|
|
77
|
+
* publishes, so it cannot bring a forgotten thread's file back.
|
|
78
|
+
*/
|
|
79
|
+
private dropFiles;
|
|
80
|
+
/** Throttled, and never the reason a call fails: pruning is housekeeping. */
|
|
81
|
+
private pruneNowAndThen;
|
|
82
|
+
private channelChecks;
|
|
83
|
+
/**
|
|
84
|
+
* Exactly one process posts a root per attempt; an attempt that follows
|
|
85
|
+
* one that may have reached Slack looks for that root before posting.
|
|
86
|
+
*/
|
|
87
|
+
private startThread;
|
|
88
|
+
/**
|
|
89
|
+
* The root this bot posted for this work since `since`, if Slack took one.
|
|
90
|
+
*
|
|
91
|
+
* Matched by the work id carried in the root's metadata, never by its
|
|
92
|
+
* text: two items can share a title, and a title can change.
|
|
93
|
+
*/
|
|
94
|
+
private findRoot;
|
|
95
|
+
/** Every message in the thread after `afterMicros`, across pages. */
|
|
96
|
+
private threadMessages;
|
|
97
|
+
private pages;
|
|
98
|
+
/** By author id, never by a marker in the text. */
|
|
99
|
+
private isOperatorAnswer;
|
|
100
|
+
/** Cached once known; a failure is not, so the next poll asks again. */
|
|
101
|
+
private botUserId;
|
|
102
|
+
/**
|
|
103
|
+
* The attachments as local paths, and a line naming each one too large to
|
|
104
|
+
* fetch — declared so by Slack, or found so while it arrived — so the
|
|
105
|
+
* reader knows a file was there rather than never seeing it.
|
|
106
|
+
*/
|
|
107
|
+
private downloadAll;
|
|
108
|
+
/** The file's local path, or nothing when its thread was forgotten. */
|
|
109
|
+
private download;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=SlackConversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SlackConversation.d.ts","sourceRoot":"","sources":["../src/SlackConversation.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAY,MAAM,eAAe,CAAC;AAGnD,MAAM,WAAW,wBAAwB;IACvC,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qFAAqF;IACrF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,uEAAuE;AACvE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAgCD,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAUzC,qBAAa,iBAAkB,YAAW,YAAY;IAQlD,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAR1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAY;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IACjE,OAAO,CAAC,OAAO,CAAC,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAA4B;gBAGzB,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,wBAAwB;IAapD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;IAoBjD,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;IAiBrF,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;IAyBjE;;;;;;;OAOG;IACG,MAAM,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;IAWrC;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK5B;;;;OAIG;IACH,KAAK,CAAC,GAAG,GAAE,IAAmB,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IAanE,OAAO,CAAC,KAAK;IAIb,0EAA0E;IAC1E,OAAO,CAAC,WAAW;IAInB,2EAA2E;IAC3E,OAAO,CAAC,eAAe;IAIvB;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAMjB,6EAA6E;IAC7E,OAAO,CAAC,eAAe;YAWT,aAAa;IAgC3B;;;OAGG;YACW,WAAW;IAmBzB;;;;;OAKG;YACW,QAAQ;IAetB,qEAAqE;IACrE,OAAO,CAAC,cAAc;YAIR,KAAK;IAYnB,mDAAmD;IACnD,OAAO,CAAC,gBAAgB;IAOxB,wEAAwE;IACxE,OAAO,CAAC,SAAS;IAWjB;;;;OAIG;YACW,WAAW;IAwBzB,uEAAuE;YACzD,QAAQ;CAqBvB"}
|