@gamalan/pi-gateway 1.0.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 +366 -0
- package/config/config.default.json +51 -0
- package/dist/adapters/base.d.ts +85 -0
- package/dist/adapters/base.js +34 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/discord.d.ts +53 -0
- package/dist/adapters/discord.js +224 -0
- package/dist/adapters/discord.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/slack.d.ts +49 -0
- package/dist/adapters/slack.js +231 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/adapters/telegram.d.ts +64 -0
- package/dist/adapters/telegram.js +274 -0
- package/dist/adapters/telegram.js.map +1 -0
- package/dist/adapters/twitch.d.ts +75 -0
- package/dist/adapters/twitch.js +222 -0
- package/dist/adapters/twitch.js.map +1 -0
- package/dist/adapters/websocket.d.ts +30 -0
- package/dist/adapters/websocket.js +132 -0
- package/dist/adapters/websocket.js.map +1 -0
- package/dist/adapters/whatsapp.d.ts +49 -0
- package/dist/adapters/whatsapp.js +251 -0
- package/dist/adapters/whatsapp.js.map +1 -0
- package/dist/background/index.d.ts +1 -0
- package/dist/background/index.js +2 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/manager.d.ts +70 -0
- package/dist/background/manager.js +291 -0
- package/dist/background/manager.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1275 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/paths.d.ts +14 -0
- package/dist/paths.js +32 -0
- package/dist/paths.js.map +1 -0
- package/dist/security/auth.d.ts +91 -0
- package/dist/security/auth.js +316 -0
- package/dist/security/auth.js.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/tool-policy.d.ts +66 -0
- package/dist/security/tool-policy.js +467 -0
- package/dist/security/tool-policy.js.map +1 -0
- package/dist/sessions/index.d.ts +1 -0
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +64 -0
- package/dist/sessions/store.js +247 -0
- package/dist/sessions/store.js.map +1 -0
- package/package.json +57 -0
- package/src/adapters/base.ts +124 -0
- package/src/adapters/discord.ts +270 -0
- package/src/adapters/index.ts +13 -0
- package/src/adapters/slack.ts +313 -0
- package/src/adapters/telegram.ts +394 -0
- package/src/adapters/twitch.ts +316 -0
- package/src/adapters/websocket.ts +158 -0
- package/src/adapters/whatsapp.ts +296 -0
- package/src/background/index.ts +1 -0
- package/src/background/manager.ts +395 -0
- package/src/index.ts +1665 -0
- package/src/logger.ts +43 -0
- package/src/paths.ts +40 -0
- package/src/security/auth.ts +458 -0
- package/src/security/index.ts +1 -0
- package/src/security/tool-policy.ts +568 -0
- package/src/sessions/index.ts +1 -0
- package/src/sessions/store.ts +360 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Adapter - Hermes-style Discord platform adapter
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - DM and guild channel support
|
|
6
|
+
* - Slash command registration
|
|
7
|
+
* - Typing indicators
|
|
8
|
+
* - Message editing/deletion
|
|
9
|
+
* - Rate limit handling
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { BaseAdapter, type PlatformMessage, type PlatformConfig } from "./base.js";
|
|
13
|
+
import { logger } from "../logger.js";
|
|
14
|
+
|
|
15
|
+
export interface DiscordConfig extends PlatformConfig {
|
|
16
|
+
platform: "discord";
|
|
17
|
+
botToken: string;
|
|
18
|
+
guildId?: string;
|
|
19
|
+
allowedChannels?: string[]; // Whitelist specific channels
|
|
20
|
+
allowedRoles?: string[]; // Whitelist roles
|
|
21
|
+
requireMention?: boolean; // Require @mention in guilds
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class DiscordAdapter extends BaseAdapter {
|
|
25
|
+
readonly platform = "discord" as const;
|
|
26
|
+
config: DiscordConfig;
|
|
27
|
+
private httpClient: typeof fetch | null = null;
|
|
28
|
+
private wsConnection: WebSocket | null = null;
|
|
29
|
+
private heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
30
|
+
private sequence: number | null = null;
|
|
31
|
+
private sessionId: string | null = null;
|
|
32
|
+
private intents: number = 0;
|
|
33
|
+
|
|
34
|
+
constructor(config: DiscordConfig) {
|
|
35
|
+
super();
|
|
36
|
+
this.config = config;
|
|
37
|
+
|
|
38
|
+
// Intents: GUILD_MESSAGES (1<<9) + DIRECT_MESSAGES (1<<12) + MESSAGE_CONTENT (1<<15)
|
|
39
|
+
this.intents = 1 << 9 | 1 << 12 | 1 << 15;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async initialize(): Promise<void> {
|
|
43
|
+
// Test bot token
|
|
44
|
+
const response = await this.apiRequest("/users/@me");
|
|
45
|
+
const data: any = await response.json();
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`Discord authentication failed: ${response.status}`);
|
|
48
|
+
}
|
|
49
|
+
logger.info(`[Discord] Bot initialized: ${data.username}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async apiRequest(endpoint: string, options: RequestInit = {}): Promise<Response> {
|
|
53
|
+
const url = `https://discord.com/api/v10${endpoint}`;
|
|
54
|
+
return fetch(url, {
|
|
55
|
+
...options,
|
|
56
|
+
headers: {
|
|
57
|
+
"Authorization": `Bot ${this.config.botToken}`,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
...options.headers,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async start(callbacks): Promise<void> {
|
|
65
|
+
await super.start(callbacks);
|
|
66
|
+
|
|
67
|
+
// Connect to Gateway
|
|
68
|
+
const gatewayResponse = await this.apiRequest("/gateway");
|
|
69
|
+
const gatewayData = (await gatewayResponse.json()) as { url: string };
|
|
70
|
+
const gatewayUrl = `${gatewayData.url}?v=10&encoding=json&intents=${this.intents}`;
|
|
71
|
+
|
|
72
|
+
this.wsConnection = new WebSocket(gatewayUrl);
|
|
73
|
+
|
|
74
|
+
this.wsConnection.onopen = () => {
|
|
75
|
+
logger.info("[Discord] WebSocket connected");
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.wsConnection.onmessage = async (event) => {
|
|
79
|
+
const data: any = JSON.parse(event.data);
|
|
80
|
+
await this.handleGatewayMessage(data);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.wsConnection.onclose = () => {
|
|
84
|
+
logger.info("[Discord] WebSocket closed");
|
|
85
|
+
this.callbacks?.onDisconnect?.();
|
|
86
|
+
// Attempt reconnect after 5 seconds
|
|
87
|
+
setTimeout(() => this.start(callbacks), 5000);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async handleGatewayMessage(data: any): Promise<void> {
|
|
92
|
+
switch (data.op) {
|
|
93
|
+
case 0: // Dispatch
|
|
94
|
+
this.sequence = data.s;
|
|
95
|
+
await this.handleDispatch(data.t, data.d);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 10: // Hello
|
|
99
|
+
this.startHeartbeat(data.d.heartbeat_interval);
|
|
100
|
+
this.identify();
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 11: // Heartbeat ACK
|
|
104
|
+
// Heartbeat acknowledged
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private startHeartbeat(interval: number): void {
|
|
110
|
+
this.heartbeatInterval = setInterval(() => {
|
|
111
|
+
if (this.wsConnection?.readyState === WebSocket.OPEN) {
|
|
112
|
+
this.wsConnection.send(JSON.stringify({
|
|
113
|
+
op: 1,
|
|
114
|
+
d: this.sequence,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}, interval);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async identify(): Promise<void> {
|
|
121
|
+
const identifyPayload = {
|
|
122
|
+
op: 2,
|
|
123
|
+
d: {
|
|
124
|
+
token: this.config.botToken,
|
|
125
|
+
intents: this.intents,
|
|
126
|
+
properties: {
|
|
127
|
+
os: "linux",
|
|
128
|
+
browser: "pi-gateway",
|
|
129
|
+
device: "pi-gateway",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
this.wsConnection?.send(JSON.stringify(identifyPayload));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async handleDispatch(type: string, data: any): Promise<void> {
|
|
138
|
+
switch (type) {
|
|
139
|
+
case "READY":
|
|
140
|
+
this.sessionId = data.session_id;
|
|
141
|
+
logger.info(`[Discord] Logged in as ${data.user.username}`);
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case "MESSAGE_CREATE":
|
|
145
|
+
await this.handleMessage(data);
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case "MESSAGE_UPDATE":
|
|
149
|
+
// Handle edits if needed
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async handleMessage(data: any): Promise<void> {
|
|
155
|
+
// Ignore bots
|
|
156
|
+
if (data.author.bot && data.author.id !== this.getBotId()) return;
|
|
157
|
+
|
|
158
|
+
// Check if DM or allowed channel
|
|
159
|
+
const isDM = !data.guild_id;
|
|
160
|
+
if (!isDM && this.config.allowedChannels?.length) {
|
|
161
|
+
if (!this.config.allowedChannels.includes(data.channel_id)) return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check mention requirement in guilds
|
|
165
|
+
if (!isDM && this.config.requireMention) {
|
|
166
|
+
const mentioned = data.content.includes(`<@${this.getBotId()}>`);
|
|
167
|
+
if (!mentioned) return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const message: PlatformMessage = {
|
|
171
|
+
id: data.id,
|
|
172
|
+
platform: this.platform,
|
|
173
|
+
channelId: data.channel_id,
|
|
174
|
+
userId: data.author.id,
|
|
175
|
+
content: data.content,
|
|
176
|
+
timestamp: new Date(data.timestamp).getTime(),
|
|
177
|
+
metadata: {
|
|
178
|
+
guildId: data.guild_id,
|
|
179
|
+
username: data.author.username,
|
|
180
|
+
discriminator: data.author.discriminator,
|
|
181
|
+
isDM,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
await this.callbacks?.onMessage(message);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private getBotId(): string {
|
|
189
|
+
// Extract from token - this is approximate
|
|
190
|
+
return this.config.botToken.split(".")[0];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async sendMessage(channelId: string, content: string): Promise<string> {
|
|
194
|
+
const response = await this.apiRequest(`/channels/${channelId}/messages`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
body: JSON.stringify({ content }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const error = await response.text();
|
|
201
|
+
throw new Error(`Failed to send message: ${error}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const data = (await response.json()) as { id: string };
|
|
205
|
+
return data.id;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async editMessage(channelId: string, messageId: string, content: string): Promise<void> {
|
|
209
|
+
await this.apiRequest(`/channels/${channelId}/messages/${messageId}`, {
|
|
210
|
+
method: "PATCH",
|
|
211
|
+
body: JSON.stringify({ content }),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deleteMessage(channelId: string, messageId: string): Promise<void> {
|
|
216
|
+
await this.apiRequest(`/channels/${channelId}/messages/${messageId}`, {
|
|
217
|
+
method: "DELETE",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async setTyping(channelId: string, isTyping: boolean): Promise<void> {
|
|
222
|
+
if (!isTyping) return; // Discord doesn't have a "stop typing" API
|
|
223
|
+
|
|
224
|
+
await this.apiRequest(`/channels/${channelId}/typing`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async getStatus(): Promise<{ connected: boolean; latency?: number }> {
|
|
230
|
+
try {
|
|
231
|
+
const response = await this.apiRequest("/gateway/bot");
|
|
232
|
+
const data: any = await response.json();
|
|
233
|
+
return {
|
|
234
|
+
connected: true,
|
|
235
|
+
latency: data.session_start_limit?.remaining ?? undefined,
|
|
236
|
+
};
|
|
237
|
+
} catch {
|
|
238
|
+
return { connected: false };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async stop(): Promise<void> {
|
|
243
|
+
if (this.heartbeatInterval) {
|
|
244
|
+
clearInterval(this.heartbeatInterval);
|
|
245
|
+
}
|
|
246
|
+
if (this.wsConnection) {
|
|
247
|
+
this.wsConnection.close();
|
|
248
|
+
}
|
|
249
|
+
await super.stop();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Helper to register slash commands
|
|
253
|
+
async registerSlashCommands(commands: Array<{
|
|
254
|
+
name: string;
|
|
255
|
+
description: string;
|
|
256
|
+
options?: any[];
|
|
257
|
+
}>): Promise<void> {
|
|
258
|
+
if (!this.config.guildId) {
|
|
259
|
+
logger.warn("[Discord] guildId required for slash commands");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
await this.apiRequest(`/applications/${this.getBotId()}/guilds/${this.config.guildId}/commands`, {
|
|
264
|
+
method: "PUT",
|
|
265
|
+
body: JSON.stringify(commands),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
logger.info(`[Discord] Registered ${commands.length} slash commands`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { BaseAdapter, type PlatformMessage, type PlatformConfig, type AdapterCallbacks, type PlatformAdapter } from "./base.js";
|
|
2
|
+
export { DiscordAdapter } from "./discord.js";
|
|
3
|
+
export type { DiscordConfig } from "./discord.js";
|
|
4
|
+
export { TwitchAdapter } from "./twitch.js";
|
|
5
|
+
export type { TwitchConfig } from "./twitch.js";
|
|
6
|
+
export { TelegramAdapter } from "./telegram.js";
|
|
7
|
+
export type { TelegramConfig } from "./telegram.js";
|
|
8
|
+
export { SlackAdapter } from "./slack.js";
|
|
9
|
+
export type { SlackConfig } from "./slack.js";
|
|
10
|
+
export { WhatsAppAdapter } from "./whatsapp.js";
|
|
11
|
+
export type { WhatsAppConfig } from "./whatsapp.js";
|
|
12
|
+
export { WebSocketAdapter } from "./websocket.js";
|
|
13
|
+
export type { WebSocketConfig } from "./websocket.js";
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Adapter - Hermes-style Slack platform adapter
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Incoming webhooks (outbound only)
|
|
6
|
+
* - Web API (with bot token)
|
|
7
|
+
* - Slash command support
|
|
8
|
+
* - Block Kit support
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BaseAdapter, type PlatformMessage, type PlatformConfig } from "./base.js";
|
|
12
|
+
import { logger } from "../logger.js";
|
|
13
|
+
|
|
14
|
+
export interface SlackConfig extends PlatformConfig {
|
|
15
|
+
platform: "slack";
|
|
16
|
+
webhookUrl?: string;
|
|
17
|
+
botToken?: string;
|
|
18
|
+
signingSecret?: string;
|
|
19
|
+
teamId?: string;
|
|
20
|
+
defaultChannel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SlackMessage {
|
|
24
|
+
type: string;
|
|
25
|
+
channel?: string;
|
|
26
|
+
user?: string;
|
|
27
|
+
username?: string;
|
|
28
|
+
text: string;
|
|
29
|
+
ts: string;
|
|
30
|
+
thread_ts?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class SlackAdapter extends BaseAdapter {
|
|
34
|
+
readonly platform = "slack" as const;
|
|
35
|
+
config: SlackConfig;
|
|
36
|
+
|
|
37
|
+
private connected = false;
|
|
38
|
+
|
|
39
|
+
constructor(config: SlackConfig) {
|
|
40
|
+
super();
|
|
41
|
+
this.config = {
|
|
42
|
+
enabled: true,
|
|
43
|
+
platform: "slack",
|
|
44
|
+
...config,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async initialize(): Promise<void> {
|
|
49
|
+
if (this.config.botToken) {
|
|
50
|
+
// Test bot token
|
|
51
|
+
const response = await this.apiRequest("auth.test", { method: "POST" });
|
|
52
|
+
const data = await response.json() as { ok: boolean; team_id?: string };
|
|
53
|
+
|
|
54
|
+
if (!data.ok) {
|
|
55
|
+
throw new Error(`Slack auth failed: ${data}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.info(`[Slack] Bot initialized for team: ${data.team_id}`);
|
|
59
|
+
} else if (this.config.webhookUrl) {
|
|
60
|
+
logger.info("[Slack] Using webhook mode (outbound only)");
|
|
61
|
+
} else {
|
|
62
|
+
throw new Error("Slack requires either botToken or webhookUrl");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async apiRequest(endpoint: string, options: RequestInit = {}): Promise<Response> {
|
|
67
|
+
const url = `https://slack.com/api/${endpoint}`;
|
|
68
|
+
return fetch(url, {
|
|
69
|
+
...options,
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"Authorization": `Bearer ${this.config.botToken}`,
|
|
73
|
+
...options.headers,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async start(callbacks): Promise<void> {
|
|
79
|
+
await super.start(callbacks);
|
|
80
|
+
this.connected = true;
|
|
81
|
+
|
|
82
|
+
if (!this.config.botToken && !this.config.webhookUrl) {
|
|
83
|
+
logger.warn("[Slack] No credentials configured - adapter is outbound-only");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async stop(): Promise<void> {
|
|
88
|
+
this.connected = false;
|
|
89
|
+
await super.stop();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async sendMessage(channelId: string, content: string): Promise<string> {
|
|
93
|
+
// Try webhook first if available
|
|
94
|
+
if (this.config.webhookUrl) {
|
|
95
|
+
const payload = {
|
|
96
|
+
text: content,
|
|
97
|
+
...(channelId && channelId.startsWith("#") ? { channel: channelId } : {}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const response = await fetch(this.config.webhookUrl, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify(payload),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
throw new Error(`Slack webhook failed: ${response.status}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `webhook-${Date.now()}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Use Web API if we have a bot token
|
|
114
|
+
if (this.config.botToken) {
|
|
115
|
+
const response = await this.apiRequest("chat.postMessage", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: JSON.stringify({
|
|
118
|
+
channel: channelId,
|
|
119
|
+
text: content,
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const data = await response.json() as { ok: boolean; ts?: string; error?: string };
|
|
124
|
+
|
|
125
|
+
if (!data.ok) {
|
|
126
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return data.ts || String(Date.now());
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error("No Slack credentials configured");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async postMessage(channelId: string, content: string, blocks?: any[]): Promise<string> {
|
|
136
|
+
if (!this.config.botToken) {
|
|
137
|
+
throw new Error("Bot token required for rich messages");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const body: Record<string, any> = {
|
|
141
|
+
channel: channelId,
|
|
142
|
+
text: content,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (blocks) {
|
|
146
|
+
body.blocks = blocks;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const response = await this.apiRequest("chat.postMessage", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const data = await response.json() as { ok: boolean; ts?: string; error?: string };
|
|
155
|
+
|
|
156
|
+
if (!data.ok) {
|
|
157
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return data.ts || String(Date.now());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async replyToThread(channelId: string, threadTs: string, content: string): Promise<string> {
|
|
164
|
+
if (!this.config.botToken) {
|
|
165
|
+
throw new Error("Bot token required for threaded replies");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const response = await this.apiRequest("chat.postMessage", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
channel: channelId,
|
|
172
|
+
text: content,
|
|
173
|
+
thread_ts: threadTs,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const data = await response.json() as { ok: boolean; ts?: string; error?: string };
|
|
178
|
+
|
|
179
|
+
if (!data.ok) {
|
|
180
|
+
throw new Error(`Slack API error: ${data.error}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return data.ts || String(Date.now());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async editMessage(channelId: string, messageTs: string, content: string): Promise<void> {
|
|
187
|
+
if (!this.config.botToken) {
|
|
188
|
+
throw new Error("Bot token required for editing");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await this.apiRequest("chat.update", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
channel: channelId,
|
|
195
|
+
ts: messageTs,
|
|
196
|
+
text: content,
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async deleteMessage(channelId: string, messageTs: string): Promise<void> {
|
|
202
|
+
if (!this.config.botToken) {
|
|
203
|
+
throw new Error("Bot token required for deletion");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await this.apiRequest("chat.delete", {
|
|
207
|
+
method: "POST",
|
|
208
|
+
body: JSON.stringify({
|
|
209
|
+
channel: channelId,
|
|
210
|
+
ts: messageTs,
|
|
211
|
+
}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async setTyping(channelId: string, isTyping: boolean): Promise<void> {
|
|
216
|
+
if (!this.config.botToken) return;
|
|
217
|
+
|
|
218
|
+
await this.apiRequest("chat.postEphemeral", {
|
|
219
|
+
method: "POST",
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
channel: channelId,
|
|
222
|
+
text: isTyping ? "_typing_" : "",
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getChannelInfo(channelId: string): Promise<{
|
|
228
|
+
id: string;
|
|
229
|
+
name: string;
|
|
230
|
+
numMembers: number;
|
|
231
|
+
topic: string;
|
|
232
|
+
} | null> {
|
|
233
|
+
if (!this.config.botToken) {
|
|
234
|
+
throw new Error("Bot token required");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const response = await this.apiRequest("conversations.info", {
|
|
238
|
+
method: "POST",
|
|
239
|
+
body: JSON.stringify({ channel: channelId }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const data = await response.json() as {
|
|
243
|
+
ok: boolean;
|
|
244
|
+
channel?: {
|
|
245
|
+
id: string;
|
|
246
|
+
name: string;
|
|
247
|
+
num_members: number;
|
|
248
|
+
topic?: { value: string };
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (!data.ok || !data.channel) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
id: data.channel.id,
|
|
258
|
+
name: data.channel.name,
|
|
259
|
+
numMembers: data.channel.num_members,
|
|
260
|
+
topic: data.channel.topic?.value || "",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async listChannels(): Promise<Array<{ id: string; name: string }>> {
|
|
265
|
+
if (!this.config.botToken) {
|
|
266
|
+
throw new Error("Bot token required");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const response = await this.apiRequest("conversations.list", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
body: JSON.stringify({ types: "public_channel,private_channel" }),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const data = await response.json() as {
|
|
275
|
+
ok: boolean;
|
|
276
|
+
channels?: Array<{ id: string; name: string }>;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (!data.ok) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return (data.channels || []).map(c => ({ id: c.id, name: c.name }));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async getStatus(): Promise<{ connected: boolean; latency?: number }> {
|
|
287
|
+
return {
|
|
288
|
+
connected: this.connected,
|
|
289
|
+
...(this.config.botToken ? { mode: "api" } : { mode: "webhook" })
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Handle incoming events (for WebSocket or Socket Mode)
|
|
294
|
+
async handleIncomingEvent(event: any): Promise<void> {
|
|
295
|
+
if (event.type === "message" && !event.subtype) {
|
|
296
|
+
const message: PlatformMessage = {
|
|
297
|
+
id: this.generateMessageId(),
|
|
298
|
+
platform: "slack",
|
|
299
|
+
channelId: event.channel,
|
|
300
|
+
userId: event.user,
|
|
301
|
+
content: event.text,
|
|
302
|
+
timestamp: parseFloat(event.ts) * 1000,
|
|
303
|
+
metadata: {
|
|
304
|
+
team: event.team,
|
|
305
|
+
threadTs: event.thread_ts,
|
|
306
|
+
username: event.username,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
this.emitMessage(message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|