@zhin.js/adapter-slack 4.0.0 → 4.1.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/CHANGELOG.md +28 -0
- package/README.md +207 -98
- package/{skills/slack → agent}/PERMITS.md +1 -1
- package/{skills/slack/SKILL.md → agent/skills/slack.md} +2 -0
- package/agent/tools/add_reaction.ts +26 -0
- package/agent/tools/archive_channel.ts +24 -0
- package/agent/tools/edit_message.ts +27 -0
- package/agent/tools/invite_to_channel.ts +26 -0
- package/agent/tools/pin_message.ts +26 -0
- package/agent/tools/remove_reaction.ts +26 -0
- package/agent/tools/set_purpose.ts +26 -0
- package/agent/tools/set_topic.ts +26 -0
- package/agent/tools/unarchive.ts +24 -0
- package/agent/tools/unpin_message.ts +26 -0
- package/agent/tools/user_info.ts +31 -0
- package/lib/agent/tools/add_reaction.js +21 -0
- package/lib/agent/tools/add_reaction.js.map +1 -0
- package/lib/agent/tools/archive_channel.js +21 -0
- package/lib/agent/tools/archive_channel.js.map +1 -0
- package/lib/agent/tools/edit_message.js +22 -0
- package/lib/agent/tools/edit_message.js.map +1 -0
- package/lib/agent/tools/invite_to_channel.js +22 -0
- package/lib/agent/tools/invite_to_channel.js.map +1 -0
- package/lib/agent/tools/pin_message.js +22 -0
- package/lib/agent/tools/pin_message.js.map +1 -0
- package/lib/agent/tools/remove_reaction.js +21 -0
- package/lib/agent/tools/remove_reaction.js.map +1 -0
- package/lib/agent/tools/set_purpose.js +22 -0
- package/lib/agent/tools/set_purpose.js.map +1 -0
- package/lib/agent/tools/set_topic.js +22 -0
- package/lib/agent/tools/set_topic.js.map +1 -0
- package/lib/agent/tools/unarchive.js +21 -0
- package/lib/agent/tools/unarchive.js.map +1 -0
- package/lib/agent/tools/unpin_message.js +22 -0
- package/lib/agent/tools/unpin_message.js.map +1 -0
- package/lib/agent/tools/user_info.js +28 -0
- package/lib/agent/tools/user_info.js.map +1 -0
- package/lib/{adapter.js → src/adapter.js} +23 -6
- package/lib/src/adapter.js.map +1 -0
- package/lib/src/endpoint.js +312 -0
- package/lib/src/endpoint.js.map +1 -0
- package/lib/src/event-dispatcher.js +233 -0
- package/lib/src/event-dispatcher.js.map +1 -0
- package/lib/src/index.js +51 -0
- package/lib/src/index.js.map +1 -0
- package/lib/src/markdown-to-mrkdwn.js +61 -0
- package/lib/src/markdown-to-mrkdwn.js.map +1 -0
- package/lib/src/mrkdwn-to-markdown.js +31 -0
- package/lib/src/mrkdwn-to-markdown.js.map +1 -0
- package/lib/{platform-permit.js → src/platform-permit.js} +5 -2
- package/lib/src/platform-permit.js.map +1 -0
- package/lib/src/segment-mapper.js +2 -0
- package/lib/src/segment-mapper.js.map +1 -0
- package/lib/src/signing.js +21 -0
- package/lib/src/signing.js.map +1 -0
- package/lib/src/slack-agent-deps.js +10 -0
- package/lib/src/slack-agent-deps.js.map +1 -0
- package/lib/src/slack-inbound-filter.js +47 -0
- package/lib/src/slack-inbound-filter.js.map +1 -0
- package/lib/src/slack-inbound.js +127 -0
- package/lib/src/slack-inbound.js.map +1 -0
- package/lib/src/slack-message-ref.js +18 -0
- package/lib/src/slack-message-ref.js.map +1 -0
- package/lib/src/slack-outbound.js +223 -0
- package/lib/src/slack-outbound.js.map +1 -0
- package/lib/src/slack-reaction.js +27 -0
- package/lib/src/slack-reaction.js.map +1 -0
- package/lib/src/slack-response-url.js +17 -0
- package/lib/src/slack-response-url.js.map +1 -0
- package/lib/src/slack-side-events.js +64 -0
- package/lib/src/slack-side-events.js.map +1 -0
- package/lib/src/transport-http.js +83 -0
- package/lib/src/transport-http.js.map +1 -0
- package/lib/src/transport-socket.js +68 -0
- package/lib/src/transport-socket.js.map +1 -0
- package/lib/src/types.js +5 -0
- package/lib/src/types.js.map +1 -0
- package/package.json +25 -7
- package/src/adapter.ts +32 -10
- package/src/endpoint.ts +209 -603
- package/src/event-dispatcher.ts +255 -0
- package/src/index.ts +31 -239
- package/src/markdown-to-mrkdwn.ts +58 -0
- package/src/mrkdwn-to-markdown.ts +29 -0
- package/src/platform-permit.ts +3 -4
- package/src/segment-mapper.ts +1 -0
- package/src/signing.ts +28 -0
- package/src/slack-agent-deps.ts +22 -0
- package/src/slack-inbound-filter.ts +60 -0
- package/src/slack-inbound.ts +140 -0
- package/src/slack-message-ref.ts +18 -0
- package/src/slack-outbound.ts +269 -0
- package/src/slack-reaction.ts +23 -0
- package/src/slack-response-url.ts +26 -0
- package/src/slack-side-events.ts +74 -0
- package/src/transport-http.ts +95 -0
- package/src/transport-socket.ts +77 -0
- package/src/types.ts +99 -4
- package/lib/adapter.d.ts +0 -18
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js.map +0 -1
- package/lib/endpoint.d.ts +0 -108
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js +0 -673
- package/lib/endpoint.js.map +0 -1
- package/lib/index.d.ts +0 -10
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -252
- package/lib/index.js.map +0 -1
- package/lib/platform-permit.d.ts +0 -17
- package/lib/platform-permit.d.ts.map +0 -1
- package/lib/platform-permit.js.map +0 -1
- package/lib/types.d.ts +0 -17
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -2
- package/lib/types.js.map +0 -1
package/src/endpoint.ts
CHANGED
|
@@ -1,225 +1,126 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Slack Endpoint
|
|
2
|
+
* Slack Endpoint — 双传输(Socket Mode / HTTP)
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
4
|
+
import { WebClient } from '@slack/web-api';
|
|
5
|
+
import { formatCompact, Endpoint, Message, segment, type SendOptions, type EditMessageOptions, expandInteractiveSegmentsInContent } from 'zhin.js';
|
|
6
|
+
import type { SlackEndpointConfig, SlackMessageEvent } from './types.js';
|
|
7
|
+
import type { SlackAdapter } from './adapter.js';
|
|
8
|
+
import { toCanonicalSegments, fromCanonicalSegments } from './segment-mapper.js';
|
|
9
|
+
import { parseSlackMessageToSegments, resolveSlackChannelType } from './slack-inbound.js';
|
|
10
|
+
import { sendSlackContent, editSlackContent } from './slack-outbound.js';
|
|
11
|
+
import { normalizeSlackReactionName } from './slack-reaction.js';
|
|
12
|
+
import { formatSlackMessageRef, parseSlackMessageRef } from './slack-message-ref.js';
|
|
13
|
+
import { SlackEventDispatcher } from './event-dispatcher.js';
|
|
14
|
+
import { SlackSocketTransport } from './transport-socket.js';
|
|
15
|
+
import { SlackHttpTransport, type RouterLike } from './transport-http.js';
|
|
11
16
|
|
|
12
17
|
export class SlackEndpoint implements Endpoint<SlackEndpointConfig, SlackMessageEvent> {
|
|
13
18
|
$connected: boolean;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
/** Slack Bot User ID(auth.test.user_id),用于 @ 触发 AI */
|
|
20
|
+
$platformUserId?: string;
|
|
21
|
+
client?: WebClient;
|
|
22
|
+
/** message ts → channel id(Activity Feedback reaction / recall 定位频道) */
|
|
23
|
+
readonly messageChannelMap = new Map<string, string>();
|
|
24
|
+
senderPermitCache = new Map<string, { at: number; role?: string; permissions: string[] }>();
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
private dispatcher?: SlackEventDispatcher;
|
|
27
|
+
private socketTransport?: SlackSocketTransport;
|
|
28
|
+
private httpTransport?: SlackHttpTransport;
|
|
21
29
|
|
|
22
|
-
get
|
|
23
|
-
|
|
24
|
-
}
|
|
30
|
+
get logger() { return this.adapter.plugin.logger; }
|
|
31
|
+
get $id() { return this.$config.name; }
|
|
25
32
|
|
|
26
33
|
constructor(public adapter: SlackAdapter, public $config: SlackEndpointConfig) {
|
|
27
34
|
this.$connected = false;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
if (this.app && this.client) return;
|
|
32
|
-
const $config = this.$config;
|
|
33
|
-
if ($config.socketMode && $config.appToken) {
|
|
34
|
-
this.app = new SlackApp({
|
|
35
|
-
token: $config.token,
|
|
36
|
-
signingSecret: $config.signingSecret,
|
|
37
|
-
appToken: $config.appToken,
|
|
38
|
-
socketMode: true,
|
|
39
|
-
logLevel: $config.logLevel || LogLevel.INFO,
|
|
40
|
-
});
|
|
41
|
-
} else {
|
|
42
|
-
this.app = new SlackApp({
|
|
43
|
-
token: $config.token,
|
|
44
|
-
signingSecret: $config.signingSecret,
|
|
45
|
-
socketMode: false,
|
|
46
|
-
logLevel: $config.logLevel || LogLevel.INFO,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
this.client = new WebClient($config.token);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async $connect(): Promise<void> {
|
|
53
|
-
this.#ensureSlackRuntime();
|
|
37
|
+
async $connect(router?: RouterLike): Promise<void> {
|
|
54
38
|
try {
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// Set up app mention handler
|
|
61
|
-
this.app!.event("app_mention", async ({ event, say }) => {
|
|
62
|
-
await this.handleSlackMessage(event as any);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Start the app
|
|
66
|
-
const port = this.$config.port || 3000;
|
|
39
|
+
this.client = new WebClient(this.$config.token);
|
|
40
|
+
this.dispatcher = new SlackEventDispatcher(this);
|
|
41
|
+
|
|
67
42
|
if (this.$config.socketMode) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
43
|
+
this.socketTransport = new SlackSocketTransport(this.$config, this.dispatcher, this.logger);
|
|
44
|
+
await this.socketTransport.connect();
|
|
45
|
+
} else if (router) {
|
|
46
|
+
this.httpTransport = new SlackHttpTransport(this.$config, this.dispatcher, this.logger);
|
|
47
|
+
this.httpTransport.registerRoutes(router);
|
|
71
48
|
}
|
|
72
49
|
|
|
73
50
|
this.$connected = true;
|
|
74
51
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.
|
|
52
|
+
const authTest = await this.client.auth.test();
|
|
53
|
+
if (authTest.user_id) {
|
|
54
|
+
this.$platformUserId = String(authTest.user_id);
|
|
55
|
+
}
|
|
56
|
+
this.logger.debug(formatCompact({
|
|
57
|
+
endpoint: this.$config.name,
|
|
58
|
+
user: authTest.user,
|
|
59
|
+
platform_user_id: this.$platformUserId,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
if (!this.$config.socketMode && router) {
|
|
63
|
+
this.logger.debug(formatCompact({ op: 'listen', mode: 'http', path: '/slack/events' }));
|
|
81
64
|
}
|
|
82
65
|
} catch (error) {
|
|
83
|
-
this.logger.error(
|
|
66
|
+
this.logger.error('Failed to connect Slack bot:', error);
|
|
84
67
|
this.$connected = false;
|
|
85
68
|
throw error;
|
|
86
69
|
}
|
|
87
70
|
}
|
|
88
71
|
|
|
89
72
|
async $disconnect(): Promise<void> {
|
|
90
|
-
if (!this.app) {
|
|
91
|
-
this.$connected = false;
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
73
|
try {
|
|
95
|
-
|
|
74
|
+
if (this.socketTransport) {
|
|
75
|
+
await this.socketTransport.disconnect();
|
|
76
|
+
this.socketTransport = undefined;
|
|
77
|
+
}
|
|
78
|
+
this.httpTransport = undefined;
|
|
96
79
|
this.$connected = false;
|
|
97
|
-
this.logger.
|
|
80
|
+
this.logger.debug(formatCompact({ op: 'disconnect', endpoint: this.$config.name }));
|
|
98
81
|
} catch (error) {
|
|
99
|
-
this.logger.error(
|
|
82
|
+
this.logger.error('Error disconnecting Slack bot:', error);
|
|
100
83
|
throw error;
|
|
101
84
|
}
|
|
102
85
|
}
|
|
103
86
|
|
|
104
|
-
private senderPermitCache = new Map<string, { at: number; role?: string; permissions: string[] }>();
|
|
105
|
-
|
|
106
|
-
private async enrichChannelSender(
|
|
107
|
-
message: Message<SlackMessageEvent>,
|
|
108
|
-
msg: SlackMessageEvent,
|
|
109
|
-
): Promise<void> {
|
|
110
|
-
if (message.$channel.type !== "group" || !("user" in msg) || !msg.user) return;
|
|
111
|
-
const channelId = msg.channel;
|
|
112
|
-
const userId = msg.user;
|
|
113
|
-
const key = `${channelId}:${userId}`;
|
|
114
|
-
const now = Date.now();
|
|
115
|
-
const cached = this.senderPermitCache.get(key);
|
|
116
|
-
if (cached && now - cached.at < 60_000) {
|
|
117
|
-
message.$sender.role = cached.role;
|
|
118
|
-
message.$sender.permissions = cached.permissions;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
const user = await this.getUserInfo(userId);
|
|
123
|
-
let isChannelManager = false;
|
|
124
|
-
try {
|
|
125
|
-
const channel = await this.getChannelInfo(channelId);
|
|
126
|
-
if (channel?.creator === userId) isChannelManager = true;
|
|
127
|
-
} catch {
|
|
128
|
-
// ignore
|
|
129
|
-
}
|
|
130
|
-
const normalized = normalizeSlackSenderForPermit({
|
|
131
|
-
isWorkspaceOwner: user.is_owner === true,
|
|
132
|
-
isWorkspaceAdmin: user.is_admin === true,
|
|
133
|
-
isChannelManager,
|
|
134
|
-
});
|
|
135
|
-
const entry = {
|
|
136
|
-
at: now,
|
|
137
|
-
role: normalized.role,
|
|
138
|
-
permissions: normalized.permissions ?? [],
|
|
139
|
-
};
|
|
140
|
-
this.senderPermitCache.set(key, entry);
|
|
141
|
-
message.$sender.role = entry.role;
|
|
142
|
-
message.$sender.permissions = entry.permissions;
|
|
143
|
-
} catch {
|
|
144
|
-
// 保守拒绝
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private async handleSlackMessage(msg: SlackMessageEvent): Promise<void> {
|
|
149
|
-
// Ignore bot messages and message changes
|
|
150
|
-
if ("subtype" in msg && (msg.subtype === "bot_message" || msg.subtype === "message_changed")) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const message = this.$formatMessage(msg);
|
|
155
|
-
await this.enrichChannelSender(message, msg);
|
|
156
|
-
this.adapter.emit("message.receive", message);
|
|
157
|
-
this.logger.debug(
|
|
158
|
-
`${this.$config.name} recv ${message.$channel.type}(${message.$channel.id}): ${segment.raw(message.$content)}`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
87
|
$formatMessage(msg: SlackMessageEvent): Message<SlackMessageEvent> {
|
|
163
|
-
|
|
164
|
-
const channelType = "channel_type" in msg && msg.channel_type === "im" ? "private" : "group";
|
|
88
|
+
const channelType = resolveSlackChannelType(msg);
|
|
165
89
|
const channelId = msg.channel;
|
|
90
|
+
this.trackMessageChannel(msg.ts, channelId);
|
|
91
|
+
const wire = parseSlackMessageToSegments(msg);
|
|
92
|
+
const content = toCanonicalSegments(wire);
|
|
93
|
+
const userId = msg.user ?? '';
|
|
94
|
+
const userName = (msg as any).username ?? (userId || 'Unknown');
|
|
95
|
+
const messageText = msg.text ?? '';
|
|
166
96
|
|
|
167
|
-
|
|
168
|
-
const content = this.parseMessageContent(msg);
|
|
169
|
-
|
|
170
|
-
// Extract user info safely
|
|
171
|
-
const userId = ("user" in msg ? msg.user : "") || "";
|
|
172
|
-
const userName = ("username" in msg ? msg.username : null) || userId || "Unknown";
|
|
173
|
-
const messageText = ("text" in msg ? msg.text : "") || "";
|
|
97
|
+
this.trackMessageChannel(msg.ts, channelId);
|
|
174
98
|
|
|
175
99
|
const result = Message.from(msg, {
|
|
176
|
-
$id: msg.ts,
|
|
177
|
-
$adapter:
|
|
100
|
+
$id: formatSlackMessageRef(channelId, msg.ts),
|
|
101
|
+
$adapter: 'slack',
|
|
178
102
|
$endpoint: this.$config.name,
|
|
179
|
-
$sender: {
|
|
180
|
-
|
|
181
|
-
name: userName,
|
|
182
|
-
},
|
|
183
|
-
$channel: {
|
|
184
|
-
id: channelId,
|
|
185
|
-
type: channelType,
|
|
186
|
-
},
|
|
103
|
+
$sender: { id: userId, name: userName },
|
|
104
|
+
$channel: { id: channelId, type: channelType },
|
|
187
105
|
$content: content,
|
|
188
106
|
$raw: messageText,
|
|
189
107
|
$timestamp: parseFloat(msg.ts) * 1000,
|
|
108
|
+
$quote_id: msg.thread_ts && msg.thread_ts !== msg.ts ? msg.thread_ts : undefined,
|
|
190
109
|
$recall: async () => {
|
|
191
|
-
|
|
192
|
-
await this.client!.chat.delete({
|
|
193
|
-
channel: channelId,
|
|
194
|
-
ts: result.$id,
|
|
195
|
-
});
|
|
196
|
-
} catch (error) {
|
|
197
|
-
this.logger.error("Error recalling Slack message:", error);
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
110
|
+
await this.client!.chat.delete({ channel: channelId, ts: msg.ts });
|
|
200
111
|
},
|
|
201
|
-
$reply: async (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
channel: channelId,
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// Handle thread reply
|
|
212
|
-
if (quote) {
|
|
213
|
-
const threadTs = typeof quote === "boolean" ? result.$id : quote;
|
|
214
|
-
sendOptions.thread_ts = threadTs;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return await this.adapter.sendMessage({
|
|
218
|
-
context: "slack",
|
|
112
|
+
$reply: async (replyContent, quote?) => {
|
|
113
|
+
if (!Array.isArray(replyContent)) replyContent = [replyContent];
|
|
114
|
+
const threadTs = quote
|
|
115
|
+
? (typeof quote === 'boolean' ? msg.ts : quote)
|
|
116
|
+
: (msg.thread_ts ?? undefined);
|
|
117
|
+
return await this.adapter.sendMessage({
|
|
118
|
+
context: 'slack',
|
|
219
119
|
endpoint: this.$config.name,
|
|
220
120
|
id: channelId,
|
|
221
|
-
type:
|
|
222
|
-
content:
|
|
121
|
+
type: channelType,
|
|
122
|
+
content: replyContent,
|
|
123
|
+
...(threadTs ? { threadId: threadTs } : {}),
|
|
223
124
|
});
|
|
224
125
|
},
|
|
225
126
|
});
|
|
@@ -227,508 +128,213 @@ export class SlackEndpoint implements Endpoint<SlackEndpointConfig, SlackMessage
|
|
|
227
128
|
return result;
|
|
228
129
|
}
|
|
229
130
|
|
|
230
|
-
private parseMessageContent(msg: SlackMessageEvent): MessageSegment[] {
|
|
231
|
-
const segments: MessageSegment[] = [];
|
|
232
|
-
|
|
233
|
-
// Handle text
|
|
234
|
-
if ("text" in msg && msg.text) {
|
|
235
|
-
// Parse Slack formatting
|
|
236
|
-
segments.push(...this.parseSlackText(msg.text));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Handle files
|
|
240
|
-
if ("files" in msg && msg.files) {
|
|
241
|
-
for (const file of msg.files) {
|
|
242
|
-
if (file.mimetype?.startsWith("image/")) {
|
|
243
|
-
segments.push({
|
|
244
|
-
type: "image",
|
|
245
|
-
data: {
|
|
246
|
-
id: file.id,
|
|
247
|
-
name: file.name,
|
|
248
|
-
url: file.url_private || file.permalink,
|
|
249
|
-
size: file.size,
|
|
250
|
-
mimetype: file.mimetype,
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
} else if (file.mimetype?.startsWith("video/")) {
|
|
254
|
-
segments.push({
|
|
255
|
-
type: "video",
|
|
256
|
-
data: {
|
|
257
|
-
id: file.id,
|
|
258
|
-
name: file.name,
|
|
259
|
-
url: file.url_private || file.permalink,
|
|
260
|
-
size: file.size,
|
|
261
|
-
mimetype: file.mimetype,
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
} else if (file.mimetype?.startsWith("audio/")) {
|
|
265
|
-
segments.push({
|
|
266
|
-
type: "audio",
|
|
267
|
-
data: {
|
|
268
|
-
id: file.id,
|
|
269
|
-
name: file.name,
|
|
270
|
-
url: file.url_private || file.permalink,
|
|
271
|
-
size: file.size,
|
|
272
|
-
mimetype: file.mimetype,
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
} else {
|
|
276
|
-
segments.push({
|
|
277
|
-
type: "file",
|
|
278
|
-
data: {
|
|
279
|
-
id: file.id,
|
|
280
|
-
name: file.name,
|
|
281
|
-
url: file.url_private || file.permalink,
|
|
282
|
-
size: file.size,
|
|
283
|
-
mimetype: file.mimetype,
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Handle attachments
|
|
291
|
-
if ("attachments" in msg && msg.attachments) {
|
|
292
|
-
for (const attachment of msg.attachments) {
|
|
293
|
-
if (attachment.image_url) {
|
|
294
|
-
segments.push({
|
|
295
|
-
type: "image",
|
|
296
|
-
data: {
|
|
297
|
-
url: attachment.image_url,
|
|
298
|
-
title: attachment.title,
|
|
299
|
-
text: attachment.text,
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return segments.length > 0
|
|
307
|
-
? segments
|
|
308
|
-
: [{ type: "text", data: { text: "" } }];
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private parseSlackText(text: string): MessageSegment[] {
|
|
312
|
-
const segments: MessageSegment[] = [];
|
|
313
|
-
let lastIndex = 0;
|
|
314
|
-
|
|
315
|
-
// Match user mentions <@U12345678>
|
|
316
|
-
const userMentionRegex = /<@([UW][A-Z0-9]+)(?:\|([^>]+))?>/g;
|
|
317
|
-
// Match channel mentions <#C12345678|general>
|
|
318
|
-
const channelMentionRegex = /<#([C][A-Z0-9]+)(?:\|([^>]+))?>/g;
|
|
319
|
-
// Match links <http://example.com|Example>
|
|
320
|
-
const linkRegex = /<(https?:\/\/[^|>]+)(?:\|([^>]+))?>/g;
|
|
321
|
-
|
|
322
|
-
const allMatches: Array<{
|
|
323
|
-
match: RegExpExecArray;
|
|
324
|
-
type: "user" | "channel" | "link";
|
|
325
|
-
}> = [];
|
|
326
|
-
|
|
327
|
-
// Collect all matches
|
|
328
|
-
let match;
|
|
329
|
-
while ((match = userMentionRegex.exec(text)) !== null) {
|
|
330
|
-
allMatches.push({ match, type: "user" });
|
|
331
|
-
}
|
|
332
|
-
while ((match = channelMentionRegex.exec(text)) !== null) {
|
|
333
|
-
allMatches.push({ match, type: "channel" });
|
|
334
|
-
}
|
|
335
|
-
while ((match = linkRegex.exec(text)) !== null) {
|
|
336
|
-
allMatches.push({ match, type: "link" });
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Sort by position
|
|
340
|
-
allMatches.sort((a, b) => a.match.index! - b.match.index!);
|
|
341
|
-
|
|
342
|
-
// Process matches
|
|
343
|
-
for (const { match, type } of allMatches) {
|
|
344
|
-
const matchStart = match.index!;
|
|
345
|
-
const matchEnd = matchStart + match[0].length;
|
|
346
|
-
|
|
347
|
-
// Add text before match
|
|
348
|
-
if (matchStart > lastIndex) {
|
|
349
|
-
const beforeText = text.slice(lastIndex, matchStart);
|
|
350
|
-
if (beforeText.trim()) {
|
|
351
|
-
segments.push({ type: "text", data: { text: beforeText } });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Add special segment
|
|
356
|
-
switch (type) {
|
|
357
|
-
case "user":
|
|
358
|
-
segments.push({
|
|
359
|
-
type: "at",
|
|
360
|
-
data: {
|
|
361
|
-
id: match[1],
|
|
362
|
-
name: match[2] || match[1],
|
|
363
|
-
text: match[0],
|
|
364
|
-
},
|
|
365
|
-
});
|
|
366
|
-
break;
|
|
367
|
-
|
|
368
|
-
case "channel":
|
|
369
|
-
segments.push({
|
|
370
|
-
type: "channel_mention",
|
|
371
|
-
data: {
|
|
372
|
-
id: match[1],
|
|
373
|
-
name: match[2] || match[1],
|
|
374
|
-
text: match[0],
|
|
375
|
-
},
|
|
376
|
-
});
|
|
377
|
-
break;
|
|
378
|
-
|
|
379
|
-
case "link":
|
|
380
|
-
segments.push({
|
|
381
|
-
type: "link",
|
|
382
|
-
data: {
|
|
383
|
-
url: match[1],
|
|
384
|
-
text: match[2] || match[1],
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
lastIndex = matchEnd;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Add remaining text
|
|
394
|
-
if (lastIndex < text.length) {
|
|
395
|
-
const remainingText = text.slice(lastIndex);
|
|
396
|
-
if (remainingText.trim()) {
|
|
397
|
-
segments.push({ type: "text", data: { text: remainingText } });
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return segments.length > 0
|
|
402
|
-
? segments
|
|
403
|
-
: [{ type: "text", data: { text } }];
|
|
404
|
-
}
|
|
405
|
-
|
|
406
131
|
async $sendMessage(options: SendOptions): Promise<string> {
|
|
407
132
|
try {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
133
|
+
const canonical = expandInteractiveSegmentsInContent(options.content);
|
|
134
|
+
const wire = fromCanonicalSegments(canonical);
|
|
135
|
+
const result = await sendSlackContent(this.client!, wire, {
|
|
136
|
+
channel: options.id,
|
|
137
|
+
threadTs: options.threadId,
|
|
138
|
+
}, this.logger);
|
|
412
139
|
this.logger.debug(
|
|
413
|
-
`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}
|
|
140
|
+
`${this.$config.name} send ${options.type}(${options.id}): ${segment.raw(options.content)}`,
|
|
414
141
|
);
|
|
415
|
-
|
|
142
|
+
this.trackMessageChannel(result.ts, options.id);
|
|
143
|
+
return formatSlackMessageRef(options.id, result.ts);
|
|
416
144
|
} catch (error) {
|
|
417
|
-
this.logger.error(
|
|
145
|
+
this.logger.error('Failed to send Slack message:', error);
|
|
418
146
|
throw error;
|
|
419
147
|
}
|
|
420
148
|
}
|
|
421
149
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const attachments: any[] = [];
|
|
431
|
-
|
|
432
|
-
for (const segment of content) {
|
|
433
|
-
if (typeof segment === "string") {
|
|
434
|
-
textContent += segment;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
150
|
+
async $editMessage(options: EditMessageOptions): Promise<void> {
|
|
151
|
+
const ref = parseSlackMessageRef(options.messageId);
|
|
152
|
+
const channel = ref?.channel ?? options.id;
|
|
153
|
+
const ts = ref?.ts ?? options.messageId;
|
|
154
|
+
const canonical = expandInteractiveSegmentsInContent(options.content);
|
|
155
|
+
const wire = fromCanonicalSegments(canonical);
|
|
156
|
+
await editSlackContent(this.client!, channel, ts, wire);
|
|
157
|
+
}
|
|
437
158
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
break;
|
|
452
|
-
|
|
453
|
-
case "link":
|
|
454
|
-
if (data.text && data.text !== data.url) {
|
|
455
|
-
textContent += `<${data.url}|${data.text}>`;
|
|
456
|
-
} else {
|
|
457
|
-
textContent += `<${data.url}>`;
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
|
-
|
|
461
|
-
case "image":
|
|
462
|
-
if (data.url) {
|
|
463
|
-
attachments.push({
|
|
464
|
-
image_url: data.url,
|
|
465
|
-
title: data.name || data.title,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
break;
|
|
469
|
-
|
|
470
|
-
case "file":
|
|
471
|
-
// Files need to be uploaded separately
|
|
472
|
-
if (data.file) {
|
|
473
|
-
try {
|
|
474
|
-
await this.client!.files.upload({
|
|
475
|
-
channels: channel,
|
|
476
|
-
file: data.file,
|
|
477
|
-
filename: data.name,
|
|
478
|
-
});
|
|
479
|
-
} catch (error) {
|
|
480
|
-
this.logger.error("Failed to upload file:", error);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
break;
|
|
484
|
-
|
|
485
|
-
default:
|
|
486
|
-
textContent += data.text || `[${type}]`;
|
|
159
|
+
async $recallMessage(id: string): Promise<void> {
|
|
160
|
+
const ref = this.resolveMessageRef(id);
|
|
161
|
+
if (!ref) {
|
|
162
|
+
this.logger.warn(formatCompact({ op: 'recall_skip', reason: 'channel_unknown', message_id: id }));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
await this.client!.chat.delete({ channel: ref.channel, ts: ref.ts });
|
|
167
|
+
} catch (error: unknown) {
|
|
168
|
+
const slackError = (error as { data?: { error?: string } })?.data?.error;
|
|
169
|
+
if (slackError === 'message_not_found') {
|
|
170
|
+
this.logger.debug(formatCompact({ op: 'recall_skip', reason: 'message_not_found', message_id: id }));
|
|
171
|
+
return;
|
|
487
172
|
}
|
|
173
|
+
throw error;
|
|
488
174
|
}
|
|
175
|
+
}
|
|
489
176
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
channel
|
|
493
|
-
text: textContent.trim() || "Message",
|
|
494
|
-
...extraOptions,
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
if (attachments.length > 0) {
|
|
498
|
-
messageOptions.attachments = attachments;
|
|
177
|
+
trackMessageChannel(ts: string, channel: string): void {
|
|
178
|
+
if (ts && channel) {
|
|
179
|
+
this.messageChannelMap.set(ts, channel);
|
|
499
180
|
}
|
|
500
|
-
|
|
501
|
-
const result = await this.client!.chat.postMessage(messageOptions as ChatPostMessageArguments);
|
|
502
|
-
return result.message || {};
|
|
503
181
|
}
|
|
504
182
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
183
|
+
resolveMessageRef(messageId: string, channelHint?: string): { channel: string; ts: string } | null {
|
|
184
|
+
const parsed = parseSlackMessageRef(messageId);
|
|
185
|
+
if (parsed) {
|
|
186
|
+
this.trackMessageChannel(parsed.ts, parsed.channel);
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
if (channelHint) {
|
|
190
|
+
return { channel: channelHint, ts: messageId };
|
|
191
|
+
}
|
|
192
|
+
const channel = this.messageChannelMap.get(messageId);
|
|
193
|
+
if (channel) {
|
|
194
|
+
return { channel, ts: messageId };
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
513
197
|
}
|
|
514
198
|
|
|
515
|
-
// ==================== 工作区管理 API ====================
|
|
516
|
-
|
|
517
199
|
/**
|
|
518
|
-
*
|
|
519
|
-
* @
|
|
520
|
-
* @param users 用户 ID 列表
|
|
200
|
+
* Activity Feedback:在用户消息上添加表情回应
|
|
201
|
+
* @returns reaction name,供 $removeReaction 使用
|
|
521
202
|
*/
|
|
522
|
-
async
|
|
203
|
+
async $addReaction(
|
|
204
|
+
messageId: string,
|
|
205
|
+
emoji: string,
|
|
206
|
+
hint?: { sceneType?: 'private' | 'group' | 'channel'; channelId?: string },
|
|
207
|
+
): Promise<string | null> {
|
|
208
|
+
const ref = this.resolveMessageRef(messageId, hint?.channelId);
|
|
209
|
+
if (!ref) {
|
|
210
|
+
this.logger.warn(formatCompact({ op: 'reaction_add_skip', reason: 'channel_unknown', message_id: messageId }));
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const name = normalizeSlackReactionName(emoji);
|
|
523
214
|
try {
|
|
524
|
-
await this.
|
|
525
|
-
|
|
526
|
-
return true;
|
|
215
|
+
await this.addReaction(ref.channel, ref.ts, name);
|
|
216
|
+
return name;
|
|
527
217
|
} catch (error) {
|
|
528
|
-
this.logger.error(
|
|
529
|
-
|
|
218
|
+
this.logger.error('Failed to add Slack reaction:', error);
|
|
219
|
+
return null;
|
|
530
220
|
}
|
|
531
221
|
}
|
|
532
222
|
|
|
533
|
-
/**
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
223
|
+
/** Activity Feedback:移除本 Bot 在消息上的表情回应 */
|
|
224
|
+
async $removeReaction(messageId: string, reactionId: string, channelHint?: string): Promise<void> {
|
|
225
|
+
const ref = this.resolveMessageRef(messageId, channelHint);
|
|
226
|
+
if (!ref) {
|
|
227
|
+
this.logger.warn(formatCompact({ op: 'reaction_remove_skip', reason: 'channel_unknown', message_id: messageId }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const name = normalizeSlackReactionName(reactionId);
|
|
539
231
|
try {
|
|
540
|
-
await this.
|
|
541
|
-
this.logger.debug(formatCompact( { op: "kick", endpoint: this.$id, channel, user }));
|
|
542
|
-
return true;
|
|
232
|
+
await this.removeReaction(ref.channel, ref.ts, name);
|
|
543
233
|
} catch (error) {
|
|
544
|
-
|
|
545
|
-
|
|
234
|
+
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
235
|
+
if (code === 'no_reaction') return;
|
|
236
|
+
this.logger.error('Failed to remove Slack reaction:', error);
|
|
546
237
|
}
|
|
547
238
|
}
|
|
548
239
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
240
|
+
// ==================== 工作区管理 API ====================
|
|
241
|
+
|
|
242
|
+
async inviteToChannel(channel: string, users: string[]): Promise<boolean> {
|
|
243
|
+
await this.client!.conversations.invite({ channel, users: users.join(',') });
|
|
244
|
+
this.logger.debug(formatCompact({ op: 'invite', endpoint: this.$id, channel, users: users.join(',') }));
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async kickFromChannel(channel: string, user: string): Promise<boolean> {
|
|
249
|
+
await this.client!.conversations.kick({ channel, user });
|
|
250
|
+
this.logger.debug(formatCompact({ op: 'kick', endpoint: this.$id, channel, user }));
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
554
254
|
async setChannelTopic(channel: string, topic: string): Promise<boolean> {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return true;
|
|
559
|
-
} catch (error) {
|
|
560
|
-
this.logger.error(`Slack Endpoint ${this.$id} 设置话题失败:`, error);
|
|
561
|
-
throw error;
|
|
562
|
-
}
|
|
255
|
+
await this.client!.conversations.setTopic({ channel, topic });
|
|
256
|
+
this.logger.debug(formatCompact({ op: 'set_topic', endpoint: this.$id, channel }));
|
|
257
|
+
return true;
|
|
563
258
|
}
|
|
564
259
|
|
|
565
|
-
/**
|
|
566
|
-
* 设置频道目的
|
|
567
|
-
* @param channel 频道 ID
|
|
568
|
-
* @param purpose 目的
|
|
569
|
-
*/
|
|
570
260
|
async setChannelPurpose(channel: string, purpose: string): Promise<boolean> {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return true;
|
|
575
|
-
} catch (error) {
|
|
576
|
-
this.logger.error(`Slack Endpoint ${this.$id} 设置目的失败:`, error);
|
|
577
|
-
throw error;
|
|
578
|
-
}
|
|
261
|
+
await this.client!.conversations.setPurpose({ channel, purpose });
|
|
262
|
+
this.logger.debug(formatCompact({ op: 'set_purpose', endpoint: this.$id, channel }));
|
|
263
|
+
return true;
|
|
579
264
|
}
|
|
580
265
|
|
|
581
|
-
/**
|
|
582
|
-
* 归档频道
|
|
583
|
-
* @param channel 频道 ID
|
|
584
|
-
*/
|
|
585
266
|
async archiveChannel(channel: string): Promise<boolean> {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
return true;
|
|
590
|
-
} catch (error) {
|
|
591
|
-
this.logger.error(`Slack Endpoint ${this.$id} 归档频道失败:`, error);
|
|
592
|
-
throw error;
|
|
593
|
-
}
|
|
267
|
+
await this.client!.conversations.archive({ channel });
|
|
268
|
+
this.logger.debug(formatCompact({ op: 'archive', endpoint: this.$id, channel }));
|
|
269
|
+
return true;
|
|
594
270
|
}
|
|
595
271
|
|
|
596
|
-
/**
|
|
597
|
-
* 取消归档频道
|
|
598
|
-
* @param channel 频道 ID
|
|
599
|
-
*/
|
|
600
272
|
async unarchiveChannel(channel: string): Promise<boolean> {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
return true;
|
|
605
|
-
} catch (error) {
|
|
606
|
-
this.logger.error(`Slack Endpoint ${this.$id} 取消归档失败:`, error);
|
|
607
|
-
throw error;
|
|
608
|
-
}
|
|
273
|
+
await this.client!.conversations.unarchive({ channel });
|
|
274
|
+
this.logger.debug(formatCompact({ op: 'unarchive', endpoint: this.$id, channel }));
|
|
275
|
+
return true;
|
|
609
276
|
}
|
|
610
277
|
|
|
611
|
-
/**
|
|
612
|
-
* 重命名频道
|
|
613
|
-
* @param channel 频道 ID
|
|
614
|
-
* @param name 新名称
|
|
615
|
-
*/
|
|
616
278
|
async renameChannel(channel: string, name: string): Promise<boolean> {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
return true;
|
|
621
|
-
} catch (error) {
|
|
622
|
-
this.logger.error(`Slack Endpoint ${this.$id} 重命名频道失败:`, error);
|
|
623
|
-
throw error;
|
|
624
|
-
}
|
|
279
|
+
await this.client!.conversations.rename({ channel, name });
|
|
280
|
+
this.logger.debug(formatCompact({ op: 'rename', endpoint: this.$id, channel, name }));
|
|
281
|
+
return true;
|
|
625
282
|
}
|
|
626
283
|
|
|
627
|
-
/**
|
|
628
|
-
* 获取频道成员列表
|
|
629
|
-
* @param channel 频道 ID
|
|
630
|
-
*/
|
|
631
284
|
async getChannelMembers(channel: string): Promise<string[]> {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
return result.members || [];
|
|
635
|
-
} catch (error) {
|
|
636
|
-
this.logger.error(`Slack Endpoint ${this.$id} 获取成员列表失败:`, error);
|
|
637
|
-
throw error;
|
|
638
|
-
}
|
|
285
|
+
const result = await this.client!.conversations.members({ channel });
|
|
286
|
+
return result.members || [];
|
|
639
287
|
}
|
|
640
288
|
|
|
641
|
-
/**
|
|
642
|
-
* 获取频道信息
|
|
643
|
-
* @param channel 频道 ID
|
|
644
|
-
*/
|
|
645
289
|
async getChannelInfo(channel: string): Promise<any> {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
return result.channel;
|
|
649
|
-
} catch (error) {
|
|
650
|
-
this.logger.error(`Slack Endpoint ${this.$id} 获取频道信息失败:`, error);
|
|
651
|
-
throw error;
|
|
652
|
-
}
|
|
290
|
+
const result = await this.client!.conversations.info({ channel });
|
|
291
|
+
return result.channel;
|
|
653
292
|
}
|
|
654
293
|
|
|
655
|
-
/**
|
|
656
|
-
* 获取用户信息
|
|
657
|
-
* @param user 用户 ID
|
|
658
|
-
*/
|
|
659
294
|
async getUserInfo(user: string): Promise<any> {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return result.user;
|
|
663
|
-
} catch (error) {
|
|
664
|
-
this.logger.error(`Slack Endpoint ${this.$id} 获取用户信息失败:`, error);
|
|
665
|
-
throw error;
|
|
666
|
-
}
|
|
295
|
+
const result = await this.client!.users.info({ user });
|
|
296
|
+
return result.user;
|
|
667
297
|
}
|
|
668
298
|
|
|
669
|
-
/**
|
|
670
|
-
* 添加消息反应
|
|
671
|
-
* @param channel 频道 ID
|
|
672
|
-
* @param timestamp 消息时间戳
|
|
673
|
-
* @param name 表情名称
|
|
674
|
-
*/
|
|
675
299
|
async addReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
676
300
|
try {
|
|
677
301
|
await this.client!.reactions.add({ channel, timestamp, name });
|
|
678
|
-
this.logger.debug(formatCompact(
|
|
302
|
+
this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name }));
|
|
679
303
|
return true;
|
|
680
304
|
} catch (error) {
|
|
681
|
-
|
|
305
|
+
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
306
|
+
if (code === 'already_reacted') {
|
|
307
|
+
this.logger.debug(formatCompact({ op: 'reaction_add', endpoint: this.$id, name, status: 'exists' }));
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
682
310
|
throw error;
|
|
683
311
|
}
|
|
684
312
|
}
|
|
685
313
|
|
|
686
|
-
/**
|
|
687
|
-
* 移除消息反应
|
|
688
|
-
* @param channel 频道 ID
|
|
689
|
-
* @param timestamp 消息时间戳
|
|
690
|
-
* @param name 表情名称
|
|
691
|
-
*/
|
|
692
314
|
async removeReaction(channel: string, timestamp: string, name: string): Promise<boolean> {
|
|
693
315
|
try {
|
|
694
316
|
await this.client!.reactions.remove({ channel, timestamp, name });
|
|
695
|
-
this.logger.debug(formatCompact(
|
|
317
|
+
this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name }));
|
|
696
318
|
return true;
|
|
697
319
|
} catch (error) {
|
|
698
|
-
|
|
320
|
+
const code = (error as { data?: { error?: string } })?.data?.error;
|
|
321
|
+
if (code === 'no_reaction') {
|
|
322
|
+
this.logger.debug(formatCompact({ op: 'reaction_remove', endpoint: this.$id, name, status: 'gone' }));
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
699
325
|
throw error;
|
|
700
326
|
}
|
|
701
327
|
}
|
|
702
328
|
|
|
703
|
-
/**
|
|
704
|
-
* 置顶消息
|
|
705
|
-
* @param channel 频道 ID
|
|
706
|
-
* @param timestamp 消息时间戳
|
|
707
|
-
*/
|
|
708
329
|
async pinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return true;
|
|
713
|
-
} catch (error) {
|
|
714
|
-
this.logger.error(`Slack Endpoint ${this.$id} 置顶消息失败:`, error);
|
|
715
|
-
throw error;
|
|
716
|
-
}
|
|
330
|
+
await this.client!.pins.add({ channel, timestamp });
|
|
331
|
+
this.logger.debug(formatCompact({ op: 'pin', endpoint: this.$id, channel }));
|
|
332
|
+
return true;
|
|
717
333
|
}
|
|
718
334
|
|
|
719
|
-
/**
|
|
720
|
-
* 取消置顶消息
|
|
721
|
-
* @param channel 频道 ID
|
|
722
|
-
* @param timestamp 消息时间戳
|
|
723
|
-
*/
|
|
724
335
|
async unpinMessage(channel: string, timestamp: string): Promise<boolean> {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
return true;
|
|
729
|
-
} catch (error) {
|
|
730
|
-
this.logger.error(`Slack Endpoint ${this.$id} 取消置顶失败:`, error);
|
|
731
|
-
throw error;
|
|
732
|
-
}
|
|
336
|
+
await this.client!.pins.remove({ channel, timestamp });
|
|
337
|
+
this.logger.debug(formatCompact({ op: 'unpin', endpoint: this.$id, channel }));
|
|
338
|
+
return true;
|
|
733
339
|
}
|
|
734
340
|
}
|