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