initial commit

This commit is contained in:
Shrev Dev
2025-10-11 00:16:00 -05:00
commit 9f69b43704
21 changed files with 3292 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
node_modules
dist
.env
.env.*
.git
.gitignore
*.log
*.md
README.md
QUICKSTART.md
docker-compose.yml
.dockerignore
Dockerfile
fly.toml
data
.github
+15
View File
@@ -0,0 +1,15 @@
# Discord Bot Configuration
DISCORD_BOT_TOKEN=your_discord_bot_token_here
# Uptime Kuma Configuration
UPTIME_KUMA_URL=http://localhost:3001
UPTIME_KUMA_USERNAME=admin
UPTIME_KUMA_PASSWORD=your_password_here
# Bot Configuration
UPDATE_INTERVAL=60
EMBED_COLOR=5814783
# Admin Discord User IDs (comma-separated, can use slash commands)
ADMIN_USER_IDS=
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
.env
*.log
.DS_Store
data/
bot-config.json
.fly/
+46
View File
@@ -0,0 +1,46 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY src ./src
# Build TypeScript
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies only
RUN npm ci --only=production && \
npm cache clean --force
# Copy built application
COPY --from=builder /app/dist ./dist
# Create non-root user
RUN addgroup -g 1001 -S appuser && \
adduser -u 1001 -S appuser -G appuser
# Change ownership
RUN chown -R appuser:appuser /app
# Switch to non-root user
USER appuser
# Start the application
CMD ["node", "dist/index.js"]
+184
View File
@@ -0,0 +1,184 @@
# Deploy to Fly.io
Deploy your Uptime Kuma Discord Bot to Fly.io with minimal resources (free tier compatible).
## Prerequisites
- Fly.io account (sign up at https://fly.io)
- Fly CLI installed (`brew install flyctl` or see https://fly.io/docs/hands-on/install-flyctl/)
- Discord bot token
- Uptime Kuma credentials
## Quick Deploy
### 1. Clone and Navigate
```bash
git clone https://github.com/jakedev796/uptime-kuma-discord-bot
cd uptime-kuma-discord-bot
```
### 2. Login to Fly.io
```bash
fly auth login
```
### 3. Deploy
```bash
fly launch --copy-config --auto-confirm --ha=false --name your-unique-bot-name --now
```
This will:
- Create the app with minimal resources (256MB RAM, 1 shared CPU)
- Create a 1GB volume for persistent data
- Deploy the bot immediately
### 4. Set Secrets
```bash
fly secrets set \
DISCORD_BOT_TOKEN=your_discord_bot_token_here \
UPTIME_KUMA_URL=https://your-uptime-kuma-url.com \
UPTIME_KUMA_USERNAME=admin \
UPTIME_KUMA_PASSWORD=your_password_here \
ADMIN_USER_IDS=your_discord_user_id
```
The bot will automatically restart with the new secrets.
### 5. Verify Deployment
```bash
fly logs # Check startup logs
fly status # Verify it's running
```
### 6. Configure in Discord
```
/set-channel #status
/set-title Production Services
/track-all
```
Done! Your bot is running on Fly.io!
## Configuration
The `fly.toml` is pre-configured for minimal resources:
- **CPU**: 1 shared CPU
- **Memory**: 256MB (minimal but sufficient for this bot)
- **Volume**: 1GB for persistent configuration
- **Region**: `iad` (Ashburn, VA - change if needed)
- **Always on**: `auto_stop_machines = false` and `min_machines_running = 1`
- **Single instance**: `ha = false` (no high availability needed)
## Managing Your Deployment
### View Logs
```bash
fly logs
```
### Check Status
```bash
fly status
```
### Scale Resources (if needed)
```bash
fly scale memory 512 # Increase to 512MB
fly scale count 1 # Ensure single instance
```
### Update Secrets
```bash
fly secrets set UPTIME_KUMA_PASSWORD=new_password
```
### Redeploy After Code Changes
```bash
fly deploy
```
### SSH into the App
```bash
fly ssh console
```
## Costs
With this configuration:
- **Shared CPU-1x @ 256MB**: ~$1.94/month
- **1GB Volume**: Free (3GB included)
- **Bandwidth**: Free (100GB included)
**Total**: ~$2/month or free with Fly.io credits
## Regions
Change the region in `fly.toml` if needed:
- `iad` - Ashburn, Virginia (US)
- `lhr` - London, UK
- `fra` - Frankfurt, Germany
- `syd` - Sydney, Australia
- `nrt` - Tokyo, Japan
Full list: https://fly.io/docs/reference/regions/
## Troubleshooting
### Bot not starting
```bash
fly logs # Check for errors
fly status # Verify it's running
```
### Update environment variables
```bash
fly secrets list # See all secrets
fly secrets set KEY=value
```
### Bot keeps restarting
- Check memory usage: `fly scale show`
- Increase if needed: `fly scale memory 512`
- Check logs for errors: `fly logs`
### Volume issues
```bash
fly volumes list # List volumes
fly volumes delete bot_data # Delete if needed
fly volumes create bot_data --size 1 # Recreate
```
## Cleanup
To delete the app:
```bash
fly apps destroy your-app-name
```
## Support
For Fly.io specific issues:
- Fly.io Docs: https://fly.io/docs/
- Fly.io Community: https://community.fly.io/
For bot issues:
- Check [README.md](README.md)
- Open GitHub issue
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+124
View File
@@ -0,0 +1,124 @@
# Quick Start Guide
**Want to deploy to Fly.io instead?** See [FLY_DEPLOY.md](FLY_DEPLOY.md) for one-click deployment.
## Prerequisites
- Docker installed (or Node.js 18+)
- Discord bot created
- Uptime Kuma instance running
## Step 1: Create Discord Bot (2 minutes)
1. Go to https://discord.com/developers/applications
2. Click **"New Application"** and name it
3. Go to **"Bot"** tab, click **"Add Bot"**, then **"Reset Token"** and copy it
4. Go to **"OAuth2" → "URL Generator"**
5. Select scopes: `bot` + `applications.commands`
6. Select permissions: `Send Messages`, `Embed Links`, `Read Message History`, `Use Slash Commands`
7. Open the generated URL to invite bot to your server
## Step 2: Setup Bot (2 minutes)
```bash
git clone https://github.com/jakedev796/uptime-kuma-discord-bot
cd uptime-kuma-discord-bot
cp .env.example .env
```
**Edit `.env`** (only 3 required):
```env
DISCORD_BOT_TOKEN=your_discord_bot_token
UPTIME_KUMA_URL=http://uptime-kuma:3001
UPTIME_KUMA_USERNAME=admin
UPTIME_KUMA_PASSWORD=your_password
ADMIN_USER_IDS=your_discord_user_id
```
**Start it:**
```bash
docker-compose up -d
# or: npm install && npm run build && npm start
```
## Step 3: Configure in Discord (1 minute)
Type these commands in Discord:
```
/set-channel #status # Pick channel from dropdown
/set-title Production Services # Custom title (optional)
/track # Type to search, select monitor
/track # Repeat for more monitors!
# Or just:
/track-all # Track everything
```
Done!
## What's Next?
### Organize with Groups
```bash
/group-create Media Servers # Create a group
/group-add-monitor # Group dropdown, then monitor dropdown
→ Select: Your Group Name
→ Type: Your Monitor Name
→ Repeat for more monitors
/groups # See your organization
/config # See full status
```
### Quick Operations
```bash
/track # Type monitor name, select from dropdown
/untrack # Type monitor name, remove it
/group-remove-monitor # Remove monitor from its group
/group-delete # Delete entire group
```
## Troubleshooting
### Commands don't appear
- Wait 5-10 minutes
- Kick and re-invite bot
- Restart Discord
### Bot won't connect
- Check `.env` credentials
- View logs: `docker-compose logs -f`
- Try `/config` in Discord
### Permission denied
- Add your Discord user ID to `ADMIN_USER_IDS` in `.env`
- Get ID: Right-click username → Copy ID
- Restart bot
## Commands Cheat Sheet
| Command | What it does |
|---------|--------------|
| `/set-channel` | Choose status channel |
| `/set-title` | Customize embed title |
| `/track` | Add monitor (autocomplete!) |
| `/untrack` | Remove monitor (autocomplete!) |
| `/track-all` | Track everything |
| `/group-create` | Create new group |
| `/group-delete` | Delete group (autocomplete!) |
| `/group-add-monitor` | Add to group (both autocomplete!) |
| `/group-remove-monitor` | Remove from group (autocomplete!) |
| `/groups` | List all groups |
| `/config` | See full status |
## Pro Tips
- **Create groups first**, then assign monitors
- **Use `/config`** to see everything at a glance
- **Start with `/track-all`** then organize into groups
---
**Need help?** Check [README.md](README.md) or open a GitHub issue.
+289
View File
@@ -0,0 +1,289 @@
# Uptime Kuma Discord Bot
A comprehensive Discord bot that integrates with [Uptime Kuma](https://github.com/louislam/uptime-kuma) to display real-time monitor status information in Discord channels.
## Features
- **Real-time Monitoring**: Socket.io connection with automatic updates
- **Slash Commands**: Configure everything directly from Discord with autocomplete
- **Rich Embeds**: Simple embeds with status indicators (green/red/yellow/blue circles)
- **Monitor Grouping**: Organize monitors into custom sections (Media, Gaming, etc.)
- **Persistent Configuration**: All settings saved automatically
- **Docker Support**: Full Docker and Docker Compose support
- **Fly.io Ready**: One-click deployment to Fly.io (free tier compatible)
- **Admin Controls**: Restrict commands to specific users
- **Auto-Reconnection**: Handles disconnections gracefully
## Commands
All configuration via simple, autocomplete-powered slash commands:
### Setup Commands
| Command | Description |
|---------|-------------|
| `/set-channel <channel>` | Set where to post status updates |
| `/set-title <title>` | Set the embed title (replaces "Uptime Kuma Status") |
| `/config` | Show full bot configuration and status |
### Monitor Commands
| Command | Description |
|---------|-------------|
| `/track <monitor>` | Add a monitor to tracking (autocomplete) |
| `/untrack <monitor>` | Remove a monitor from tracking (autocomplete) |
| `/track-all` | Track all available monitors |
### Group Commands
| Command | Description |
|---------|-------------|
| `/group-create <name>` | Create a new group (e.g., "Media Servers") |
| `/group-delete <group>` | Delete a group (autocomplete) |
| `/group-add-monitor <group> <monitor>` | Add monitor to group (both autocomplete) |
| `/group-remove-monitor <monitor>` | Remove monitor from its group (autocomplete) |
| `/groups` | List all groups and their monitors |
## Quick Start
### Deploy to Fly.io (Recommended)
See **[FLY_DEPLOY.md](FLY_DEPLOY.md)** for full instructions.
Super quick version:
```bash
fly launch --copy-config --auto-confirm --ha=false --name my-bot --now
fly secrets set DISCORD_BOT_TOKEN=xxx UPTIME_KUMA_URL=xxx UPTIME_KUMA_USERNAME=xxx UPTIME_KUMA_PASSWORD=xxx
# Then use /set-channel in Discord
```
### Self-Host with Docker
See **[QUICKSTART.md](QUICKSTART.md)** for detailed setup instructions.
**TL;DR:**
1. Create Discord bot and get token
2. Configure `.env` with bot token and Uptime Kuma credentials
3. Run: `docker-compose up -d` or `npm start`
4. Use `/set-channel` and `/track-all` in Discord
## Usage
### Basic Setup
```bash
/set-channel #status # Set status channel
/set-title My Services # Customize title (optional)
/track-all # Track all monitors
/config # Verify setup
```
### Organizing with Groups
```bash
/group-create Media Servers # Create group
/group-add-monitor # Assign monitors (autocomplete)
/groups # View organization
```
For detailed usage examples, see [QUICKSTART.md](QUICKSTART.md).
## Embed Preview
With groups configured, your embed looks like this:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Production Services
Overall Status: 87.5% Operational
🟢 Online: 7
🔴 Offline: 1
🟡 Pending: 0
🔵 Maintenance: 0
━━━━━━━━━━━━━━━━━━━━
Media Servers
🟢 Plex - UP
🟢 Jellyfin - UP
━━━━━━━━━━━━━━━━━━━━
Gaming Servers
🔴 Minecraft - DOWN
🟢 Valheim - UP
━━━━━━━━━━━━━━━━━━━━
Other Services
🟢 Website - UP
Last updated: 2:30 PM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## Configuration
### Environment Variables
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `DISCORD_BOT_TOKEN` | Your Discord bot token | Yes | - |
| `UPTIME_KUMA_URL` | URL of your Uptime Kuma instance | Yes | `http://localhost:3001` |
| `UPTIME_KUMA_USERNAME` | Uptime Kuma username | Yes | - |
| `UPTIME_KUMA_PASSWORD` | Uptime Kuma password | Yes | - |
| `UPDATE_INTERVAL` | Update interval in seconds | No | `60` |
| `EMBED_COLOR` | Decimal color code | No | `5814783` |
| `ADMIN_USER_IDS` | Comma-separated Discord user IDs | No | `` (all users) |
### Setting Up Discord Bot
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Click "New Application" and name it
3. Go to "Bot" section, click "Add Bot", then "Reset Token" and copy it
4. Go to "OAuth2" then "URL Generator"
5. Select scopes: `bot` and `applications.commands`
6. Select permissions: `Send Messages`, `Embed Links`, `Read Message History`, `Use Slash Commands`
7. Open the generated URL to invite the bot to your server
### Getting Your Discord User ID (For Admin Access)
1. Enable Developer Mode (User Settings → Advanced → Developer Mode)
2. Right-click your username and select "Copy ID"
3. Add to `.env`: `ADMIN_USER_IDS=your_user_id`
## Advanced Features
### Monitor Groups
Create organized sections in your embed:
- **Media Servers**: Plex, Jellyfin, etc.
- **Gaming**: Minecraft, Valheim, etc.
- **Infrastructure**: Nginx, databases, etc.
- **Other Services**: Ungrouped monitors appear here
Monitors can only be in ONE group at a time. Assigning to a new group automatically removes from the old one.
## Docker Configuration
### Docker Network (Same Host as Uptime Kuma)
```bash
# Create network
docker network create uptime-kuma-network
# Connect Uptime Kuma
docker network connect uptime-kuma-network uptime-kuma
# Update docker-compose.yml (uncomment networks section)
# Update .env:
UPTIME_KUMA_URL=http://uptime-kuma:3001
```
### Persistent Data
Configuration saved to Docker volume `bot-data`:
- Channel ID
- Message IDs (for reuse)
- Tracked monitors
- Groups and assignments
- Custom title
## Security
1. **Secure `.env`**: Never commit to version control
2. **Restrict admins**: Set `ADMIN_USER_IDS` to specific users
3. **Dedicated account**: Create a bot-specific Uptime Kuma user
4. **Minimal permissions**: Only grant necessary Discord permissions
## Troubleshooting
### Bot doesn't post updates
- Use `/set-channel` to set a channel
- Use `/config` to verify setup
- Check bot permissions in that channel
### Commands don't appear
- Wait 5-10 minutes (Discord caches)
- Try kicking and re-inviting bot
- Restart Discord client
### "Permission denied" on commands
- Add your user ID to `ADMIN_USER_IDS`
- Restart bot
- Get ID: Right-click username → Copy ID
### Autocomplete doesn't show monitors
- Ensure bot is connected to Uptime Kuma
- Check `/config` for connection status
- View logs: `docker-compose logs -f`
### Bot creates new messages after restart
- This is fixed! Bot now reuses messages
- Message IDs saved to `data/bot-config.json`
- Check logs for message handling status
## Documentation Files
- **[README.md](README.md)** - This file
- **[QUICKSTART.md](QUICKSTART.md)** - Self-hosting setup guide
- **[FLY_DEPLOY.md](FLY_DEPLOY.md)** - Deploy to Fly.io (recommended)
## Tips
- Use `/config` to see everything at a glance
- Create groups BEFORE assigning monitors to them
- Use `/groups` to see your current organization
- `/track-all` then `/untrack` unwanted monitors is often faster
## Development
```bash
npm install
npm run dev # Development mode with ts-node
```
## Project Structure
```
uptime-kuma-discord-bot/
├── src/
│ ├── config/
│ │ ├── config.ts # Environment configuration
│ │ └── storage.ts # Persistent configuration storage
│ ├── services/
│ │ ├── commands.service.ts # Discord slash commands with autocomplete
│ │ ├── discord.service.ts # Discord bot service
│ │ └── uptime-kuma.service.ts # Uptime Kuma Socket.io integration
│ ├── types/
│ │ └── uptime-kuma.ts # TypeScript type definitions
│ ├── utils/
│ │ └── logger.ts # Logging utility
│ └── index.ts # Application entry point
├── data/ # Persistent configuration (auto-created)
│ └── bot-config.json # Stored settings and monitor groups
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Docker image definition
├── package.json # Dependencies
└── tsconfig.json # TypeScript configuration
```
## Why Not Use Uptime Kuma API Keys?
Uptime Kuma's [API Keys feature](https://github.com/louislam/uptime-kuma/wiki/API-Keys) (version >= 1.21.0) is designed for REST endpoints like Prometheus metrics, not Socket.io connections. This bot uses Socket.io for real-time monitor updates, which requires username/password authentication. API Keys are not supported for Socket.io authentication in Uptime Kuma.
## License
MIT License - see [LICENSE](LICENSE)
## Resources
- [Uptime Kuma](https://github.com/louislam/uptime-kuma)
- [Discord.js](https://discord.js.org/)
- [Socket.io](https://socket.io/)
## Support
If you encounter issues:
1. Check the logs: `docker-compose logs -f`
2. Verify your `.env` configuration
3. Try `/config` command to check connection status
4. Open an issue on GitHub with details
+52
View File
@@ -0,0 +1,52 @@
version: '3.8'
services:
uptime-kuma-discord-bot:
build:
context: .
dockerfile: Dockerfile
container_name: uptime-kuma-discord-bot
restart: unless-stopped
environment:
# Discord Configuration
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
# Uptime Kuma Configuration
UPTIME_KUMA_URL: ${UPTIME_KUMA_URL:-http://localhost:3001}
UPTIME_KUMA_USERNAME: ${UPTIME_KUMA_USERNAME}
UPTIME_KUMA_PASSWORD: ${UPTIME_KUMA_PASSWORD}
# Bot Configuration
UPDATE_INTERVAL: ${UPDATE_INTERVAL:-60}
EMBED_COLOR: ${EMBED_COLOR:-5814783}
# Admin User IDs (comma-separated Discord user IDs)
ADMIN_USER_IDS: ${ADMIN_USER_IDS:-}
# Data directory for persistent configuration
DATA_DIR: /app/data
# Persistent volume for bot configuration
volumes:
- bot-data:/app/data
# If Uptime Kuma is running in the same Docker network
# networks:
# - uptime-kuma-network
# Logging configuration
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
bot-data:
driver: local
# Uncomment if using Docker network
# networks:
# uptime-kuma-network:
# external: true
+33
View File
@@ -0,0 +1,33 @@
# fly.toml app configuration file generated for boker-discord-uptime-kuma on 2025-10-11T00:11:47-05:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = 'boker-discord-uptime-kuma'
primary_region = 'iad'
[build]
dockerfile = 'Dockerfile'
[deploy]
strategy = 'immediate'
[env]
DATA_DIR = '/app/data'
EMBED_COLOR = '5814783'
UPDATE_INTERVAL = '60'
[[mounts]]
source = 'bot_data'
destination = '/app/data'
[http_service]
internal_port = 8080
auto_stop_machines = 'off'
auto_start_machines = false
min_machines_running = 1
[[vm]]
cpu_kind = 'shared'
cpus = 1
memory_mb = 256
+640
View File
@@ -0,0 +1,640 @@
{
"name": "uptime-kuma-discord-bot",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "uptime-kuma-discord-bot",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"socket.io-client": "^4.7.2"
},
"devDependencies": {
"@types/node": "^20.10.5",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@discordjs/builders": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.12.2.tgz",
"integrity": "sha512-AugKmrgRJOHXEyMkABH/hXpAmIBKbYokCEl9VAM4Kh6FvkdobQ+Zhv7AR6K+y5hS7+VQ7gKXPYCe1JQmV07H1g==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/formatters": "^0.6.1",
"@discordjs/util": "^1.1.1",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.26",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/collection": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.11.0"
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz",
"integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.1"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.1.1",
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.3",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.16",
"magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3",
"undici": "6.21.3"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/util": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.5.1",
"@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.10",
"@vladfrangu/async_event_emitter": "^2.2.4",
"discord-api-types": "^0.38.1",
"tslib": "^2.6.2",
"ws": "^8.17.0"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sapphire/shapeshift": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v16"
}
},
"node_modules/@sapphire/snowflake": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.20.tgz",
"integrity": "sha512-2Q7WS25j4pS1cS8yw3d6buNCVJukOTeQ39bAnwR6sOJbaxvyCGebzTMypDFN82CxBLnl+lSWVdCCWbRY6y9yZQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-walk": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true,
"license": "MIT"
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/discord-api-types": {
"version": "0.38.29",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.29.tgz",
"integrity": "sha512-+5BfrjLJN1hrrcK0MxDQli6NSv5lQH7Y3/qaOfk9+k7itex8RkA/UcevVMMLe8B4IKIawr4ITBTb2fBB2vDORg==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
]
},
"node_modules/discord.js": {
"version": "14.23.2",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.23.2.tgz",
"integrity": "sha512-tU2NFr823X3TXEc8KyR/4m296KLxPai4nirN3q9kHCpY4TKj96n9lHZnyLzRNMui8EbL07jg9hgH2PWWfKMGIg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.12.1",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.1",
"@discordjs/rest": "^2.6.0",
"@discordjs/util": "^1.1.1",
"@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.38.29",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.10.0",
"tslib": "^2.6.3",
"undici": "6.21.3"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
"license": "MIT"
},
"node_modules/magic-bytes.js": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
"license": "MIT"
},
"node_modules/make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true,
"license": "ISC"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
"license": "MIT"
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"v8-compile-cache-lib": "^3.0.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-esm": "dist/bin-esm.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT"
},
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
}
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"name": "uptime-kuma-discord-bot",
"version": "1.0.0",
"description": "Discord bot for monitoring Uptime Kuma service status",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts",
"watch": "tsc --watch"
},
"keywords": [
"discord",
"bot",
"uptime-kuma",
"monitoring"
],
"author": "",
"license": "MIT",
"dependencies": {
"discord.js": "^14.14.1",
"socket.io-client": "^4.7.2",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/node": "^20.10.5",
"typescript": "^5.3.3",
"ts-node": "^10.9.2"
}
}
+90
View File
@@ -0,0 +1,90 @@
import { config as loadEnv } from 'dotenv';
import { existsSync } from 'fs';
loadEnv();
export interface Config {
discord: {
token: string;
adminUserIds: string[];
};
uptimeKuma: {
url: string;
username: string;
password: string;
};
bot: {
updateInterval: number;
embedColor: number;
};
}
class ConfigManager {
private config: Config;
constructor() {
this.config = this.loadConfig();
this.validateConfig();
}
private loadConfig(): Config {
return {
discord: {
token: process.env.DISCORD_BOT_TOKEN || '',
adminUserIds: this.parseAdminUserIds(process.env.ADMIN_USER_IDS),
},
uptimeKuma: {
url: process.env.UPTIME_KUMA_URL || 'http://localhost:3001',
username: process.env.UPTIME_KUMA_USERNAME || '',
password: process.env.UPTIME_KUMA_PASSWORD || '',
},
bot: {
updateInterval: parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000,
embedColor: parseInt(process.env.EMBED_COLOR || '5814783', 10),
},
};
}
private parseAdminUserIds(ids: string | undefined): string[] {
if (!ids || ids.trim() === '') {
return [];
}
return ids.split(',').map(id => id.trim()).filter(id => id.length > 0);
}
private validateConfig(): void {
const errors: string[] = [];
if (!this.config.discord.token) {
errors.push('DISCORD_BOT_TOKEN is required');
}
if (!this.config.uptimeKuma.url) {
errors.push('UPTIME_KUMA_URL is required');
}
if (!this.config.uptimeKuma.username) {
errors.push('UPTIME_KUMA_USERNAME is required');
}
if (!this.config.uptimeKuma.password) {
errors.push('UPTIME_KUMA_PASSWORD is required');
}
if (this.config.bot.updateInterval < 10000) {
errors.push('UPDATE_INTERVAL must be at least 10 seconds');
}
if (errors.length > 0) {
throw new Error(`Configuration validation failed:\n${errors.join('\n')}`);
}
}
public getConfig(): Config {
return this.config;
}
}
export const configManager = new ConfigManager();
export const config = configManager.getConfig();
+217
View File
@@ -0,0 +1,217 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { Logger } from '../utils/logger';
export interface MonitorGroup {
name: string;
monitorIds: number[];
}
export interface BotConfig {
channelId: string | null;
messageIds: string[];
monitorIds: number[];
groups: MonitorGroup[];
updateInterval: number;
embedColor: number;
statusMessage: string;
}
export class ConfigStorage {
private configPath: string;
private config: BotConfig;
private logger: Logger;
constructor() {
this.logger = new Logger('ConfigStorage');
const dataDir = process.env.DATA_DIR || './data';
if (!existsSync(dataDir)) {
mkdirSync(dataDir, { recursive: true });
}
this.configPath = join(dataDir, 'bot-config.json');
this.config = this.load();
}
private load(): BotConfig {
if (existsSync(this.configPath)) {
try {
const data = readFileSync(this.configPath, 'utf-8');
const loaded = JSON.parse(data);
this.logger.info('Loaded configuration from storage');
return {
channelId: loaded.channelId || null,
messageIds: loaded.messageIds || [],
monitorIds: loaded.monitorIds || [],
groups: loaded.groups || [],
updateInterval: loaded.updateInterval || parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000,
embedColor: loaded.embedColor || parseInt(process.env.EMBED_COLOR || '5814783', 10),
statusMessage: loaded.statusMessage || 'Service Status',
};
} catch (error: any) {
this.logger.error(`Failed to load config: ${error.message}`);
}
}
return {
channelId: null,
messageIds: [],
monitorIds: [],
groups: [],
updateInterval: parseInt(process.env.UPDATE_INTERVAL || '60', 10) * 1000,
embedColor: parseInt(process.env.EMBED_COLOR || '5814783', 10),
statusMessage: 'Service Status',
};
}
private save(): void {
try {
writeFileSync(this.configPath, JSON.stringify(this.config, null, 2), 'utf-8');
this.logger.info('Saved configuration to storage');
} catch (error: any) {
this.logger.error(`Failed to save config: ${error.message}`);
}
}
public getMonitorIds(): number[] {
return [...this.config.monitorIds];
}
public setMonitorIds(ids: number[]): void {
this.config.monitorIds = ids;
this.save();
}
public addMonitor(id: number): boolean {
if (!this.config.monitorIds.includes(id)) {
this.config.monitorIds.push(id);
this.save();
return true;
}
return false;
}
public removeMonitor(id: number): boolean {
const index = this.config.monitorIds.indexOf(id);
if (index > -1) {
this.config.monitorIds.splice(index, 1);
this.save();
return true;
}
return false;
}
public clearMonitors(): void {
this.config.monitorIds = [];
this.save();
}
public getUpdateInterval(): number {
return this.config.updateInterval;
}
public setUpdateInterval(interval: number): void {
if (interval >= 10000) {
this.config.updateInterval = interval;
this.save();
}
}
public getEmbedColor(): number {
return this.config.embedColor;
}
public setEmbedColor(color: number): void {
this.config.embedColor = color;
this.save();
}
public getConfig(): BotConfig {
return { ...this.config };
}
public getChannelId(): string | null {
return this.config.channelId;
}
public setChannelId(channelId: string): void {
this.config.channelId = channelId;
this.save();
}
public getStatusMessage(): string {
return this.config.statusMessage;
}
public setStatusMessage(message: string): void {
this.config.statusMessage = message;
this.save();
}
public getMessageIds(): string[] {
return [...this.config.messageIds];
}
public setMessageIds(ids: string[]): void {
this.config.messageIds = ids;
this.save();
}
public getGroups(): MonitorGroup[] {
return [...this.config.groups];
}
public addGroup(name: string): boolean {
if (this.config.groups.some(g => g.name.toLowerCase() === name.toLowerCase())) {
return false;
}
this.config.groups.push({ name, monitorIds: [] });
this.save();
return true;
}
public removeGroup(name: string): boolean {
const index = this.config.groups.findIndex(g => g.name.toLowerCase() === name.toLowerCase());
if (index > -1) {
this.config.groups.splice(index, 1);
this.save();
return true;
}
return false;
}
public addMonitorToGroup(groupName: string, monitorId: number): boolean {
const group = this.config.groups.find(g => g.name.toLowerCase() === groupName.toLowerCase());
if (group && !group.monitorIds.includes(monitorId)) {
for (const g of this.config.groups) {
const idx = g.monitorIds.indexOf(monitorId);
if (idx > -1) {
g.monitorIds.splice(idx, 1);
}
}
group.monitorIds.push(monitorId);
this.save();
return true;
}
return false;
}
public removeMonitorFromGroup(monitorId: number): boolean {
let found = false;
for (const group of this.config.groups) {
const index = group.monitorIds.indexOf(monitorId);
if (index > -1) {
group.monitorIds.splice(index, 1);
found = true;
}
}
if (found) {
this.save();
}
return found;
}
}
export const configStorage = new ConfigStorage();
+141
View File
@@ -0,0 +1,141 @@
import { config } from './config/config';
import { configStorage } from './config/storage';
import { UptimeKumaService } from './services/uptime-kuma.service';
import { DiscordService } from './services/discord.service';
import { Logger } from './utils/logger';
class UptimeKumaDiscordBot {
private uptimeKuma: UptimeKumaService;
private discord: DiscordService;
private updateInterval: NodeJS.Timeout | null = null;
private logger: Logger;
private isShuttingDown = false;
constructor() {
this.uptimeKuma = new UptimeKumaService();
this.discord = new DiscordService();
this.discord.setUptimeKumaService(this.uptimeKuma);
this.logger = new Logger('Bot');
this.setupSignalHandlers();
}
private setupSignalHandlers(): void {
const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
signals.forEach(signal => {
process.on(signal, () => {
this.logger.info(`Received ${signal}, shutting down gracefully...`);
this.shutdown();
});
});
process.on('uncaughtException', (error: Error) => {
this.logger.error(`Uncaught Exception: ${error.message}`);
this.logger.error(error.stack || '');
this.shutdown();
});
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
this.logger.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
this.shutdown();
});
}
public async start(): Promise<void> {
try {
this.logger.info('Starting Uptime Kuma Discord Bot...');
this.logger.info('Connecting to Discord...');
await this.discord.connect();
this.logger.info('Connecting to Uptime Kuma...');
await this.uptimeKuma.connect();
this.setupEventListeners();
this.startUpdateInterval();
this.logger.info('Bot started successfully!');
} catch (error: any) {
this.logger.error(`Failed to start bot: ${error.message}`);
this.logger.error(error.stack || '');
process.exit(1);
}
}
private setupEventListeners(): void {
this.uptimeKuma.on('monitorsUpdated', (monitors: any) => {
this.discord.updateMonitorStatus(monitors).catch((error: Error) => {
this.logger.error(`Failed to update Discord status: ${error.message}`);
});
});
this.uptimeKuma.on('statusChanged', (stats: any) => {
this.logger.info(`Monitor status changed: ${stats.monitor.name} is now ${stats.currentStatus}`);
});
this.uptimeKuma.on('disconnected', (reason: string) => {
this.logger.warn(`Uptime Kuma disconnected: ${reason}`);
if (reason === 'io server disconnect') {
this.logger.info('Attempting to reconnect...');
}
});
}
private startUpdateInterval(): void {
const updateFn = () => {
if (!this.uptimeKuma.isConnected()) {
this.logger.warn('Uptime Kuma is not connected, skipping update');
return;
}
const trackedIds = configStorage.getMonitorIds();
const monitors = this.uptimeKuma.getMonitorStats(trackedIds);
this.discord.updateMonitorStatus(monitors).catch(error => {
this.logger.error(`Failed to update Discord status: ${error.message}`);
});
};
const interval = configStorage.getUpdateInterval();
this.updateInterval = setInterval(updateFn, interval);
this.logger.info(`Update interval set to ${interval / 1000} seconds`);
const trackedIds = configStorage.getMonitorIds();
if (trackedIds.length > 0) {
this.logger.info(`Tracking ${trackedIds.length} specific monitor(s): ${trackedIds.join(', ')}`);
} else {
this.logger.info('Tracking all monitors');
}
}
private async shutdown(): Promise<void> {
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
try {
this.logger.info('Shutting down...');
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
this.uptimeKuma.disconnect();
this.discord.disconnect();
this.logger.info('Shutdown complete');
process.exit(0);
} catch (error: any) {
this.logger.error(`Error during shutdown: ${error.message}`);
process.exit(1);
}
}
}
const bot = new UptimeKumaDiscordBot();
bot.start();
+652
View File
@@ -0,0 +1,652 @@
import {
SlashCommandBuilder,
ChatInputCommandInteraction,
AutocompleteInteraction,
EmbedBuilder,
PermissionFlagsBits,
SlashCommandOptionsOnlyBuilder,
MessageFlags,
} from 'discord.js';
import { config } from '../config/config';
import { configStorage } from '../config/storage';
import { UptimeKumaService } from './uptime-kuma.service';
import { Logger } from '../utils/logger';
export interface Command {
data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder;
execute: (interaction: ChatInputCommandInteraction, uptimeKuma: UptimeKumaService) => Promise<void>;
}
export class CommandsService {
private logger: Logger;
private commands: Map<string, Command>;
constructor() {
this.logger = new Logger('CommandsService');
this.commands = new Map();
this.registerCommands();
}
private registerCommands(): void {
const commands: Command[] = [
{
data: new SlashCommandBuilder()
.setName('track')
.setDescription('Add a monitor to tracking')
.addStringOption(option =>
option
.setName('monitor')
.setDescription('Select monitor to track')
.setRequired(true)
.setAutocomplete(true)
),
execute: this.addMonitor.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('untrack')
.setDescription('Remove a monitor from tracking')
.addStringOption(option =>
option
.setName('monitor')
.setDescription('Select monitor to untrack')
.setRequired(true)
.setAutocomplete(true)
),
execute: this.removeMonitor.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('track-all')
.setDescription('Track all available monitors'),
execute: this.trackAll.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('group-create')
.setDescription('Create a new monitor group')
.addStringOption(option =>
option
.setName('name')
.setDescription('Name of the group (e.g., "Media Servers")')
.setRequired(true)
.setMaxLength(50)
),
execute: this.addGroup.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('group-delete')
.setDescription('Delete a monitor group')
.addStringOption(option =>
option
.setName('group')
.setDescription('Select group to delete')
.setRequired(true)
.setAutocomplete(true)
),
execute: this.removeGroup.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('group-add-monitor')
.setDescription('Add a monitor to a group')
.addStringOption(option =>
option
.setName('group')
.setDescription('Select group')
.setRequired(true)
.setAutocomplete(true)
)
.addStringOption(option =>
option
.setName('monitor')
.setDescription('Select monitor')
.setRequired(true)
.setAutocomplete(true)
),
execute: this.assignMonitorToGroup.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('group-remove-monitor')
.setDescription('Remove a monitor from its group')
.addStringOption(option =>
option
.setName('monitor')
.setDescription('Select monitor to unassign')
.setRequired(true)
.setAutocomplete(true)
),
execute: this.unassignMonitor.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('groups')
.setDescription('List all groups and monitors'),
execute: this.listGroups.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('set-channel')
.setDescription('Set the status update channel')
.addChannelOption(option =>
option
.setName('channel')
.setDescription('The channel to post status updates in')
.setRequired(true)
),
execute: this.setChannel.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('set-title')
.setDescription('Set the embed title')
.addStringOption(option =>
option
.setName('title')
.setDescription('Embed title (e.g., "Production Services")')
.setRequired(true)
.setMaxLength(100)
),
execute: this.setMessage.bind(this),
},
{
data: new SlashCommandBuilder()
.setName('config')
.setDescription('Show current bot configuration'),
execute: this.showConfig.bind(this),
},
];
for (const command of commands) {
this.commands.set(command.data.name, command);
}
this.logger.info(`Registered ${this.commands.size} slash commands`);
}
private isAdmin(userId: string): boolean {
if (config.discord.adminUserIds.length === 0) {
return true;
}
return config.discord.adminUserIds.includes(userId);
}
private async checkAdmin(interaction: ChatInputCommandInteraction): Promise<boolean> {
if (!this.isAdmin(interaction.user.id)) {
await interaction.reply({
content: '❌ You do not have permission to use this command.',
flags: MessageFlags.Ephemeral,
});
return false;
}
return true;
}
private async addMonitor(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10);
const currentIds = configStorage.getMonitorIds();
if (currentIds.includes(monitorId)) {
await interaction.reply({
content: '⚠️ This monitor is already being tracked.',
flags: MessageFlags.Ephemeral,
});
return;
}
const newIds = [...currentIds, monitorId];
configStorage.setMonitorIds(newIds);
const monitors = uptimeKuma.getAllMonitors();
const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`;
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('✅ Monitor Added')
.setDescription(`Now tracking: **${monitorName}**\n\nTotal tracked: ${newIds.length} monitors`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
}
private async removeMonitor(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10);
const currentIds = configStorage.getMonitorIds();
const newIds = currentIds.filter(id => id !== monitorId);
if (currentIds.length === newIds.length) {
await interaction.reply({
content: '⚠️ This monitor was not being tracked.',
flags: MessageFlags.Ephemeral,
});
return;
}
configStorage.setMonitorIds(newIds);
const monitors = uptimeKuma.getAllMonitors();
const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`;
const embed = new EmbedBuilder()
.setColor(0xff9900)
.setTitle('🗑️ Monitor Removed')
.setDescription(`Stopped tracking: **${monitorName}**\n\nTotal tracked: ${newIds.length || 'All monitors'}`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
}
private async trackAll(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
configStorage.clearMonitors();
const embed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle('🌐 Tracking All Monitors')
.setDescription('Now tracking all available monitors from Uptime Kuma')
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
}
private async setChannel(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const channel = interaction.options.getChannel('channel', true);
try {
const discordService = (interaction.client as any).discordService;
if (discordService) {
await discordService.setChannel(channel.id);
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('✅ Status Channel Updated')
.setDescription(`Status updates will now be posted in <#${channel.id}>`)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} else {
throw new Error('Discord service not available');
}
} catch (error: any) {
await interaction.editReply({
content: `❌ Failed to set channel: ${error.message}`,
});
}
}
private async setMessage(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const message = interaction.options.getString('message', true);
configStorage.setStatusMessage(message);
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('✅ Status Message Updated')
.setDescription(`Status message set to: **${message}**\n\nThis will appear in the embed title on the next update.`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
}
private async showConfig(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const botConfig = configStorage.getConfig();
const trackedIds = botConfig.monitorIds;
const channelId = botConfig.channelId;
const groups = configStorage.getGroups();
const monitors = uptimeKuma.getAllMonitors();
const embed = new EmbedBuilder()
.setColor(configStorage.getEmbedColor())
.setTitle('⚙️ Bot Configuration & Status')
.addFields(
{
name: '📢 Status Channel',
value: channelId ? `<#${channelId}>` : '⚠️ Not set - use `/set-channel`',
inline: false,
},
{
name: '📝 Embed Title',
value: botConfig.statusMessage,
inline: true,
},
{
name: '⏱️ Update Interval',
value: `${botConfig.updateInterval / 1000}s`,
inline: true,
},
{
name: '🔌 Uptime Kuma',
value: uptimeKuma.isConnected() ? '🟢 Connected' : '🔴 Disconnected',
inline: true,
},
{
name: '📊 Total Monitors',
value: `${monitors.size} available`,
inline: true,
},
{
name: '🎯 Tracking',
value: trackedIds.length === 0 ? 'All monitors' : `${trackedIds.length} monitors`,
inline: true,
},
{
name: '📁 Groups',
value: groups.length === 0 ? 'None' : `${groups.length} groups`,
inline: true,
}
);
if (trackedIds.length > 0 && trackedIds.length <= 10) {
const trackedNames = trackedIds
.map(id => monitors.get(id)?.name || `ID ${id}`)
.join(', ');
embed.addFields({
name: '━━━━━━━━━━━━━━\n🎯 Tracked Monitors',
value: trackedNames,
inline: false,
});
}
if (groups.length > 0) {
const groupSummary = groups
.map(g => `**${g.name}**: ${g.monitorIds.length} monitors`)
.join('\n');
embed.addFields({
name: '━━━━━━━━━━━━━━\n📁 Groups',
value: groupSummary,
inline: false,
});
}
embed.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
}
private async addGroup(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const name = interaction.options.getString('name', true);
const success = configStorage.addGroup(name);
if (success) {
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('✅ Group Created')
.setDescription(`Group **${name}** has been created.\n\nUse \`/group-add-monitor\` to add monitors to this group.`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({
content: `❌ A group named **${name}** already exists.`,
flags: MessageFlags.Ephemeral,
});
}
}
private async removeGroup(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const groupName = interaction.options.getString('group', true);
const success = configStorage.removeGroup(groupName);
if (success) {
const embed = new EmbedBuilder()
.setColor(0xff9900)
.setTitle('🗑️ Group Deleted')
.setDescription(`Group **${groupName}** has been deleted.\n\nMonitors are now ungrouped.`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({
content: `❌ Group **${groupName}** not found.`,
flags: MessageFlags.Ephemeral,
});
}
}
private async assignMonitorToGroup(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const groupName = interaction.options.getString('group', true);
const monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10);
const success = configStorage.addMonitorToGroup(groupName, monitorId);
if (success) {
const monitors = uptimeKuma.getAllMonitors();
const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`;
const embed = new EmbedBuilder()
.setColor(0x00ff00)
.setTitle('✅ Monitor Assigned')
.setDescription(`**${monitorName}** → **${groupName}**`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({
content: `❌ Failed to assign. Group **${groupName}** may not exist.`,
flags: MessageFlags.Ephemeral,
});
}
}
private async unassignMonitor(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const monitorIdStr = interaction.options.getString('monitor', true);
const monitorId = parseInt(monitorIdStr, 10);
const success = configStorage.removeMonitorFromGroup(monitorId);
if (success) {
const monitors = uptimeKuma.getAllMonitors();
const monitorName = monitors.get(monitorId)?.name || `ID ${monitorId}`;
const embed = new EmbedBuilder()
.setColor(0xff9900)
.setTitle('🗑️ Monitor Unassigned')
.setDescription(`**${monitorName}** has been removed from its group.`)
.setTimestamp();
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({
content: `❌ This monitor was not assigned to any group.`,
flags: MessageFlags.Ephemeral,
});
}
}
private async listGroups(
interaction: ChatInputCommandInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
if (!await this.checkAdmin(interaction)) return;
const groups = configStorage.getGroups();
if (groups.length === 0) {
await interaction.reply({
content: '📋 No groups created yet. Use `/group-add` to create one.',
flags: MessageFlags.Ephemeral,
});
return;
}
const monitors = uptimeKuma.getAllMonitors();
const embed = new EmbedBuilder()
.setColor(configStorage.getEmbedColor())
.setTitle('📋 Monitor Groups')
.setTimestamp();
for (const group of groups) {
const monitorNames = group.monitorIds
.map(id => {
const monitor = monitors.get(id);
return monitor ? `${monitor.name}` : `• ID ${id} (not found)`;
});
const value = monitorNames.length > 0
? monitorNames.join('\n')
: '*No monitors assigned*';
embed.addFields({
name: `${group.name} (${group.monitorIds.length} monitors)`,
value: value,
inline: false,
});
}
await interaction.reply({ embeds: [embed], flags: MessageFlags.Ephemeral });
}
private parseIds(idsString: string): number[] {
return idsString
.split(',')
.map(id => parseInt(id.trim(), 10))
.filter(id => !isNaN(id) && id > 0);
}
private chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
public getCommands(): Command[] {
return Array.from(this.commands.values());
}
public getCommand(name: string): Command | undefined {
return this.commands.get(name);
}
public async handleAutocomplete(
interaction: AutocompleteInteraction,
uptimeKuma: UptimeKumaService
): Promise<void> {
const focusedOption = interaction.options.getFocused(true);
try {
if (focusedOption.name === 'monitor') {
await this.autocompleteMonitors(interaction, uptimeKuma, focusedOption.value as string);
} else if (focusedOption.name === 'group') {
await this.autocompleteGroups(interaction, focusedOption.value as string);
}
} catch (error: any) {
this.logger.error(`Autocomplete error: ${error.message}`);
await interaction.respond([]);
}
}
private async autocompleteMonitors(
interaction: AutocompleteInteraction,
uptimeKuma: UptimeKumaService,
query: string
): Promise<void> {
const monitors = uptimeKuma.getAllMonitors();
const lowerQuery = query.toLowerCase();
const filtered = Array.from(monitors.entries())
.filter(([id, monitor]) =>
monitor.name.toLowerCase().includes(lowerQuery) ||
id.toString().includes(query) ||
query === ''
)
.slice(0, 25)
.map(([id, monitor]) => ({
name: `${monitor.name} (ID: ${id})`,
value: id.toString(),
}));
await interaction.respond(filtered);
}
private async autocompleteGroups(
interaction: AutocompleteInteraction,
query: string
): Promise<void> {
const groups = configStorage.getGroups();
const lowerQuery = query.toLowerCase();
const filtered = groups
.filter(group => group.name.toLowerCase().includes(lowerQuery))
.slice(0, 25)
.map(group => ({
name: `${group.name} (${group.monitorIds.length} monitors)`,
value: group.name,
}));
if (filtered.length === 0 && groups.length > 0) {
const allGroups = groups
.slice(0, 25)
.map(group => ({
name: `${group.name} (${group.monitorIds.length} monitors)`,
value: group.name,
}));
await interaction.respond(allGroups);
} else {
await interaction.respond(filtered);
}
}
}
+374
View File
@@ -0,0 +1,374 @@
import { Client, GatewayIntentBits, TextChannel, EmbedBuilder, Message, REST, Routes, MessageFlags } from 'discord.js';
import { config } from '../config/config';
import { configStorage } from '../config/storage';
import { MonitorStats, HeartbeatStatus } from '../types/uptime-kuma';
import { UptimeKumaService } from './uptime-kuma.service';
import { CommandsService } from './commands.service';
import { Logger } from '../utils/logger';
export class DiscordService {
private client: Client;
private channel: TextChannel | null = null;
private logger: Logger;
private maxMonitorsPerEmbed = 20;
private commandsService: CommandsService;
private uptimeKumaService: UptimeKumaService | null = null;
constructor() {
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
],
});
this.logger = new Logger('DiscordService');
this.commandsService = new CommandsService();
(this.client as any).discordService = this;
}
public setUptimeKumaService(service: UptimeKumaService): void {
this.uptimeKumaService = service;
}
public getClient(): Client {
return this.client;
}
public async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.once('ready', async (client) => {
this.logger.info(`Discord bot logged in as ${client.user?.tag}`);
try {
await this.initializeChannel();
await this.registerCommands();
this.setupCommandHandler();
resolve();
} catch (error: any) {
this.logger.warn(`Channel initialization skipped: ${error.message}`);
await this.registerCommands();
this.setupCommandHandler();
resolve();
}
});
this.client.on('error', (error: Error) => {
this.logger.error(`Discord client error: ${error.message}`);
});
this.client.login(config.discord.token).catch(reject);
});
}
private async registerCommands(): Promise<void> {
try {
const commands = this.commandsService.getCommands().map(cmd => cmd.data.toJSON());
const rest = new REST({ version: '10' }).setToken(config.discord.token);
this.logger.info('Registering slash commands...');
await rest.put(
Routes.applicationCommands(this.client.user!.id),
{ body: commands }
);
this.logger.info(`Successfully registered ${commands.length} slash commands`);
} catch (error: any) {
this.logger.error(`Failed to register commands: ${error.message}`);
throw error;
}
}
private setupCommandHandler(): void {
this.client.on('interactionCreate', async (interaction: any) => {
if (interaction.isAutocomplete()) {
await this.commandsService.handleAutocomplete(interaction, this.uptimeKumaService!);
return;
}
if (!interaction.isChatInputCommand()) return;
const command = this.commandsService.getCommand(interaction.commandName);
if (!command) {
this.logger.warn(`Unknown command: ${interaction.commandName}`);
return;
}
try {
if (!this.uptimeKumaService) {
await interaction.reply({
content: '❌ Uptime Kuma service not initialized',
flags: MessageFlags.Ephemeral,
});
return;
}
await command.execute(interaction, this.uptimeKumaService);
} catch (error: any) {
this.logger.error(`Error executing command ${interaction.commandName}: ${error.message}`);
const errorMessage = '❌ An error occurred while executing this command.';
if (interaction.replied || interaction.deferred) {
await interaction.followUp({ content: errorMessage, flags: MessageFlags.Ephemeral });
} else {
await interaction.reply({ content: errorMessage, flags: MessageFlags.Ephemeral });
}
}
});
}
private async initializeChannel(): Promise<void> {
const channelId = configStorage.getChannelId();
if (!channelId) {
throw new Error('No channel configured. Use /set-channel command to set one.');
}
try {
const channel = await this.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased() || channel.isDMBased()) {
throw new Error('Invalid channel or channel is not a text channel');
}
this.channel = channel as TextChannel;
this.logger.info(`Initialized channel: ${this.channel.name}`);
} catch (error: any) {
throw new Error(`Failed to initialize Discord channel: ${error.message}`);
}
}
public async setChannel(channelId: string): Promise<void> {
try {
const channel = await this.client.channels.fetch(channelId);
if (!channel || !channel.isTextBased() || channel.isDMBased()) {
throw new Error('Invalid channel or channel is not a text channel');
}
this.channel = channel as TextChannel;
configStorage.setMessageIds([]);
configStorage.setChannelId(channelId);
this.logger.info(`Changed status channel to: ${this.channel.name}`);
} catch (error: any) {
throw new Error(`Failed to set channel: ${error.message}`);
}
}
public async updateMonitorStatus(monitors: MonitorStats[]): Promise<void> {
if (!this.channel) {
this.logger.warn('Channel not initialized, skipping update');
return;
}
try {
const trackedIds = configStorage.getMonitorIds();
const filteredMonitors = trackedIds.length === 0
? monitors
: monitors.filter(m => trackedIds.includes(m.monitor.id));
if (filteredMonitors.length === 0) {
this.logger.warn('No monitors to display after filtering');
return;
}
const embeds = this.createEmbeds(filteredMonitors, monitors.length);
const messageIds = configStorage.getMessageIds();
if (messageIds.length === 0) {
await this.createNewMessages(embeds);
} else {
await this.updateExistingMessages(embeds);
}
} catch (error: any) {
this.logger.error(`Failed to update monitor status: ${error.message}`);
}
}
private createEmbeds(monitors: MonitorStats[], totalMonitors: number): EmbedBuilder[] {
const embeds: EmbedBuilder[] = [];
const statusMessage = configStorage.getStatusMessage();
const groups = configStorage.getGroups();
const embed = new EmbedBuilder()
.setColor(configStorage.getEmbedColor())
.setTitle(statusMessage)
.setTimestamp()
.setFooter({ text: 'Last updated' });
const summary = this.generateSummary(monitors, totalMonitors);
embed.setDescription(summary);
if (groups.length > 0) {
const monitorMap = new Map(monitors.map(m => [m.monitor.id, m]));
const assignedIds = new Set<number>();
for (const group of groups) {
const groupMonitors = group.monitorIds
.map(id => monitorMap.get(id))
.filter((m): m is MonitorStats => m !== undefined);
if (groupMonitors.length > 0) {
groupMonitors.forEach(m => assignedIds.add(m.monitor.id));
const monitorsList = groupMonitors.map(stats =>
`${this.getStatusEmoji(stats.currentStatus)} **${stats.monitor.name}** - ${this.formatMonitorStatus(stats)}`
).join('\n');
embed.addFields({
name: `━━━━━━━━━━━━━━━━━━━━\n${group.name}`,
value: monitorsList,
});
}
}
const ungrouped = monitors.filter(m => !assignedIds.has(m.monitor.id));
if (ungrouped.length > 0) {
const monitorsList = ungrouped.map(stats =>
`${this.getStatusEmoji(stats.currentStatus)} **${stats.monitor.name}** - ${this.formatMonitorStatus(stats)}`
).join('\n');
embed.addFields({
name: '━━━━━━━━━━━━━━━━━━━━\nOther Services',
value: monitorsList,
});
}
} else {
const monitorsList = monitors.map(stats =>
`${this.getStatusEmoji(stats.currentStatus)} **${stats.monitor.name}** - ${this.formatMonitorStatus(stats)}`
).join('\n');
embed.addFields({
name: '━━━━━━━━━━━━━━━━━━━━\nMonitored Services',
value: monitorsList || 'No monitors',
});
}
embeds.push(embed);
return embeds;
}
private getStatusEmoji(status: HeartbeatStatus): string {
switch (status) {
case HeartbeatStatus.UP:
return '🟢';
case HeartbeatStatus.DOWN:
return '🔴';
case HeartbeatStatus.PENDING:
return '🟡';
case HeartbeatStatus.MAINTENANCE:
return '🔵';
default:
return '⚪';
}
}
private formatMonitorStatus(stats: MonitorStats): string {
const parts: string[] = [];
parts.push(HeartbeatStatus[stats.currentStatus]);
if (stats.uptime24h !== undefined) {
parts.push(`${stats.uptime24h.toFixed(1)}% uptime`);
}
if (stats.avgPing !== undefined) {
parts.push(`${stats.avgPing.toFixed(0)}ms`);
}
return parts.join(' • ');
}
private generateSummary(monitors: MonitorStats[], totalMonitors: number): string {
if (monitors.length === 0) return '';
const statusCounts = {
up: monitors.filter(m => m.currentStatus === HeartbeatStatus.UP).length,
down: monitors.filter(m => m.currentStatus === HeartbeatStatus.DOWN).length,
pending: monitors.filter(m => m.currentStatus === HeartbeatStatus.PENDING).length,
maintenance: monitors.filter(m => m.currentStatus === HeartbeatStatus.MAINTENANCE).length,
};
const total = monitors.length;
const uptimePercentage = total > 0 ? ((statusCounts.up / total) * 100).toFixed(1) : '0';
const trackingInfo = monitors.length < totalMonitors
? `**Tracking ${total} of ${totalMonitors} monitors**\n\n`
: '';
return trackingInfo +
`**Overall Status:** ${uptimePercentage}% Operational\n\n` +
`🟢 **Online:** ${statusCounts.up}\n` +
`🔴 **Offline:** ${statusCounts.down}\n` +
`🟡 **Pending:** ${statusCounts.pending}\n` +
`🔵 **Maintenance:** ${statusCounts.maintenance}`;
}
private async createNewMessages(embeds: EmbedBuilder[]): Promise<void> {
if (!this.channel) return;
const newMessageIds: string[] = [];
for (const embed of embeds) {
const message = await this.channel.send({ embeds: [embed] });
newMessageIds.push(message.id);
}
configStorage.setMessageIds(newMessageIds);
this.logger.info(`Created ${embeds.length} new status message(s)`);
}
private async updateExistingMessages(embeds: EmbedBuilder[]): Promise<void> {
if (!this.channel) return;
const messageIds = configStorage.getMessageIds();
const newMessageIds: string[] = [];
for (let i = 0; i < embeds.length; i++) {
try {
if (i < messageIds.length) {
const message = await this.channel.messages.fetch(messageIds[i]);
await message.edit({ embeds: [embeds[i]] });
newMessageIds.push(messageIds[i]);
} else {
const message = await this.channel.send({ embeds: [embeds[i]] });
newMessageIds.push(message.id);
}
} catch (error: any) {
this.logger.error(`Failed to update message: ${error.message}`);
configStorage.setMessageIds([]);
await this.createNewMessages(embeds);
return;
}
}
if (embeds.length < messageIds.length) {
const toDelete = messageIds.slice(embeds.length);
for (const messageId of toDelete) {
try {
const message = await this.channel.messages.fetch(messageId);
await message.delete();
} catch (error: any) {
this.logger.warn(`Failed to delete message ${messageId}: ${error.message}`);
}
}
}
configStorage.setMessageIds(newMessageIds);
}
private chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
public disconnect(): void {
this.logger.info('Disconnecting Discord bot');
this.client.destroy();
}
}
+254
View File
@@ -0,0 +1,254 @@
import { io, Socket } from 'socket.io-client';
import { EventEmitter } from 'events';
import { config } from '../config/config';
import { Monitor, Heartbeat, MonitorStats, HeartbeatStatus, UptimeKumaResponse } from '../types/uptime-kuma';
import { Logger } from '../utils/logger';
export class UptimeKumaService extends EventEmitter {
private socket: Socket | null = null;
private monitors: Map<number, MonitorStats> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
private reconnectDelay = 5000;
private isAuthenticated = false;
private logger: Logger;
constructor() {
super();
this.logger = new Logger('UptimeKumaService');
}
public async connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.logger.info(`Connecting to Uptime Kuma at ${config.uptimeKuma.url}`);
this.socket = io(config.uptimeKuma.url, {
reconnection: true,
reconnectionDelay: this.reconnectDelay,
reconnectionAttempts: this.maxReconnectAttempts,
transports: ['websocket', 'polling'],
});
this.setupSocketListeners();
this.socket.once('connect', () => {
this.logger.info('Connected to Uptime Kuma, attempting authentication...');
this.authenticate()
.then(() => {
this.reconnectAttempts = 0;
resolve();
})
.catch(reject);
});
this.socket.once('connect_error', (error) => {
this.logger.error(`Connection error: ${error.message}`);
reject(error);
});
} catch (error) {
reject(error);
}
});
}
private setupSocketListeners(): void {
if (!this.socket) return;
this.socket.on('disconnect', (reason) => {
this.logger.warn(`Disconnected from Uptime Kuma: ${reason}`);
this.isAuthenticated = false;
this.emit('disconnected', reason);
});
this.socket.on('connect', () => {
if (this.isAuthenticated) {
this.logger.info('Reconnected to Uptime Kuma');
this.authenticate().catch(err => {
this.logger.error(`Re-authentication failed: ${err.message}`);
});
}
});
this.socket.on('monitorList', (data: Record<string, Monitor>) => {
this.handleMonitorList(data);
});
this.socket.on('heartbeat', (heartbeat: Heartbeat) => {
this.handleHeartbeat(heartbeat);
});
this.socket.on('avgPing', (data: { monitorID: number; avgPing: number | null }) => {
this.handleAvgPing(data.monitorID, data.avgPing);
});
this.socket.on('uptime', (data: { monitorID: number; periodKey: string; percentage: number }) => {
if (data.periodKey === '24') {
this.handleUptime(data.monitorID, data.percentage);
}
});
this.socket.on('connect_error', (error) => {
this.logger.error(`Socket.io connection error: ${error.message}`);
});
this.socket.on('error', (error) => {
this.logger.error(`Socket.io error: ${error}`);
});
}
private async authenticate(): Promise<void> {
return new Promise((resolve, reject) => {
if (!this.socket || !this.socket.connected) {
return reject(new Error('Socket not connected'));
}
const timeout = setTimeout(() => {
reject(new Error('Authentication timeout'));
}, 10000);
this.logger.info('Authenticating with Uptime Kuma...');
this.socket.emit(
'login',
{
username: config.uptimeKuma.username,
password: config.uptimeKuma.password,
token: '',
},
(response: UptimeKumaResponse) => {
clearTimeout(timeout);
if (response.ok) {
this.isAuthenticated = true;
this.logger.info('Successfully authenticated with Uptime Kuma');
resolve();
} else {
const errorMsg = response.msg || 'Authentication failed';
this.logger.error(`Authentication failed: ${errorMsg}`);
reject(new Error(errorMsg));
}
}
);
});
}
public getAllMonitors(): Map<number, Monitor> {
const allMonitors = new Map<number, Monitor>();
for (const [id, stats] of this.monitors.entries()) {
allMonitors.set(id, stats.monitor);
}
return allMonitors;
}
private handleMonitorList(data: Record<string, Monitor>, filterByIds?: number[]): void {
this.logger.info(`Received monitor list with ${Object.keys(data).length} monitors`);
for (const [id, monitor] of Object.entries(data)) {
const monitorId = parseInt(id, 10);
const existing = this.monitors.get(monitorId);
this.monitors.set(monitorId, {
monitor,
currentStatus: existing?.currentStatus || HeartbeatStatus.PENDING,
lastHeartbeat: existing?.lastHeartbeat,
avgPing: existing?.avgPing,
uptime24h: existing?.uptime24h,
});
}
this.emit('monitorsUpdated', this.getMonitorStats());
}
private handleHeartbeat(heartbeat: Heartbeat): void {
if (!this.shouldTrackMonitor(heartbeat.monitorID)) {
return;
}
const stats = this.monitors.get(heartbeat.monitorID);
if (stats) {
const oldStatus = stats.currentStatus;
stats.currentStatus = heartbeat.status;
stats.lastHeartbeat = heartbeat;
if (oldStatus !== heartbeat.status && heartbeat.important) {
this.logger.info(`Monitor ${stats.monitor.name} status changed: ${HeartbeatStatus[oldStatus]} -> ${HeartbeatStatus[heartbeat.status]}`);
this.emit('statusChanged', stats);
}
this.emit('monitorsUpdated', this.getMonitorStats());
}
}
private handleAvgPing(monitorID: number, avgPing: number | null): void {
if (!this.shouldTrackMonitor(monitorID)) {
return;
}
const stats = this.monitors.get(monitorID);
if (stats) {
stats.avgPing = avgPing || undefined;
}
}
private handleUptime(monitorID: number, percentage: number): void {
if (!this.shouldTrackMonitor(monitorID)) {
return;
}
const stats = this.monitors.get(monitorID);
if (stats) {
stats.uptime24h = percentage;
}
}
public setMonitorIds(ids: number[]): void {
const oldIds = Array.from(this.monitors.keys());
const newIds = ids.length === 0 ? oldIds : ids;
for (const id of oldIds) {
if (!newIds.includes(id)) {
this.monitors.delete(id);
}
}
this.emit('monitorsUpdated', this.getMonitorStats());
}
private shouldTrackMonitor(monitorId: number, monitorIds?: number[]): boolean {
const ids = monitorIds || this.getCurrentMonitorIds();
if (ids.length === 0) {
return true;
}
return ids.includes(monitorId);
}
private getCurrentMonitorIds(): number[] {
return [];
}
public getMonitorStats(filterIds?: number[]): MonitorStats[] {
const stats = Array.from(this.monitors.values());
if (!filterIds || filterIds.length === 0) {
return stats.sort((a, b) => a.monitor.name.localeCompare(b.monitor.name));
}
return stats
.filter(s => filterIds.includes(s.monitor.id))
.sort((a, b) => a.monitor.name.localeCompare(b.monitor.name));
}
public isConnected(): boolean {
return this.socket !== null && this.socket.connected && this.isAuthenticated;
}
public disconnect(): void {
if (this.socket) {
this.logger.info('Disconnecting from Uptime Kuma');
this.socket.disconnect();
this.socket = null;
this.isAuthenticated = false;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
export interface Monitor {
id: number;
name: string;
type: string;
url?: string;
active: boolean;
interval: number;
tags?: MonitorTag[];
}
export interface MonitorTag {
tag_id: number;
monitor_id: number;
value: string | null;
name: string;
color: string;
}
export interface Heartbeat {
monitorID: number;
status: HeartbeatStatus;
time: string;
msg: string;
ping: number | null;
important: boolean;
duration: number;
localDateTime: string;
timezone: string;
retries: number;
downCount: number;
}
export enum HeartbeatStatus {
DOWN = 0,
UP = 1,
PENDING = 2,
MAINTENANCE = 3,
}
export interface MonitorStats {
monitor: Monitor;
currentStatus: HeartbeatStatus;
lastHeartbeat?: Heartbeat;
avgPing?: number;
uptime24h?: number;
}
export interface UptimeKumaResponse {
ok: boolean;
msg?: string;
token?: string;
}
+29
View File
@@ -0,0 +1,29 @@
export class Logger {
private context: string;
constructor(context: string) {
this.context = context;
}
private formatMessage(level: string, message: string): string {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${level}] [${this.context}] ${message}`;
}
public info(message: string): void {
console.log(this.formatMessage('INFO', message));
}
public warn(message: string): void {
console.warn(this.formatMessage('WARN', message));
}
public error(message: string): void {
console.error(this.formatMessage('ERROR', message));
}
public debug(message: string): void {
console.debug(this.formatMessage('DEBUG', message));
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}