Compare commits
16 Commits
1.0
...
ed2919224b
| Author | SHA1 | Date | |
|---|---|---|---|
| ed2919224b | |||
| ec8e2f0a7b | |||
| ce578ab377 | |||
| 5bbcb0f53f | |||
| 9fbd8e6d51 | |||
| 3da019b8c7 | |||
| b0f153f135 | |||
| 362437bcd5 | |||
| ea7762dc4b | |||
| 6608d05f6f | |||
| 6b259ddaa2 | |||
| 610cbf7b4d | |||
| 5ee6959662 | |||
| f17cde44c3 | |||
| 0435056271 | |||
| 1dcd1cad98 |
@@ -1,4 +1,6 @@
|
||||
DISCORD_TOKEN=dein_bot_token
|
||||
CLIENT_ID=deine_client_id
|
||||
TWITCH_CLIENT_ID=deine_twitch_id
|
||||
TWITCH_CLIENT_SECRET=dein_twitch_secret
|
||||
TWITCH_CLIENT_SECRET=dein_twitch_secret
|
||||
KICK_CLIENT_ID=deine_kick_id
|
||||
KICK_CLIENT_SECRET=dein_kick_secret
|
||||
7
.gitattributes
vendored
Normal file
7
.gitattributes
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
* text=auto
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.json text eol=lf
|
||||
*.md text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
179
AGENTS-BOT.md
179
AGENTS-BOT.md
@@ -12,9 +12,12 @@ The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScrip
|
||||
3. **Interface-Driven Commands:** All commands implement `Command` interface.
|
||||
4. **SQLite Database:** Persistent data storage with `better-sqlite3`.
|
||||
5. **Twitch Monitoring:** Periodic API polling with Stream ID tracking.
|
||||
6. **Reminder System:** Periodic reminder checking with target_time tracking.
|
||||
7. **Auto-Response System:** Trigger word detection for automatic replies.
|
||||
8. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands.
|
||||
6. **Kick Monitoring:** Periodic API polling with `@nekiro/kick-api` library.
|
||||
7. **Reminder System:** Periodic reminder checking with target_time tracking.
|
||||
8. **Auto-Response System:** Trigger word detection for automatic replies.
|
||||
9. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
|
||||
10. **Logging System:** Configurable event logging (messages, roles, moderation, etc.).
|
||||
11. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands.
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
@@ -28,18 +31,30 @@ pixelpoebel/
|
||||
│ │ ├── ping.ts
|
||||
│ │ ├── help.ts
|
||||
│ │ ├── twitch.ts
|
||||
│ │ ├── kick.ts # Kick streaming
|
||||
│ │ ├── trigger.ts # Auto-responses
|
||||
│ │ ├── timer.ts # Reminders
|
||||
│ │ ├── admin.ts # All admin subcommands
|
||||
│ │ └── owner.ts # All owner subcommands
|
||||
│ │ ├── timer.ts # Reminders
|
||||
│ │ ├── welcome.ts # Welcome/Goodbye messages
|
||||
│ │ ├── log.ts # Event logging
|
||||
│ │ ├── admin.ts # All admin subcommands
|
||||
│ │ └── owner.ts # All owner subcommands
|
||||
│ ├── events/
|
||||
│ │ ├── ready.ts
|
||||
│ │ └── interactionCreate.ts
|
||||
│ │ ├── interactionCreate.ts
|
||||
│ │ ├── messageCreate.ts # Auto-response triggers
|
||||
│ │ ├── messageDelete.ts # Deleted messages
|
||||
│ │ ├── messageUpdate.ts # Edited messages
|
||||
│ │ ├── guildMemberAdd.ts # Welcome messages
|
||||
│ │ ├── guildMemberRemove.ts # Goodbye messages
|
||||
│ │ ├── guildMemberUpdate.ts # Nickname/role changes
|
||||
│ │ ├── channelCreate.ts # Channel create
|
||||
│ │ └── channelDelete.ts # Channel delete
|
||||
│ └── structures/
|
||||
│ ├── ExtendedClient.ts
|
||||
│ ├── Command.ts
|
||||
│ ├── Database.ts
|
||||
│ ├── TwitchManager.ts
|
||||
│ ├── KickManager.ts
|
||||
│ ├── ReminderManager.ts
|
||||
│ └── Deployer.ts
|
||||
├── data/ # SQLite database (volume mounted)
|
||||
@@ -47,7 +62,8 @@ pixelpoebel/
|
||||
├── tsconfig.json
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
└── .env.example
|
||||
├── .env.example
|
||||
└── .gitattributes
|
||||
```
|
||||
|
||||
## 📝 Implementation Steps
|
||||
@@ -88,6 +104,10 @@ CREATE TABLE guild_settings (
|
||||
prefix TEXT DEFAULT '!',
|
||||
mod_log_channel TEXT,
|
||||
welcome_channel TEXT,
|
||||
welcome_message TEXT,
|
||||
goodbye_message TEXT,
|
||||
log_channel TEXT,
|
||||
log_events TEXT DEFAULT 'messages,roles,nicks,channels,warns,mutes,kicks,bans',
|
||||
warn_threshold INTEGER DEFAULT 3,
|
||||
warn_action TEXT DEFAULT 'ban',
|
||||
warn_mute_duration INTEGER DEFAULT 1800
|
||||
@@ -129,6 +149,18 @@ CREATE TABLE twitch_monitors (
|
||||
UNIQUE(guild_id, channel_name)
|
||||
);
|
||||
|
||||
-- kick_monitors
|
||||
CREATE TABLE kick_monitors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_name TEXT,
|
||||
discord_channel_id TEXT,
|
||||
custom_message TEXT,
|
||||
last_status TEXT DEFAULT 'offline',
|
||||
last_stream_id TEXT,
|
||||
UNIQUE(guild_id, channel_name)
|
||||
);
|
||||
|
||||
-- auto_responses
|
||||
CREATE TABLE auto_responses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -227,8 +259,65 @@ export class ReminderManager {
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Auto-Response (Trigger) System
|
||||
### 5. Welcome/Goodbye System
|
||||
|
||||
**Variables in messages:** `{user}`, `{username}`, `{server}`, `{membercount}`
|
||||
|
||||
**Auto-use Discord system channel if no custom channel set:**
|
||||
```typescript
|
||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||
```
|
||||
|
||||
### 6. Logging System
|
||||
|
||||
**Events that can be logged:**
|
||||
- `messages` - Deleted and edited messages
|
||||
- `roles` - Role changes
|
||||
- `nicks` - Nickname changes
|
||||
- `channels` - Channel create/delete
|
||||
- `warns` - User warnings
|
||||
- `mutes` - User mutes
|
||||
- `kicks` - User kicks
|
||||
- `bans` - User bans
|
||||
|
||||
**Log moderation action:**
|
||||
```typescript
|
||||
async function sendModerationLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Auto-Response (Trigger) System
|
||||
|
||||
**Event Handler (`messageCreate.ts`):**
|
||||
```typescript
|
||||
// Listens for messages and checks against auto_responses
|
||||
// Ignores bots and DM messages
|
||||
const triggers = DB.all('SELECT * FROM auto_responses WHERE guild_id = ?', guildId);
|
||||
const contentLower = message.content.toLowerCase();
|
||||
|
||||
for (const trigger of triggers) {
|
||||
if (contentLower.includes(trigger.trigger_word.toLowerCase())) {
|
||||
await message.channel.send(trigger.response_text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Command (`trigger.ts`):**
|
||||
```typescript
|
||||
// Add trigger (admin only)
|
||||
DB.run('INSERT INTO auto_responses (guild_id, trigger_word, response_text) VALUES (?, ?, ?)',
|
||||
@@ -241,7 +330,7 @@ DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word AS
|
||||
DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
||||
```
|
||||
|
||||
### 6. Twitch Monitoring with Stream ID
|
||||
### 8. Twitch Monitoring with Stream ID
|
||||
|
||||
```typescript
|
||||
// Stream ID tracking prevents duplicate notifications
|
||||
@@ -255,7 +344,42 @@ if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Configurable Warn System
|
||||
### 9. Kick Monitoring with @nekiro/kick-api
|
||||
|
||||
```typescript
|
||||
// Uses @nekiro/kick-api library for OAuth 2.1 authentication
|
||||
import { client as KickClient } from '@nekiro/kick-api';
|
||||
|
||||
const kickClient = new KickClient({
|
||||
clientId: process.env.KICK_CLIENT_ID,
|
||||
clientSecret: process.env.KICK_CLIENT_SECRET,
|
||||
});
|
||||
|
||||
// Fetch channel and livestream data
|
||||
const channels = await kickClient.channels.getChannels({ slug: [channelName] });
|
||||
const livestreams = await kickClient.livestreams.getLivestreams({
|
||||
broadcaster_user_id: [channel.user_id]
|
||||
});
|
||||
|
||||
// Stream ID tracking prevents duplicate notifications
|
||||
const streamId = livestream.session_id?.toString();
|
||||
const isNewStream = monitor.last_stream_id !== streamId;
|
||||
const wasOffline = monitor.last_status === 'offline';
|
||||
|
||||
if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
||||
// Send notification to Discord channel
|
||||
DB.run('UPDATE kick_monitors SET last_status = "online", last_stream_id = ? WHERE id = ?',
|
||||
streamId, monitor.id);
|
||||
}
|
||||
```
|
||||
|
||||
**Required Environment Variables:**
|
||||
- `KICK_CLIENT_ID` - Kick Developer App Client ID
|
||||
- `KICK_CLIENT_SECRET` - Kick Developer App Client Secret
|
||||
|
||||
**Kick Developer App Setup:** https://docs.kick.com/getting-started/kick-apps-setup
|
||||
|
||||
### 10. Configurable Warn System
|
||||
|
||||
**Database Schema:**
|
||||
- `warn_threshold`: Number of warns before action
|
||||
@@ -275,7 +399,7 @@ if (warnCount >= warnThreshold) {
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Mute with Multiple Time Units
|
||||
### 10. Mute with Multiple Time Units
|
||||
|
||||
```typescript
|
||||
const unitSeconds: Record<string, number> = {
|
||||
@@ -295,6 +419,29 @@ if (unit === 'perm') {
|
||||
}
|
||||
```
|
||||
|
||||
### 11. Purge Command (Message Deletion)
|
||||
|
||||
**Subcommands:**
|
||||
- `purge amount <1-100>` - Delete X messages
|
||||
- `purge time <duration> <min|hour>` - Delete messages from last X time
|
||||
- `purge user <user> <1-100>` - Delete X messages from specific user
|
||||
- `purge all` - Delete all possible messages (max 100)
|
||||
|
||||
**Discord API Constraints:**
|
||||
- Max 100 messages per bulk delete
|
||||
- Messages older than 14 days cannot be bulk deleted
|
||||
- Requires `ModerateMembers` permission
|
||||
|
||||
```typescript
|
||||
// Filter messages older than 14 days (Discord API limit)
|
||||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||
const filtered = messages.filter((m: any) => m.createdTimestamp > twoWeeksAgo);
|
||||
|
||||
await channel.bulkDelete(filtered, true);
|
||||
```
|
||||
|
||||
**Logging:** Purge actions are logged to the moderation log channel with event type `messages`.
|
||||
|
||||
## 🚀 Build & Deploy
|
||||
|
||||
```bash
|
||||
@@ -319,6 +466,7 @@ docker-compose up -d --build
|
||||
4. **Twitch API:** Max 100 channels per request, batch properly.
|
||||
5. **Deploy:** Commands NOT auto-registered on start – use `/owner deploy`.
|
||||
6. **Ephemeral Replies:** Use `flags: [MessageFlags.Ephemeral]` instead of deprecated `ephemeral: true`.
|
||||
7. **Line Endings:** Use `.gitattributes` to ensure LF line endings for consistency.
|
||||
|
||||
## 📋 Command Summary
|
||||
|
||||
@@ -326,8 +474,11 @@ docker-compose up -d --build
|
||||
|---------|-------------|------------|
|
||||
| `/ping` | – | Public |
|
||||
| `/help` | – | Public |
|
||||
| `/twitch` | online, add, remove, list, help | Public/Admin |
|
||||
| `/twitch` | online, add, remove, list, listall, help | Public/Admin |
|
||||
| `/kick` | online, add, remove, list, help | Public/Admin |
|
||||
| `/trigger` | add, remove, list, help | Public/Admin |
|
||||
| `/timer` | add, remove, list, listall, help | Public/Admin |
|
||||
| `/admin` | kick, warn, unwarn, mute, unmute, ban, unban, config, help | Admin |
|
||||
| `/welcome` | setchannel, add, remove, list, help | Admin |
|
||||
| `/log` | setchannel, enable, disable, list, help | Admin |
|
||||
| `/admin` | kick, warn, unwarn, mute, unmute, ban, unban, purge, config, help | Admin |
|
||||
| `/owner` | deploy, stats, servers, help | Owner |
|
||||
44
README.md
44
README.md
@@ -16,6 +16,8 @@ DISCORD_TOKEN=dein_bot_token
|
||||
CLIENT_ID=deine_client_id
|
||||
TWITCH_CLIENT_ID=deine_twitch_id
|
||||
TWITCH_CLIENT_SECRET=dein_twitch_secret
|
||||
KICK_CLIENT_ID=deine_kick_id
|
||||
KICK_CLIENT_SECRET=dein_kick_secret
|
||||
```
|
||||
|
||||
## 🚀 Start
|
||||
@@ -59,10 +61,11 @@ npm run deploy
|
||||
- `/twitch online <channel>` – Twitch-Status
|
||||
- `/twitch list` – Überwachte Kanäle
|
||||
- `/twitch help` – Twitch-Hilfe
|
||||
|
||||
### Trigger (Auto-Antworten)
|
||||
- `/trigger add <wort> <antwort>` – Trigger hinzufügen (Mod)
|
||||
- `/trigger remove <wort>` – Trigger entfernen (Mod)
|
||||
- `/kick online <channel>` – Kick-Status
|
||||
- `/kick list` – Überwachte Kanäle
|
||||
- `/kick help` – Kick-Hilfe
|
||||
- `/timer list` – Timer dieses Kanals
|
||||
- `/timer help` – Timer-Hilfe
|
||||
- `/trigger list` – Alle Trigger anzeigen
|
||||
- `/trigger help` – Trigger-Hilfe
|
||||
|
||||
@@ -73,9 +76,28 @@ npm run deploy
|
||||
- `/timer listall` – Alle Timer (Mod)
|
||||
- `/timer help` – Timer-Hilfe
|
||||
|
||||
### Trigger (Auto-Antworten)
|
||||
- `/trigger add <wort> <antwort>` – Trigger hinzufügen (Mod)
|
||||
- `/trigger remove <wort>` – Trigger entfernen (Mod)
|
||||
- `/trigger list` – Alle Trigger anzeigen
|
||||
- `/trigger help` – Trigger-Hilfe
|
||||
|
||||
### Twitch
|
||||
- `/twitch online <kanal>` – Prüft ob Kanal online ist
|
||||
- `/twitch add <streamer1, streamer2, ...>` – Streamer überwachen (Mod)
|
||||
- `/twitch remove <streamer1, streamer2, ...>` – Streamer entfernen (Mod)
|
||||
- `/twitch list` – Überwachte Kanäle
|
||||
- `/twitch listall` – Alle überwachten Streamer (Mod)
|
||||
- `/twitch help` – Twitch-Hilfe
|
||||
|
||||
### Kick
|
||||
- `/kick online <kanal>` – Prüft ob Kanal online ist
|
||||
- `/kick add <channel> <discord_channel>` – Streamer überwachen (Mod)
|
||||
- `/kick remove <channel>` – Streamer entfernen (Mod)
|
||||
- `/kick list` – Überwachte Kanäle
|
||||
- `/kick help` – Kick-Hilfe
|
||||
|
||||
### Admin
|
||||
- `/twitch add <channel>` – Kanal überwachen
|
||||
- `/twitch remove <channel>` – Kanal entfernen
|
||||
- `/admin kick <user>` – Nutzer kicken
|
||||
- `/admin warn <user> <reason>` – Nutzer verwarnen
|
||||
- `/admin unwarn <user>` – Letzte Warnung entfernen
|
||||
@@ -83,6 +105,10 @@ npm run deploy
|
||||
- `/admin unmute <user>` – Nutzer entmuten
|
||||
- `/admin ban <user>` – Nutzer bannen
|
||||
- `/admin unban <id>` – Nutzer entbannen
|
||||
- `/admin purge amount <1-100>` – X Nachrichten löschen
|
||||
- `/admin purge time <dauer> <min|hour>` – Nachrichten der letzten X Zeit löschen
|
||||
- `/admin purge user <user> <1-100>` – X Nachrichten eines Nutzers löschen
|
||||
- `/admin purge all` – Alle Nachrichten löschen (max 100)
|
||||
- `/admin config` – Warn-System konfigurieren
|
||||
- `/admin help` – Admin-Hilfe
|
||||
|
||||
@@ -98,9 +124,11 @@ npm run deploy
|
||||
- ✅ TypeScript
|
||||
- ✅ SQLite Database
|
||||
- ✅ Twitch Monitoring
|
||||
- ✅ Auto-Deploy beim Start
|
||||
- ✅ Kick Monitoring
|
||||
- ✅ Moderation Tools
|
||||
- ✅ Konfigurierbares Warn-System
|
||||
- ✅ Auto-Antworten (Trigger)
|
||||
- ✅ Timer & Erinnerungen
|
||||
- ✅ Docker Support
|
||||
- ✅ Welcome/Goodbye Messages
|
||||
- ✅ Event Logging
|
||||
- ✅ Docker Support
|
||||
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -9,16 +9,17 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nekiro/kick-api": "^2.1.2",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.4.7"
|
||||
"dotenv": "^16.4.7",
|
||||
"typescript": "^5.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^22.13.9",
|
||||
"nodemon": "^3.1.9",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -192,6 +193,14 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@nekiro/kick-api": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@nekiro/kick-api/-/kick-api-2.1.2.tgz",
|
||||
"integrity": "sha512-D2Yq+j3U7TBK4WNc48lM+fHYh3s+9b6+EGV66wVzh6QPTr0kcQ0ThgrXkyMbppkqv+nZwFYrIw+PxmECQ3BnfQ==",
|
||||
"engines": {
|
||||
"node": ">=20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||
@@ -1274,7 +1283,6 @@
|
||||
"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",
|
||||
|
||||
10
package.json
10
package.json
@@ -10,10 +10,16 @@
|
||||
"dev": "nodemon --watch 'src/**/*.ts' --exec 'node --loader ts-node/esm' src/index.ts",
|
||||
"deploy": "node --loader ts-node/esm src/deploy-commands.ts"
|
||||
},
|
||||
"keywords": ["discord", "bot", "typescript", "modular"],
|
||||
"keywords": [
|
||||
"discord",
|
||||
"bot",
|
||||
"typescript",
|
||||
"modular"
|
||||
],
|
||||
"author": "sarah",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nekiro/kick-api": "^2.1.2",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.4.7",
|
||||
@@ -25,4 +31,4 @@
|
||||
"nodemon": "^3.1.9",
|
||||
"ts-node": "^10.9.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember, MessageFlags } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
async function sendModerationLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending moderation log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('admin')
|
||||
@@ -116,6 +137,55 @@ const command: Command = {
|
||||
option.setName('duration_value')
|
||||
.setDescription('Dauer in Sekunden')
|
||||
.setMinValue(0)))
|
||||
.addSubcommandGroup(group =>
|
||||
group
|
||||
.setName('purge')
|
||||
.setDescription('Löscht Nachrichten im Channel')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('amount')
|
||||
.setDescription('Löscht X Nachrichten')
|
||||
.addIntegerOption(option =>
|
||||
option.setName('count')
|
||||
.setDescription('Anzahl der Nachrichten (1-100)')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(100)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('time')
|
||||
.setDescription('Löscht Nachrichten der letzten X Minuten/Stunden')
|
||||
.addIntegerOption(option =>
|
||||
option.setName('duration')
|
||||
.setDescription('Dauer')
|
||||
.setRequired(true)
|
||||
.setMinValue(1))
|
||||
.addStringOption(option =>
|
||||
option.setName('unit')
|
||||
.setDescription('Zeiteinheit')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'Minuten', value: 'min' },
|
||||
{ name: 'Stunden', value: 'hour' }
|
||||
)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('user')
|
||||
.setDescription('Löscht Nachrichten eines bestimmten Nutzers')
|
||||
.addUserOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Der Nutzer')
|
||||
.setRequired(true))
|
||||
.addIntegerOption(option =>
|
||||
option.setName('count')
|
||||
.setDescription('Anzahl der Nachrichten (1-100)')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(100)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('all')
|
||||
.setDescription('Löscht alle möglichen Nachrichten (max 100)')))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
@@ -141,6 +211,10 @@ const command: Command = {
|
||||
{ name: '`/admin unmute <nutzer>`', value: 'Entmutet einen Nutzer.', inline: false },
|
||||
{ name: '`/admin ban <nutzer> [grund] [delete_days]`', value: 'Bannt einen Nutzer vom Server.\ndelete_days: Nachrichten der letzten X Tage löschen (0-7).', inline: false },
|
||||
{ name: '`/admin unban <id>`', value: 'Entbannt einen Nutzer (via ID oder @username).', inline: false },
|
||||
{ name: '`/admin purge amount <1-100>`', value: 'Löscht X Nachrichten im Channel.', inline: false },
|
||||
{ name: '`/admin purge time <dauer> <min|hour>`', value: 'Löscht Nachrichten der letzten X Minuten/Stunden.', inline: false },
|
||||
{ name: '`/admin purge user <nutzer> <1-100>`', value: 'Löscht X Nachrichten eines bestimmten Nutzers.', inline: false },
|
||||
{ name: '`/admin purge all`', value: 'Löscht alle möglichen Nachrichten (max 100).', inline: false },
|
||||
{ name: '`/admin config <setting> [wert]`', value: 'Konfiguriert das Warn-System.\nSettings: warn_threshold, warn_action, warn_mute_duration, show', inline: false },
|
||||
{ name: 'Warn-System', value: 'Automatische Aktionen bei X Warnungen:\n`/admin config warn_action kick|mute|ban`', inline: false }
|
||||
)
|
||||
@@ -174,6 +248,17 @@ const command: Command = {
|
||||
try {
|
||||
await target.kick(reason);
|
||||
await interaction.reply(`✅ Gekickt: ${target.user.tag}\nGrund: ${reason}`);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('👢 User gekickt')
|
||||
.setColor(0xf39c12)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${target.user.tag} (<@${target.id}>)`, inline: true },
|
||||
{ name: 'Moderator', value: `${interaction.user.tag} (<@${interaction.user.id}>)`, inline: true },
|
||||
{ name: 'Grund', value: reason, inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await sendModerationLog(interaction.client, guildId, 'kicks', embed);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Kicken.', flags: [MessageFlags.Ephemeral] });
|
||||
@@ -236,6 +321,18 @@ const command: Command = {
|
||||
}
|
||||
|
||||
await interaction.reply({ content: message });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('⚠️ User verwarnt')
|
||||
.setColor(0xf39c12)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${target.user.tag} (<@${target.id}>)`, inline: true },
|
||||
{ name: 'Moderator', value: `${interaction.user.tag} (<@${interaction.user.id}>)`, inline: true },
|
||||
{ name: 'Grund', value: reason, inline: false },
|
||||
{ name: 'Warnungen', value: `${warnCount}/${warnThreshold}`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
await sendModerationLog(interaction.client, guildId, 'warns', embed);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -320,6 +417,18 @@ const command: Command = {
|
||||
try {
|
||||
await target.timeout(durationMs, reason);
|
||||
await interaction.reply(`✅ Gemuted: ${target.user.tag}\nDauer: ${durationText}\nGrund: ${reason}`);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🔇 User gemutet')
|
||||
.setColor(0xf39c12)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${target.user.tag} (<@${target.id}>)`, inline: true },
|
||||
{ name: 'Moderator', value: `${interaction.user.tag} (<@${interaction.user.id}>)`, inline: true },
|
||||
{ name: 'Dauer', value: durationText, inline: true },
|
||||
{ name: 'Grund', value: reason, inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await sendModerationLog(interaction.client, guildId, 'mutes', embed);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Muten.', flags: [MessageFlags.Ephemeral] });
|
||||
@@ -363,6 +472,18 @@ const command: Command = {
|
||||
try {
|
||||
await target.ban({ deleteMessageSeconds: deleteDays * 86400, reason });
|
||||
await interaction.reply(`✅ Gebannt: ${target.user.tag}\nGrund: ${reason}`);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🔨 User gebannt')
|
||||
.setColor(0xe74c3c)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${target.user.tag} (<@${target.id}>)`, inline: true },
|
||||
{ name: 'Moderator', value: `${interaction.user.tag} (<@${interaction.user.id}>)`, inline: true },
|
||||
{ name: 'Grund', value: reason, inline: false },
|
||||
{ name: 'Nachrichten gelöscht', value: `${deleteDays} Tage`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
await sendModerationLog(interaction.client, guildId, 'bans', embed);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await interaction.reply({ content: '❌ Fehler beim Bannen.', flags: [MessageFlags.Ephemeral] });
|
||||
@@ -439,6 +560,115 @@ const command: Command = {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle purge subcommands
|
||||
const subcommandGroup = interaction.options.getSubcommandGroup();
|
||||
if (subcommandGroup === 'purge') {
|
||||
const purgeSubcommand = interaction.options.getSubcommand();
|
||||
const channel = interaction.channel;
|
||||
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
await interaction.reply({ content: '❌ Dieser Befehl kann nur in Text-Channels verwendet werden.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer reply since bulk delete can take time
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
let deletedCount = 0;
|
||||
|
||||
try {
|
||||
if (purgeSubcommand === 'amount') {
|
||||
const count = interaction.options.getInteger('count')!;
|
||||
const messages = await channel.messages.fetch({ limit: count + 1 }); // +1 for the command message
|
||||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||
const filtered = messages.filter((m: any) => m.createdTimestamp > twoWeeksAgo);
|
||||
|
||||
await (channel as any).bulkDelete(filtered, true);
|
||||
deletedCount = filtered.size - 1; // -1 for command message
|
||||
|
||||
await interaction.editReply(`✅ ${deletedCount} Nachrichten gelöscht.`);
|
||||
}
|
||||
|
||||
else if (purgeSubcommand === 'time') {
|
||||
const duration = interaction.options.getInteger('duration')!;
|
||||
const unit = interaction.options.getString('unit')!;
|
||||
|
||||
const unitMs: Record<string, number> = {
|
||||
min: 60 * 1000,
|
||||
hour: 60 * 60 * 1000
|
||||
};
|
||||
|
||||
const cutoff = Date.now() - (duration * unitMs[unit]);
|
||||
const messages = await channel.messages.fetch({ limit: 100 });
|
||||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||
const filtered = messages.filter((m: any) =>
|
||||
m.createdTimestamp > cutoff &&
|
||||
m.createdTimestamp > twoWeeksAgo
|
||||
);
|
||||
|
||||
await (channel as any).bulkDelete(filtered, true);
|
||||
deletedCount = filtered.size;
|
||||
|
||||
const unitName = unit === 'min' ? 'Minute(n)' : 'Stunde(n)';
|
||||
await interaction.editReply(`✅ ${deletedCount} Nachrichten der letzten ${duration} ${unitName} gelöscht.`);
|
||||
}
|
||||
|
||||
else if (purgeSubcommand === 'user') {
|
||||
const target = interaction.options.getUser('target')!;
|
||||
const count = interaction.options.getInteger('count')!;
|
||||
|
||||
const messages = await channel.messages.fetch({ limit: 100 });
|
||||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||
const userMessages = messages.filter((m: any) =>
|
||||
m.author.id === target.id &&
|
||||
m.createdTimestamp > twoWeeksAgo
|
||||
).first(count);
|
||||
|
||||
if (userMessages.length === 0) {
|
||||
await interaction.editReply(`Keine Nachrichten von ${target.tag} gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await (channel as any).bulkDelete(userMessages, true);
|
||||
deletedCount = userMessages.length;
|
||||
|
||||
await interaction.editReply(`✅ ${deletedCount} Nachrichten von ${target.tag} gelöscht.`);
|
||||
}
|
||||
|
||||
else if (purgeSubcommand === 'all') {
|
||||
const messages = await channel.messages.fetch({ limit: 100 });
|
||||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||
const filtered = messages.filter((m: any) => m.createdTimestamp > twoWeeksAgo);
|
||||
|
||||
await (channel as any).bulkDelete(filtered, true);
|
||||
deletedCount = filtered.size;
|
||||
|
||||
await interaction.editReply(`✅ ${deletedCount} Nachrichten gelöscht.`);
|
||||
}
|
||||
|
||||
// Log the purge action
|
||||
if (deletedCount > 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🗑️ Nachrichten gelöscht (Purge)')
|
||||
.setColor(0x9b59b6)
|
||||
.addFields(
|
||||
{ name: 'Channel', value: `<#${channel.id}>`, inline: true },
|
||||
{ name: 'Moderator', value: `${interaction.user.tag} (<@${interaction.user.id}>)`, inline: true },
|
||||
{ name: 'Anzahl', value: `${deletedCount} Nachrichten`, inline: true },
|
||||
{ name: 'Typ', value: purgeSubcommand === 'amount' ? 'Nach Anzahl' :
|
||||
purgeSubcommand === 'time' ? 'Nach Zeit' :
|
||||
purgeSubcommand === 'user' ? 'Nach Nutzer' : 'Alle', inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
await sendModerationLog(interaction.client, guildId, 'messages', embed);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ADMIN] Purge error:', error);
|
||||
await interaction.editReply('❌ Fehler beim Löschen der Nachrichten.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
170
src/commands/utility/kick.ts
Normal file
170
src/commands/utility/kick.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags, ChannelType } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
import { KickManager } from '../../structures/KickManager.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('kick')
|
||||
.setDescription('Kick-Streamer Benachrichtigungen verwalten.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Fügt einen Kick-Streamer zum Monitoring hinzu.')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Kick-Channel-Name (Slug)')
|
||||
.setRequired(true))
|
||||
.addChannelOption(option =>
|
||||
option.setName('discord_channel')
|
||||
.setDescription('Der Discord-Kanal für Benachrichtigungen')
|
||||
.addChannelTypes(ChannelType.GuildText)
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('nachricht')
|
||||
.setDescription('Optionale custom Nachricht')))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Entfernt einen Kick-Streamer vom Monitoring.')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Kick-Channel-Name')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Listet alle überwachten Kick-Streamer auf.'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('online')
|
||||
.setDescription('Prüft ob ein Kick-Streamer online ist.')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Kick-Channel-Name')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Zeigt Hilfe zu den Kick-Befehlen an.')),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
||||
const DB = interaction.client.DB;
|
||||
|
||||
if (subcommand === 'help') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🟢 Kick Streaming Hilfe')
|
||||
.setColor(0x53fc18)
|
||||
.setDescription('Benachrichtigungen bei Kick-Streams.')
|
||||
.addFields(
|
||||
{ name: '`/kick add <channel> <discord_channel> [nachricht]`', value: 'Fügt einen Streamer hinzu (Mod).', inline: false },
|
||||
{ name: '`/kick remove <channel>`', value: 'Entfernt einen Streamer (Mod).', inline: false },
|
||||
{ name: '`/kick list`', value: 'Zeigt alle überwachten Streamer.', inline: false },
|
||||
{ name: '`/kick online <channel>`', value: 'Prüft ob ein Streamer live ist.', inline: false },
|
||||
{ name: 'Hinweis', value: 'Benötigt KICK_CLIENT_ID und KICK_CLIENT_SECRET in der .env', inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'online') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase().replace(/^@/, '');
|
||||
await interaction.deferReply();
|
||||
|
||||
const { channel, livestream } = await KickManager.fetchChannelData(channelName);
|
||||
if (!channel) {
|
||||
await interaction.editReply({ content: `❌ Channel **${channelName}** nicht gefunden.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const isLive = livestream !== null;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(isLive ? `🟢 ${channel.user.username} ist LIVE` : `⚫ ${channel.user.username} ist offline`)
|
||||
.setURL(`https://kick.com/${channel.slug}`)
|
||||
.setColor(isLive ? 0x53fc18 : 0x5865F2);
|
||||
|
||||
if (isLive && livestream) {
|
||||
embed.addFields(
|
||||
{ name: 'Titel', value: livestream.stream_title || 'Kein Titel', inline: false },
|
||||
{ name: 'Kategorie', value: livestream.category?.name || 'Unbekannt', inline: true },
|
||||
{ name: 'Zuschauer', value: livestream.viewer_count.toString(), inline: true }
|
||||
);
|
||||
if (livestream.thumbnail) {
|
||||
embed.setImage(livestream.thumbnail);
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const monitors: any[] = DB.all('SELECT * FROM kick_monitors WHERE guild_id = ? ORDER BY channel_name ASC', guildId);
|
||||
|
||||
if (monitors.length === 0) {
|
||||
await interaction.reply({ content: 'Keine Kick-Streamer überwacht.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🟢 Kick-Monitoring')
|
||||
.setColor(0x53fc18)
|
||||
.setDescription(monitors.map((m: any) =>
|
||||
`• **${m.channel_name}** → <#${m.discord_channel_id}> ${m.last_status === 'online' ? '🔴' : '⚫'}`
|
||||
).join('\n'))
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase().replace(/^@/, '');
|
||||
const discordChannel = interaction.options.getChannel('discord_channel');
|
||||
const customMessage = interaction.options.getString('nachricht');
|
||||
|
||||
const { channel } = await KickManager.fetchChannelData(channelName);
|
||||
if (!channel) {
|
||||
await interaction.reply({ content: `❌ Kick-Channel **${channelName}** nicht gefunden.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO kick_monitors (guild_id, channel_name, discord_channel_id, custom_message) VALUES (?, ?, ?, ?) ON CONFLICT(guild_id, channel_name) DO UPDATE SET discord_channel_id = ?, custom_message = ?',
|
||||
guildId,
|
||||
channelName,
|
||||
discordChannel.id,
|
||||
customMessage,
|
||||
discordChannel.id,
|
||||
customMessage
|
||||
);
|
||||
|
||||
await interaction.reply({ content: `✅ **${channelName}** wird jetzt überwacht.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'remove') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase().replace(/^@/, '');
|
||||
|
||||
const result = DB.run('DELETE FROM kick_monitors WHERE guild_id = ? AND channel_name = ?', guildId, channelName);
|
||||
|
||||
if (result.changes === 0) {
|
||||
await interaction.reply({ content: `❌ **${channelName}** nicht gefunden.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply({ content: `✅ **${channelName}** entfernt.`, flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
176
src/commands/utility/log.ts
Normal file
176
src/commands/utility/log.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const AVAILABLE_EVENTS = [
|
||||
{ value: 'messages', label: 'Nachrichten', desc: 'Gelöschte und edierte Nachrichten' },
|
||||
{ value: 'roles', label: 'Rollen', desc: 'Rollenänderungen' },
|
||||
{ value: 'nicks', label: 'Nicknames', desc: 'Nickname-Änderungen' },
|
||||
{ value: 'channels', label: 'Channels', desc: 'Channel erstellt/gelöscht' },
|
||||
{ value: 'leaves', label: 'Leaves', desc: 'User hat Server verlassen' },
|
||||
{ value: 'warns', label: 'Verwarnungen', desc: 'User verwarnt' },
|
||||
{ value: 'mutes', label: 'Mutes', desc: 'User gemutet' },
|
||||
{ value: 'kicks', label: 'Kicks', desc: 'User gekickt' },
|
||||
{ value: 'bans', label: 'Bans', desc: 'User gebannt' },
|
||||
];
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('log')
|
||||
.setDescription('Log-Einstellungen verwalten (Mod).')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('setchannel')
|
||||
.setDescription('Log-Kanal setzen (Mod).')
|
||||
.addChannelOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Discord-Kanal für Logs')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('enable')
|
||||
.setDescription('Event-Logging aktivieren (Mod).')
|
||||
.addStringOption(option =>
|
||||
option.setName('event')
|
||||
.setDescription('Welches Event loggen?')
|
||||
.setRequired(true)
|
||||
.addChoices(...AVAILABLE_EVENTS.map(e => ({ name: `${e.label} - ${e.desc}`, value: e.value })))))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('disable')
|
||||
.setDescription('Event-Logging deaktivieren (Mod).')
|
||||
.addStringOption(option =>
|
||||
option.setName('event')
|
||||
.setDescription('Welches Event nicht mehr loggen?')
|
||||
.setRequired(true)
|
||||
.addChoices(...AVAILABLE_EVENTS.map(e => ({ name: `${e.label} - ${e.desc}`, value: e.value })))))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Aktive Log-Events anzeigen (Mod).'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Hilfe zu Log-Befehlen (Mod).')),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
||||
const DB = interaction.client.DB;
|
||||
|
||||
// Ensure guild settings exist
|
||||
DB.run('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)', guildId);
|
||||
|
||||
if (subcommand === 'help') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📋 Log Hilfe')
|
||||
.setColor(0x9b59b6)
|
||||
.setDescription('Loggt verschiedene Server-Events in einen Kanal.')
|
||||
.addFields(
|
||||
{ name: '`/log setchannel <#kanal>`', value: 'Setzt den Kanal für Logs (Mod).', inline: false },
|
||||
{ name: '`/log enable <event>`', value: 'Aktiviert ein Event zum Loggen (Mod).', inline: false },
|
||||
{ name: '`/log disable <event>`', value: 'Deaktiviert ein Event (Mod).', inline: false },
|
||||
{ name: '`/log list`', value: 'Zeigt aktive Log-Events (Mod).', inline: false },
|
||||
{ name: 'Events', value: AVAILABLE_EVENTS.map(e => `• **${e.value}**: ${e.desc}`).join('\n'), inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin only for all commands
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'setchannel') {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
DB.run(
|
||||
'UPDATE guild_settings SET log_channel = ? WHERE guild_id = ?',
|
||||
channel.id,
|
||||
guildId
|
||||
);
|
||||
|
||||
await interaction.reply({ content: `✅ Log-Kanal auf ${channel} gesetzt.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
const channel = settings?.log_channel
|
||||
? `<#${settings.log_channel}>`
|
||||
: 'Nicht gesetzt';
|
||||
|
||||
const activeEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
||||
const eventsList = activeEvents.length > 0
|
||||
? AVAILABLE_EVENTS.filter(e => activeEvents.includes(e.value))
|
||||
.map(e => `• **${e.label}**: ${e.desc}`)
|
||||
.join('\n')
|
||||
: 'Keine Events aktiv';
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📋 Log Einstellungen')
|
||||
.setColor(0x9b59b6)
|
||||
.setDescription(`Server: **${interaction.guild.name}**`)
|
||||
.addFields(
|
||||
{ name: 'Log-Kanal', value: channel, inline: true },
|
||||
{ name: 'Aktive Events', value: eventsList, inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'enable') {
|
||||
const event = interaction.options.getString('event')!;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
const currentEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
||||
|
||||
if (currentEvents.includes(event)) {
|
||||
await interaction.reply({ content: `ℹ️ Event **${event}** ist bereits aktiviert.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
currentEvents.push(event);
|
||||
|
||||
DB.run(
|
||||
'UPDATE guild_settings SET log_events = ? WHERE guild_id = ?',
|
||||
currentEvents.join(','),
|
||||
guildId
|
||||
);
|
||||
|
||||
await interaction.reply({ content: `✅ Event **${event}** aktiviert.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'disable') {
|
||||
const event = interaction.options.getString('event')!;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
const currentEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
||||
|
||||
if (!currentEvents.includes(event)) {
|
||||
await interaction.reply({ content: `ℹ️ Event **${event}** ist nicht aktiviert.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
const newEvents = currentEvents.filter((e: string) => e !== event);
|
||||
|
||||
DB.run(
|
||||
'UPDATE guild_settings SET log_events = ? WHERE guild_id = ?',
|
||||
newEvents.join(','),
|
||||
guildId
|
||||
);
|
||||
|
||||
await interaction.reply({ content: `✅ Event **${event}** deaktiviert.`, flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -293,7 +293,7 @@ const command: Command = {
|
||||
}
|
||||
|
||||
const reminders: any[] = DB.all(
|
||||
'SELECT r.*, c.name as channel_name FROM reminders r LEFT JOIN channels c ON r.channel_id = c.id WHERE r.guild_id = ? ORDER BY r.target_time ASC',
|
||||
'SELECT * FROM reminders WHERE guild_id = ? ORDER BY target_time ASC',
|
||||
guildId
|
||||
);
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ const command: Command = {
|
||||
.setName('add')
|
||||
.setDescription('Kanal überwachen (Admin).')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
option.setName('streamers')
|
||||
.setDescription('Twitch-Kanäle (kommagetrennt, z.B. shroud, xQc, ninja)')
|
||||
.setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option.setName('message')
|
||||
@@ -28,15 +28,19 @@ const command: Command = {
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Kanal aus Überwachung entfernen (Admin).')
|
||||
.setDescription('Streamer aus Überwachung entfernen (Admin).')
|
||||
.addStringOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Der Name des Twitch-Kanals')
|
||||
option.setName('streamers')
|
||||
.setDescription('Twitch-Kanäle (kommagetrennt, z.B. shroud, xQc, ninja)')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Listet überwachte Kanäle.'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('listall')
|
||||
.setDescription('Listet alle überwachten Kanäle des Servers (Admin).'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
@@ -55,10 +59,11 @@ const command: Command = {
|
||||
.setDescription('Überwache Twitch-Streams und bekomme Benachrichtigungen.')
|
||||
.addFields(
|
||||
{ name: '`/twitch online <kanal>`', value: 'Prüft, ob ein Kanal gerade online ist.', inline: false },
|
||||
{ name: '`/twitch add <kanal> [nachricht]`', value: 'Überwacht einen Kanal in diesem Kanal (Mod).\nWenn der Streamer live geht, wird benachrichtigt.', inline: false },
|
||||
{ name: '`/twitch remove <kanal>`', value: 'Entfernt einen Kanal aus der Überwachung (Mod).', inline: false },
|
||||
{ name: '`/twitch list`', value: 'Zeigt alle überwachten Kanäle in diesem Kanal an.', inline: false },
|
||||
{ name: 'Beispiel', value: '`/twitch add shroud Hallo! Shroud ist live!`', inline: false }
|
||||
{ name: '`/twitch add streamer1, streamer2, ...`', value: 'Überwacht Twitch-Kanäle in diesem Kanal (Mod).', inline: false },
|
||||
{ name: '`/twitch remove streamer1, streamer2, ...`', value: 'Entfernt Twitch-Kanäle (Mod).', inline: false },
|
||||
{ name: '`/twitch list`', value: 'Zeigt überwachte Kanäle in diesem Kanal.', inline: false },
|
||||
{ name: '`/twitch listall`', value: 'Zeigt alle überwachten Kanäle des Servers (Mod).', inline: false },
|
||||
{ name: 'Beispiele', value: '`/twitch add shroud, ninja`\n`/twitch add #general shroud, ninja "Live!"`\n`/twitch remove shroud, ninja`', inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
@@ -100,7 +105,7 @@ const command: Command = {
|
||||
if (subcommand === 'list') {
|
||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? AND discord_channel_id = ?', guildId, interaction.channelId);
|
||||
if (monitors.length === 0) {
|
||||
await interaction.reply('Keine Twitch-Kanäle überwacht.');
|
||||
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,32 +115,143 @@ const command: Command = {
|
||||
.setColor(0x6441a5)
|
||||
.setDescription(list);
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin only
|
||||
if (subcommand === 'listall') {
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? ORDER BY channel_name ASC', guildId);
|
||||
if (monitors.length === 0) {
|
||||
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📺 Alle überwachten Twitch-Kanäle')
|
||||
.setColor(0x6441a5)
|
||||
.setTimestamp();
|
||||
|
||||
const chunks: string[] = [];
|
||||
const chunkSize = 15;
|
||||
|
||||
for (let i = 0; i < monitors.length; i += chunkSize) {
|
||||
const chunk = monitors.slice(i, i + chunkSize).map((m: any) => {
|
||||
const channel = interaction.guild.channels.cache.get(m.discord_channel_id);
|
||||
const channelName = channel ? channel.name : m.discord_channel_id;
|
||||
return `• **${m.channel_name}** → #${channelName}`;
|
||||
}).join('\n');
|
||||
chunks.push(chunk);
|
||||
}
|
||||
|
||||
embed.setDescription(chunks[0]);
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
for (let i = 1; i < chunks.length; i++) {
|
||||
const nextEmbed = new EmbedBuilder()
|
||||
.setColor(0x6441a5)
|
||||
.setDescription(chunks[i]);
|
||||
await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin only for add/remove
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase();
|
||||
const discordChannel = interaction.options.getChannel('channel');
|
||||
const streamersInput = interaction.options.getString('streamers')!;
|
||||
const message = interaction.options.getString('message');
|
||||
const discordChannelId = interaction.channelId;
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO twitch_monitors (guild_id, channel_name, discord_channel_id, custom_message) VALUES (?, ?, ?, ?) ON CONFLICT(guild_id, channel_name) DO UPDATE SET discord_channel_id = ?, custom_message = ?',
|
||||
guildId, channelName, discordChannelId, message, discordChannelId, message
|
||||
);
|
||||
await interaction.reply(`✅ Kanal **${channelName}** wird überwacht.`);
|
||||
// Parse comma-separated streamers
|
||||
const streamers = streamersInput
|
||||
.split(',')
|
||||
.map((s: string) => s.trim().toLowerCase())
|
||||
.filter((s: string) => s.length > 0);
|
||||
|
||||
if (streamers.length === 0) {
|
||||
await interaction.reply({ content: '❌ Keine gültigen Streamer angegeben.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
const results = { added: 0, updated: 0, errors: 0 };
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const streamer of streamers) {
|
||||
try {
|
||||
// Check if stream exists
|
||||
const stream = await TwitchManager.fetchStreamData(streamer);
|
||||
if (!stream) {
|
||||
// Streamer might be valid but offline - still add them
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
const existing = DB.get('SELECT id FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, streamer);
|
||||
|
||||
DB.run(
|
||||
'INSERT INTO twitch_monitors (guild_id, channel_name, discord_channel_id, custom_message) VALUES (?, ?, ?, ?) ON CONFLICT(guild_id, channel_name) DO UPDATE SET discord_channel_id = ?, custom_message = ?',
|
||||
guildId, streamer, discordChannelId, message, discordChannelId, message
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
results.updated++;
|
||||
} else {
|
||||
results.added++;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(streamer);
|
||||
results.errors++;
|
||||
}
|
||||
}
|
||||
|
||||
let reply = `✅ **${streamers.length}** Twitch-Kanal/Kanäle für <#${discordChannelId}> konfiguriert.`;
|
||||
if (results.added > 0) reply += `\n🆕 Neu: ${results.added}`;
|
||||
if (results.updated > 0) reply += `\n🔄 Aktualisiert: ${results.updated}`;
|
||||
if (results.errors > 0) reply += `\n❌ Fehler: ${results.errors} (${errors.join(', ')})`;
|
||||
|
||||
await interaction.reply({ content: reply, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'remove') {
|
||||
const channelName = interaction.options.getString('channel')!.toLowerCase();
|
||||
DB.run('DELETE FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, channelName);
|
||||
await interaction.reply(`✅ Kanal **${channelName}** entfernt.`);
|
||||
const streamersInput = interaction.options.getString('streamers')!;
|
||||
const streamers = streamersInput
|
||||
.split(',')
|
||||
.map((s: string) => s.trim().toLowerCase())
|
||||
.filter((s: string) => s.length > 0);
|
||||
|
||||
if (streamers.length === 0) {
|
||||
await interaction.reply({ content: '❌ Keine gültigen Streamer angegeben.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
const results = { removed: 0, notFound: 0 };
|
||||
const notFound: string[] = [];
|
||||
|
||||
for (const streamer of streamers) {
|
||||
const result = DB.run('DELETE FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, streamer);
|
||||
if (result.changes === 0) {
|
||||
notFound.push(streamer);
|
||||
results.notFound++;
|
||||
} else {
|
||||
results.removed++;
|
||||
}
|
||||
}
|
||||
|
||||
let reply = '';
|
||||
if (results.removed > 0) reply += `✅ ${results.removed} Streamer entfernt.`;
|
||||
if (results.notFound > 0) reply += `\n❌ Nicht gefunden: ${notFound.join(', ')}`;
|
||||
|
||||
await interaction.reply({ content: reply, flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
158
src/commands/utility/welcome.ts
Normal file
158
src/commands/utility/welcome.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
||||
import { Command } from '../../structures/Command.js';
|
||||
|
||||
const command: Command = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('welcome')
|
||||
.setDescription('Willkommens- und Abschiedsnachrichten verwalten.')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('setchannel')
|
||||
.setDescription('Kanal für Willkommen/Abschied setzen (Mod).')
|
||||
.addChannelOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('Discord-Kanal')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Nachricht setzen (Mod).')
|
||||
.addStringOption(option =>
|
||||
option.setName('type')
|
||||
.setDescription('Art der Nachricht')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'Willkommen', value: 'welcome' },
|
||||
{ name: 'Abschied', value: 'goodbye' }
|
||||
))
|
||||
.addStringOption(option =>
|
||||
option.setName('message')
|
||||
.setDescription('Die Nachricht. Variablen: {user}, {username}, {server}, {membercount}')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Nachricht entfernen (Mod).')
|
||||
.addStringOption(option =>
|
||||
option.setName('type')
|
||||
.setDescription('Art der Nachricht')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'Willkommen', value: 'welcome' },
|
||||
{ name: 'Abschied', value: 'goodbye' }
|
||||
)))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('Aktuelle Einstellungen anzeigen.'))
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('help')
|
||||
.setDescription('Hilfe zu Welcome-Befehlen.')),
|
||||
category: 'Admin',
|
||||
async execute(interaction: any) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId;
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
||||
const DB = interaction.client.DB;
|
||||
|
||||
// Ensure guild settings exist
|
||||
DB.run('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)', guildId);
|
||||
|
||||
if (subcommand === 'help') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('👋 Welcome Hilfe')
|
||||
.setColor(0x2ecc71)
|
||||
.setDescription('Begrüße neue Mitglieder oder verabschiede dich von ihnen.')
|
||||
.addFields(
|
||||
{ name: '`/welcome setchannel <#kanal>`', value: 'Setzt den Kanal für Willkommen/Abschied (Mod).\nStandard: Discord System-Channel.', inline: false },
|
||||
{ name: '`/welcome add welcome <nachricht>`', value: 'Setzt die Willkommensnachricht (Mod).', inline: false },
|
||||
{ name: '`/welcome add goodbye <nachricht>`', value: 'Setzt die Abschiedsnachricht (Mod).', inline: false },
|
||||
{ name: '`/welcome remove welcome|goodbye`', value: 'Entfernt eine Nachricht (Mod).', inline: false },
|
||||
{ name: '`/welcome list`', value: 'Zeigt aktuelle Einstellungen.', inline: false },
|
||||
{ name: 'Variablen', value: '`{user}` - Nutzer erwähnen\n`{username}` - Nutzername\n`{server}` - Servername\n`{membercount}` - Mitgliederanzahl', inline: false },
|
||||
{ name: 'Beispiel', value: '`/welcome add welcome Willkommen {user} auf {server}! Du bist der {membercount}. Nutzer.`', inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
const channel = settings?.welcome_channel
|
||||
? `<#${settings.welcome_channel}>`
|
||||
: 'Nicht gesetzt';
|
||||
|
||||
const welcomeMsg = settings?.welcome_message || 'Nicht gesetzt';
|
||||
const goodbyeMsg = settings?.goodbye_message || 'Nicht gesetzt';
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('👋 Welcome Einstellungen')
|
||||
.setColor(0x2ecc71)
|
||||
.setDescription(`Server: **${interaction.guild.name}**`)
|
||||
.addFields(
|
||||
{ name: 'Kanal', value: channel, inline: true },
|
||||
{ name: '👋 Willkommen', value: welcomeMsg, inline: false },
|
||||
{ name: '👋 Abschied', value: goodbyeMsg, inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin only for modify commands
|
||||
if (!isAdmin) {
|
||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'setchannel') {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
|
||||
DB.run(
|
||||
'UPDATE guild_settings SET welcome_channel = ? WHERE guild_id = ?',
|
||||
channel.id,
|
||||
guildId
|
||||
);
|
||||
|
||||
await interaction.reply({ content: `✅ Kanal für Willkommen/Abschied auf ${channel} gesetzt.`, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'add') {
|
||||
const type = interaction.options.getString('type') as 'welcome' | 'goodbye';
|
||||
const message = interaction.options.getString('message')!;
|
||||
|
||||
const column = type === 'welcome' ? 'welcome_message' : 'goodbye_message';
|
||||
|
||||
DB.run(
|
||||
`UPDATE guild_settings SET ${column} = ? WHERE guild_id = ?`,
|
||||
message,
|
||||
guildId
|
||||
);
|
||||
|
||||
const label = type === 'welcome' ? 'Willkommensnachricht' : 'Abschiedsnachricht';
|
||||
await interaction.reply({ content: `✅ ${label} gesetzt:\n\`\`\`\n${message}\n\`\`\``, flags: [MessageFlags.Ephemeral] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'remove') {
|
||||
const type = interaction.options.getString('type') as 'welcome' | 'goodbye';
|
||||
const column = type === 'welcome' ? 'welcome_message' : 'goodbye_message';
|
||||
|
||||
DB.run(
|
||||
`UPDATE guild_settings SET ${column} = NULL WHERE guild_id = ?`,
|
||||
guildId
|
||||
);
|
||||
|
||||
const label = type === 'welcome' ? 'Willkommensnachricht' : 'Abschiedsnachricht';
|
||||
await interaction.reply({ content: `✅ ${label} entfernt.`, flags: [MessageFlags.Ephemeral] });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
38
src/events/channelCreate.ts
Normal file
38
src/events/channelCreate.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: Events.ChannelCreate,
|
||||
async execute(channel: GuildChannel) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📁 Channel erstellt')
|
||||
.setColor(0x27ae60)
|
||||
.addFields(
|
||||
{ name: 'Name', value: channel.name, inline: true },
|
||||
{ name: 'Typ', value: channel.type.toString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(channel.client, channel.guild!.id, 'channels', embed);
|
||||
},
|
||||
};
|
||||
38
src/events/channelDelete.ts
Normal file
38
src/events/channelDelete.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: Events.ChannelDelete,
|
||||
async execute(channel: GuildChannel) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📁 Channel gelöscht')
|
||||
.setColor(0xe74c3c)
|
||||
.addFields(
|
||||
{ name: 'Name', value: channel.name, inline: true },
|
||||
{ name: 'Typ', value: channel.type.toString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(channel.client, channel.guild!.id, 'channels', embed);
|
||||
},
|
||||
};
|
||||
34
src/events/guildMemberAdd.ts
Normal file
34
src/events/guildMemberAdd.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: Events.GuildMemberAdd,
|
||||
async execute(member: GuildMember) {
|
||||
const DB = (member.client as any).DB;
|
||||
const guildId = member.guild.id;
|
||||
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
// Use custom channel or fall back to Discord's system channel
|
||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||
if (!channelId || !settings?.welcome_message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = member.guild.channels.cache.get(channelId);
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = settings.welcome_message
|
||||
.replace(/{user}/g, `<@${member.id}>`)
|
||||
.replace(/{username}/g, member.user.username)
|
||||
.replace(/{server}/g, member.guild.name)
|
||||
.replace(/{membercount}/g, member.guild.memberCount.toString());
|
||||
|
||||
try {
|
||||
await (channel as TextChannel).send(message);
|
||||
} catch (error) {
|
||||
console.error('[WELCOME] Error sending welcome message:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
67
src/events/guildMemberRemove.ts
Normal file
67
src/events/guildMemberRemove.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: Events.GuildMemberRemove,
|
||||
async execute(member: GuildMember) {
|
||||
const DB = (member.client as any).DB;
|
||||
const guildId = member.guild.id;
|
||||
|
||||
// Log the leave event
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('👋 User hat Server verlassen')
|
||||
.setColor(0x95a5a6)
|
||||
.addFields(
|
||||
{ name: 'User', value: `${member.user.tag} (<@${member.id}>)`, inline: true },
|
||||
{ name: 'Beitritt', value: member.joinedTimestamp ? `<t:${Math.floor(member.joinedTimestamp / 1000)}:R>` : 'Unbekannt', inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(member.client, guildId, 'leaves', embed);
|
||||
|
||||
// Send goodbye message if configured
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||
if (!channelId || !settings?.goodbye_message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = member.guild.channels.cache.get(channelId);
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = settings.goodbye_message
|
||||
.replace(/{user}/g, `<@${member.id}>`)
|
||||
.replace(/{username}/g, member.user.username)
|
||||
.replace(/{server}/g, member.guild.name)
|
||||
.replace(/{membercount}/g, member.guild.memberCount.toString());
|
||||
|
||||
try {
|
||||
await (channel as TextChannel).send(message);
|
||||
} catch (error) {
|
||||
console.error('[WELCOME] Error sending goodbye message:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
80
src/events/guildMemberUpdate.ts
Normal file
80
src/events/guildMemberUpdate.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Events, GuildMember, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Guild Member Update (nickname + roles)
|
||||
export default {
|
||||
name: Events.GuildMemberUpdate,
|
||||
async execute(oldMember: GuildMember, newMember: GuildMember) {
|
||||
const guildId = newMember.guild.id;
|
||||
|
||||
// Nickname change
|
||||
if (oldMember.nickname !== newMember.nickname) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('📝 Nickname geändert')
|
||||
.setColor(0x3498db)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true }
|
||||
)
|
||||
.addFields(
|
||||
{ name: 'Vorher', value: oldMember.nickname || newMember.user.username, inline: true },
|
||||
{ name: 'Nachher', value: newMember.nickname || newMember.user.username, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(newMember.client, guildId, 'nicks', embed);
|
||||
}
|
||||
|
||||
// Role changes
|
||||
const oldRoles = oldMember.roles.cache;
|
||||
const newRoles = newMember.roles.cache;
|
||||
|
||||
const addedRoles = newRoles.filter(r => !oldRoles.has(r.id));
|
||||
const removedRoles = oldRoles.filter(r => !newRoles.has(r.id));
|
||||
|
||||
for (const role of addedRoles) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🎭 Rolle hinzugefügt')
|
||||
.setColor(0x27ae60)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true },
|
||||
{ name: 'Rolle', value: `${role[1].name} (<@&${role[1].id}>)`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(newMember.client, guildId, 'roles', embed);
|
||||
}
|
||||
|
||||
for (const role of removedRoles) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🎭 Rolle entfernt')
|
||||
.setColor(0xe74c3c)
|
||||
.addFields(
|
||||
{ name: 'Nutzer', value: `${newMember.user.tag} (<@${newMember.id}>)`, inline: true },
|
||||
{ name: 'Rolle', value: `${role[1].name} (<@&${role[1].id}>)`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(newMember.client, guildId, 'roles', embed);
|
||||
}
|
||||
},
|
||||
};
|
||||
23
src/events/messageCreate.ts
Normal file
23
src/events/messageCreate.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Events } from 'discord.js';
|
||||
|
||||
export default {
|
||||
name: Events.MessageCreate,
|
||||
async execute(message: any, client: any) {
|
||||
if (message.author.bot) return;
|
||||
if (!message.guildId) return;
|
||||
|
||||
const triggers: any[] = client.DB.all(
|
||||
'SELECT * FROM auto_responses WHERE guild_id = ?',
|
||||
message.guildId
|
||||
);
|
||||
|
||||
const contentLower = message.content.toLowerCase();
|
||||
|
||||
for (const trigger of triggers) {
|
||||
if (contentLower.includes(trigger.trigger_word.toLowerCase())) {
|
||||
await message.channel.send(trigger.response_text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
73
src/events/messageDelete.ts
Normal file
73
src/events/messageDelete.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Events, Message, TextChannel, GuildChannel, AuditLogEvent, EmbedBuilder } from 'discord.js';
|
||||
|
||||
function getLogChannel(client: any, guildId: string): TextChannel | null {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return channel as TextChannel;
|
||||
}
|
||||
|
||||
function isEventEnabled(client: any, guildId: string, event: string): boolean {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
return activeEvents.includes(event);
|
||||
}
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
if (!isEventEnabled(client, guildId, event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = getLogChannel(client, guildId);
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await channel.send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Message Delete
|
||||
export default {
|
||||
name: Events.MessageDelete,
|
||||
async execute(message: Message) {
|
||||
if (!message.guild || message.author.bot) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('🗑️ Nachricht gelöscht')
|
||||
.setColor(0xe74c3c)
|
||||
.addFields(
|
||||
{ name: 'Autor', value: `${message.author.tag} (<@${message.author.id}>)`, inline: true },
|
||||
{ name: 'Kanal', value: `<#${message.channelId}>`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (message.content) {
|
||||
embed.addFields({ name: 'Inhalt', value: message.content.substring(0, 1024), inline: false });
|
||||
}
|
||||
|
||||
if (message.attachments.size > 0) {
|
||||
embed.addFields({ name: 'Anhänge', value: `${message.attachments.size} Datei(en)`, inline: true });
|
||||
}
|
||||
|
||||
await sendLog(message.client, message.guildId!, 'messages', embed);
|
||||
},
|
||||
};
|
||||
47
src/events/messageUpdate.ts
Normal file
47
src/events/messageUpdate.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Events, Message, EmbedBuilder } from 'discord.js';
|
||||
|
||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||
const DB = client.DB;
|
||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
|
||||
if (!settings?.log_events) return;
|
||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||
if (!activeEvents.includes(event)) return;
|
||||
|
||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
||||
if (!logSettings?.log_channel) return;
|
||||
|
||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
|
||||
try {
|
||||
await (channel as any).send({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Message Update (Edit)
|
||||
export default {
|
||||
name: Events.MessageUpdate,
|
||||
async execute(oldMessage: Message, newMessage: Message) {
|
||||
if (!newMessage.guild || newMessage.author?.bot) return;
|
||||
if (!oldMessage.content || !newMessage.content) return;
|
||||
if (oldMessage.content === newMessage.content) return;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('✏️ Nachricht editiert')
|
||||
.setColor(0xf39c12)
|
||||
.addFields(
|
||||
{ name: 'Autor', value: `${newMessage.author.tag} (<@${newMessage.author.id}>)`, inline: true },
|
||||
{ name: 'Kanal', value: `<#${newMessage.channelId}>`, inline: true }
|
||||
)
|
||||
.addFields(
|
||||
{ name: 'Vorher', value: oldMessage.content.substring(0, 1024), inline: false },
|
||||
{ name: 'Nachher', value: newMessage.content.substring(0, 1024), inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await sendLog(newMessage.client, newMessage.guildId!, 'messages', embed);
|
||||
},
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { dirname, join } from 'node:path';
|
||||
import { Command } from './structures/Command.js';
|
||||
import { DB } from './structures/Database.js';
|
||||
import { TwitchManager } from './structures/TwitchManager.js';
|
||||
import { KickManager } from './structures/KickManager.js';
|
||||
import { ReminderManager } from './structures/ReminderManager.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -60,6 +61,9 @@ for (const file of eventFiles) {
|
||||
// Start Twitch Polling
|
||||
TwitchManager.startPolling(client);
|
||||
|
||||
// Start Kick Polling
|
||||
KickManager.startPolling(client);
|
||||
|
||||
// Start Reminder Polling
|
||||
ReminderManager.startPolling(client);
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ export class DB {
|
||||
prefix TEXT DEFAULT '!',
|
||||
mod_log_channel TEXT,
|
||||
welcome_channel TEXT,
|
||||
welcome_message TEXT,
|
||||
goodbye_message TEXT,
|
||||
log_channel TEXT,
|
||||
log_events TEXT DEFAULT 'messages,roles,nicks,channels,moderation',
|
||||
warn_threshold INTEGER DEFAULT 3,
|
||||
warn_action TEXT DEFAULT 'ban',
|
||||
warn_mute_duration INTEGER DEFAULT 1800
|
||||
@@ -75,6 +79,20 @@ export class DB {
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Kick Monitors
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS kick_monitors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
guild_id TEXT,
|
||||
channel_name TEXT,
|
||||
discord_channel_id TEXT,
|
||||
custom_message TEXT,
|
||||
last_status TEXT DEFAULT 'offline',
|
||||
last_stream_id TEXT,
|
||||
UNIQUE(guild_id, channel_name)
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Auto Responses
|
||||
db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS auto_responses (
|
||||
@@ -99,6 +117,23 @@ export class DB {
|
||||
)
|
||||
`).run();
|
||||
|
||||
// Migration: Add missing columns to existing guild_settings
|
||||
const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[];
|
||||
const columnNames = columns.map((c: any) => c.name);
|
||||
|
||||
if (!columnNames.includes('welcome_message')) {
|
||||
db.prepare('ALTER TABLE guild_settings ADD COLUMN welcome_message TEXT').run();
|
||||
}
|
||||
if (!columnNames.includes('goodbye_message')) {
|
||||
db.prepare('ALTER TABLE guild_settings ADD COLUMN goodbye_message TEXT').run();
|
||||
}
|
||||
if (!columnNames.includes('log_channel')) {
|
||||
db.prepare('ALTER TABLE guild_settings ADD COLUMN log_channel TEXT').run();
|
||||
}
|
||||
if (!columnNames.includes('log_events')) {
|
||||
db.prepare('ALTER TABLE guild_settings ADD COLUMN log_events TEXT DEFAULT \'messages,roles,nicks,channels,leaves,warns,mutes,kicks,bans\'').run();
|
||||
}
|
||||
|
||||
console.log('[DATABASE] Initialization complete.');
|
||||
}
|
||||
|
||||
|
||||
142
src/structures/KickManager.ts
Normal file
142
src/structures/KickManager.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||
import { client as KickClient } from '@nekiro/kick-api';
|
||||
|
||||
interface KickChannel {
|
||||
id: number;
|
||||
user_id: number;
|
||||
slug: string;
|
||||
user: { username: string };
|
||||
category?: { id: number; name: string };
|
||||
followers_count: number;
|
||||
}
|
||||
|
||||
interface KickLivestream {
|
||||
broadcaster_user_id: number;
|
||||
category: { id: number; name: string };
|
||||
channel_id: number;
|
||||
slug: string;
|
||||
started_at: string;
|
||||
stream_title: string;
|
||||
thumbnail: string;
|
||||
viewer_count: number;
|
||||
session_id?: string;
|
||||
}
|
||||
|
||||
export class KickManager {
|
||||
private static kickClient: InstanceType<typeof KickClient> | null = null;
|
||||
private static channelLivestreamMap: Map<string, KickLivestream> = new Map();
|
||||
|
||||
private static initClient() {
|
||||
if (this.kickClient) return this.kickClient;
|
||||
|
||||
const clientId = process.env.KICK_CLIENT_ID;
|
||||
const clientSecret = process.env.KICK_CLIENT_SECRET;
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
console.error('[KICK] Missing KICK_CLIENT_ID or KICK_CLIENT_SECRET in .env');
|
||||
return null;
|
||||
}
|
||||
|
||||
this.kickClient = new KickClient({
|
||||
clientId,
|
||||
clientSecret,
|
||||
});
|
||||
|
||||
return this.kickClient;
|
||||
}
|
||||
|
||||
static async fetchChannelData(channelName: string): Promise<{ channel: KickChannel | null; livestream: KickLivestream | null }> {
|
||||
const client = this.initClient();
|
||||
if (!client) return { channel: null, livestream: null };
|
||||
|
||||
try {
|
||||
const channels = await client.channels.getChannels({ slug: [channelName] }) as KickChannel[];
|
||||
const channel = channels?.[0] || null;
|
||||
|
||||
if (!channel) return { channel: null, livestream: null };
|
||||
|
||||
const livestreams = await client.livestreams.getLivestreams({
|
||||
broadcaster_user_id: [channel.user_id]
|
||||
}) as KickLivestream[];
|
||||
const livestream = livestreams?.[0] || null;
|
||||
|
||||
return { channel, livestream };
|
||||
} catch (error) {
|
||||
console.error(`[KICK] Error fetching data for ${channelName}:`, error);
|
||||
return { channel: null, livestream: null };
|
||||
}
|
||||
}
|
||||
|
||||
static async checkStreams(client: any) {
|
||||
if (!this.initClient()) return;
|
||||
|
||||
const monitors: any[] = (client as any).DB?.all('SELECT * FROM kick_monitors') || [];
|
||||
if (monitors.length === 0) return;
|
||||
|
||||
for (const monitor of monitors) {
|
||||
try {
|
||||
const { channel, livestream } = await this.fetchChannelData(monitor.channel_name);
|
||||
|
||||
if (channel && livestream) {
|
||||
const streamId = livestream.session_id?.toString() || livestream.broadcaster_user_id.toString();
|
||||
const isNewStream = monitor.last_stream_id !== streamId;
|
||||
const wasOffline = monitor.last_status === 'offline';
|
||||
|
||||
if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
||||
await this.sendNotification(client, monitor, channel, livestream);
|
||||
(client as any).DB?.run(
|
||||
"UPDATE kick_monitors SET last_status = 'online', last_stream_id = ? WHERE id = ?",
|
||||
streamId,
|
||||
monitor.id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (monitor.last_status === 'online') {
|
||||
(client as any).DB?.run("UPDATE kick_monitors SET last_status = 'offline', last_stream_id = NULL WHERE id = ?", monitor.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[KICK] Error checking stream ${monitor.channel_name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async sendNotification(client: any, monitor: any, channel: KickChannel, livestream: KickLivestream) {
|
||||
const guild = client.guilds.cache.get(monitor.guild_id);
|
||||
if (!guild || !monitor.discord_channel_id) return;
|
||||
|
||||
const discordChannel = guild.channels.cache.get(monitor.discord_channel_id) as TextChannel;
|
||||
if (!discordChannel) return;
|
||||
|
||||
const thumbnailUrl = livestream.thumbnail || `https://kick.com/img/channel-thumbnails/${channel.id}?t=${Date.now()}`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`${channel.user.username} ist jetzt LIVE auf Kick!`)
|
||||
.setURL(`https://kick.com/${channel.slug}`)
|
||||
.setColor(0x53fc18)
|
||||
.setImage(thumbnailUrl)
|
||||
.addFields(
|
||||
{ name: 'Titel', value: livestream.stream_title || 'Kein Titel', inline: false },
|
||||
{ name: 'Kategorie', value: livestream.category?.name || 'Unbekannt', inline: true },
|
||||
{ name: 'Zuschauer', value: livestream.viewer_count.toString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
let content = `📢 **${channel.user.username}** ist jetzt auf Kick online!`;
|
||||
if (monitor.custom_message) {
|
||||
content += `\n\n${monitor.custom_message}`;
|
||||
}
|
||||
|
||||
await discordChannel.send({ content, embeds: [embed] });
|
||||
}
|
||||
|
||||
static startPolling(client: any) {
|
||||
if (!process.env.KICK_CLIENT_ID || !process.env.KICK_CLIENT_SECRET) {
|
||||
console.log('[KICK] Skipping polling - missing credentials.');
|
||||
return;
|
||||
}
|
||||
|
||||
setInterval(() => this.checkStreams(client), 5 * 60 * 1000);
|
||||
console.log('[KICK] Polling started (5m interval).');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user