@pioneer-platform/discord 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/.turbo/turbo-build.log +2 -0
- package/README.md +231 -0
- package/lib/index.d.ts +74 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +233 -0
- package/lib/index.js.map +1 -0
- package/lib/test.d.ts +15 -0
- package/lib/test.d.ts.map +1 -0
- package/lib/test.js +202 -0
- package/lib/test.js.map +1 -0
- package/package.json +30 -0
- package/src/index.ts +297 -0
- package/src/test.ts +193 -0
- package/tsconfig.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# @pioneer-platform/discord
|
|
2
|
+
|
|
3
|
+
Discord integration for Pioneer Platform - Redis to Discord event bridge.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This module subscribes to Redis pub/sub channels and forwards events to Discord channels. It's designed to monitor server health, errors, and important events in real-time.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- š **Redis ā Discord Bridge**: Subscribe to Redis channels, forward to Discord
|
|
12
|
+
- šØ **Rich Embeds**: Color-coded messages with severity levels
|
|
13
|
+
- š **Event Formatting**: Structured event display with fields
|
|
14
|
+
- ā” **Real-time**: Instant notification delivery
|
|
15
|
+
- š”ļø **Error Handling**: Robust error recovery
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun install
|
|
21
|
+
bun run build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Required environment variables:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Discord Bot Configuration
|
|
30
|
+
KEEPKEY_DISCORD_BOT_TOKEN=<your-bot-token>
|
|
31
|
+
KEEPKEY_BOT_NAME=pioneer
|
|
32
|
+
KEEPKEY_DISCORD_BOT_CHANNEL=keepkey-events
|
|
33
|
+
|
|
34
|
+
# Optional
|
|
35
|
+
DISCORD_ADMIN_USERID=<discord-user-id>
|
|
36
|
+
|
|
37
|
+
# Redis Connection
|
|
38
|
+
REDIS_CONNECTION=redis://localhost:6379
|
|
39
|
+
# or
|
|
40
|
+
REDIS_CONNECTION_PROD=rediss://...
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Basic Usage
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { createDiscordBridge } from '@pioneer-platform/discord';
|
|
49
|
+
import Redis from 'ioredis';
|
|
50
|
+
|
|
51
|
+
const redis = new Redis(process.env.REDIS_CONNECTION);
|
|
52
|
+
|
|
53
|
+
const bridge = await createDiscordBridge({
|
|
54
|
+
botToken: process.env.KEEPKEY_DISCORD_BOT_TOKEN!,
|
|
55
|
+
botName: 'pioneer',
|
|
56
|
+
defaultChannel: 'keepkey-events',
|
|
57
|
+
redisChannels: ['keepkey-events']
|
|
58
|
+
}, redis);
|
|
59
|
+
|
|
60
|
+
// Bridge is now listening and will forward events
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Publishing Events
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { publisher } from '@pioneer-platform/default-redis';
|
|
67
|
+
|
|
68
|
+
// Publish an event
|
|
69
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
70
|
+
type: 'server_startup',
|
|
71
|
+
service: 'pioneer-server',
|
|
72
|
+
level: 'success',
|
|
73
|
+
message: 'Server started successfully',
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
details: {
|
|
76
|
+
port: '9001',
|
|
77
|
+
environment: 'production'
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Event Format
|
|
83
|
+
|
|
84
|
+
Events should follow this structure:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface DiscordEvent {
|
|
88
|
+
type: string; // Event type (e.g., 'server_startup', 'error')
|
|
89
|
+
service?: string; // Service name (e.g., 'pioneer-server')
|
|
90
|
+
level?: 'info' | 'success' | 'warning' | 'error' | 'critical';
|
|
91
|
+
message: string; // Main message
|
|
92
|
+
timestamp?: string; // ISO timestamp
|
|
93
|
+
details?: Record<string, any>; // Additional fields
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Severity Levels
|
|
98
|
+
|
|
99
|
+
| Level | Color | Icon | Use Case |
|
|
100
|
+
|-------|-------|------|----------|
|
|
101
|
+
| `info` | Blue | ā¹ļø | General information |
|
|
102
|
+
| `success` | Green | ā
| Successful operations |
|
|
103
|
+
| `warning` | Orange | ā ļø | Non-critical issues |
|
|
104
|
+
| `error` | Red | ā | Errors |
|
|
105
|
+
| `critical` | Dark Red | šØ | Critical failures |
|
|
106
|
+
|
|
107
|
+
## Testing
|
|
108
|
+
|
|
109
|
+
### Standalone Test
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Set environment variables in .env file
|
|
113
|
+
KEEPKEY_DISCORD_BOT_TOKEN=...
|
|
114
|
+
REDIS_CONNECTION_PROD=...
|
|
115
|
+
|
|
116
|
+
# Run the test
|
|
117
|
+
bun run src/test.ts
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This will:
|
|
121
|
+
1. Connect to Discord
|
|
122
|
+
2. Subscribe to `keepkey-events` Redis channel
|
|
123
|
+
3. Publish 5 test events with different severity levels
|
|
124
|
+
4. Verify they appear in Discord
|
|
125
|
+
|
|
126
|
+
### Example Events
|
|
127
|
+
|
|
128
|
+
**Server Startup:**
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"type": "server_startup",
|
|
132
|
+
"service": "pioneer-server",
|
|
133
|
+
"level": "success",
|
|
134
|
+
"message": "Pioneer Server started successfully",
|
|
135
|
+
"details": {
|
|
136
|
+
"port": "9001",
|
|
137
|
+
"environment": "production"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Error:**
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"type": "database_error",
|
|
146
|
+
"service": "pioneer-server",
|
|
147
|
+
"level": "error",
|
|
148
|
+
"message": "Failed to connect to MongoDB",
|
|
149
|
+
"details": {
|
|
150
|
+
"error": "Connection timeout",
|
|
151
|
+
"retries": 3
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Discord Bot Setup
|
|
157
|
+
|
|
158
|
+
1. **Create Discord Application**:
|
|
159
|
+
- Go to https://discord.com/developers/applications
|
|
160
|
+
- Click "New Application"
|
|
161
|
+
- Name it "Pioneer Bot"
|
|
162
|
+
|
|
163
|
+
2. **Create Bot**:
|
|
164
|
+
- Go to "Bot" tab
|
|
165
|
+
- Click "Add Bot"
|
|
166
|
+
- Copy the token ā `KEEPKEY_DISCORD_BOT_TOKEN`
|
|
167
|
+
|
|
168
|
+
3. **Bot Permissions**:
|
|
169
|
+
- Enable "Message Content Intent"
|
|
170
|
+
- Required permissions:
|
|
171
|
+
- Send Messages
|
|
172
|
+
- Embed Links
|
|
173
|
+
- Read Message History
|
|
174
|
+
|
|
175
|
+
4. **Invite Bot to Server**:
|
|
176
|
+
- Go to "OAuth2" ā "URL Generator"
|
|
177
|
+
- Select scopes: `bot`
|
|
178
|
+
- Select permissions: `2048` (Send Messages), `16384` (Embed Links)
|
|
179
|
+
- Use generated URL to invite bot
|
|
180
|
+
|
|
181
|
+
5. **Create Channel**:
|
|
182
|
+
- Create `#keepkey-events` channel in your Discord server
|
|
183
|
+
- Bot will automatically find it by name
|
|
184
|
+
|
|
185
|
+
## Integration with Pioneer Server
|
|
186
|
+
|
|
187
|
+
See `../../services/pioneer-server/docs/DISCORD_INTEGRATION.md` for server integration details.
|
|
188
|
+
|
|
189
|
+
## API Reference
|
|
190
|
+
|
|
191
|
+
### DiscordBridge
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
class DiscordBridge {
|
|
195
|
+
constructor(config: DiscordConfig, redis: Redis)
|
|
196
|
+
async start(): Promise<void>
|
|
197
|
+
async stop(): Promise<void>
|
|
198
|
+
isConnected(): boolean
|
|
199
|
+
getClient(): Client
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### createDiscordBridge
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
function createDiscordBridge(
|
|
207
|
+
config: DiscordConfig,
|
|
208
|
+
redis: Redis
|
|
209
|
+
): Promise<DiscordBridge>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Troubleshooting
|
|
213
|
+
|
|
214
|
+
### Bot doesn't connect
|
|
215
|
+
- Verify `KEEPKEY_DISCORD_BOT_TOKEN` is correct
|
|
216
|
+
- Check bot has "Message Content Intent" enabled
|
|
217
|
+
- Ensure bot is invited to server
|
|
218
|
+
|
|
219
|
+
### Messages don't appear in Discord
|
|
220
|
+
- Verify channel name matches `KEEPKEY_DISCORD_BOT_CHANNEL`
|
|
221
|
+
- Check bot has permissions in the channel
|
|
222
|
+
- Look for errors in logs
|
|
223
|
+
|
|
224
|
+
### Redis connection fails
|
|
225
|
+
- Verify `REDIS_CONNECTION_PROD` is correct
|
|
226
|
+
- Check Redis server is accessible
|
|
227
|
+
- Confirm Redis channels match configuration
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Integration for Pioneer Platform
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to Redis channels and forwards events to Discord channels
|
|
5
|
+
*/
|
|
6
|
+
import { Client } from 'discord.js';
|
|
7
|
+
import type { Redis } from 'ioredis';
|
|
8
|
+
export interface DiscordConfig {
|
|
9
|
+
botToken: string;
|
|
10
|
+
botName?: string;
|
|
11
|
+
defaultChannel: string;
|
|
12
|
+
adminUserId?: string;
|
|
13
|
+
redisChannels: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface DiscordEvent {
|
|
16
|
+
type: string;
|
|
17
|
+
service?: string;
|
|
18
|
+
level?: 'info' | 'success' | 'warning' | 'error' | 'critical';
|
|
19
|
+
message: string;
|
|
20
|
+
timestamp?: string;
|
|
21
|
+
details?: Record<string, any>;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
export declare class DiscordBridge {
|
|
25
|
+
private client;
|
|
26
|
+
private config;
|
|
27
|
+
private redis;
|
|
28
|
+
private isReady;
|
|
29
|
+
private targetChannel?;
|
|
30
|
+
private environment;
|
|
31
|
+
constructor(config: DiscordConfig, redis: Redis);
|
|
32
|
+
/**
|
|
33
|
+
* Setup Discord client event handlers
|
|
34
|
+
*/
|
|
35
|
+
private setupEventHandlers;
|
|
36
|
+
/**
|
|
37
|
+
* Find the Discord channel to post to
|
|
38
|
+
*/
|
|
39
|
+
private findTargetChannel;
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to Redis channels
|
|
42
|
+
*/
|
|
43
|
+
private subscribeToRedis;
|
|
44
|
+
/**
|
|
45
|
+
* Handle incoming Redis messages
|
|
46
|
+
*/
|
|
47
|
+
private handleRedisMessage;
|
|
48
|
+
/**
|
|
49
|
+
* Post event to Discord channel
|
|
50
|
+
*/
|
|
51
|
+
private postToDiscord;
|
|
52
|
+
/**
|
|
53
|
+
* Start the Discord bridge
|
|
54
|
+
*/
|
|
55
|
+
start(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Stop the Discord bridge
|
|
58
|
+
*/
|
|
59
|
+
stop(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Check if the bridge is ready
|
|
62
|
+
*/
|
|
63
|
+
isConnected(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Get the Discord client (for advanced usage)
|
|
66
|
+
*/
|
|
67
|
+
getClient(): Client;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create and start a Discord bridge
|
|
71
|
+
*/
|
|
72
|
+
export declare function createDiscordBridge(config: DiscordConfig, redis: Redis): Promise<DiscordBridge>;
|
|
73
|
+
export default DiscordBridge;
|
|
74
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAAgD,MAAM,YAAY,CAAC;AAClF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAKrC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE9B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAkBD,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,aAAa,CAAC,CAAc;IACpC,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK;IAiB/C;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAsBxB;;OAEG;YACW,kBAAkB;IAuBhC;;OAEG;YACW,aAAa;IAmD3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,SAAS,IAAI,MAAM;CAGpB;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,aAAa,CAAC,CAIxB;AAED,eAAe,aAAa,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Discord Integration for Pioneer Platform
|
|
4
|
+
*
|
|
5
|
+
* Subscribes to Redis channels and forwards events to Discord channels
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.DiscordBridge = void 0;
|
|
9
|
+
exports.createDiscordBridge = createDiscordBridge;
|
|
10
|
+
const discord_js_1 = require("discord.js");
|
|
11
|
+
const log = require('@pioneer-platform/loggerdog')();
|
|
12
|
+
const TAG = ' | Discord Integration | ';
|
|
13
|
+
const LEVEL_COLORS = {
|
|
14
|
+
info: 0x3498db, // Blue
|
|
15
|
+
success: 0x2ecc71, // Green
|
|
16
|
+
warning: 0xf39c12, // Orange
|
|
17
|
+
error: 0xe74c3c, // Red
|
|
18
|
+
critical: 0x992d22 // Dark Red
|
|
19
|
+
};
|
|
20
|
+
const LEVEL_ICONS = {
|
|
21
|
+
info: 'ā¹ļø',
|
|
22
|
+
success: 'ā
',
|
|
23
|
+
warning: 'ā ļø',
|
|
24
|
+
error: 'ā',
|
|
25
|
+
critical: 'šØ'
|
|
26
|
+
};
|
|
27
|
+
class DiscordBridge {
|
|
28
|
+
constructor(config, redis) {
|
|
29
|
+
this.isReady = false;
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.redis = redis;
|
|
32
|
+
this.environment = process.env.LOCAL_ENV || 'production';
|
|
33
|
+
// Create Discord client with necessary intents
|
|
34
|
+
this.client = new discord_js_1.Client({
|
|
35
|
+
intents: [
|
|
36
|
+
discord_js_1.GatewayIntentBits.Guilds,
|
|
37
|
+
discord_js_1.GatewayIntentBits.GuildMessages,
|
|
38
|
+
discord_js_1.GatewayIntentBits.MessageContent
|
|
39
|
+
]
|
|
40
|
+
});
|
|
41
|
+
this.setupEventHandlers();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Setup Discord client event handlers
|
|
45
|
+
*/
|
|
46
|
+
setupEventHandlers() {
|
|
47
|
+
this.client.on('ready', async () => {
|
|
48
|
+
log.info(TAG, `[${this.environment.toUpperCase()}] ā
Discord bot logged in as ${this.client.user?.tag}`);
|
|
49
|
+
this.isReady = true;
|
|
50
|
+
// Find the target channel
|
|
51
|
+
await this.findTargetChannel();
|
|
52
|
+
// Subscribe to Redis channels
|
|
53
|
+
this.subscribeToRedis();
|
|
54
|
+
});
|
|
55
|
+
this.client.on('error', (error) => {
|
|
56
|
+
log.error(TAG, 'Discord client error:', error);
|
|
57
|
+
});
|
|
58
|
+
this.client.on('warn', (warning) => {
|
|
59
|
+
log.warn(TAG, 'Discord client warning:', warning);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Find the Discord channel to post to
|
|
64
|
+
*/
|
|
65
|
+
async findTargetChannel() {
|
|
66
|
+
const tag = TAG + ' | findTargetChannel | ';
|
|
67
|
+
try {
|
|
68
|
+
// Get all channels
|
|
69
|
+
const channels = this.client.channels.cache;
|
|
70
|
+
log.info(tag, `Searching ${channels.size} cached channels for: ${this.config.defaultChannel}`);
|
|
71
|
+
// Find by name
|
|
72
|
+
const channel = channels.find((ch) => ch.name === this.config.defaultChannel && ch.isTextBased());
|
|
73
|
+
if (channel) {
|
|
74
|
+
this.targetChannel = channel;
|
|
75
|
+
log.info(tag, `ā
Found target channel: #${channel.name} (${channel.id})`);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
log.error(tag, `ā Could not find channel: #${this.config.defaultChannel}`);
|
|
79
|
+
log.info(tag, 'Available channels:', channels.map((ch) => ch.name).join(', '));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
log.error(tag, 'Error finding target channel:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Subscribe to Redis channels
|
|
88
|
+
*/
|
|
89
|
+
subscribeToRedis() {
|
|
90
|
+
const tag = TAG + ' | subscribeToRedis | ';
|
|
91
|
+
log.info(tag, `Subscribing to Redis channels: ${this.config.redisChannels.join(', ')}`);
|
|
92
|
+
// Subscribe to all configured channels
|
|
93
|
+
this.config.redisChannels.forEach(channel => {
|
|
94
|
+
this.redis.subscribe(channel, (err) => {
|
|
95
|
+
if (err) {
|
|
96
|
+
log.error(tag, `Failed to subscribe to ${channel}:`, err);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
log.info(tag, `ā
Subscribed to Redis channel: ${channel}`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
// Handle incoming messages
|
|
104
|
+
this.redis.on('message', async (channel, message) => {
|
|
105
|
+
await this.handleRedisMessage(channel, message);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Handle incoming Redis messages
|
|
110
|
+
*/
|
|
111
|
+
async handleRedisMessage(channel, message) {
|
|
112
|
+
const tag = TAG + ' | handleRedisMessage | ';
|
|
113
|
+
try {
|
|
114
|
+
log.debug(tag, `Received message from ${channel}`);
|
|
115
|
+
// Parse the message
|
|
116
|
+
let event;
|
|
117
|
+
try {
|
|
118
|
+
event = JSON.parse(message);
|
|
119
|
+
}
|
|
120
|
+
catch (parseError) {
|
|
121
|
+
log.error(tag, 'Failed to parse Redis message:', parseError);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Post to Discord
|
|
125
|
+
await this.postToDiscord(event);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
log.error(tag, 'Error handling Redis message:', error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Post event to Discord channel
|
|
133
|
+
*/
|
|
134
|
+
async postToDiscord(event) {
|
|
135
|
+
const tag = TAG + ' | postToDiscord | ';
|
|
136
|
+
if (!this.isReady) {
|
|
137
|
+
log.warn(tag, 'Discord client not ready yet');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (!this.targetChannel) {
|
|
141
|
+
log.error(tag, 'No target channel found');
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
// Determine level
|
|
146
|
+
const level = event.level || 'info';
|
|
147
|
+
const icon = LEVEL_ICONS[level] || 'ā¹ļø';
|
|
148
|
+
const color = LEVEL_COLORS[level] || LEVEL_COLORS.info;
|
|
149
|
+
// Create embed
|
|
150
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
151
|
+
.setColor(color)
|
|
152
|
+
.setTitle(`${icon} ${event.type || 'Event'}`)
|
|
153
|
+
.setDescription(event.message)
|
|
154
|
+
.setTimestamp(event.timestamp ? new Date(event.timestamp) : new Date());
|
|
155
|
+
// Add service field if present
|
|
156
|
+
if (event.service) {
|
|
157
|
+
embed.addFields({ name: 'Service', value: event.service, inline: true });
|
|
158
|
+
}
|
|
159
|
+
// Add details fields if present
|
|
160
|
+
if (event.details) {
|
|
161
|
+
Object.entries(event.details).forEach(([key, value]) => {
|
|
162
|
+
embed.addFields({
|
|
163
|
+
name: key,
|
|
164
|
+
value: String(value),
|
|
165
|
+
inline: true
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Send to Discord
|
|
170
|
+
await this.targetChannel.send({ embeds: [embed] });
|
|
171
|
+
log.info(tag, `ā
Posted to #${this.targetChannel.name}: ${event.type}`);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
log.error(tag, 'Failed to post to Discord:', error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Start the Discord bridge
|
|
179
|
+
*/
|
|
180
|
+
async start() {
|
|
181
|
+
const tag = TAG + ' | start | ';
|
|
182
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Starting Discord bridge...`);
|
|
183
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Bot name: ${this.config.botName || 'N/A'}`);
|
|
184
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Target channel: #${this.config.defaultChannel}`);
|
|
185
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Redis channels: ${this.config.redisChannels.join(', ')}`);
|
|
186
|
+
try {
|
|
187
|
+
await this.client.login(this.config.botToken);
|
|
188
|
+
log.info(tag, 'ā
Discord bridge started successfully');
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
log.error(tag, 'ā Failed to start Discord bridge:', error);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Stop the Discord bridge
|
|
197
|
+
*/
|
|
198
|
+
async stop() {
|
|
199
|
+
const tag = TAG + ' | stop | ';
|
|
200
|
+
log.info(tag, 'Stopping Discord bridge...');
|
|
201
|
+
// Unsubscribe from Redis channels
|
|
202
|
+
this.config.redisChannels.forEach(channel => {
|
|
203
|
+
this.redis.unsubscribe(channel);
|
|
204
|
+
});
|
|
205
|
+
// Destroy Discord client
|
|
206
|
+
this.client.destroy();
|
|
207
|
+
this.isReady = false;
|
|
208
|
+
log.info(tag, 'ā
Discord bridge stopped');
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check if the bridge is ready
|
|
212
|
+
*/
|
|
213
|
+
isConnected() {
|
|
214
|
+
return this.isReady && !!this.targetChannel;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get the Discord client (for advanced usage)
|
|
218
|
+
*/
|
|
219
|
+
getClient() {
|
|
220
|
+
return this.client;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
exports.DiscordBridge = DiscordBridge;
|
|
224
|
+
/**
|
|
225
|
+
* Create and start a Discord bridge
|
|
226
|
+
*/
|
|
227
|
+
async function createDiscordBridge(config, redis) {
|
|
228
|
+
const bridge = new DiscordBridge(config, redis);
|
|
229
|
+
await bridge.start();
|
|
230
|
+
return bridge;
|
|
231
|
+
}
|
|
232
|
+
exports.default = DiscordBridge;
|
|
233
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/test.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Standalone test for Discord integration
|
|
4
|
+
*
|
|
5
|
+
* Tests the Discord bridge by:
|
|
6
|
+
* 1. Connecting to Discord
|
|
7
|
+
* 2. Subscribing to keepkey-events Redis channel
|
|
8
|
+
* 3. Publishing test events
|
|
9
|
+
* 4. Verifying they appear in Discord
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* REDIS_CONNECTION_PROD=redis://... bun run src/test.ts
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;GAWG"}
|
package/lib/test.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Standalone test for Discord integration
|
|
5
|
+
*
|
|
6
|
+
* Tests the Discord bridge by:
|
|
7
|
+
* 1. Connecting to Discord
|
|
8
|
+
* 2. Subscribing to keepkey-events Redis channel
|
|
9
|
+
* 3. Publishing test events
|
|
10
|
+
* 4. Verifying they appear in Discord
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* REDIS_CONNECTION_PROD=redis://... bun run src/test.ts
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
49
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
50
|
+
};
|
|
51
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
|
+
const dotenv = __importStar(require("dotenv"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
55
|
+
const index_1 = require("./index");
|
|
56
|
+
// Load environment variables
|
|
57
|
+
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
|
58
|
+
const log = require('@pioneer-platform/loggerdog')();
|
|
59
|
+
const TAG = ' | Discord Test | ';
|
|
60
|
+
async function runTest() {
|
|
61
|
+
log.info(TAG, 'š§Ŗ Testing Discord Integration\n');
|
|
62
|
+
// Get config from environment
|
|
63
|
+
const botToken = process.env.KEEPKEY_DISCORD_BOT_TOKEN;
|
|
64
|
+
const botName = process.env.KEEPKEY_BOT_NAME || 'pioneer';
|
|
65
|
+
const channelName = process.env.KEEPKEY_DISCORD_BOT_CHANNEL || 'keepkey-events';
|
|
66
|
+
const redisUrl = process.env.REDIS_CONNECTION_PROD || process.env.REDIS_CONNECTION || 'redis://127.0.0.1:6379';
|
|
67
|
+
if (!botToken) {
|
|
68
|
+
console.error('ā KEEPKEY_DISCORD_BOT_TOKEN not set in environment');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
log.info(TAG, 'Configuration:');
|
|
72
|
+
log.info(TAG, ` Bot Name: ${botName}`);
|
|
73
|
+
log.info(TAG, ` Channel: #${channelName}`);
|
|
74
|
+
log.info(TAG, ` Redis: ${redisUrl.includes('@') ? redisUrl.split('@')[1] : redisUrl}`);
|
|
75
|
+
log.info(TAG, '');
|
|
76
|
+
// Create Redis clients
|
|
77
|
+
const subscriber = new ioredis_1.default(redisUrl);
|
|
78
|
+
const publisher = new ioredis_1.default(redisUrl);
|
|
79
|
+
// Discord bridge config
|
|
80
|
+
const config = {
|
|
81
|
+
botToken,
|
|
82
|
+
botName,
|
|
83
|
+
defaultChannel: channelName,
|
|
84
|
+
adminUserId: process.env.DISCORD_ADMIN_USERID,
|
|
85
|
+
redisChannels: ['keepkey-events']
|
|
86
|
+
};
|
|
87
|
+
let bridge;
|
|
88
|
+
try {
|
|
89
|
+
// Start Discord bridge
|
|
90
|
+
log.info(TAG, 'Starting Discord bridge...');
|
|
91
|
+
bridge = await (0, index_1.createDiscordBridge)(config, subscriber);
|
|
92
|
+
// Wait for Discord to be ready
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
94
|
+
if (!bridge.isConnected()) {
|
|
95
|
+
log.error(TAG, 'ā Discord bridge failed to connect or find channel');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
log.info(TAG, 'ā
Discord bridge connected and ready\n');
|
|
99
|
+
// Publish test events
|
|
100
|
+
log.info(TAG, 'Publishing test events...\n');
|
|
101
|
+
await sleep(1000);
|
|
102
|
+
// Test 1: Startup event
|
|
103
|
+
log.info(TAG, 'Test 1: Server startup event');
|
|
104
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
105
|
+
type: 'server_startup',
|
|
106
|
+
service: 'pioneer-server',
|
|
107
|
+
level: 'success',
|
|
108
|
+
message: 'Pioneer Server started successfully',
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
details: {
|
|
111
|
+
port: '9001',
|
|
112
|
+
environment: 'test',
|
|
113
|
+
version: '3.0.0'
|
|
114
|
+
}
|
|
115
|
+
}));
|
|
116
|
+
await sleep(2000);
|
|
117
|
+
// Test 2: Health check
|
|
118
|
+
log.info(TAG, 'Test 2: Health check event');
|
|
119
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
120
|
+
type: 'health_check',
|
|
121
|
+
service: 'pioneer-server',
|
|
122
|
+
level: 'info',
|
|
123
|
+
message: 'Health check passed - all systems operational',
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
details: {
|
|
126
|
+
status: 'healthy',
|
|
127
|
+
uptime: '24h',
|
|
128
|
+
connections: 42
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
await sleep(2000);
|
|
132
|
+
// Test 3: Warning
|
|
133
|
+
log.info(TAG, 'Test 3: Warning event');
|
|
134
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
135
|
+
type: 'high_latency',
|
|
136
|
+
service: 'pioneer-server',
|
|
137
|
+
level: 'warning',
|
|
138
|
+
message: 'API response time exceeded threshold',
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
details: {
|
|
141
|
+
endpoint: '/api/v1/balances',
|
|
142
|
+
latency: '5200ms',
|
|
143
|
+
threshold: '2000ms'
|
|
144
|
+
}
|
|
145
|
+
}));
|
|
146
|
+
await sleep(2000);
|
|
147
|
+
// Test 4: Error
|
|
148
|
+
log.info(TAG, 'Test 4: Error event');
|
|
149
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
150
|
+
type: 'database_error',
|
|
151
|
+
service: 'pioneer-server',
|
|
152
|
+
level: 'error',
|
|
153
|
+
message: 'Failed to connect to MongoDB',
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
details: {
|
|
156
|
+
error: 'Connection timeout',
|
|
157
|
+
retries: 3,
|
|
158
|
+
lastAttempt: new Date().toISOString()
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
await sleep(2000);
|
|
162
|
+
// Test 5: Critical
|
|
163
|
+
log.info(TAG, 'Test 5: Critical event');
|
|
164
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
165
|
+
type: 'service_down',
|
|
166
|
+
service: 'pioneer-server',
|
|
167
|
+
level: 'critical',
|
|
168
|
+
message: 'šØ Service is DOWN - immediate attention required',
|
|
169
|
+
timestamp: new Date().toISOString(),
|
|
170
|
+
details: {
|
|
171
|
+
reason: 'Out of memory',
|
|
172
|
+
affectedUsers: '1000+',
|
|
173
|
+
impact: 'Total service outage'
|
|
174
|
+
}
|
|
175
|
+
}));
|
|
176
|
+
log.info(TAG, '\nā
All test events published!');
|
|
177
|
+
log.info(TAG, '\nCheck your Discord #keepkey-events channel to verify messages appeared.\n');
|
|
178
|
+
// Keep running for a bit to see any responses
|
|
179
|
+
log.info(TAG, 'Keeping bridge running for 10 seconds...');
|
|
180
|
+
await sleep(10000);
|
|
181
|
+
log.info(TAG, 'ā
Test completed successfully!\n');
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
log.error(TAG, 'ā Test failed:', error);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
// Cleanup
|
|
189
|
+
if (bridge) {
|
|
190
|
+
await bridge.stop();
|
|
191
|
+
}
|
|
192
|
+
await publisher.quit();
|
|
193
|
+
await subscriber.quit();
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function sleep(ms) {
|
|
198
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
199
|
+
}
|
|
200
|
+
// Run the test
|
|
201
|
+
runTest();
|
|
202
|
+
//# sourceMappingURL=test.js.map
|
package/lib/test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pioneer-platform/discord",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Discord integration for Pioneer platform - Redis to Discord bridge",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"build:watch": "tsc --watch",
|
|
10
|
+
"test": "bun test",
|
|
11
|
+
"clean": "rm -rf lib node_modules"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"discord",
|
|
15
|
+
"redis",
|
|
16
|
+
"bridge",
|
|
17
|
+
"notifications"
|
|
18
|
+
],
|
|
19
|
+
"author": "KeepKey",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"discord.js": "^14.14.1",
|
|
23
|
+
"ioredis": "^5.0.0",
|
|
24
|
+
"@pioneer-platform/loggerdog": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.14.1",
|
|
28
|
+
"typescript": "^5.8.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord Integration for Pioneer Platform
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to Redis channels and forwards events to Discord channels
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Client, GatewayIntentBits, TextChannel, EmbedBuilder } from 'discord.js';
|
|
8
|
+
import type { Redis } from 'ioredis';
|
|
9
|
+
|
|
10
|
+
const log = require('@pioneer-platform/loggerdog')();
|
|
11
|
+
const TAG = ' | Discord Integration | ';
|
|
12
|
+
|
|
13
|
+
export interface DiscordConfig {
|
|
14
|
+
botToken: string;
|
|
15
|
+
botName?: string;
|
|
16
|
+
defaultChannel: string;
|
|
17
|
+
adminUserId?: string;
|
|
18
|
+
redisChannels: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DiscordEvent {
|
|
22
|
+
type: string;
|
|
23
|
+
service?: string;
|
|
24
|
+
level?: 'info' | 'success' | 'warning' | 'error' | 'critical';
|
|
25
|
+
message: string;
|
|
26
|
+
timestamp?: string;
|
|
27
|
+
details?: Record<string, any>;
|
|
28
|
+
// Allow any additional fields
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const LEVEL_COLORS = {
|
|
33
|
+
info: 0x3498db, // Blue
|
|
34
|
+
success: 0x2ecc71, // Green
|
|
35
|
+
warning: 0xf39c12, // Orange
|
|
36
|
+
error: 0xe74c3c, // Red
|
|
37
|
+
critical: 0x992d22 // Dark Red
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const LEVEL_ICONS = {
|
|
41
|
+
info: 'ā¹ļø',
|
|
42
|
+
success: 'ā
',
|
|
43
|
+
warning: 'ā ļø',
|
|
44
|
+
error: 'ā',
|
|
45
|
+
critical: 'šØ'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export class DiscordBridge {
|
|
49
|
+
private client: Client;
|
|
50
|
+
private config: DiscordConfig;
|
|
51
|
+
private redis: Redis;
|
|
52
|
+
private isReady: boolean = false;
|
|
53
|
+
private targetChannel?: TextChannel;
|
|
54
|
+
private environment: string;
|
|
55
|
+
|
|
56
|
+
constructor(config: DiscordConfig, redis: Redis) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.redis = redis;
|
|
59
|
+
this.environment = process.env.LOCAL_ENV || 'production';
|
|
60
|
+
|
|
61
|
+
// Create Discord client with necessary intents
|
|
62
|
+
this.client = new Client({
|
|
63
|
+
intents: [
|
|
64
|
+
GatewayIntentBits.Guilds,
|
|
65
|
+
GatewayIntentBits.GuildMessages,
|
|
66
|
+
GatewayIntentBits.MessageContent
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.setupEventHandlers();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Setup Discord client event handlers
|
|
75
|
+
*/
|
|
76
|
+
private setupEventHandlers(): void {
|
|
77
|
+
this.client.on('ready', async () => {
|
|
78
|
+
log.info(TAG, `[${this.environment.toUpperCase()}] ā
Discord bot logged in as ${this.client.user?.tag}`);
|
|
79
|
+
this.isReady = true;
|
|
80
|
+
|
|
81
|
+
// Find the target channel
|
|
82
|
+
await this.findTargetChannel();
|
|
83
|
+
|
|
84
|
+
// Subscribe to Redis channels
|
|
85
|
+
this.subscribeToRedis();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.client.on('error', (error) => {
|
|
89
|
+
log.error(TAG, 'Discord client error:', error);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.client.on('warn', (warning) => {
|
|
93
|
+
log.warn(TAG, 'Discord client warning:', warning);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Find the Discord channel to post to
|
|
99
|
+
*/
|
|
100
|
+
private async findTargetChannel(): Promise<void> {
|
|
101
|
+
const tag = TAG + ' | findTargetChannel | ';
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Get all channels
|
|
105
|
+
const channels = this.client.channels.cache;
|
|
106
|
+
log.info(tag, `Searching ${channels.size} cached channels for: ${this.config.defaultChannel}`);
|
|
107
|
+
|
|
108
|
+
// Find by name
|
|
109
|
+
const channel = channels.find((ch: any) =>
|
|
110
|
+
ch.name === this.config.defaultChannel && ch.isTextBased()
|
|
111
|
+
) as TextChannel;
|
|
112
|
+
|
|
113
|
+
if (channel) {
|
|
114
|
+
this.targetChannel = channel;
|
|
115
|
+
log.info(tag, `ā
Found target channel: #${channel.name} (${channel.id})`);
|
|
116
|
+
} else {
|
|
117
|
+
log.error(tag, `ā Could not find channel: #${this.config.defaultChannel}`);
|
|
118
|
+
log.info(tag, 'Available channels:', channels.map((ch: any) => ch.name).join(', '));
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
log.error(tag, 'Error finding target channel:', error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Subscribe to Redis channels
|
|
127
|
+
*/
|
|
128
|
+
private subscribeToRedis(): void {
|
|
129
|
+
const tag = TAG + ' | subscribeToRedis | ';
|
|
130
|
+
|
|
131
|
+
log.info(tag, `Subscribing to Redis channels: ${this.config.redisChannels.join(', ')}`);
|
|
132
|
+
|
|
133
|
+
// Subscribe to all configured channels
|
|
134
|
+
this.config.redisChannels.forEach(channel => {
|
|
135
|
+
this.redis.subscribe(channel, (err) => {
|
|
136
|
+
if (err) {
|
|
137
|
+
log.error(tag, `Failed to subscribe to ${channel}:`, err);
|
|
138
|
+
} else {
|
|
139
|
+
log.info(tag, `ā
Subscribed to Redis channel: ${channel}`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Handle incoming messages
|
|
145
|
+
this.redis.on('message', async (channel: string, message: string) => {
|
|
146
|
+
await this.handleRedisMessage(channel, message);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle incoming Redis messages
|
|
152
|
+
*/
|
|
153
|
+
private async handleRedisMessage(channel: string, message: string): Promise<void> {
|
|
154
|
+
const tag = TAG + ' | handleRedisMessage | ';
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
log.debug(tag, `Received message from ${channel}`);
|
|
158
|
+
|
|
159
|
+
// Parse the message
|
|
160
|
+
let event: DiscordEvent;
|
|
161
|
+
try {
|
|
162
|
+
event = JSON.parse(message);
|
|
163
|
+
} catch (parseError) {
|
|
164
|
+
log.error(tag, 'Failed to parse Redis message:', parseError);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Post to Discord
|
|
169
|
+
await this.postToDiscord(event);
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
log.error(tag, 'Error handling Redis message:', error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Post event to Discord channel
|
|
178
|
+
*/
|
|
179
|
+
private async postToDiscord(event: DiscordEvent): Promise<void> {
|
|
180
|
+
const tag = TAG + ' | postToDiscord | ';
|
|
181
|
+
|
|
182
|
+
if (!this.isReady) {
|
|
183
|
+
log.warn(tag, 'Discord client not ready yet');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!this.targetChannel) {
|
|
188
|
+
log.error(tag, 'No target channel found');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// Determine level
|
|
194
|
+
const level = event.level || 'info';
|
|
195
|
+
const icon = LEVEL_ICONS[level] || 'ā¹ļø';
|
|
196
|
+
const color = LEVEL_COLORS[level] || LEVEL_COLORS.info;
|
|
197
|
+
|
|
198
|
+
// Create embed
|
|
199
|
+
const embed = new EmbedBuilder()
|
|
200
|
+
.setColor(color)
|
|
201
|
+
.setTitle(`${icon} ${event.type || 'Event'}`)
|
|
202
|
+
.setDescription(event.message)
|
|
203
|
+
.setTimestamp(event.timestamp ? new Date(event.timestamp) : new Date());
|
|
204
|
+
|
|
205
|
+
// Add service field if present
|
|
206
|
+
if (event.service) {
|
|
207
|
+
embed.addFields({ name: 'Service', value: event.service, inline: true });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Add details fields if present
|
|
211
|
+
if (event.details) {
|
|
212
|
+
Object.entries(event.details).forEach(([key, value]) => {
|
|
213
|
+
embed.addFields({
|
|
214
|
+
name: key,
|
|
215
|
+
value: String(value),
|
|
216
|
+
inline: true
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Send to Discord
|
|
222
|
+
await this.targetChannel.send({ embeds: [embed] });
|
|
223
|
+
log.info(tag, `ā
Posted to #${this.targetChannel.name}: ${event.type}`);
|
|
224
|
+
|
|
225
|
+
} catch (error) {
|
|
226
|
+
log.error(tag, 'Failed to post to Discord:', error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Start the Discord bridge
|
|
232
|
+
*/
|
|
233
|
+
async start(): Promise<void> {
|
|
234
|
+
const tag = TAG + ' | start | ';
|
|
235
|
+
|
|
236
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Starting Discord bridge...`);
|
|
237
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Bot name: ${this.config.botName || 'N/A'}`);
|
|
238
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Target channel: #${this.config.defaultChannel}`);
|
|
239
|
+
log.info(tag, `[${this.environment.toUpperCase()}] Redis channels: ${this.config.redisChannels.join(', ')}`);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await this.client.login(this.config.botToken);
|
|
243
|
+
log.info(tag, 'ā
Discord bridge started successfully');
|
|
244
|
+
} catch (error) {
|
|
245
|
+
log.error(tag, 'ā Failed to start Discord bridge:', error);
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Stop the Discord bridge
|
|
252
|
+
*/
|
|
253
|
+
async stop(): Promise<void> {
|
|
254
|
+
const tag = TAG + ' | stop | ';
|
|
255
|
+
|
|
256
|
+
log.info(tag, 'Stopping Discord bridge...');
|
|
257
|
+
|
|
258
|
+
// Unsubscribe from Redis channels
|
|
259
|
+
this.config.redisChannels.forEach(channel => {
|
|
260
|
+
this.redis.unsubscribe(channel);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Destroy Discord client
|
|
264
|
+
this.client.destroy();
|
|
265
|
+
this.isReady = false;
|
|
266
|
+
|
|
267
|
+
log.info(tag, 'ā
Discord bridge stopped');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check if the bridge is ready
|
|
272
|
+
*/
|
|
273
|
+
isConnected(): boolean {
|
|
274
|
+
return this.isReady && !!this.targetChannel;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get the Discord client (for advanced usage)
|
|
279
|
+
*/
|
|
280
|
+
getClient(): Client {
|
|
281
|
+
return this.client;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Create and start a Discord bridge
|
|
287
|
+
*/
|
|
288
|
+
export async function createDiscordBridge(
|
|
289
|
+
config: DiscordConfig,
|
|
290
|
+
redis: Redis
|
|
291
|
+
): Promise<DiscordBridge> {
|
|
292
|
+
const bridge = new DiscordBridge(config, redis);
|
|
293
|
+
await bridge.start();
|
|
294
|
+
return bridge;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export default DiscordBridge;
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standalone test for Discord integration
|
|
5
|
+
*
|
|
6
|
+
* Tests the Discord bridge by:
|
|
7
|
+
* 1. Connecting to Discord
|
|
8
|
+
* 2. Subscribing to keepkey-events Redis channel
|
|
9
|
+
* 3. Publishing test events
|
|
10
|
+
* 4. Verifying they appear in Discord
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* REDIS_CONNECTION_PROD=redis://... bun run src/test.ts
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as dotenv from 'dotenv';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import Redis from 'ioredis';
|
|
19
|
+
import { createDiscordBridge, DiscordConfig } from './index';
|
|
20
|
+
|
|
21
|
+
// Load environment variables
|
|
22
|
+
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
|
23
|
+
|
|
24
|
+
const log = require('@pioneer-platform/loggerdog')();
|
|
25
|
+
const TAG = ' | Discord Test | ';
|
|
26
|
+
|
|
27
|
+
async function runTest() {
|
|
28
|
+
log.info(TAG, 'š§Ŗ Testing Discord Integration\n');
|
|
29
|
+
|
|
30
|
+
// Get config from environment
|
|
31
|
+
const botToken = process.env.KEEPKEY_DISCORD_BOT_TOKEN;
|
|
32
|
+
const botName = process.env.KEEPKEY_BOT_NAME || 'pioneer';
|
|
33
|
+
const channelName = process.env.KEEPKEY_DISCORD_BOT_CHANNEL || 'keepkey-events';
|
|
34
|
+
const redisUrl = process.env.REDIS_CONNECTION_PROD || process.env.REDIS_CONNECTION || 'redis://127.0.0.1:6379';
|
|
35
|
+
|
|
36
|
+
if (!botToken) {
|
|
37
|
+
console.error('ā KEEPKEY_DISCORD_BOT_TOKEN not set in environment');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log.info(TAG, 'Configuration:');
|
|
42
|
+
log.info(TAG, ` Bot Name: ${botName}`);
|
|
43
|
+
log.info(TAG, ` Channel: #${channelName}`);
|
|
44
|
+
log.info(TAG, ` Redis: ${redisUrl.includes('@') ? redisUrl.split('@')[1] : redisUrl}`);
|
|
45
|
+
log.info(TAG, '');
|
|
46
|
+
|
|
47
|
+
// Create Redis clients
|
|
48
|
+
const subscriber = new Redis(redisUrl);
|
|
49
|
+
const publisher = new Redis(redisUrl);
|
|
50
|
+
|
|
51
|
+
// Discord bridge config
|
|
52
|
+
const config: DiscordConfig = {
|
|
53
|
+
botToken,
|
|
54
|
+
botName,
|
|
55
|
+
defaultChannel: channelName,
|
|
56
|
+
adminUserId: process.env.DISCORD_ADMIN_USERID,
|
|
57
|
+
redisChannels: ['keepkey-events']
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let bridge;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Start Discord bridge
|
|
64
|
+
log.info(TAG, 'Starting Discord bridge...');
|
|
65
|
+
bridge = await createDiscordBridge(config, subscriber);
|
|
66
|
+
|
|
67
|
+
// Wait for Discord to be ready
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
69
|
+
|
|
70
|
+
if (!bridge.isConnected()) {
|
|
71
|
+
log.error(TAG, 'ā Discord bridge failed to connect or find channel');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
log.info(TAG, 'ā
Discord bridge connected and ready\n');
|
|
76
|
+
|
|
77
|
+
// Publish test events
|
|
78
|
+
log.info(TAG, 'Publishing test events...\n');
|
|
79
|
+
|
|
80
|
+
await sleep(1000);
|
|
81
|
+
|
|
82
|
+
// Test 1: Startup event
|
|
83
|
+
log.info(TAG, 'Test 1: Server startup event');
|
|
84
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
85
|
+
type: 'server_startup',
|
|
86
|
+
service: 'pioneer-server',
|
|
87
|
+
level: 'success',
|
|
88
|
+
message: 'Pioneer Server started successfully',
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
details: {
|
|
91
|
+
port: '9001',
|
|
92
|
+
environment: 'test',
|
|
93
|
+
version: '3.0.0'
|
|
94
|
+
}
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
await sleep(2000);
|
|
98
|
+
|
|
99
|
+
// Test 2: Health check
|
|
100
|
+
log.info(TAG, 'Test 2: Health check event');
|
|
101
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
102
|
+
type: 'health_check',
|
|
103
|
+
service: 'pioneer-server',
|
|
104
|
+
level: 'info',
|
|
105
|
+
message: 'Health check passed - all systems operational',
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
details: {
|
|
108
|
+
status: 'healthy',
|
|
109
|
+
uptime: '24h',
|
|
110
|
+
connections: 42
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
await sleep(2000);
|
|
115
|
+
|
|
116
|
+
// Test 3: Warning
|
|
117
|
+
log.info(TAG, 'Test 3: Warning event');
|
|
118
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
119
|
+
type: 'high_latency',
|
|
120
|
+
service: 'pioneer-server',
|
|
121
|
+
level: 'warning',
|
|
122
|
+
message: 'API response time exceeded threshold',
|
|
123
|
+
timestamp: new Date().toISOString(),
|
|
124
|
+
details: {
|
|
125
|
+
endpoint: '/api/v1/balances',
|
|
126
|
+
latency: '5200ms',
|
|
127
|
+
threshold: '2000ms'
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
|
|
131
|
+
await sleep(2000);
|
|
132
|
+
|
|
133
|
+
// Test 4: Error
|
|
134
|
+
log.info(TAG, 'Test 4: Error event');
|
|
135
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
136
|
+
type: 'database_error',
|
|
137
|
+
service: 'pioneer-server',
|
|
138
|
+
level: 'error',
|
|
139
|
+
message: 'Failed to connect to MongoDB',
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
details: {
|
|
142
|
+
error: 'Connection timeout',
|
|
143
|
+
retries: 3,
|
|
144
|
+
lastAttempt: new Date().toISOString()
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
await sleep(2000);
|
|
149
|
+
|
|
150
|
+
// Test 5: Critical
|
|
151
|
+
log.info(TAG, 'Test 5: Critical event');
|
|
152
|
+
await publisher.publish('keepkey-events', JSON.stringify({
|
|
153
|
+
type: 'service_down',
|
|
154
|
+
service: 'pioneer-server',
|
|
155
|
+
level: 'critical',
|
|
156
|
+
message: 'šØ Service is DOWN - immediate attention required',
|
|
157
|
+
timestamp: new Date().toISOString(),
|
|
158
|
+
details: {
|
|
159
|
+
reason: 'Out of memory',
|
|
160
|
+
affectedUsers: '1000+',
|
|
161
|
+
impact: 'Total service outage'
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
log.info(TAG, '\nā
All test events published!');
|
|
166
|
+
log.info(TAG, '\nCheck your Discord #keepkey-events channel to verify messages appeared.\n');
|
|
167
|
+
|
|
168
|
+
// Keep running for a bit to see any responses
|
|
169
|
+
log.info(TAG, 'Keeping bridge running for 10 seconds...');
|
|
170
|
+
await sleep(10000);
|
|
171
|
+
|
|
172
|
+
log.info(TAG, 'ā
Test completed successfully!\n');
|
|
173
|
+
|
|
174
|
+
} catch (error: any) {
|
|
175
|
+
log.error(TAG, 'ā Test failed:', error);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
} finally {
|
|
178
|
+
// Cleanup
|
|
179
|
+
if (bridge) {
|
|
180
|
+
await bridge.stop();
|
|
181
|
+
}
|
|
182
|
+
await publisher.quit();
|
|
183
|
+
await subscriber.quit();
|
|
184
|
+
process.exit(0);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sleep(ms: number): Promise<void> {
|
|
189
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Run the test
|
|
193
|
+
runTest();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./lib",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"moduleResolution": "node"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "lib", "**/*.test.ts"]
|
|
19
|
+
}
|