Compare commits
13 Commits
ce578ab377
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e1630f80b | ||
| e2018be4c6 | |||
| d546035bb7 | |||
| b2c3f5731b | |||
| 21c3b01e64 | |||
| c2c009501f | |||
| 2fcb7cec66 | |||
| b6caf4b3b3 | |||
| 14781b8e52 | |||
| ad988c9333 | |||
| f90f0cb7ab | |||
| ed2919224b | |||
| ec8e2f0a7b |
@@ -2,5 +2,9 @@ DISCORD_TOKEN=dein_bot_token
|
|||||||
CLIENT_ID=deine_client_id
|
CLIENT_ID=deine_client_id
|
||||||
TWITCH_CLIENT_ID=deine_twitch_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
|
TWITCH_USERNAME=dein_twitch_username
|
||||||
KICK_CLIENT_SECRET=dein_kick_secret
|
TWITCH_OAUTH_TOKEN=oauth:xxxxxxxxxxxxxxxxxx
|
||||||
|
BOT_OWNER_ID=deine_discord_user_id
|
||||||
|
|
||||||
|
# Optional: Automatische Slash-Command Registrierung bei Bot-Start (default: true)
|
||||||
|
AUTO_DEPLOY=true
|
||||||
517
AGENTS-BOT.md
517
AGENTS-BOT.md
@@ -4,59 +4,42 @@ This document is intended for AI agents to understand and recreate the `pixelpö
|
|||||||
|
|
||||||
## 🏗 Architecture Overview
|
## 🏗 Architecture Overview
|
||||||
|
|
||||||
The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScript Modules)** and **TypeScript**.
|
The bot uses a **Modular Command & Event Loading** pattern with **ESM (ECMAScript Modules)** and **TypeScript**, emphasizing strict typing and performance.
|
||||||
|
|
||||||
### Key Design Patterns:
|
### Key Design Patterns:
|
||||||
1. **Extended Client Pattern:** Extend `discord.js` `Client` class to hold global state.
|
1. **Extended Client Pattern:** Extend `discord.js` `Client` class to hold global state and a typed `DB` instance.
|
||||||
2. **Dynamic Discovery:** `index.ts` uses `readdirSync` and dynamic `import()` to register commands/events.
|
2. **Dynamic Discovery:** `index.ts` uses `readdirSync` and dynamic `import()` to register commands/events.
|
||||||
3. **Interface-Driven Commands:** All commands implement `Command` interface.
|
3. **Automatic Command Deployment:** Commands are automatically registered with Discord on startup via `Deployer.deployCommands()`, unless disabled via `AUTO_DEPLOY=false`.
|
||||||
4. **SQLite Database:** Persistent data storage with `better-sqlite3`.
|
4. **Interface-Driven Commands:** All commands implement the `Command` interface.
|
||||||
5. **Twitch Monitoring:** Periodic API polling with Stream ID tracking.
|
5. **Optimized SQLite Database:** Persistent storage with `better-sqlite3`, utilizing **WAL (Write-Ahead Logging)**, **Foreign Key support**, and **surgical In-Memory Caching** (clearing specific guild data on updates).
|
||||||
6. **Kick Monitoring:** Periodic API polling with `@nekiro/kick-api` library.
|
6. **Twitch Monitoring:** Periodic API polling using **Batch-Requests** (100 channels per request) with Stream ID tracking and **database transactions** for status updates.
|
||||||
7. **Reminder System:** Periodic reminder checking with target_time tracking.
|
7. **TwitchMonitor IRC:** TMI.js-based IRC chat monitoring for mod events, using **reusable WebhookClient instances** for efficiency.
|
||||||
8. **Auto-Response System:** Trigger word detection for automatic replies.
|
8. **Reminder System:** Periodic reminder checking with `target_time` tracking.
|
||||||
9. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
|
9. **Auto-Response System:** Trigger word detection for automatic replies (cached).
|
||||||
10. **Logging System:** Configurable event logging (messages, roles, moderation, etc.).
|
10. **Welcome/Goodbye System:** Guild member add/remove events with customizable messages.
|
||||||
11. **Grouped Commands:** Admin/Owner/Trigger/Timer commands grouped under subcommands.
|
11. **Logging System:** Configurable event logging (messages, roles, moderation, etc.).
|
||||||
|
12. **Role Selection System:** Self-service role assignment via select menus with support for exclusive categories and max-role limits.
|
||||||
|
|
||||||
## 📁 Project Structure
|
## 📁 Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
pixelpoebel/
|
pixelpoebel/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── index.ts # Entry point, loads commands/events
|
│ ├── index.ts # Entry point, loads commands/events/auto-deploy
|
||||||
│ ├── deploy-commands.ts # Deploy script for Discord
|
│ ├── deploy-commands.ts # Manual deploy script for Discord
|
||||||
│ ├── commands/
|
│ ├── commands/
|
||||||
│ │ └── utility/ # All commands grouped here
|
│ │ └── utility/ # All commands grouped here
|
||||||
│ │ ├── ping.ts
|
|
||||||
│ │ ├── help.ts
|
|
||||||
│ │ ├── twitch.ts
|
|
||||||
│ │ ├── kickstream.ts # Kick streaming
|
|
||||||
│ │ ├── trigger.ts # Auto-responses
|
|
||||||
│ │ ├── timer.ts # Reminders
|
|
||||||
│ │ ├── welcome.ts # Welcome/Goodbye messages
|
|
||||||
│ │ ├── log.ts # Event logging
|
|
||||||
│ │ ├── admin.ts # All admin subcommands
|
|
||||||
│ │ └── owner.ts # All owner subcommands
|
|
||||||
│ ├── events/
|
│ ├── events/
|
||||||
│ │ ├── ready.ts
|
│ │ ├── ... # All Discord event handlers
|
||||||
│ │ ├── 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/
|
│ └── structures/
|
||||||
│ ├── ExtendedClient.ts
|
│ ├── ExtendedClient.ts # Typed client with DB property
|
||||||
│ ├── Command.ts
|
│ ├── Command.ts
|
||||||
│ ├── Database.ts
|
│ ├── Database.ts # Database with Transaction & Surgical Caching
|
||||||
│ ├── TwitchManager.ts
|
│ ├── TwitchManager.ts # Batch Polling & Transaction logic
|
||||||
│ ├── KickManager.ts
|
│ ├── TwitchMonitor.ts # IRC Monitoring & Webhook reuse
|
||||||
|
│ ├── TwitchCache.ts # IRC Message FIFO Cache
|
||||||
│ ├── ReminderManager.ts
|
│ ├── ReminderManager.ts
|
||||||
│ └── Deployer.ts
|
│ └── Deployer.ts # Centralized deployment logic
|
||||||
├── data/ # SQLite database (volume mounted)
|
├── data/ # SQLite database (volume mounted)
|
||||||
├── package.json
|
├── package.json
|
||||||
├── tsconfig.json
|
├── tsconfig.json
|
||||||
@@ -68,417 +51,99 @@ pixelpoebel/
|
|||||||
|
|
||||||
## 📝 Implementation Steps
|
## 📝 Implementation Steps
|
||||||
|
|
||||||
### 1. Project Setup
|
### 1. Database with Surgical Caching
|
||||||
|
|
||||||
```json
|
The database uses WAL mode and foreign keys. Caching is surgical (clears specific guild entries when possible).
|
||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "nodemon --watch 'src/**/*.ts' --exec 'node --loader ts-node/esm' src/index.ts",
|
|
||||||
"deploy": "node --loader ts-node/esm src/deploy-commands.ts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Database Structure
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- guild_settings
|
|
||||||
CREATE TABLE guild_settings (
|
|
||||||
guild_id TEXT PRIMARY KEY,
|
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
-- mod_logs
|
|
||||||
CREATE TABLE mod_logs (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
guild_id TEXT,
|
|
||||||
user_id TEXT,
|
|
||||||
moderator_id TEXT,
|
|
||||||
action TEXT,
|
|
||||||
reason TEXT,
|
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- command_stats
|
|
||||||
CREATE TABLE command_stats (
|
|
||||||
command_name TEXT PRIMARY KEY,
|
|
||||||
uses INTEGER DEFAULT 0
|
|
||||||
);
|
|
||||||
|
|
||||||
-- dm_users
|
|
||||||
CREATE TABLE dm_users (
|
|
||||||
user_id TEXT PRIMARY KEY,
|
|
||||||
username TEXT,
|
|
||||||
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- twitch_monitors
|
|
||||||
CREATE TABLE twitch_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)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 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,
|
|
||||||
guild_id TEXT,
|
|
||||||
trigger_word TEXT,
|
|
||||||
response_text TEXT,
|
|
||||||
UNIQUE(guild_id, trigger_word)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- reminders
|
|
||||||
CREATE TABLE reminders (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
guild_id TEXT,
|
|
||||||
channel_id TEXT,
|
|
||||||
user_id TEXT,
|
|
||||||
reminder_text TEXT,
|
|
||||||
target_time DATETIME,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Key Components
|
|
||||||
|
|
||||||
**ExtendedClient.ts:**
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Client, Collection, GatewayIntentBits } from 'discord.js';
|
// src/structures/Database.ts
|
||||||
import { Command } from './Command.js';
|
export class DB {
|
||||||
|
private static settingsCache = new Map<string, any>();
|
||||||
|
|
||||||
export class ExtendedClient extends Client {
|
static init() {
|
||||||
public commands: Collection<string, Command> = new Collection();
|
db.pragma('journal_mode = WAL');
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
// ... Table creation ...
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
static run(query: string, ...params: any[]) {
|
||||||
super({
|
const result = db.prepare(query).run(...params);
|
||||||
intents: [
|
if (query.toLowerCase().includes('guild_settings')) {
|
||||||
GatewayIntentBits.Guilds,
|
const guildId = params.find(p => typeof p === 'string' && /^\d{17,20}$/.test(p));
|
||||||
GatewayIntentBits.GuildMessages,
|
if (guildId) this.settingsCache.delete(guildId);
|
||||||
GatewayIntentBits.MessageContent,
|
else this.settingsCache.clear();
|
||||||
GatewayIntentBits.GuildMembers,
|
}
|
||||||
],
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static transaction<T>(fn: () => T): T {
|
||||||
|
return db.transaction(fn)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Twitch Batch Polling with Transactions
|
||||||
|
|
||||||
|
Updates are collected and saved in a single transaction to reduce disk I/O.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/structures/TwitchManager.ts
|
||||||
|
static async checkStreams(client: ExtendedClient) {
|
||||||
|
// ... fetch streams ...
|
||||||
|
const updates = []; // collect updates here
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
client.DB.transaction(() => {
|
||||||
|
for (const update of updates) {
|
||||||
|
// ... client.DB.run update ...
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Command Interface:**
|
### 3. Webhook Reuse in TwitchMonitor
|
||||||
```typescript
|
|
||||||
export interface Command {
|
|
||||||
data: any;
|
|
||||||
category: 'Owner' | 'Admin' | 'Public';
|
|
||||||
execute: (interaction: any, client: any) => Promise<void> | void;
|
|
||||||
cooldown?: number;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Timer System
|
Avoid recreating `WebhookClient` instances for every message to improve performance.
|
||||||
|
|
||||||
**Time Parsing:**
|
|
||||||
```typescript
|
|
||||||
// Duration parsing: "30s", "2h", "1d30m", "3wochen"
|
|
||||||
function parseDuration(input: string): number | null {
|
|
||||||
const unitMs: Record<string, number> = {
|
|
||||||
's': 1000, 'sec': 1000, 'sek': 1000, 'sekunden': 1000,
|
|
||||||
'm': 60000, 'min': 60000, 'minuten': 60000,
|
|
||||||
'h': 3600000, 'hour': 3600000, 'std': 3600000, 'stunden': 3600000,
|
|
||||||
'd': 86400000, 'day': 86400000, 't': 86400000, 'tage': 86400000,
|
|
||||||
'w': 604800000, 'week': 604800000, 'wochen': 604800000,
|
|
||||||
'mo': 2592000000, 'month': 2592000000, 'monat': 2592000000, 'monate': 2592000000,
|
|
||||||
'y': 31536000000, 'year': 31536000000, 'j': 31536000000, 'jahr': 31536000000, 'jahre': 31536000000
|
|
||||||
};
|
|
||||||
// Parse and calculate total milliseconds
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date parsing: "25.12.2024 14:30", "morgen 15:00"
|
|
||||||
function parseDateTime(input: string): Date | null {
|
|
||||||
// German date format, ISO format, or relative "tomorrow"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reminder Polling:**
|
|
||||||
```typescript
|
|
||||||
export class ReminderManager {
|
|
||||||
static async checkReminders(client: any) {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const dueReminders = DB.all('SELECT * FROM reminders WHERE target_time <= ?', now);
|
|
||||||
|
|
||||||
for (const reminder of dueReminders) {
|
|
||||||
const guild = client.guilds.cache.get(reminder.guild_id);
|
|
||||||
const channel = guild.channels.cache.get(reminder.channel_id);
|
|
||||||
// Send notification and delete reminder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static startPolling(client: any) {
|
|
||||||
setInterval(() => this.checkReminders(client), 30 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 (?, ?, ?)',
|
|
||||||
guildId, triggerWord.toLowerCase(), responseText);
|
|
||||||
|
|
||||||
// List triggers (public)
|
|
||||||
DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word ASC', guildId);
|
|
||||||
|
|
||||||
// Remove trigger (admin only)
|
|
||||||
DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. Twitch Monitoring with Stream ID
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Stream ID tracking prevents duplicate notifications
|
// src/structures/TwitchMonitor.ts
|
||||||
const isNewStream = monitor.last_stream_id !== stream.id;
|
private async sendWebhook(webhookUrl: string, data: any, monitorKey?: string) {
|
||||||
const wasOffline = monitor.last_status === 'offline';
|
let webhook = monitorKey ? this.webhookClients.get(monitorKey) : new WebhookClient({ url: webhookUrl });
|
||||||
|
await webhook.send({ embeds: [embed] });
|
||||||
if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
|
||||||
// Send notification
|
|
||||||
DB.run('UPDATE twitch_monitors SET last_status = "online", last_stream_id = ? WHERE id = ?',
|
|
||||||
stream.id, monitor.id);
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 9. Kick Monitoring with @nekiro/kick-api
|
### 4. Automatic Deployment
|
||||||
|
|
||||||
|
Commands are loaded into an array and deployed during the `index.ts` startup sequence.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Uses @nekiro/kick-api library for OAuth 2.1 authentication
|
// src/index.ts
|
||||||
import { client as KickClient } from '@nekiro/kick-api';
|
const commandData = [];
|
||||||
|
// ... while loading commands ...
|
||||||
|
commandData.push(command.data.toJSON());
|
||||||
|
|
||||||
const kickClient = new KickClient({
|
if (process.env.AUTO_DEPLOY !== 'false') {
|
||||||
clientId: process.env.KICK_CLIENT_ID,
|
await Deployer.deployCommands(process.env.CLIENT_ID, process.env.DISCORD_TOKEN, commandData);
|
||||||
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:**
|
## 🚀 Performance Benchmarks (approx.)
|
||||||
- `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
|
| Metric | Old (Individual) | New (Batch/Cached/Transaction) |
|
||||||
|
|--------|------------------|-------------------------------|
|
||||||
### 10. Configurable Warn System
|
| API Calls (500 channels) | 500 | 5 |
|
||||||
|
| DB Access (Polling) | Individual writes | Single Transaction |
|
||||||
**Database Schema:**
|
| Webhook Overhead | New Connection | Connection Reuse |
|
||||||
- `warn_threshold`: Number of warns before action
|
| Notification Delay | ~5-7 min | ~2 min |
|
||||||
- `warn_action`: 'none', 'kick', 'mute', 'ban'
|
|
||||||
- `warn_mute_duration`: Duration in seconds for mute
|
|
||||||
|
|
||||||
**Auto-Action Logic:**
|
|
||||||
```typescript
|
|
||||||
const warnCount = countResult?.count ?? 0;
|
|
||||||
const warnThreshold = settings?.warn_threshold ?? 3;
|
|
||||||
const warnAction = settings?.warn_action ?? 'ban';
|
|
||||||
|
|
||||||
if (warnCount >= warnThreshold) {
|
|
||||||
if (warnAction === 'kick') await target.kick(...);
|
|
||||||
else if (warnAction === 'mute') await target.timeout(duration, ...);
|
|
||||||
else if (warnAction === 'ban') await target.ban(...);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10. Mute with Multiple Time Units
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const unitSeconds: Record<string, number> = {
|
|
||||||
sec: 1,
|
|
||||||
min: 60,
|
|
||||||
hour: 60 * 60,
|
|
||||||
day: 60 * 60 * 24,
|
|
||||||
week: 60 * 60 * 24 * 7,
|
|
||||||
month: 60 * 60 * 24 * 30,
|
|
||||||
year: 60 * 60 * 24 * 365
|
|
||||||
};
|
|
||||||
|
|
||||||
if (unit === 'perm') {
|
|
||||||
durationMs = null; // Permanent
|
|
||||||
} else {
|
|
||||||
durationMs = duration * unitSeconds[unit!] * 1000;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
# Build
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Deploy Commands
|
|
||||||
npm run deploy
|
|
||||||
|
|
||||||
# Run
|
|
||||||
npm start
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ Critical Constraints
|
## ⚠️ Critical Constraints
|
||||||
|
|
||||||
1. **Environment Variables:** Always use `process.env`, never hardcode.
|
1. **Transaction Safety:** Never call async functions (like `fetch` or `channel.send`) inside a synchronous `db.transaction()`.
|
||||||
2. **Type Safety:** Use `any` for command `data` and `execute` parameters (compatibility).
|
2. **Foreign Keys:** Always enable `foreign_keys = ON` to maintain data integrity.
|
||||||
3. **Interaction States:** Check `interaction.replied` before replying.
|
3. **Type Safety:** Use `ExtendedClient` instead of `any` for the client instance.
|
||||||
4. **Twitch API:** Max 100 channels per request, batch properly.
|
4. **Webhook Reuse:** Map `WebhookClient` instances to their monitor keys to prevent leaks.
|
||||||
5. **Deploy:** Commands NOT auto-registered on start – use `/owner deploy`.
|
5. **Partial Message Handling:** In `messageDelete` and `messageUpdate` events, always check for null `author` property to avoid runtime crashes on uncached partial messages.
|
||||||
6. **Ephemeral Replies:** Use `flags: [MessageFlags.Ephemeral]` instead of deprecated `ephemeral: true`.
|
6. **Centralized Settings Cache:** Always use `client.DB.getSettings(guildId)` instead of raw SQLite select queries for `guild_settings` to leverage central caching and automatic default generation.
|
||||||
7. **Line Endings:** Use `.gitattributes` to ensure LF line endings for consistency.
|
7. **Category Role Selection Limits:** Do not count general guild roles when checking role selection limits; filter the user's role list using only the specific role IDs registered under that category.
|
||||||
|
|
||||||
## 📋 Command Summary
|
|
||||||
|
|
||||||
| Command | Subcommands | Permission |
|
|
||||||
|---------|-------------|------------|
|
|
||||||
| `/ping` | – | Public |
|
|
||||||
| `/help` | – | Public |
|
|
||||||
| `/twitch` | online, add, remove, list, listall, help | Public/Admin |
|
|
||||||
| `/kickstream` | online, add, remove, list, help | Public/Admin |
|
|
||||||
| `/trigger` | add, remove, list, help | Public/Admin |
|
|
||||||
| `/timer` | add, remove, list, listall, help | Public/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 |
|
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -15,9 +15,22 @@ Erstelle `.env` Datei:
|
|||||||
DISCORD_TOKEN=dein_bot_token
|
DISCORD_TOKEN=dein_bot_token
|
||||||
CLIENT_ID=deine_client_id
|
CLIENT_ID=deine_client_id
|
||||||
TWITCH_CLIENT_ID=deine_twitch_id
|
TWITCH_CLIENT_ID=deine_twitch_id
|
||||||
TWITCH_CLIENT_SECRET=dein_twitch_secret
|
TWITCH_CLIENT_SECRET=deine_twitch_secret
|
||||||
|
TWITCH_USERNAME=dein_twitch_username
|
||||||
|
TWITCH_OAUTH_TOKEN=oauth:xxxxxxxxxxxxxxxxxx
|
||||||
|
BOT_OWNER_ID=deine_discord_user_id
|
||||||
|
|
||||||
|
# Optional: Automatische Slash-Command Registrierung bei Bot-Start (default: true)
|
||||||
|
AUTO_DEPLOY=true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TwitchMonitor Setup
|
||||||
|
1. **Twitch OAuth Token holen:** https://chatterino.com/client_login
|
||||||
|
2. **Username:** Dein Twitch Username
|
||||||
|
3. **Bot Owner ID:** Deine Discord User ID (für Fehlerbenachrichtigungen)
|
||||||
|
|
||||||
|
> ⚠️ **Hinweis:** Der Twitch OAuth Token kann ungültig werden. In diesem Fall wirst du automatisch per Discord DM benachrichtigt.
|
||||||
|
|
||||||
## 🚀 Start
|
## 🚀 Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -47,6 +60,9 @@ docker-compose up -d --build --force-recreate
|
|||||||
|
|
||||||
## 📋 Commands registrieren
|
## 📋 Commands registrieren
|
||||||
|
|
||||||
|
Befehle werden standardmäßig **automatisch bei jedem Bot-Start** registriert. Du kannst dies über `AUTO_DEPLOY=false` in der `.env` deaktivieren.
|
||||||
|
|
||||||
|
Für manuelle Updates oder im Notfall:
|
||||||
```bash
|
```bash
|
||||||
npm run deploy
|
npm run deploy
|
||||||
```
|
```
|
||||||
@@ -85,6 +101,21 @@ npm run deploy
|
|||||||
- `/twitch listall` – Alle überwachten Streamer (Mod)
|
- `/twitch listall` – Alle überwachten Streamer (Mod)
|
||||||
- `/twitch help` – Twitch-Hilfe
|
- `/twitch help` – Twitch-Hilfe
|
||||||
|
|
||||||
|
### TwitchMonitor (IRC Chat Logging)
|
||||||
|
- `/twitchmonitor add <twitch> <discord>` – Twitch-Kanal zum IRC-Monitoring hinzufügen (Mod)
|
||||||
|
- `/twitchmonitor remove <twitch>` – Kanal vom Monitoring entfernen (Mod)
|
||||||
|
- `/twitchmonitor list` – Alle überwachten Twitch-Kanäle
|
||||||
|
- `/twitchmonitor help` – Hilfe anzeigen
|
||||||
|
|
||||||
|
### Rollen-Auswahl
|
||||||
|
- `/rolesetup create <#kanal> <name>` – Rollen-Auswahl Message erstellen
|
||||||
|
- `/rolesetup add <kategorie> <@rolle> [emoji]` – Rolle zur Auswahl hinzufügen
|
||||||
|
- `/rolesetup edit <kategorie> <rolle> [neues_emoji]` – Emoji ändern
|
||||||
|
- `/rolesetup remove <kategorie> <rolle>` – Rolle entfernen
|
||||||
|
- `/rolesetup delete <kategorie>` – Kategorie löschen
|
||||||
|
- `/rolesetup config <kategorie> max:1 exclusive:true` – Einstellungen
|
||||||
|
- `/rolesetup list` – Alle Kategorien anzeigen
|
||||||
|
|
||||||
### Admin
|
### Admin
|
||||||
- `/admin kick <user>` – Nutzer kicken
|
- `/admin kick <user>` – Nutzer kicken
|
||||||
- `/admin warn <user> <reason>` – Nutzer verwarnen
|
- `/admin warn <user> <reason>` – Nutzer verwarnen
|
||||||
@@ -106,16 +137,14 @@ npm run deploy
|
|||||||
- `/owner servers` – Alle Server
|
- `/owner servers` – Alle Server
|
||||||
- `/owner help` – Owner-Hilfe
|
- `/owner help` – Owner-Hilfe
|
||||||
|
|
||||||
## 📡 Features
|
## 📡 Features & Optimierungen
|
||||||
|
|
||||||
- ✅ Modular Architecture
|
- ✅ **Modular Architecture:** TypeScript-basiert, ESM-Unterstützung, strikte Typisierung.
|
||||||
- ✅ TypeScript
|
- ✅ **High Performance Database:** SQLite mit **WAL-Modus**, **Fremdschlüssel-Unterstützung** und **chirurgischem In-Memory Caching** für Guild-Settings und Trigger.
|
||||||
- ✅ SQLite Database
|
- ✅ **Optimized Twitch Polling:** Nutzt **Batch-Requests** (100 Kanäle pro Request), Datenbank-Transaktionen und ein 2-Minuten-Intervall für zeitnahe Benachrichtigungen.
|
||||||
- ✅ Twitch Monitoring
|
- ✅ **TwitchMonitor IRC:** Echtzeit IRC Chat-Logging (Mod-Events) via **wiederverwendbaren Webhooks** und FIFO-Cache.
|
||||||
- ✅ Moderation Tools
|
- ✅ **Advanced Moderation:** Warn-System mit Auto-Actions, umfangreiches Purge-System.
|
||||||
- ✅ Konfigurierbares Warn-System
|
- ✅ **Role Selection:** Self-Service Rollen-System über Discord Select Menus.
|
||||||
- ✅ Auto-Antworten (Trigger)
|
- ✅ **Reminders:** Intelligentes Zeit-Parsing (z.B. "morgen 15:00", "30min").
|
||||||
- ✅ Timer & Erinnerungen
|
- ✅ **Event Logging:** Detaillierte Erfassung von Server-Ereignissen.
|
||||||
- ✅ Welcome/Goodbye Messages
|
- ✅ **Docker Support:** Containerisiertes Deployment inklusive Volume-Mounts für Daten.
|
||||||
- ✅ Event Logging
|
|
||||||
- ✅ Docker Support
|
|
||||||
|
|||||||
67
package-lock.json
generated
67
package-lock.json
generated
@@ -9,15 +9,16 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nekiro/kick-api": "^2.1.2",
|
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"tmi.js": "^1.8.5",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.9",
|
||||||
|
"@types/tmi.js": "^1.8.6",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"ts-node": "^10.9.2"
|
"ts-node": "^10.9.2"
|
||||||
}
|
}
|
||||||
@@ -193,14 +194,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@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": {
|
"node_modules/@sapphire/async-queue": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||||
@@ -281,6 +274,12 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/tmi.js": {
|
||||||
|
"version": "1.8.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tmi.js/-/tmi.js-1.8.6.tgz",
|
||||||
|
"integrity": "sha512-LVzNK7AxTMyh9qHLanAQR1o0I9XzfbIcXk85cx85igmCJHHO1Sm71sdhQ8Mj1WmRGzynPIoCXx6mVaFynWbsQw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -892,6 +891,25 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.14",
|
"version": "3.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||||
@@ -1188,6 +1206,18 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tmi.js": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmi.js/-/tmi.js-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-A9qrydfe1e0VWM9MViVhhxVgvLpnk7pFShVUWePsSTtoi+A1X+Zjdoa7OJd7/YsgHXGj3GkNEvnWop/1WwZuew==",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"ws": "^8.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
@@ -1211,6 +1241,11 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||||
|
},
|
||||||
"node_modules/ts-mixer": {
|
"node_modules/ts-mixer": {
|
||||||
"version": "6.0.4",
|
"version": "6.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||||
@@ -1327,6 +1362,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrappy": {
|
"node_modules/wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|||||||
@@ -19,15 +19,16 @@
|
|||||||
"author": "sarah",
|
"author": "sarah",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nekiro/kick-api": "^2.1.2",
|
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.18.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"tmi.js": "^1.8.5",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/node": "^22.13.9",
|
"@types/node": "^22.13.9",
|
||||||
|
"@types/tmi.js": "^1.8.6",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"ts-node": "^10.9.2"
|
"ts-node": "^10.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, GuildMember } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
|
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||||
|
|
||||||
async function sendModerationLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
async function sendModerationLog(client: ExtendedClient, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
const DB = client.DB;
|
const DB = client.DB;
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
if (!settings) return;
|
||||||
|
|
||||||
if (!settings?.log_events) return;
|
if (!settings.log_events) return;
|
||||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!activeEvents.includes(event)) return;
|
if (!activeEvents.includes(event)) return;
|
||||||
|
|
||||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
if (!settings.log_channel) return;
|
||||||
if (!logSettings?.log_channel) return;
|
|
||||||
|
|
||||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
if (!channel || !channel.isTextBased()) return;
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -193,8 +194,9 @@ const command: Command = {
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
|
||||||
category: 'Admin',
|
category: 'Admin',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
|
const client = interaction.client as ExtendedClient;
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const DB = interaction.client.DB;
|
const DB = client.DB;
|
||||||
const guild = interaction.guild;
|
const guild = interaction.guild;
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
|
|
||||||
@@ -220,28 +222,24 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create guild settings
|
// Get or create guild settings
|
||||||
let settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
if (!settings) {
|
|
||||||
DB.run('INSERT INTO guild_settings (guild_id) VALUES (?)', guildId);
|
|
||||||
settings = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (subcommand === 'kick') {
|
if (subcommand === 'kick') {
|
||||||
const target = interaction.options.getMember('target') as GuildMember;
|
const target = interaction.options.getMember('target') as GuildMember;
|
||||||
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target.kickable) {
|
if (!target.kickable) {
|
||||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht kicken.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +259,7 @@ const command: Command = {
|
|||||||
await sendModerationLog(interaction.client, guildId, 'kicks', embed);
|
await sendModerationLog(interaction.client, guildId, 'kicks', embed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Kicken.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Fehler beim Kicken.', ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -271,7 +269,7 @@ const command: Command = {
|
|||||||
const reason = interaction.options.getString('reason')!;
|
const reason = interaction.options.getString('reason')!;
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +338,7 @@ const command: Command = {
|
|||||||
const target = interaction.options.getMember('target') as GuildMember;
|
const target = interaction.options.getMember('target') as GuildMember;
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +350,7 @@ const command: Command = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!latestWarn) {
|
if (!latestWarn) {
|
||||||
await interaction.reply({ content: 'Keine Warnung für diesen Nutzer gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Warnung für diesen Nutzer gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +366,7 @@ const command: Command = {
|
|||||||
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
const reason = interaction.options.getString('reason') ?? 'Kein Grund';
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,7 +390,7 @@ const command: Command = {
|
|||||||
|
|
||||||
const multiplier = unitSeconds[unit!];
|
const multiplier = unitSeconds[unit!];
|
||||||
if (!multiplier) {
|
if (!multiplier) {
|
||||||
await interaction.reply({ content: '❌ Ungültige Zeiteinheit.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Ungültige Zeiteinheit.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +408,7 @@ const command: Command = {
|
|||||||
};
|
};
|
||||||
durationText = `${duration} ${unitNames[unit!]}`;
|
durationText = `${duration} ${unitNames[unit!]}`;
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Dauer oder Zeiteinheit angeben.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +429,7 @@ const command: Command = {
|
|||||||
await sendModerationLog(interaction.client, guildId, 'mutes', embed);
|
await sendModerationLog(interaction.client, guildId, 'mutes', embed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Muten.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Fehler beim Muten.', ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -440,7 +438,7 @@ const command: Command = {
|
|||||||
const target = interaction.options.getMember('target') as GuildMember;
|
const target = interaction.options.getMember('target') as GuildMember;
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,7 +447,7 @@ const command: Command = {
|
|||||||
await interaction.reply(`✅ Entmutet: ${target.user.tag}`);
|
await interaction.reply(`✅ Entmutet: ${target.user.tag}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Entmuten.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Fehler beim Entmuten.', ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -460,12 +458,12 @@ const command: Command = {
|
|||||||
const deleteDays = interaction.options.getInteger('delete_days') ?? 0;
|
const deleteDays = interaction.options.getInteger('delete_days') ?? 0;
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
await interaction.reply({ content: 'Nutzer nicht gefunden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Nutzer nicht gefunden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target.bannable) {
|
if (!target.bannable) {
|
||||||
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Ich kann diesen Nutzer nicht bannen.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +484,7 @@ const command: Command = {
|
|||||||
await sendModerationLog(interaction.client, guildId, 'bans', embed);
|
await sendModerationLog(interaction.client, guildId, 'bans', embed);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: '❌ Fehler beim Bannen.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Fehler beim Bannen.', ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -503,7 +501,7 @@ const command: Command = {
|
|||||||
await interaction.reply(`✅ Entbannt: ${targetId}`);
|
await interaction.reply(`✅ Entbannt: ${targetId}`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
await interaction.reply({ content: `❌ Fehler beim Entbannen: ${error.message}`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `❌ Fehler beim Entbannen: ${error.message}`, ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -523,14 +521,14 @@ const command: Command = {
|
|||||||
{ name: 'Mute-Dauer', value: `${(settings?.warn_mute_duration ?? 1800) / 60} Min`, inline: true }
|
{ name: 'Mute-Dauer', value: `${(settings?.warn_mute_duration ?? 1800) / 60} Min`, inline: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setting === 'warn_threshold') {
|
if (setting === 'warn_threshold') {
|
||||||
const threshold = parseInt(value!);
|
const threshold = parseInt(value!);
|
||||||
if (isNaN(threshold) || threshold < 1) {
|
if (isNaN(threshold) || threshold < 1) {
|
||||||
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Ungültiger Wert. Mindestens 1.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId);
|
DB.run('UPDATE guild_settings SET warn_threshold = ? WHERE guild_id = ?', threshold, guildId);
|
||||||
@@ -541,7 +539,7 @@ const command: Command = {
|
|||||||
if (setting === 'warn_action') {
|
if (setting === 'warn_action') {
|
||||||
const validActions = ['none', 'kick', 'mute', 'ban'];
|
const validActions = ['none', 'kick', 'mute', 'ban'];
|
||||||
if (!validActions.includes(value!)) {
|
if (!validActions.includes(value!)) {
|
||||||
await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `❌ Ungültige Aktion. Möglich: ${validActions.join(', ')}`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId);
|
DB.run('UPDATE guild_settings SET warn_action = ? WHERE guild_id = ?', value, guildId);
|
||||||
@@ -552,7 +550,7 @@ const command: Command = {
|
|||||||
if (setting === 'warn_mute_duration') {
|
if (setting === 'warn_mute_duration') {
|
||||||
const duration = durationValue ?? 1800;
|
const duration = durationValue ?? 1800;
|
||||||
if (duration < 0) {
|
if (duration < 0) {
|
||||||
await interaction.reply({ content: '❌ Ungültige Dauer.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Ungültige Dauer.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId);
|
DB.run('UPDATE guild_settings SET warn_mute_duration = ? WHERE guild_id = ?', duration, guildId);
|
||||||
@@ -568,24 +566,24 @@ const command: Command = {
|
|||||||
const channel = interaction.channel;
|
const channel = interaction.channel;
|
||||||
|
|
||||||
if (!channel || !channel.isTextBased()) {
|
if (!channel || !channel.isTextBased()) {
|
||||||
await interaction.reply({ content: '❌ Dieser Befehl kann nur in Text-Channels verwendet werden.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Dieser Befehl kann nur in Text-Channels verwendet werden.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer reply since bulk delete can take time
|
// Defer reply since bulk delete can take time
|
||||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
let deletedCount = 0;
|
let deletedCount = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (purgeSubcommand === 'amount') {
|
if (purgeSubcommand === 'amount') {
|
||||||
const count = interaction.options.getInteger('count')!;
|
const count = interaction.options.getInteger('count')!;
|
||||||
const messages = await channel.messages.fetch({ limit: count + 1 }); // +1 for the command message
|
const messages = await channel.messages.fetch({ limit: count });
|
||||||
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
const twoWeeksAgo = Date.now() - 14 * 24 * 60 * 60 * 1000;
|
||||||
const filtered = messages.filter((m: any) => m.createdTimestamp > twoWeeksAgo);
|
const filtered = messages.filter((m: any) => m.createdTimestamp > twoWeeksAgo);
|
||||||
|
|
||||||
await (channel as any).bulkDelete(filtered, true);
|
await (channel as any).bulkDelete(filtered, true);
|
||||||
deletedCount = filtered.size - 1; // -1 for command message
|
deletedCount = filtered.size;
|
||||||
|
|
||||||
await interaction.editReply(`✅ ${deletedCount} Nachrichten gelöscht.`);
|
await interaction.editReply(`✅ ${deletedCount} Nachrichten gelöscht.`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
@@ -52,7 +52,7 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
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('kickstream')
|
|
||||||
.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: '`/kickstream add <channel> <discord_channel> [nachricht]`', value: 'Fügt einen Streamer hinzu (Mod).', inline: false },
|
|
||||||
{ name: '`/kickstream remove <channel>`', value: 'Entfernt einen Streamer (Mod).', inline: false },
|
|
||||||
{ name: '`/kickstream list`', value: 'Zeigt alle überwachten Streamer.', inline: false },
|
|
||||||
{ name: '`/kickstream 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;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
|
|
||||||
const AVAILABLE_EVENTS = [
|
const AVAILABLE_EVENTS = [
|
||||||
@@ -58,8 +58,6 @@ const command: Command = {
|
|||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
// Ensure guild settings exist
|
|
||||||
DB.run('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)', guildId);
|
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
@@ -75,13 +73,13 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin only for all commands
|
// Admin only for all commands
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,12 +92,12 @@ const command: Command = {
|
|||||||
guildId
|
guildId
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Log-Kanal auf ${channel} gesetzt.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Log-Kanal auf ${channel} gesetzt.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'list') {
|
if (subcommand === 'list') {
|
||||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
const channel = settings?.log_channel
|
const channel = settings?.log_channel
|
||||||
? `<#${settings.log_channel}>`
|
? `<#${settings.log_channel}>`
|
||||||
@@ -122,18 +120,18 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'enable') {
|
if (subcommand === 'enable') {
|
||||||
const event = interaction.options.getString('event')!;
|
const event = interaction.options.getString('event')!;
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
const currentEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
const currentEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
||||||
|
|
||||||
if (currentEvents.includes(event)) {
|
if (currentEvents.includes(event)) {
|
||||||
await interaction.reply({ content: `ℹ️ Event **${event}** ist bereits aktiviert.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `ℹ️ Event **${event}** ist bereits aktiviert.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,18 +143,18 @@ const command: Command = {
|
|||||||
guildId
|
guildId
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Event **${event}** aktiviert.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Event **${event}** aktiviert.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'disable') {
|
if (subcommand === 'disable') {
|
||||||
const event = interaction.options.getString('event')!;
|
const event = interaction.options.getString('event')!;
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
const currentEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
const currentEvents = (settings?.log_events || '').split(',').filter(Boolean);
|
||||||
|
|
||||||
if (!currentEvents.includes(event)) {
|
if (!currentEvents.includes(event)) {
|
||||||
await interaction.reply({ content: `ℹ️ Event **${event}** ist nicht aktiviert.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `ℹ️ Event **${event}** ist nicht aktiviert.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +166,7 @@ const command: Command = {
|
|||||||
guildId
|
guildId
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Event **${event}** deaktiviert.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Event **${event}** deaktiviert.`, ephemeral: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
import { Deployer } from '../../structures/Deployer.js';
|
import { Deployer } from '../../structures/Deployer.js';
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ const command: Command = {
|
|||||||
if (!client.application?.owner) await client.application?.fetch();
|
if (!client.application?.owner) await client.application?.fetch();
|
||||||
|
|
||||||
if (interaction.user.id !== client.application?.owner?.id) {
|
if (interaction.user.id !== client.application?.owner?.id) {
|
||||||
await interaction.reply({ content: 'Keine Berechtigung.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Berechtigung.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +46,12 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'deploy') {
|
if (subcommand === 'deploy') {
|
||||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const count = await Deployer.deploy(client.user!.id, client.token!);
|
const count = await Deployer.deploy(client.user!.id, client.token!);
|
||||||
@@ -64,7 +64,7 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'stats') {
|
if (subcommand === 'stats') {
|
||||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
const uptime = process.uptime();
|
const uptime = process.uptime();
|
||||||
const days = Math.floor(uptime / 86400);
|
const days = Math.floor(uptime / 86400);
|
||||||
@@ -89,7 +89,7 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'servers') {
|
if (subcommand === 'servers') {
|
||||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
const guilds = client.guilds.cache.map((guild: any) =>
|
const guilds = client.guilds.cache.map((guild: any) =>
|
||||||
`• **${guild.name}** \`(${guild.id})\` - ${guild.memberCount} Mitglieder`
|
`• **${guild.name}** \`(${guild.id})\` - ${guild.memberCount} Mitglieder`
|
||||||
@@ -117,7 +117,7 @@ const command: Command = {
|
|||||||
const nextEmbed = new EmbedBuilder()
|
const nextEmbed = new EmbedBuilder()
|
||||||
.setColor(0xf1c40f)
|
.setColor(0xf1c40f)
|
||||||
.setDescription(chunks[i].substring(0, 4000));
|
.setDescription(chunks[i].substring(0, 4000));
|
||||||
await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] });
|
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
416
src/commands/utility/rolesetup.ts
Normal file
416
src/commands/utility/rolesetup.ts
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, ChannelType, ComponentType, StringSelectMenuBuilder, StringSelectMenuOptionBuilder } from 'discord.js';
|
||||||
|
import { Command } from '../../structures/Command.js';
|
||||||
|
|
||||||
|
export async function updateRoleMessage(client: any, category: any) {
|
||||||
|
try {
|
||||||
|
const channel = client.guilds.cache.get(category.guild_id)?.channels.cache.get(category.channel_id);
|
||||||
|
if (!channel) return;
|
||||||
|
|
||||||
|
const message = await channel.messages.fetch(category.message_id);
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||||
|
|
||||||
|
if (options.length === 0) {
|
||||||
|
await message.edit({
|
||||||
|
content: `**${category.category_name}**\nWähle unten eine Rolle aus:\n\n*Noch keine Rollen konfiguriert.*`,
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleOptions = options.map((opt: any) =>
|
||||||
|
new StringSelectMenuOptionBuilder()
|
||||||
|
.setLabel(opt.label.replace(/^[^\s]+\s/, ''))
|
||||||
|
.setValue(opt.role_id)
|
||||||
|
.setEmoji(opt.emoji || '')
|
||||||
|
);
|
||||||
|
|
||||||
|
roleOptions.push(
|
||||||
|
new StringSelectMenuOptionBuilder()
|
||||||
|
.setLabel('Keine')
|
||||||
|
.setValue(`remove_${category.id}`)
|
||||||
|
.setEmoji('❌')
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectMenu = new StringSelectMenuBuilder()
|
||||||
|
.setCustomId(`role_select_${category.category_name}`)
|
||||||
|
.setPlaceholder('Wähle eine Rolle...')
|
||||||
|
.addOptions(roleOptions);
|
||||||
|
|
||||||
|
await message.edit({
|
||||||
|
content: `**${category.category_name}**\nWähle unten eine Rolle aus:\n*Klicke erneut auf eine Rolle um sie zu entfernen.*`,
|
||||||
|
components: [{ type: ComponentType.ActionRow, components: [selectMenu] }]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ROLESETUP] Error updating message:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const command: Command = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('rolesetup')
|
||||||
|
.setDescription('Rollen-Auswahl konfigurieren')
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('create')
|
||||||
|
.setDescription('Erstellt eine neue Rollen-Auswahl Message')
|
||||||
|
.addChannelOption(option =>
|
||||||
|
option.setName('channel')
|
||||||
|
.setDescription('Der Kanal für die Rollen-Message')
|
||||||
|
.addChannelTypes(ChannelType.GuildText)
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('name')
|
||||||
|
.setDescription('Name der Kategorie (z.B. "Farben", "Spiele")')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('add')
|
||||||
|
.setDescription('Fügt eine neue Rolle zur Auswahl hinzu')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('kategorie')
|
||||||
|
.setDescription('Der Name der Kategorie')
|
||||||
|
.setRequired(true))
|
||||||
|
.addRoleOption(option =>
|
||||||
|
option.setName('rolle')
|
||||||
|
.setDescription('Die Discord-Rolle')
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('emoji')
|
||||||
|
.setDescription('Optional: Emoji für die Option')
|
||||||
|
.setRequired(false)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('edit')
|
||||||
|
.setDescription('Ändert eine bestehende Option')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('kategorie')
|
||||||
|
.setDescription('Der Name der Kategorie')
|
||||||
|
.setRequired(true))
|
||||||
|
.addRoleOption(option =>
|
||||||
|
option.setName('rolle')
|
||||||
|
.setDescription('Die Discord-Rolle zum Ändern')
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('neue_rolle')
|
||||||
|
.setDescription('Die neue Discord-Rolle')
|
||||||
|
.setRequired(false))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('neues_emoji')
|
||||||
|
.setDescription('Das neue Emoji')
|
||||||
|
.setRequired(false)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('remove')
|
||||||
|
.setDescription('Entfernt eine Rolle von der Auswahl')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('kategorie')
|
||||||
|
.setDescription('Der Name der Kategorie')
|
||||||
|
.setRequired(true))
|
||||||
|
.addRoleOption(option =>
|
||||||
|
option.setName('rolle')
|
||||||
|
.setDescription('Die Discord-Rolle zum Entfernen')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('delete')
|
||||||
|
.setDescription('Löscht eine Kategorie und deren Message')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('kategorie')
|
||||||
|
.setDescription('Der Name der Kategorie')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('config')
|
||||||
|
.setDescription('Konfiguriert die Kategorie-Einstellungen')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('kategorie')
|
||||||
|
.setDescription('Der Name der Kategorie')
|
||||||
|
.setRequired(true))
|
||||||
|
.addIntegerOption(option =>
|
||||||
|
option.setName('max_rollen')
|
||||||
|
.setDescription('Maximale Rollen pro User (0 = unbegrenzt)')
|
||||||
|
.setRequired(false)
|
||||||
|
.setMinValue(0))
|
||||||
|
.addBooleanOption(option =>
|
||||||
|
option.setName('exklusiv')
|
||||||
|
.setDescription('Nur eine Rolle pro User erlaubt')
|
||||||
|
.setRequired(false))
|
||||||
|
.addBooleanOption(option =>
|
||||||
|
option.setName('entfernen_bei_loeschung')
|
||||||
|
.setDescription('Rolle von Usern entfernen wenn gelöscht')
|
||||||
|
.setRequired(false)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('list')
|
||||||
|
.setDescription('Listet alle Rollen-Kategorien auf')),
|
||||||
|
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 === 'list') {
|
||||||
|
const categories: any[] = DB.all('SELECT * FROM role_categories WHERE guild_id = ? ORDER BY category_name ASC', guildId);
|
||||||
|
|
||||||
|
if (categories.length === 0) {
|
||||||
|
await interaction.reply({ content: 'Keine Rollen-Kategorien konfiguriert.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('Rollen-Kategorien')
|
||||||
|
.setColor(0x3498db)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
const options: any[] = DB.all('SELECT * FROM role_options WHERE category_id = ?', cat.id);
|
||||||
|
const channel = interaction.guild?.channels.cache.get(cat.channel_id);
|
||||||
|
const configStr = [];
|
||||||
|
if (cat.max_roles > 0) configStr.push(`Max: ${cat.max_roles}`);
|
||||||
|
if (cat.exclusive) configStr.push('Exklusiv');
|
||||||
|
if (cat.remove_on_delete) configStr.push('Auto-Entfernen');
|
||||||
|
|
||||||
|
embed.addFields({
|
||||||
|
name: `${cat.category_name} ${configStr.length > 0 ? `(${configStr.join(', ')})` : ''}`,
|
||||||
|
value: `Kanal: ${channel?.toString() || '#' + cat.channel_id}\nOptionen: ${options.length}\nNachricht: ${cat.message_id !== 'pending' ? '[✅]' : '[⏳]'}`,
|
||||||
|
inline: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'create') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = interaction.options.getChannel('channel');
|
||||||
|
const categoryName = interaction.options.getString('name');
|
||||||
|
|
||||||
|
const existing = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, categoryName);
|
||||||
|
if (existing) {
|
||||||
|
await interaction.reply({ content: `Kategorie "${categoryName}" existiert bereits.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentMessage = await channel.send({
|
||||||
|
content: `**${categoryName}**\nWähle unten eine Rolle aus:\n\n*Noch keine Rollen konfiguriert. Füge Rollen mit "/rolesetup add" hinzu.*`
|
||||||
|
});
|
||||||
|
|
||||||
|
DB.run(
|
||||||
|
'INSERT INTO role_categories (guild_id, channel_id, message_id, category_name, max_roles, exclusive, remove_on_delete) VALUES (?, ?, ?, ?, 0, 0, 0)',
|
||||||
|
guildId,
|
||||||
|
channel.id,
|
||||||
|
sentMessage.id,
|
||||||
|
categoryName
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.reply({ content: `Kategorie "${categoryName}" erstellt in ${channel}.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'add') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kategorie = interaction.options.getString('kategorie');
|
||||||
|
const role = interaction.options.getRole('rolle');
|
||||||
|
const emoji = interaction.options.getString('emoji');
|
||||||
|
|
||||||
|
const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie);
|
||||||
|
if (!category) {
|
||||||
|
await interaction.reply({ content: `Kategorie "${kategorie}" nicht gefunden.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = DB.get('SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', category.id, role.id);
|
||||||
|
if (existing) {
|
||||||
|
await interaction.reply({ content: `Rolle ${role.name} ist bereits in "${kategorie}".`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = emoji ? `${emoji} ${role.name}` : role.name;
|
||||||
|
|
||||||
|
DB.run(
|
||||||
|
'INSERT INTO role_options (category_id, label, emoji, role_id) VALUES (?, ?, ?, ?)',
|
||||||
|
category.id,
|
||||||
|
label,
|
||||||
|
emoji || null,
|
||||||
|
role.id
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateRoleMessage(interaction.client, category);
|
||||||
|
await interaction.reply({ content: `${role.name} zu "${kategorie}" hinzugefügt.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'edit') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kategorie = interaction.options.getString('kategorie');
|
||||||
|
const oldRole = interaction.options.getRole('rolle');
|
||||||
|
const newRole = interaction.options.getRole('neue_rolle');
|
||||||
|
const newEmoji = interaction.options.getString('neues_emoji');
|
||||||
|
|
||||||
|
const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie);
|
||||||
|
if (!category) {
|
||||||
|
await interaction.reply({ content: `Kategorie "${kategorie}" nicht gefunden.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option: any = DB.get('SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', category.id, oldRole.id);
|
||||||
|
if (!option) {
|
||||||
|
await interaction.reply({ content: `Rolle ${oldRole.name} ist nicht in "${kategorie}".`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalEmoji = newEmoji !== null ? newEmoji : option.emoji;
|
||||||
|
const finalRoleId = newRole ? newRole.id : oldRole.id;
|
||||||
|
const finalLabel = finalEmoji ? `${finalEmoji} ${newRole ? newRole.name : oldRole.name}` : (newRole ? newRole.name : oldRole.name);
|
||||||
|
|
||||||
|
DB.run(
|
||||||
|
'UPDATE role_options SET label = ?, emoji = ?, role_id = ? WHERE id = ?',
|
||||||
|
finalLabel,
|
||||||
|
finalEmoji,
|
||||||
|
finalRoleId,
|
||||||
|
option.id
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateRoleMessage(interaction.client, category);
|
||||||
|
await interaction.reply({ content: `Option in "${kategorie}" aktualisiert.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'remove') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kategorie = interaction.options.getString('kategorie');
|
||||||
|
const role = interaction.options.getRole('rolle');
|
||||||
|
|
||||||
|
const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie);
|
||||||
|
if (!category) {
|
||||||
|
await interaction.reply({ content: `Kategorie "${kategorie}" nicht gefunden.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option: any = DB.get('SELECT * FROM role_options WHERE category_id = ? AND role_id = ?', category.id, role.id);
|
||||||
|
if (!option) {
|
||||||
|
await interaction.reply({ content: `Rolle ${role.name} ist nicht in "${kategorie}".`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.remove_on_delete) {
|
||||||
|
const guild = interaction.guild;
|
||||||
|
const discordRole = guild.roles.cache.get(role.id);
|
||||||
|
if (discordRole) {
|
||||||
|
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(role.id));
|
||||||
|
for (const member of members.values()) {
|
||||||
|
await member.roles.remove(role.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DB.run('DELETE FROM role_options WHERE id = ?', option.id);
|
||||||
|
await updateRoleMessage(interaction.client, category);
|
||||||
|
await interaction.reply({ content: `${role.name} aus "${kategorie}" entfernt.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'delete') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kategorie = interaction.options.getString('kategorie');
|
||||||
|
|
||||||
|
const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie);
|
||||||
|
if (!category) {
|
||||||
|
await interaction.reply({ content: `Kategorie "${kategorie}" nicht gefunden.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.remove_on_delete) {
|
||||||
|
const options: any[] = DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||||
|
const guild = interaction.guild;
|
||||||
|
for (const opt of options) {
|
||||||
|
const discordRole = guild.roles.cache.get(opt.role_id);
|
||||||
|
if (discordRole) {
|
||||||
|
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(opt.role_id));
|
||||||
|
for (const member of members.values()) {
|
||||||
|
await member.roles.remove(opt.role_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const channel = interaction.guild?.channels.cache.get(category.channel_id);
|
||||||
|
if (channel) {
|
||||||
|
const message = await channel.messages.fetch(category.message_id);
|
||||||
|
if (message) await message.delete();
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
DB.run('DELETE FROM role_categories WHERE id = ?', category.id);
|
||||||
|
|
||||||
|
await interaction.reply({ content: `Kategorie "${kategorie}" geloescht.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'config') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kategorie = interaction.options.getString('kategorie');
|
||||||
|
const maxRollen = interaction.options.getInteger('max_rollen');
|
||||||
|
const exklusiv = interaction.options.getBoolean('exklusiv');
|
||||||
|
const entfernenBeiLoeschung = interaction.options.getBoolean('entfernen_bei_loeschung');
|
||||||
|
|
||||||
|
const category: any = DB.get('SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?', guildId, kategorie);
|
||||||
|
if (!category) {
|
||||||
|
await interaction.reply({ content: `Kategorie "${kategorie}" nicht gefunden.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: string[] = [];
|
||||||
|
if (maxRollen !== null) {
|
||||||
|
DB.run('UPDATE role_categories SET max_roles = ? WHERE id = ?', maxRollen, category.id);
|
||||||
|
updates.push(`Max-Rollen: ${maxRollen === 0 ? 'unbegrenzt' : maxRollen}`);
|
||||||
|
}
|
||||||
|
if (exklusiv !== null) {
|
||||||
|
DB.run('UPDATE role_categories SET exclusive = ? WHERE id = ?', exklusiv ? 1 : 0, category.id);
|
||||||
|
updates.push(`Exklusiv: ${exklusiv ? 'ja' : 'nein'}`);
|
||||||
|
}
|
||||||
|
if (entfernenBeiLoeschung !== null) {
|
||||||
|
DB.run('UPDATE role_categories SET remove_on_delete = ? WHERE id = ?', entfernenBeiLoeschung ? 1 : 0, category.id);
|
||||||
|
updates.push(`Auto-Entfernen: ${entfernenBeiLoeschung ? 'ja' : 'nein'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
await interaction.reply({ content: 'Keine Aenderungen angegeben.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply({ content: `Konfiguration aktualisiert: ${updates.join(', ')}`, ephemeral: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default command;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
|
|
||||||
// Parse time duration strings like "30s", "2h", "1d30m"
|
// Parse time duration strings like "30s", "2h", "1d30m"
|
||||||
@@ -159,7 +159,7 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ const command: Command = {
|
|||||||
if (!targetTime || targetTime <= now) {
|
if (!targetTime || targetTime <= now) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: '❌ Ungültige Zeitangabe. Verwende z.B.: "30min", "2h30m", "25.12.2024 14:30" oder "morgen 15:00"',
|
content: '❌ Ungültige Zeitangabe. Verwende z.B.: "30min", "2h30m", "25.12.2024 14:30" oder "morgen 15:00"',
|
||||||
flags: [MessageFlags.Ephemeral]
|
ephemeral: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -209,14 +209,14 @@ const command: Command = {
|
|||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `✅ Erinnerung gesetzt (ID: ${result.lastInsertRowid}) für **${timeStr.trim()}**`,
|
content: `✅ Erinnerung gesetzt (ID: ${result.lastInsertRowid}) für **${timeStr.trim()}**`,
|
||||||
flags: [MessageFlags.Ephemeral]
|
ephemeral: true
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'remove') {
|
if (subcommand === 'remove') {
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,11 +225,11 @@ const command: Command = {
|
|||||||
const result = DB.run('DELETE FROM reminders WHERE id = ? AND guild_id = ?', timerId, guildId);
|
const result = DB.run('DELETE FROM reminders WHERE id = ? AND guild_id = ?', timerId, guildId);
|
||||||
|
|
||||||
if (result.changes === 0) {
|
if (result.changes === 0) {
|
||||||
await interaction.reply({ content: `❌ Timer mit ID ${timerId} nicht gefunden.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `❌ Timer mit ID ${timerId} nicht gefunden.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Timer mit ID ${timerId} entfernt.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Timer mit ID ${timerId} entfernt.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ const command: Command = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (reminders.length === 0) {
|
if (reminders.length === 0) {
|
||||||
await interaction.reply({ content: 'Keine Timer in diesem Kanal.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Timer in diesem Kanal.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,20 +275,20 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
embed.setDescription(chunks[0]);
|
embed.setDescription(chunks[0]);
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
|
||||||
for (let i = 1; i < chunks.length; i++) {
|
for (let i = 1; i < chunks.length; i++) {
|
||||||
const nextEmbed = new EmbedBuilder()
|
const nextEmbed = new EmbedBuilder()
|
||||||
.setColor(0x3498db)
|
.setColor(0x3498db)
|
||||||
.setDescription(chunks[i]);
|
.setDescription(chunks[i]);
|
||||||
await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] });
|
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'listall') {
|
if (subcommand === 'listall') {
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +298,7 @@ const command: Command = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (reminders.length === 0) {
|
if (reminders.length === 0) {
|
||||||
await interaction.reply({ content: 'Keine Timer auf dem Server.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Timer auf dem Server.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,13 +332,13 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
embed.setDescription(chunks[0]);
|
embed.setDescription(chunks[0]);
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
|
||||||
for (let i = 1; i < chunks.length; i++) {
|
for (let i = 1; i < chunks.length; i++) {
|
||||||
const nextEmbed = new EmbedBuilder()
|
const nextEmbed = new EmbedBuilder()
|
||||||
.setColor(0xe74c3c)
|
.setColor(0xe74c3c)
|
||||||
.setDescription(chunks[i]);
|
.setDescription(chunks[i]);
|
||||||
await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] });
|
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
@@ -53,7 +53,7 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const command: Command = {
|
|||||||
const triggers: any[] = DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word ASC', guildId);
|
const triggers: any[] = DB.all('SELECT * FROM auto_responses WHERE guild_id = ? ORDER BY trigger_word ASC', guildId);
|
||||||
|
|
||||||
if (triggers.length === 0) {
|
if (triggers.length === 0) {
|
||||||
await interaction.reply({ content: 'Keine Trigger konfiguriert.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Trigger konfiguriert.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,20 +80,20 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
embed.setDescription(chunks[0]);
|
embed.setDescription(chunks[0]);
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
|
||||||
for (let i = 1; i < chunks.length; i++) {
|
for (let i = 1; i < chunks.length; i++) {
|
||||||
const nextEmbed = new EmbedBuilder()
|
const nextEmbed = new EmbedBuilder()
|
||||||
.setColor(0x3498db)
|
.setColor(0x3498db)
|
||||||
.setDescription(chunks[i]);
|
.setDescription(chunks[i]);
|
||||||
await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] });
|
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin only for add/remove
|
// Admin only for add/remove
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ const command: Command = {
|
|||||||
responseText
|
responseText
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Trigger **${triggerWord}** hinzugefügt/aktualisiert.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Trigger **${triggerWord}** hinzugefügt/aktualisiert.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,11 +119,11 @@ const command: Command = {
|
|||||||
const result = DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
const result = DB.run('DELETE FROM auto_responses WHERE guild_id = ? AND trigger_word = ?', guildId, triggerWord);
|
||||||
|
|
||||||
if (result.changes === 0) {
|
if (result.changes === 0) {
|
||||||
await interaction.reply({ content: `❌ Trigger **${triggerWord}** nicht gefunden.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `❌ Trigger **${triggerWord}** nicht gefunden.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Trigger **${triggerWord}** entfernt.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Trigger **${triggerWord}** entfernt.`, ephemeral: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
import { TwitchManager } from '../../structures/TwitchManager.js';
|
import { TwitchManager } from '../../structures/TwitchManager.js';
|
||||||
|
import { ExtendedClient } from '../../structures/ExtendedClient.js';
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -47,10 +48,11 @@ const command: Command = {
|
|||||||
.setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.')),
|
.setDescription('Zeigt Hilfe zu den Twitch-Befehlen an.')),
|
||||||
category: 'Public',
|
category: 'Public',
|
||||||
async execute(interaction: any) {
|
async execute(interaction: any) {
|
||||||
|
const client = interaction.client as ExtendedClient;
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
const guildId = interaction.guildId;
|
const guildId = interaction.guildId;
|
||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
||||||
const DB = interaction.client.DB;
|
const DB = client.DB;
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
@@ -67,7 +69,7 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ const command: Command = {
|
|||||||
if (subcommand === 'list') {
|
if (subcommand === 'list') {
|
||||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? AND discord_channel_id = ?', guildId, interaction.channelId);
|
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? AND discord_channel_id = ?', guildId, interaction.channelId);
|
||||||
if (monitors.length === 0) {
|
if (monitors.length === 0) {
|
||||||
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,19 +117,19 @@ const command: Command = {
|
|||||||
.setColor(0x6441a5)
|
.setColor(0x6441a5)
|
||||||
.setDescription(list);
|
.setDescription(list);
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'listall') {
|
if (subcommand === 'listall') {
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? ORDER BY channel_name ASC', guildId);
|
const monitors: any[] = DB.all('SELECT * FROM twitch_monitors WHERE guild_id = ? ORDER BY channel_name ASC', guildId);
|
||||||
if (monitors.length === 0) {
|
if (monitors.length === 0) {
|
||||||
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,25 +151,24 @@ const command: Command = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
embed.setDescription(chunks[0]);
|
embed.setDescription(chunks[0]);
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
|
||||||
for (let i = 1; i < chunks.length; i++) {
|
for (let i = 1; i < chunks.length; i++) {
|
||||||
const nextEmbed = new EmbedBuilder()
|
const nextEmbed = new EmbedBuilder()
|
||||||
.setColor(0x6441a5)
|
.setColor(0x6441a5)
|
||||||
.setDescription(chunks[i]);
|
.setDescription(chunks[i]);
|
||||||
await interaction.followUp({ embeds: [nextEmbed], flags: [MessageFlags.Ephemeral] });
|
await interaction.followUp({ embeds: [nextEmbed], ephemeral: true });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin only for add/remove
|
// Admin only for add/remove
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'add') {
|
if (subcommand === 'add') {
|
||||||
const discordChannel = interaction.options.getChannel('channel');
|
|
||||||
const streamersInput = interaction.options.getString('streamers')!;
|
const streamersInput = interaction.options.getString('streamers')!;
|
||||||
const message = interaction.options.getString('message');
|
const message = interaction.options.getString('message');
|
||||||
const discordChannelId = interaction.channelId;
|
const discordChannelId = interaction.channelId;
|
||||||
@@ -179,7 +180,7 @@ const command: Command = {
|
|||||||
.filter((s: string) => s.length > 0);
|
.filter((s: string) => s.length > 0);
|
||||||
|
|
||||||
if (streamers.length === 0) {
|
if (streamers.length === 0) {
|
||||||
await interaction.reply({ content: '❌ Keine gültigen Streamer angegeben.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine gültigen Streamer angegeben.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +190,7 @@ const command: Command = {
|
|||||||
for (const streamer of streamers) {
|
for (const streamer of streamers) {
|
||||||
try {
|
try {
|
||||||
// Check if stream exists
|
// Check if stream exists
|
||||||
const stream = await TwitchManager.fetchStreamData(streamer);
|
await TwitchManager.fetchStreamData(streamer);
|
||||||
if (!stream) {
|
|
||||||
// Streamer might be valid but offline - still add them
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already exists
|
// Check if already exists
|
||||||
const existing = DB.get('SELECT id FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, streamer);
|
const existing = DB.get('SELECT id FROM twitch_monitors WHERE guild_id = ? AND channel_name = ?', guildId, streamer);
|
||||||
@@ -218,7 +216,7 @@ const command: Command = {
|
|||||||
if (results.updated > 0) reply += `\n🔄 Aktualisiert: ${results.updated}`;
|
if (results.updated > 0) reply += `\n🔄 Aktualisiert: ${results.updated}`;
|
||||||
if (results.errors > 0) reply += `\n❌ Fehler: ${results.errors} (${errors.join(', ')})`;
|
if (results.errors > 0) reply += `\n❌ Fehler: ${results.errors} (${errors.join(', ')})`;
|
||||||
|
|
||||||
await interaction.reply({ content: reply, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: reply, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +228,7 @@ const command: Command = {
|
|||||||
.filter((s: string) => s.length > 0);
|
.filter((s: string) => s.length > 0);
|
||||||
|
|
||||||
if (streamers.length === 0) {
|
if (streamers.length === 0) {
|
||||||
await interaction.reply({ content: '❌ Keine gültigen Streamer angegeben.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine gültigen Streamer angegeben.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +249,7 @@ const command: Command = {
|
|||||||
if (results.removed > 0) reply += `✅ ${results.removed} Streamer entfernt.`;
|
if (results.removed > 0) reply += `✅ ${results.removed} Streamer entfernt.`;
|
||||||
if (results.notFound > 0) reply += `\n❌ Nicht gefunden: ${notFound.join(', ')}`;
|
if (results.notFound > 0) reply += `\n❌ Nicht gefunden: ${notFound.join(', ')}`;
|
||||||
|
|
||||||
await interaction.reply({ content: reply, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: reply, ephemeral: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
141
src/commands/utility/twitchmonitor.ts
Normal file
141
src/commands/utility/twitchmonitor.ts
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, ChannelType } from 'discord.js';
|
||||||
|
import { Command } from '../../structures/Command.js';
|
||||||
|
import { TwitchMonitor } from '../../structures/TwitchMonitor.js';
|
||||||
|
|
||||||
|
const command: Command = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('twitchmonitor')
|
||||||
|
.setDescription('Twitch IRC Chat-Monitoring konfigurieren')
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('add')
|
||||||
|
.setDescription('Fügt einen Twitch-Kanal zum Monitoring hinzu')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('twitch')
|
||||||
|
.setDescription('Twitch-Kanalname')
|
||||||
|
.setRequired(true))
|
||||||
|
.addChannelOption(option =>
|
||||||
|
option.setName('discord')
|
||||||
|
.setDescription('Discord-Kanal für Logs')
|
||||||
|
.addChannelTypes(ChannelType.GuildText)
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('remove')
|
||||||
|
.setDescription('Entfernt einen Twitch-Kanal vom Monitoring')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('twitch')
|
||||||
|
.setDescription('Twitch-Kanalname')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('list')
|
||||||
|
.setDescription('Listet alle überwachten Twitch-Kanäle auf'))
|
||||||
|
.addSubcommand(subcommand =>
|
||||||
|
subcommand
|
||||||
|
.setName('help')
|
||||||
|
.setDescription('Zeigt Hilfe zu den TwitchMonitor-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;
|
||||||
|
|
||||||
|
if (subcommand === 'help') {
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('TwitchMonitor Hilfe')
|
||||||
|
.setColor(0x9146ff)
|
||||||
|
.setDescription('Überwacht Twitch IRC-Chat für Moderations-Events.')
|
||||||
|
.addFields(
|
||||||
|
{ name: '/twitchmonitor add <twitch> <discord>', value: 'Kanal hinzufügen (Admin)', inline: false },
|
||||||
|
{ name: '/twitchmonitor remove <twitch>', value: 'Kanal entfernen (Admin)', inline: false },
|
||||||
|
{ name: '/twitchmonitor list', value: 'Alle überwachten Kanäle', inline: false }
|
||||||
|
)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'list') {
|
||||||
|
const monitors: any[] = DB.all('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? ORDER BY twitch_channel ASC', guildId);
|
||||||
|
|
||||||
|
if (monitors.length === 0) {
|
||||||
|
await interaction.reply({ content: 'Keine Twitch-Kanäle überwacht.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle('TwitchMonitor Kanäle')
|
||||||
|
.setColor(0x9146ff)
|
||||||
|
.setDescription(monitors.map((m: any) =>
|
||||||
|
`• **${m.twitch_channel}** → <#${m.discord_channel_id}>`
|
||||||
|
).join('\n'))
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin) {
|
||||||
|
await interaction.reply({ content: 'Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'add') {
|
||||||
|
const twitchChannel = interaction.options.getString('twitch').toLowerCase().replace(/^@/, '');
|
||||||
|
const discordChannel = interaction.options.getChannel('discord');
|
||||||
|
|
||||||
|
const existing = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
||||||
|
if (existing) {
|
||||||
|
await interaction.reply({ content: `${twitchChannel} wird bereits überwacht.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhook = await discordChannel.createWebhook({
|
||||||
|
name: `${twitchChannel} | Mod-Log`,
|
||||||
|
avatar: undefined,
|
||||||
|
}).catch(async () => {
|
||||||
|
const existing_webhook = await discordChannel.fetchWebhooks().then((webhooks: any) =>
|
||||||
|
webhooks.find((w: any) => w.name?.includes(twitchChannel))
|
||||||
|
);
|
||||||
|
if (existing_webhook) return existing_webhook;
|
||||||
|
throw new Error('Could not create webhook');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB.run(
|
||||||
|
'INSERT INTO twitch_monitor_channels (guild_id, twitch_channel, discord_channel_id, webhook_url) VALUES (?, ?, ?, ?)',
|
||||||
|
guildId,
|
||||||
|
twitchChannel,
|
||||||
|
discordChannel.id,
|
||||||
|
webhook.url
|
||||||
|
);
|
||||||
|
|
||||||
|
const monitor = TwitchMonitor.getInstance();
|
||||||
|
await monitor.addMonitor(guildId, twitchChannel, discordChannel.id, webhook.url);
|
||||||
|
|
||||||
|
await interaction.reply({ content: `${twitchChannel} wird jetzt überwacht.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand === 'remove') {
|
||||||
|
const twitchChannel = interaction.options.getString('twitch').toLowerCase().replace(/^@/, '');
|
||||||
|
|
||||||
|
const monitor: any = DB.get('SELECT * FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
||||||
|
if (!monitor) {
|
||||||
|
await interaction.reply({ content: `${twitchChannel} wird nicht überwacht.`, ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB.run('DELETE FROM twitch_monitor_channels WHERE guild_id = ? AND twitch_channel = ?', guildId, twitchChannel);
|
||||||
|
|
||||||
|
const twitchMonitor = TwitchMonitor.getInstance();
|
||||||
|
await twitchMonitor.removeMonitor(guildId, twitchChannel);
|
||||||
|
|
||||||
|
await interaction.reply({ content: `${twitchChannel} wurde entfernt.`, ephemeral: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default command;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||||||
import { Command } from '../../structures/Command.js';
|
import { Command } from '../../structures/Command.js';
|
||||||
|
|
||||||
const command: Command = {
|
const command: Command = {
|
||||||
@@ -56,8 +56,6 @@ const command: Command = {
|
|||||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ModerateMembers);
|
||||||
const DB = interaction.client.DB;
|
const DB = interaction.client.DB;
|
||||||
|
|
||||||
// Ensure guild settings exist
|
|
||||||
DB.run('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)', guildId);
|
|
||||||
|
|
||||||
if (subcommand === 'help') {
|
if (subcommand === 'help') {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
@@ -75,12 +73,12 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === 'list') {
|
if (subcommand === 'list') {
|
||||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
const channel = settings?.welcome_channel
|
const channel = settings?.welcome_channel
|
||||||
? `<#${settings.welcome_channel}>`
|
? `<#${settings.welcome_channel}>`
|
||||||
@@ -100,13 +98,13 @@ const command: Command = {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed], flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin only for modify commands
|
// Admin only for modify commands
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: '❌ Keine Berechtigung. Moderatoren erforderlich.', ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +117,7 @@ const command: Command = {
|
|||||||
guildId
|
guildId
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.reply({ content: `✅ Kanal für Willkommen/Abschied auf ${channel} gesetzt.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ Kanal für Willkommen/Abschied auf ${channel} gesetzt.`, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +134,7 @@ const command: Command = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const label = type === 'welcome' ? 'Willkommensnachricht' : 'Abschiedsnachricht';
|
const label = type === 'welcome' ? 'Willkommensnachricht' : 'Abschiedsnachricht';
|
||||||
await interaction.reply({ content: `✅ ${label} gesetzt:\n\`\`\`\n${message}\n\`\`\``, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ ${label} gesetzt:\n\`\`\`\n${message}\n\`\`\``, ephemeral: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +148,7 @@ const command: Command = {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const label = type === 'welcome' ? 'Willkommensnachricht' : 'Abschiedsnachricht';
|
const label = type === 'welcome' ? 'Willkommensnachricht' : 'Abschiedsnachricht';
|
||||||
await interaction.reply({ content: `✅ ${label} entfernt.`, flags: [MessageFlags.Ephemeral] });
|
await interaction.reply({ content: `✅ ${label} entfernt.`, ephemeral: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
||||||
|
|
||||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
const DB = client.DB;
|
const settings = client.DB.getSettings(guildId);
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
if (!settings?.log_events || !settings?.log_channel) return;
|
||||||
|
|
||||||
if (!settings?.log_events) return;
|
|
||||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!activeEvents.includes(event)) return;
|
if (!activeEvents.includes(event)) return;
|
||||||
|
|
||||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
if (!logSettings?.log_channel) return;
|
|
||||||
|
|
||||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
|
||||||
if (!channel || !channel.isTextBased()) return;
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -24,6 +20,8 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
|||||||
export default {
|
export default {
|
||||||
name: Events.ChannelCreate,
|
name: Events.ChannelCreate,
|
||||||
async execute(channel: GuildChannel) {
|
async execute(channel: GuildChannel) {
|
||||||
|
if (!channel.guild) return;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle('📁 Channel erstellt')
|
.setTitle('📁 Channel erstellt')
|
||||||
.setColor(0x27ae60)
|
.setColor(0x27ae60)
|
||||||
@@ -33,6 +31,6 @@ export default {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await sendLog(channel.client, channel.guild!.id, 'channels', embed);
|
await sendLog(channel.client, channel.guild.id, 'channels', embed);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
import { Events, GuildChannel, EmbedBuilder } from 'discord.js';
|
||||||
|
|
||||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
const DB = client.DB;
|
const settings = client.DB.getSettings(guildId);
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
if (!settings?.log_events || !settings?.log_channel) return;
|
||||||
|
|
||||||
if (!settings?.log_events) return;
|
|
||||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!activeEvents.includes(event)) return;
|
if (!activeEvents.includes(event)) return;
|
||||||
|
|
||||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
if (!logSettings?.log_channel) return;
|
|
||||||
|
|
||||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
|
||||||
if (!channel || !channel.isTextBased()) return;
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -24,6 +20,8 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
|||||||
export default {
|
export default {
|
||||||
name: Events.ChannelDelete,
|
name: Events.ChannelDelete,
|
||||||
async execute(channel: GuildChannel) {
|
async execute(channel: GuildChannel) {
|
||||||
|
if (!channel.guild) return;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle('📁 Channel gelöscht')
|
.setTitle('📁 Channel gelöscht')
|
||||||
.setColor(0xe74c3c)
|
.setColor(0xe74c3c)
|
||||||
@@ -33,6 +31,6 @@ export default {
|
|||||||
)
|
)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
||||||
await sendLog(channel.client, channel.guild!.id, 'channels', embed);
|
await sendLog(channel.client, channel.guild.id, 'channels', embed);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ export default {
|
|||||||
const DB = (member.client as any).DB;
|
const DB = (member.client as any).DB;
|
||||||
const guildId = member.guild.id;
|
const guildId = member.guild.id;
|
||||||
|
|
||||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
// Use custom channel or fall back to Discord's system channel
|
// Use custom channel or fall back to Discord's system channel
|
||||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ import { Events, GuildMember, TextChannel, EmbedBuilder } from 'discord.js';
|
|||||||
|
|
||||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
const DB = client.DB;
|
const DB = client.DB;
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
if (!settings?.log_events) return;
|
if (!settings?.log_events) return;
|
||||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!activeEvents.includes(event)) return;
|
if (!activeEvents.includes(event)) return;
|
||||||
|
|
||||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
if (!settings?.log_channel) return;
|
||||||
if (!logSettings?.log_channel) return;
|
|
||||||
|
|
||||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
if (!channel || !channel.isTextBased()) return;
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +39,7 @@ export default {
|
|||||||
await sendLog(member.client, guildId, 'leaves', embed);
|
await sendLog(member.client, guildId, 'leaves', embed);
|
||||||
|
|
||||||
// Send goodbye message if configured
|
// Send goodbye message if configured
|
||||||
const settings: any = DB.get('SELECT * FROM guild_settings WHERE guild_id = ?', guildId);
|
const settings = DB.getSettings(guildId);
|
||||||
|
|
||||||
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
const channelId = settings?.welcome_channel || member.guild.systemChannelId;
|
||||||
if (!channelId || !settings?.goodbye_message) {
|
if (!channelId || !settings?.goodbye_message) {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { Events, GuildMember, EmbedBuilder } from 'discord.js';
|
import { Events, GuildMember, EmbedBuilder } from 'discord.js';
|
||||||
|
|
||||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
const DB = client.DB;
|
const settings = client.DB.getSettings(guildId);
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
if (!settings?.log_events || !settings?.log_channel) return;
|
||||||
|
|
||||||
if (!settings?.log_events) return;
|
|
||||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!activeEvents.includes(event)) return;
|
if (!activeEvents.includes(event)) return;
|
||||||
|
|
||||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
if (!logSettings?.log_channel) return;
|
|
||||||
|
|
||||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
|
||||||
if (!channel || !channel.isTextBased()) return;
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -21,7 +17,6 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guild Member Update (nickname + roles)
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.GuildMemberUpdate,
|
name: Events.GuildMemberUpdate,
|
||||||
async execute(oldMember: GuildMember, newMember: GuildMember) {
|
async execute(oldMember: GuildMember, newMember: GuildMember) {
|
||||||
|
|||||||
@@ -1,8 +1,124 @@
|
|||||||
import { Events, MessageFlags } from 'discord.js';
|
import { Events, Interaction, GuildMember } from 'discord.js';
|
||||||
|
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.InteractionCreate,
|
name: Events.InteractionCreate,
|
||||||
async execute(interaction: any, client: any) {
|
async execute(interaction: Interaction, client: ExtendedClient) {
|
||||||
|
if (interaction.isStringSelectMenu()) {
|
||||||
|
const customId = interaction.customId;
|
||||||
|
|
||||||
|
if (customId.startsWith('role_select_')) {
|
||||||
|
const categoryName = customId.replace('role_select_', '');
|
||||||
|
const selectedValue = interaction.values[0];
|
||||||
|
const guild = interaction.guild;
|
||||||
|
const member = interaction.member as GuildMember;
|
||||||
|
|
||||||
|
if (!guild || !member || !member.roles) {
|
||||||
|
await interaction.reply({ content: 'Dieser Befehl kann nur auf Servern verwendet werden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const category: any = client.DB.get(
|
||||||
|
'SELECT * FROM role_categories WHERE guild_id = ? AND category_name = ?',
|
||||||
|
guild.id,
|
||||||
|
categoryName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
await interaction.reply({ content: 'Kategorie nicht gefunden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedValue.startsWith('remove_')) {
|
||||||
|
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||||
|
let removedCount = 0;
|
||||||
|
for (const opt of options) {
|
||||||
|
if (member.roles.cache.has(opt.role_id)) {
|
||||||
|
await member.roles.remove(opt.role_id);
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removedCount > 0) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Alle Rollen aus "${categoryName}" wurden entfernt.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Du hast keine Rollen aus "${categoryName}".`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const option: any = client.DB.get(
|
||||||
|
'SELECT * FROM role_options WHERE category_id = ? AND role_id = ?',
|
||||||
|
category.id,
|
||||||
|
selectedValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!option) {
|
||||||
|
await interaction.reply({ content: 'Rolle nicht gefunden.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discordRole = guild.roles.cache.get(selectedValue);
|
||||||
|
if (!discordRole) {
|
||||||
|
await interaction.reply({ content: 'Diese Rolle existiert nicht mehr.', ephemeral: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasRole = member.roles.cache.has(selectedValue);
|
||||||
|
|
||||||
|
if (category.exclusive) {
|
||||||
|
if (hasRole) {
|
||||||
|
await member.roles.remove(selectedValue);
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||||
|
for (const opt of options) {
|
||||||
|
if (opt.role_id !== selectedValue && member.roles.cache.has(opt.role_id)) {
|
||||||
|
await member.roles.remove(opt.role_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await member.roles.add(selectedValue);
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasRole) {
|
||||||
|
await member.roles.remove(selectedValue);
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name} wurde entfernt.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||||
|
const userCategoryRoles = options.filter((opt: any) => member.roles.cache.has(opt.role_id)).length;
|
||||||
|
if (category.max_roles > 0 && userCategoryRoles >= category.max_roles) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Du kannst maximal ${category.max_roles} Rollen aus dieser Kategorie haben.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await member.roles.add(selectedValue);
|
||||||
|
await interaction.reply({
|
||||||
|
content: `Du hast jetzt die Rolle ${option.emoji ? option.emoji + ' ' : ''}${discordRole.name}!`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
const command = client.commands.get(interaction.commandName);
|
const command = client.commands.get(interaction.commandName);
|
||||||
@@ -21,7 +137,7 @@ export default {
|
|||||||
`, interaction.commandName);
|
`, interaction.commandName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const replyOptions = { content: 'There was an error while executing this command!', flags: [MessageFlags.Ephemeral] };
|
const replyOptions = { content: 'There was an error while executing this command!', ephemeral: true };
|
||||||
if (interaction.replied || interaction.deferred) {
|
if (interaction.replied || interaction.deferred) {
|
||||||
await interaction.followUp(replyOptions);
|
await interaction.followUp(replyOptions);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ export default {
|
|||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
if (!message.guildId) return;
|
if (!message.guildId) return;
|
||||||
|
|
||||||
const triggers: any[] = client.DB.all(
|
const triggers = client.DB.getTriggers(message.guildId);
|
||||||
'SELECT * FROM auto_responses WHERE guild_id = ?',
|
|
||||||
message.guildId
|
if (triggers.length === 0) return;
|
||||||
);
|
|
||||||
|
|
||||||
const contentLower = message.content.toLowerCase();
|
const contentLower = message.content.toLowerCase();
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,26 @@
|
|||||||
import { Events, Message, TextChannel, GuildChannel, AuditLogEvent, EmbedBuilder } from 'discord.js';
|
import { Events, Message, TextChannel, 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) {
|
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
if (!isEventEnabled(client, guildId, event)) {
|
const settings = client.DB.getSettings(guildId);
|
||||||
return;
|
if (!settings?.log_events || !settings?.log_channel) return;
|
||||||
}
|
|
||||||
|
|
||||||
const channel = getLogChannel(client, guildId);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!channel) {
|
if (!activeEvents.includes(event)) return;
|
||||||
return;
|
|
||||||
}
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await channel.send({ embeds: [embed] });
|
await (channel as any).send({ embeds: [embed] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[LOG] Error sending log (${event}):`, error);
|
console.error(`[LOG] Error sending log (${event}):`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message Delete
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.MessageDelete,
|
name: Events.MessageDelete,
|
||||||
async execute(message: Message) {
|
async execute(message: Message) {
|
||||||
if (!message.guild || message.author.bot) return;
|
if (!message.guild || !message.author || message.author.bot) return;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle('🗑️ Nachricht gelöscht')
|
.setTitle('🗑️ Nachricht gelöscht')
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import { Events, Message, EmbedBuilder } from 'discord.js';
|
import { Events, Message, EmbedBuilder } from 'discord.js';
|
||||||
|
|
||||||
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
async function sendLog(client: any, guildId: string, event: string, embed: EmbedBuilder) {
|
||||||
const DB = client.DB;
|
const settings = client.DB.getSettings(guildId);
|
||||||
const settings: any = DB.get('SELECT log_events FROM guild_settings WHERE guild_id = ?', guildId);
|
if (!settings?.log_events || !settings?.log_channel) return;
|
||||||
|
|
||||||
if (!settings?.log_events) return;
|
|
||||||
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
const activeEvents = settings.log_events.split(',').filter(Boolean);
|
||||||
if (!activeEvents.includes(event)) return;
|
if (!activeEvents.includes(event)) return;
|
||||||
|
|
||||||
const logSettings: any = DB.get('SELECT log_channel FROM guild_settings WHERE guild_id = ?', guildId);
|
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(settings.log_channel);
|
||||||
if (!logSettings?.log_channel) return;
|
|
||||||
|
|
||||||
const channel = client.guilds.cache.get(guildId)?.channels.cache.get(logSettings.log_channel);
|
|
||||||
if (!channel || !channel.isTextBased()) return;
|
if (!channel || !channel.isTextBased()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -21,11 +17,10 @@ async function sendLog(client: any, guildId: string, event: string, embed: Embed
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message Update (Edit)
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.MessageUpdate,
|
name: Events.MessageUpdate,
|
||||||
async execute(oldMessage: Message, newMessage: Message) {
|
async execute(oldMessage: Message, newMessage: Message) {
|
||||||
if (!newMessage.guild || newMessage.author?.bot) return;
|
if (!newMessage.guild || !newMessage.author || newMessage.author.bot) return;
|
||||||
if (!oldMessage.content || !newMessage.content) return;
|
if (!oldMessage.content || !newMessage.content) return;
|
||||||
if (oldMessage.content === newMessage.content) return;
|
if (oldMessage.content === newMessage.content) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,85 @@
|
|||||||
import { Events } from 'discord.js';
|
import { Events } from 'discord.js';
|
||||||
|
import { TwitchMonitor } from '../structures/TwitchMonitor.js';
|
||||||
|
import { ExtendedClient } from '../structures/ExtendedClient.js';
|
||||||
|
import { updateRoleMessage } from '../commands/utility/rolesetup.js';
|
||||||
|
|
||||||
|
async function validateRoles(client: ExtendedClient) {
|
||||||
|
const categories: any[] = client.DB.all('SELECT * FROM role_categories');
|
||||||
|
let removedCount = 0;
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
const guild = client.guilds.cache.get(category.guild_id);
|
||||||
|
if (!guild) continue;
|
||||||
|
|
||||||
|
const options: any[] = client.DB.all('SELECT * FROM role_options WHERE category_id = ?', category.id);
|
||||||
|
const validOptions: any[] = [];
|
||||||
|
let categoryUpdated = false;
|
||||||
|
|
||||||
|
for (const option of options) {
|
||||||
|
const roleExists = guild.roles.cache.has(option.role_id);
|
||||||
|
if (roleExists) {
|
||||||
|
validOptions.push(option);
|
||||||
|
} else {
|
||||||
|
if (category.remove_on_delete) {
|
||||||
|
const members = guild.members.cache.filter((m: any) => m.roles.cache.has(option.role_id));
|
||||||
|
for (const member of members.values()) {
|
||||||
|
await member.roles.remove(option.role_id).catch(() => null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.DB.run('DELETE FROM role_options WHERE id = ?', option.id);
|
||||||
|
categoryUpdated = true;
|
||||||
|
removedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryUpdated) {
|
||||||
|
await updateRoleMessage(client, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validOptions.length === 0 && options.length > 0) {
|
||||||
|
console.log(`[ROLESETUP] Category "${category.category_name}" has no valid roles left. Consider deleting it.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0) {
|
||||||
|
console.log(`[ROLESETUP] Removed ${removedCount} invalid role options from database.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initTwitchMonitor(client: ExtendedClient) {
|
||||||
|
if (!process.env.TWITCH_OAUTH_TOKEN || !process.env.TWITCH_USERNAME) {
|
||||||
|
console.warn('[TWITCH] Missing TWITCH_OAUTH_TOKEN or TWITCH_USERNAME - moderation events will not be received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitors: any[] = client.DB.all('SELECT * FROM twitch_monitor_channels');
|
||||||
|
|
||||||
|
if (monitors.length === 0) {
|
||||||
|
console.log('[TWITCH] No channels to monitor.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitor = TwitchMonitor.getInstance();
|
||||||
|
monitor.setDiscordClient(client);
|
||||||
|
|
||||||
|
for (const m of monitors) {
|
||||||
|
const webhookExists = m.webhook_url && m.webhook_url.length > 0;
|
||||||
|
if (webhookExists) {
|
||||||
|
await monitor.addMonitor(m.guild_id, m.twitch_channel, m.discord_channel_id, m.webhook_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await monitor.connect();
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: Events.ClientReady,
|
name: Events.ClientReady,
|
||||||
once: true,
|
once: true,
|
||||||
execute(client: any) {
|
async execute(client: ExtendedClient) {
|
||||||
console.log(`[READY] Logged in as ${client.user.tag}`);
|
console.log(`[READY] Logged in as ${client.user?.tag}`);
|
||||||
console.log(`[READY] Serving ${client.guilds.cache.size} servers`);
|
console.log(`[READY] Serving ${client.guilds.cache.size} servers`);
|
||||||
|
|
||||||
|
await validateRoles(client);
|
||||||
|
await initTwitchMonitor(client);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
14
src/index.ts
14
src/index.ts
@@ -6,8 +6,8 @@ import { dirname, join } from 'node:path';
|
|||||||
import { Command } from './structures/Command.js';
|
import { Command } from './structures/Command.js';
|
||||||
import { DB } from './structures/Database.js';
|
import { DB } from './structures/Database.js';
|
||||||
import { TwitchManager } from './structures/TwitchManager.js';
|
import { TwitchManager } from './structures/TwitchManager.js';
|
||||||
import { KickManager } from './structures/KickManager.js';
|
|
||||||
import { ReminderManager } from './structures/ReminderManager.js';
|
import { ReminderManager } from './structures/ReminderManager.js';
|
||||||
|
import { Deployer } from './structures/Deployer.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -16,12 +16,11 @@ const __dirname = dirname(__filename);
|
|||||||
DB.init();
|
DB.init();
|
||||||
|
|
||||||
const client = new ExtendedClient();
|
const client = new ExtendedClient();
|
||||||
// Make DB available on client
|
|
||||||
(client as any).DB = DB;
|
|
||||||
|
|
||||||
// Load Commands
|
// Load Commands
|
||||||
const commandsPath = join(__dirname, 'commands');
|
const commandsPath = join(__dirname, 'commands');
|
||||||
const commandFolders = readdirSync(commandsPath);
|
const commandFolders = readdirSync(commandsPath);
|
||||||
|
const commandData: any[] = [];
|
||||||
|
|
||||||
for (const folder of commandFolders) {
|
for (const folder of commandFolders) {
|
||||||
const folderPath = join(commandsPath, folder);
|
const folderPath = join(commandsPath, folder);
|
||||||
@@ -34,6 +33,7 @@ for (const folder of commandFolders) {
|
|||||||
|
|
||||||
if (command && 'data' in command && 'execute' in command) {
|
if (command && 'data' in command && 'execute' in command) {
|
||||||
client.commands.set(command.data.name, command);
|
client.commands.set(command.data.name, command);
|
||||||
|
commandData.push(command.data.toJSON());
|
||||||
console.log(`[COMMAND] Loaded: ${command.data.name}`);
|
console.log(`[COMMAND] Loaded: ${command.data.name}`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[COMMAND] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
console.warn(`[COMMAND] The command at ${filePath} is missing a required "data" or "execute" property.`);
|
||||||
@@ -41,6 +41,11 @@ for (const folder of commandFolders) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-deploy commands if configured (default: true)
|
||||||
|
if (process.env.AUTO_DEPLOY !== 'false' && process.env.CLIENT_ID && process.env.DISCORD_TOKEN) {
|
||||||
|
await Deployer.deployCommands(process.env.CLIENT_ID, process.env.DISCORD_TOKEN, commandData);
|
||||||
|
}
|
||||||
|
|
||||||
// Load Events
|
// Load Events
|
||||||
const eventsPath = join(__dirname, 'events');
|
const eventsPath = join(__dirname, 'events');
|
||||||
const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
||||||
@@ -61,9 +66,6 @@ for (const file of eventFiles) {
|
|||||||
// Start Twitch Polling
|
// Start Twitch Polling
|
||||||
TwitchManager.startPolling(client);
|
TwitchManager.startPolling(client);
|
||||||
|
|
||||||
// Start Kick Polling
|
|
||||||
KickManager.startPolling(client);
|
|
||||||
|
|
||||||
// Start Reminder Polling
|
// Start Reminder Polling
|
||||||
ReminderManager.startPolling(client);
|
ReminderManager.startPolling(client);
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,17 @@ const dbPath = join(dataDir, 'database.sqlite');
|
|||||||
const db = new Database(dbPath);
|
const db = new Database(dbPath);
|
||||||
|
|
||||||
export class DB {
|
export class DB {
|
||||||
|
private static settingsCache = new Map<string, any>();
|
||||||
|
private static triggersCache = new Map<string, any[]>();
|
||||||
|
|
||||||
static init() {
|
static init() {
|
||||||
console.log(`[DATABASE] Initializing at ${dbPath}...`);
|
console.log(`[DATABASE] Initializing at ${dbPath}...`);
|
||||||
|
|
||||||
|
// Enable WAL mode for better performance
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
// Enable foreign key support
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
|
||||||
// Guild Settings
|
// Guild Settings
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS guild_settings (
|
CREATE TABLE IF NOT EXISTS guild_settings (
|
||||||
@@ -28,7 +36,7 @@ export class DB {
|
|||||||
welcome_message TEXT,
|
welcome_message TEXT,
|
||||||
goodbye_message TEXT,
|
goodbye_message TEXT,
|
||||||
log_channel TEXT,
|
log_channel TEXT,
|
||||||
log_events TEXT DEFAULT 'messages,roles,nicks,channels,moderation',
|
log_events TEXT DEFAULT 'messages,roles,nicks,channels,leaves,warns,mutes,kicks,bans',
|
||||||
warn_threshold INTEGER DEFAULT 3,
|
warn_threshold INTEGER DEFAULT 3,
|
||||||
warn_action TEXT DEFAULT 'ban',
|
warn_action TEXT DEFAULT 'ban',
|
||||||
warn_mute_duration INTEGER DEFAULT 1800
|
warn_mute_duration INTEGER DEFAULT 1800
|
||||||
@@ -65,7 +73,7 @@ export class DB {
|
|||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
// Twitch Monitors
|
// Twitch Monitors (Stream notifications)
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS twitch_monitors (
|
CREATE TABLE IF NOT EXISTS twitch_monitors (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -79,17 +87,15 @@ export class DB {
|
|||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
// Kick Monitors
|
// Twitch Monitor Channels (IRC chat logging)
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS kick_monitors (
|
CREATE TABLE IF NOT EXISTS twitch_monitor_channels (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
guild_id TEXT,
|
guild_id TEXT NOT NULL,
|
||||||
channel_name TEXT,
|
twitch_channel TEXT NOT NULL,
|
||||||
discord_channel_id TEXT,
|
discord_channel_id TEXT NOT NULL,
|
||||||
custom_message TEXT,
|
webhook_url TEXT,
|
||||||
last_status TEXT DEFAULT 'offline',
|
UNIQUE(guild_id, twitch_channel)
|
||||||
last_stream_id TEXT,
|
|
||||||
UNIQUE(guild_id, channel_name)
|
|
||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
@@ -117,7 +123,35 @@ export class DB {
|
|||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
|
||||||
// Migration: Add missing columns to existing guild_settings
|
// Role Categories
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS role_categories (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
category_name TEXT NOT NULL,
|
||||||
|
max_roles INTEGER DEFAULT 0,
|
||||||
|
exclusive BOOLEAN DEFAULT 0,
|
||||||
|
remove_on_delete BOOLEAN DEFAULT 0,
|
||||||
|
UNIQUE(guild_id, category_name)
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
// Role Options
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS role_options (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
emoji TEXT,
|
||||||
|
role_id TEXT NOT NULL,
|
||||||
|
UNIQUE(category_id, role_id),
|
||||||
|
FOREIGN KEY (category_id) REFERENCES role_categories(id) ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
|
// Migration logic
|
||||||
const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[];
|
const columns = db.prepare("PRAGMA table_info(guild_settings)").all() as any[];
|
||||||
const columnNames = columns.map((c: any) => c.name);
|
const columnNames = columns.map((c: any) => c.name);
|
||||||
|
|
||||||
@@ -137,6 +171,51 @@ export class DB {
|
|||||||
console.log('[DATABASE] Initialization complete.');
|
console.log('[DATABASE] Initialization complete.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized getter for guild settings with caching (automatically creates settings if missing)
|
||||||
|
*/
|
||||||
|
static getSettings(guildId: string): any {
|
||||||
|
if (this.settingsCache.has(guildId)) {
|
||||||
|
return this.settingsCache.get(guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?').get(guildId);
|
||||||
|
if (!settings) {
|
||||||
|
db.prepare('INSERT OR IGNORE INTO guild_settings (guild_id) VALUES (?)').run(guildId);
|
||||||
|
settings = db.prepare('SELECT * FROM guild_settings WHERE guild_id = ?').get(guildId);
|
||||||
|
}
|
||||||
|
if (settings) {
|
||||||
|
this.settingsCache.set(guildId, settings);
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimized getter for auto responses with caching
|
||||||
|
*/
|
||||||
|
static getTriggers(guildId: string): any[] {
|
||||||
|
if (this.triggersCache.has(guildId)) {
|
||||||
|
return this.triggersCache.get(guildId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggers = db.prepare('SELECT * FROM auto_responses WHERE guild_id = ?').all(guildId);
|
||||||
|
this.triggersCache.set(guildId, triggers);
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache for a specific guild or all
|
||||||
|
*/
|
||||||
|
static clearCache(guildId?: string) {
|
||||||
|
if (guildId) {
|
||||||
|
this.settingsCache.delete(guildId);
|
||||||
|
this.triggersCache.delete(guildId);
|
||||||
|
} else {
|
||||||
|
this.settingsCache.clear();
|
||||||
|
this.triggersCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static get(query: string, ...params: any[]) {
|
static get(query: string, ...params: any[]) {
|
||||||
return db.prepare(query).get(...params);
|
return db.prepare(query).get(...params);
|
||||||
}
|
}
|
||||||
@@ -145,7 +224,34 @@ export class DB {
|
|||||||
return db.prepare(query).all(...params);
|
return db.prepare(query).all(...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a function within a database transaction
|
||||||
|
*/
|
||||||
|
static transaction<T>(fn: () => T): T {
|
||||||
|
return db.transaction(fn)();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended run method that automatically clears relevant caches on write
|
||||||
|
*/
|
||||||
static run(query: string, ...params: any[]) {
|
static run(query: string, ...params: any[]) {
|
||||||
return db.prepare(query).run(...params);
|
const result = db.prepare(query).run(...params);
|
||||||
|
|
||||||
|
// Simple heuristic to clear cache on updates to settings or triggers
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
const isSettings = lowerQuery.includes('guild_settings');
|
||||||
|
const isTriggers = lowerQuery.includes('auto_responses');
|
||||||
|
|
||||||
|
if (isSettings || isTriggers) {
|
||||||
|
// Try to find guild_id in params to be surgical
|
||||||
|
const guildId = params.find(p => typeof p === 'string' && /^\d{17,20}$/.test(p));
|
||||||
|
if (guildId) {
|
||||||
|
this.clearCache(guildId);
|
||||||
|
} else {
|
||||||
|
this.clearCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,23 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
export class Deployer {
|
export class Deployer {
|
||||||
|
static async deployCommands(clientId: string, token: string, commandData: any[]) {
|
||||||
|
const rest = new REST().setToken(token);
|
||||||
|
console.log(`[DEPLOYER] Refreshing ${commandData.length} application (/) commands.`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data: any = await rest.put(
|
||||||
|
Routes.applicationCommands(clientId),
|
||||||
|
{ body: commandData },
|
||||||
|
);
|
||||||
|
console.log(`[DEPLOYER] Successfully reloaded ${data.length} application (/) commands.`);
|
||||||
|
return data.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[DEPLOYER] Error deploying commands:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async deploy(clientId: string, token: string) {
|
static async deploy(clientId: string, token: string) {
|
||||||
const commands = [];
|
const commands = [];
|
||||||
const rootDir = join(__dirname, '..');
|
const rootDir = join(__dirname, '..');
|
||||||
@@ -15,7 +32,7 @@ export class Deployer {
|
|||||||
|
|
||||||
for (const folder of commandFolders) {
|
for (const folder of commandFolders) {
|
||||||
const folderPath = join(commandsPath, folder);
|
const folderPath = join(commandsPath, folder);
|
||||||
const commandFiles = readdirSync(folderPath).filter(file => file.endsWith('.ts'));
|
const commandFiles = readdirSync(folderPath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
||||||
|
|
||||||
for (const file of commandFiles) {
|
for (const file of commandFiles) {
|
||||||
const filePath = join(folderPath, file);
|
const filePath = join(folderPath, file);
|
||||||
@@ -28,17 +45,7 @@ export class Deployer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rest = new REST().setToken(token);
|
return this.deployCommands(clientId, token, commands);
|
||||||
|
|
||||||
console.log(`[DEPLOYER] Refreshing ${commands.length} application (/) commands.`);
|
|
||||||
|
|
||||||
const data: any = await rest.put(
|
|
||||||
Routes.applicationCommands(clientId),
|
|
||||||
{ body: commands },
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[DEPLOYER] Successfully reloaded ${data.length} application (/) commands.`);
|
|
||||||
return data.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async deployIfMissing(clientId: string, token: string) {
|
static async deployIfMissing(clientId: string, token: string) {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Client, Collection, GatewayIntentBits } from 'discord.js';
|
import { Client, Collection, GatewayIntentBits } from 'discord.js';
|
||||||
import { Command } from './Command.js';
|
import { Command } from './Command.js';
|
||||||
|
import { DB } from './Database.js';
|
||||||
|
|
||||||
export class ExtendedClient extends Client {
|
export class ExtendedClient extends Client {
|
||||||
public commands: Collection<string, Command> = new Collection();
|
public commands: Collection<string, Command> = new Collection();
|
||||||
|
public DB = DB;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
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).');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { TextChannel, EmbedBuilder } from 'discord.js';
|
import { TextChannel, EmbedBuilder } from 'discord.js';
|
||||||
|
import { ExtendedClient } from './ExtendedClient.js';
|
||||||
|
|
||||||
export class ReminderManager {
|
export class ReminderManager {
|
||||||
static async checkReminders(client: any) {
|
static async checkReminders(client: ExtendedClient) {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// Get all due reminders
|
// Get all due reminders
|
||||||
const dueReminders: any[] = (client as any).DB?.all(
|
const dueReminders: any[] = client.DB?.all(
|
||||||
'SELECT * FROM reminders WHERE target_time <= ?',
|
'SELECT * FROM reminders WHERE target_time <= ?',
|
||||||
now
|
now
|
||||||
) || [];
|
) || [];
|
||||||
@@ -17,14 +18,14 @@ export class ReminderManager {
|
|||||||
const guild = client.guilds.cache.get(reminder.guild_id);
|
const guild = client.guilds.cache.get(reminder.guild_id);
|
||||||
if (!guild) {
|
if (!guild) {
|
||||||
// Remove reminder if guild doesn't exist
|
// Remove reminder if guild doesn't exist
|
||||||
(client as any).DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id);
|
client.DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const channel = guild.channels.cache.get(reminder.channel_id) as TextChannel;
|
const channel = guild.channels.cache.get(reminder.channel_id) as TextChannel;
|
||||||
if (!channel || !channel.isTextBased()) {
|
if (!channel || !channel.isTextBased()) {
|
||||||
// Remove reminder if channel doesn't exist or is not text-based
|
// Remove reminder if channel doesn't exist or is not text-based
|
||||||
(client as any).DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id);
|
client.DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ export class ReminderManager {
|
|||||||
await channel.send({ content: `${userMention} deine Erinnerung:`, embeds: [embed] });
|
await channel.send({ content: `${userMention} deine Erinnerung:`, embeds: [embed] });
|
||||||
|
|
||||||
// Delete the reminder after sending
|
// Delete the reminder after sending
|
||||||
(client as any).DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id);
|
client.DB?.run('DELETE FROM reminders WHERE id = ?', reminder.id);
|
||||||
|
|
||||||
console.log(`[REMINDER] Sent reminder ID ${reminder.id} in ${guild.name}`);
|
console.log(`[REMINDER] Sent reminder ID ${reminder.id} in ${guild.name}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -54,7 +55,7 @@ export class ReminderManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static startPolling(client: any) {
|
static startPolling(client: ExtendedClient) {
|
||||||
// Check every 30 seconds
|
// Check every 30 seconds
|
||||||
setInterval(() => this.checkReminders(client), 30 * 1000);
|
setInterval(() => this.checkReminders(client), 30 * 1000);
|
||||||
console.log('[REMINDER] Polling started (30s interval).');
|
console.log('[REMINDER] Polling started (30s interval).');
|
||||||
|
|||||||
41
src/structures/TwitchCache.ts
Normal file
41
src/structures/TwitchCache.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export interface ChatMessage {
|
||||||
|
channel: string;
|
||||||
|
user: string;
|
||||||
|
text: string;
|
||||||
|
timestamp: number;
|
||||||
|
msgId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TwitchCache {
|
||||||
|
private buffer: ChatMessage[] = [];
|
||||||
|
private maxSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number = 1000) {
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(msg: ChatMessage): void {
|
||||||
|
if (this.buffer.length >= this.maxSize) {
|
||||||
|
this.buffer.shift();
|
||||||
|
}
|
||||||
|
this.buffer.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastN(user: string, channel: string, n: number): ChatMessage[] {
|
||||||
|
return this.buffer
|
||||||
|
.filter(m => m.user.toLowerCase() === user.toLowerCase() && m.channel === channel)
|
||||||
|
.slice(-n);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByMsgId(msgId: string): ChatMessage | undefined {
|
||||||
|
return this.buffer.find(m => m.msgId === msgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.buffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
return this.buffer.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { EmbedBuilder, TextChannel } from 'discord.js';
|
import { EmbedBuilder, TextChannel } from 'discord.js';
|
||||||
|
import { ExtendedClient } from './ExtendedClient.js';
|
||||||
|
|
||||||
export class TwitchManager {
|
export class TwitchManager {
|
||||||
private static accessToken: string | null = null;
|
private static accessToken: string | null = null;
|
||||||
@@ -29,35 +30,59 @@ export class TwitchManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchStreamData(channelName: string) {
|
/**
|
||||||
|
* Fetches stream data for multiple channels in a single batch (max 100).
|
||||||
|
*/
|
||||||
|
private static async fetchStreamsBatch(channelNames: string[]) {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
if (!token) return null;
|
if (!token || channelNames.length === 0) return [];
|
||||||
|
|
||||||
|
// Twitch API allows up to 100 user_login parameters
|
||||||
|
const query = channelNames.map(name => `user_login=${encodeURIComponent(name)}`).join('&');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`https://api.twitch.tv/helix/streams?user_login=${channelName}`, {
|
const response = await fetch(`https://api.twitch.tv/helix/streams?${query}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Client-ID': process.env.TWITCH_CLIENT_ID!,
|
'Client-ID': process.env.TWITCH_CLIENT_ID!,
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const data: any = await response.json();
|
const data: any = await response.json();
|
||||||
return data.data?.[0] || null;
|
return data.data || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[TWITCH] Error fetching stream data for ${channelName}:`, error);
|
console.error(`[TWITCH] Error fetching batch (${channelNames.length} channels):`, error);
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async checkStreams(client: any) {
|
/**
|
||||||
const token = await this.getAccessToken();
|
* Legacy helper for single channel fetch (e.g. for commands)
|
||||||
if (!token) return;
|
*/
|
||||||
|
static async fetchStreamData(channelName: string) {
|
||||||
|
const streams = await this.fetchStreamsBatch([channelName]);
|
||||||
|
return streams[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
const monitors: any[] = (client as any).DB?.all('SELECT * FROM twitch_monitors') || [];
|
static async checkStreams(client: ExtendedClient) {
|
||||||
|
const monitors: any[] = client.DB?.all('SELECT * FROM twitch_monitors') || [];
|
||||||
if (monitors.length === 0) return;
|
if (monitors.length === 0) return;
|
||||||
|
|
||||||
|
const uniqueChannels = [...new Set(monitors.map(m => m.channel_name.toLowerCase()))];
|
||||||
|
const onlineStreamsMap = new Map<string, any>();
|
||||||
|
|
||||||
|
// Process in chunks of 100 (Twitch API limit)
|
||||||
|
for (let i = 0; i < uniqueChannels.length; i += 100) {
|
||||||
|
const chunk = uniqueChannels.slice(i, i + 100);
|
||||||
|
const streams = await this.fetchStreamsBatch(chunk);
|
||||||
|
for (const stream of streams) {
|
||||||
|
onlineStreamsMap.set(stream.user_login.toLowerCase(), stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: { id: number, status: 'online' | 'offline', streamId?: string }[] = [];
|
||||||
|
|
||||||
for (const monitor of monitors) {
|
for (const monitor of monitors) {
|
||||||
try {
|
const stream = onlineStreamsMap.get(monitor.channel_name.toLowerCase());
|
||||||
const stream = await this.fetchStreamData(monitor.channel_name);
|
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
const isNewStream = monitor.last_stream_id !== stream.id;
|
const isNewStream = monitor.last_stream_id !== stream.id;
|
||||||
@@ -65,24 +90,37 @@ export class TwitchManager {
|
|||||||
|
|
||||||
if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
if (wasOffline || (monitor.last_status === 'online' && isNewStream)) {
|
||||||
await this.sendNotification(client, monitor, stream);
|
await this.sendNotification(client, monitor, stream);
|
||||||
(client as any).DB?.run(
|
updates.push({ id: monitor.id, status: 'online', streamId: stream.id });
|
||||||
"UPDATE twitch_monitors SET last_status = 'online', last_stream_id = ? WHERE id = ?",
|
|
||||||
stream.id,
|
|
||||||
monitor.id
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (monitor.last_status === 'online') {
|
if (monitor.last_status === 'online') {
|
||||||
(client as any).DB?.run("UPDATE twitch_monitors SET last_status = 'offline', last_stream_id = NULL WHERE id = ?", monitor.id);
|
updates.push({ id: monitor.id, status: 'offline' });
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[TWITCH] Error checking stream ${monitor.channel_name}:`, error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async sendNotification(client: any, monitor: any, stream: any) {
|
if (updates.length > 0) {
|
||||||
|
client.DB?.transaction(() => {
|
||||||
|
for (const update of updates) {
|
||||||
|
if (update.status === 'online') {
|
||||||
|
client.DB?.run(
|
||||||
|
"UPDATE twitch_monitors SET last_status = 'online', last_stream_id = ? WHERE id = ?",
|
||||||
|
update.streamId,
|
||||||
|
update.id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
client.DB?.run(
|
||||||
|
"UPDATE twitch_monitors SET last_status = 'offline', last_stream_id = NULL WHERE id = ?",
|
||||||
|
update.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async sendNotification(client: ExtendedClient, monitor: any, stream: any) {
|
||||||
|
try {
|
||||||
const guild = client.guilds.cache.get(monitor.guild_id);
|
const guild = client.guilds.cache.get(monitor.guild_id);
|
||||||
if (!guild || !monitor.discord_channel_id) return;
|
if (!guild || !monitor.discord_channel_id) return;
|
||||||
|
|
||||||
@@ -111,10 +149,14 @@ export class TwitchManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await channel.send({ content, embeds: [embed] });
|
await channel.send({ content, embeds: [embed] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[TWITCH] Failed to send notification for ${monitor.channel_name}:`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static startPolling(client: any) {
|
static startPolling(client: any) {
|
||||||
setInterval(() => this.checkStreams(client), 5 * 60 * 1000);
|
// Polling every 2 minutes for faster notifications
|
||||||
console.log('[TWITCH] Polling started (5m interval).');
|
setInterval(() => this.checkStreams(client), 2 * 60 * 1000);
|
||||||
|
console.log('[TWITCH] Batch-Polling started (2m interval).');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
315
src/structures/TwitchMonitor.ts
Normal file
315
src/structures/TwitchMonitor.ts
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import tmi, { Client, ChatUserstate, DeleteUserstate, SubMethods, SubUserstate } from 'tmi.js';
|
||||||
|
import { EmbedBuilder, WebhookClient } from 'discord.js';
|
||||||
|
import { TwitchCache, ChatMessage } from './TwitchCache.js';
|
||||||
|
|
||||||
|
const MOD_ICON = 'https://static-cdn.jtvnw.net/jtv_user_pictures/a736384e-8a1d-40ed-8a7a-808a23a15476-profile_image-300x300.png';
|
||||||
|
const TOKEN_CHECK_INTERVAL = 5 * 60 * 1000;
|
||||||
|
const TOKEN_CHECK_MAX_FAILURES = 3;
|
||||||
|
const TOKEN_CHECK_RETRY_DELAY = 30 * 1000;
|
||||||
|
|
||||||
|
interface MonitorChannel {
|
||||||
|
guildId: string;
|
||||||
|
twitchChannel: string;
|
||||||
|
discordChannelId: string;
|
||||||
|
webhookUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TwitchMonitor {
|
||||||
|
private static instance: TwitchMonitor | null = null;
|
||||||
|
private client: Client | null = null;
|
||||||
|
private cache: TwitchCache;
|
||||||
|
private monitors: Map<string, MonitorChannel> = new Map();
|
||||||
|
private webhookClients: Map<string, WebhookClient> = new Map();
|
||||||
|
private tokenFailureCount = 0;
|
||||||
|
private tokenCheckInterval: NodeJS.Timeout | null = null;
|
||||||
|
private isTokenInvalid = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.cache = new TwitchCache(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): TwitchMonitor {
|
||||||
|
if (!TwitchMonitor.instance) {
|
||||||
|
TwitchMonitor.instance = new TwitchMonitor();
|
||||||
|
}
|
||||||
|
return TwitchMonitor.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOAuthToken(): string {
|
||||||
|
const token = process.env.TWITCH_OAUTH_TOKEN || '';
|
||||||
|
return token.startsWith('oauth:') ? token : 'oauth:' + token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyOwner(message: string): Promise<void> {
|
||||||
|
const ownerId = process.env.BOT_OWNER_ID;
|
||||||
|
if (!ownerId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { Client: DiscordClient } = await import('discord.js');
|
||||||
|
const client = TwitchMonitor.getInstance();
|
||||||
|
const discordClient = (client as any).discordClient;
|
||||||
|
if (!discordClient) return;
|
||||||
|
|
||||||
|
const owner = await discordClient.users.fetch(ownerId).catch(() => null);
|
||||||
|
if (owner) {
|
||||||
|
await owner.send(`⚠️ **TwitchMonitor Warning**\n${message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TWITCH] Failed to notify owner:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTokenValidation(): void {
|
||||||
|
this.tokenCheckInterval = setInterval(async () => {
|
||||||
|
if (this.isTokenInvalid) return;
|
||||||
|
|
||||||
|
const isConnected = this.client?.readyState() === 'OPEN';
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
this.tokenFailureCount++;
|
||||||
|
console.warn(`[TWITCH] Connection check failed (${this.tokenFailureCount}/${TOKEN_CHECK_MAX_FAILURES})`);
|
||||||
|
|
||||||
|
if (this.tokenFailureCount >= TOKEN_CHECK_MAX_FAILURES) {
|
||||||
|
this.isTokenInvalid = true;
|
||||||
|
console.error('[TWITCH] Token validation failed - stopping monitoring');
|
||||||
|
this.notifyOwner('TwitchMonitor Token ungültig. Bitte neuen Token holen: https://chatterino.com/client_login');
|
||||||
|
this.stopTokenValidation();
|
||||||
|
this.disconnect();
|
||||||
|
} else {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, TOKEN_CHECK_RETRY_DELAY));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.tokenFailureCount = 0;
|
||||||
|
}
|
||||||
|
}, TOKEN_CHECK_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTokenValidation(): void {
|
||||||
|
if (this.tokenCheckInterval) {
|
||||||
|
clearInterval(this.tokenCheckInterval);
|
||||||
|
this.tokenCheckInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(): Promise<void> {
|
||||||
|
if (this.client) return;
|
||||||
|
|
||||||
|
const channels = Array.from(this.monitors.values()).map(m => m.twitchChannel);
|
||||||
|
|
||||||
|
this.client = new Client({
|
||||||
|
options: {
|
||||||
|
debug: false,
|
||||||
|
},
|
||||||
|
identity: {
|
||||||
|
username: process.env.TWITCH_USERNAME,
|
||||||
|
password: this.getOAuthToken(),
|
||||||
|
},
|
||||||
|
channels: channels,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('message', (channel: string, userstate: ChatUserstate, text: string, self: boolean) => {
|
||||||
|
if (self) return;
|
||||||
|
this.cache.push({
|
||||||
|
channel,
|
||||||
|
user: userstate['display-name'] || userstate.username || 'unknown',
|
||||||
|
text,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
msgId: userstate.id || '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('messagedeleted', (channel: string, username: string, deletedMessage: string, userstate: DeleteUserstate) => {
|
||||||
|
const msgId = userstate['target-msg-id'] || '';
|
||||||
|
const cached = this.cache.getByMsgId(msgId);
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '🗑️ Nachricht gelöscht',
|
||||||
|
description: `**${username}** hat eine Nachricht gelöscht.\n\n${cached ? `~~${cached.text}~~` : `(Nachricht nicht im Cache)`}`,
|
||||||
|
color: 0xff0000,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('ban', (channel: string, username: string, reason: string) => {
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const lastMessages = this.cache.getLastN(username, channel, 5);
|
||||||
|
const context = lastMessages.length > 0
|
||||||
|
? `\n\n**Letzte Nachrichten:**\n${lastMessages.map(m => `• ${m.text}`).join('\n')}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '🔨 User gebannt',
|
||||||
|
description: `**${username}** wurde gebannt.\n${reason ? `Grund: *${reason}*` : ''}${context}`,
|
||||||
|
color: 0x8b0000,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('timeout', (channel: string, username: string, reason: string) => {
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '⏱️ User getimeouted',
|
||||||
|
description: `**${username}** wurde getimeouted.\n${reason ? `Grund: *${reason}*` : ''}`,
|
||||||
|
color: 0xffa500,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('clearchat', (channel: string) => {
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '🧹 Chat geleert',
|
||||||
|
description: `Der Chat in **${channel}** wurde geleert.`,
|
||||||
|
color: 0xffa500,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('cheer', (channel: string, userstate: ChatUserstate, message: string) => {
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
const amount = userstate.bits || 0;
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '💰 Bit-Cheer',
|
||||||
|
description: `**${userstate['display-name'] || userstate.username}** hat ${amount} Bits gespendet!\n${message || ''}`,
|
||||||
|
color: 0x9b59b6,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('subscription', (channel: string, username: string, methods: SubMethods, message: string, userstate: SubUserstate) => {
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '🅿️ Subscription',
|
||||||
|
description: `**${username}** hat subscribed!${methods?.prime ? ' (Prime)' : ''}${message ? `\nNachricht: ${message}` : ''}`,
|
||||||
|
color: 0x9146ff,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('resub', (channel: string, username: string, months: number, message: string, userstate: SubUserstate, methods: SubMethods) => {
|
||||||
|
const result = this.findMonitor(channel);
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
this.sendWebhook(result.monitor.webhookUrl, {
|
||||||
|
title: '🅿️ Resubscription',
|
||||||
|
description: `**${username}** resubbed für ${months} Monate!${methods?.prime ? ' (Prime)' : ''}\n${message || ''}`,
|
||||||
|
color: 0x9146ff,
|
||||||
|
}, result.key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('disconnected', async () => {
|
||||||
|
console.warn('[TWITCH] Disconnected from IRC');
|
||||||
|
if (!this.isTokenInvalid) {
|
||||||
|
this.tokenFailureCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('reconnect', () => {
|
||||||
|
console.log('[TWITCH] Attempting to reconnect...');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.connect();
|
||||||
|
console.log('[TWITCH] Monitor connected to IRC');
|
||||||
|
this.tokenFailureCount = 0;
|
||||||
|
this.startTokenValidation();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[TWITCH] Failed to connect:', error);
|
||||||
|
this.isTokenInvalid = true;
|
||||||
|
this.notifyOwner('TwitchMonitor: Verbindung fehlgeschlagen. Token prüfen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDiscordClient(discordClient: any): void {
|
||||||
|
(this as any).discordClient = discordClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findMonitor(channel: string): { monitor: MonitorChannel; key: string } | undefined {
|
||||||
|
const normalizedChannel = channel.startsWith('#') ? channel : `#${channel}`;
|
||||||
|
const entry = Array.from(this.monitors.entries()).find(
|
||||||
|
([_, m]) => m.twitchChannel.toLowerCase() === normalizedChannel.toLowerCase() ||
|
||||||
|
m.twitchChannel.toLowerCase() === channel.toLowerCase()
|
||||||
|
);
|
||||||
|
return entry ? { key: entry[0], monitor: entry[1] } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMonitor(guildId: string, twitchChannel: string, discordChannelId: string, webhookUrl: string): Promise<void> {
|
||||||
|
const normalizedChannel = twitchChannel.startsWith('#') ? twitchChannel : `#${twitchChannel}`;
|
||||||
|
const key = `${guildId}:${normalizedChannel}`;
|
||||||
|
|
||||||
|
this.monitors.set(key, {
|
||||||
|
guildId,
|
||||||
|
twitchChannel: normalizedChannel,
|
||||||
|
discordChannelId,
|
||||||
|
webhookUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.webhookClients.set(key, new WebhookClient({ url: webhookUrl }));
|
||||||
|
|
||||||
|
if (this.client) {
|
||||||
|
await this.client.join(normalizedChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMonitor(guildId: string, twitchChannel: string): Promise<void> {
|
||||||
|
const normalizedChannel = twitchChannel.startsWith('#') ? twitchChannel : `#${twitchChannel}`;
|
||||||
|
const key = `${guildId}:${normalizedChannel}`;
|
||||||
|
const monitor = this.monitors.get(key);
|
||||||
|
|
||||||
|
if (monitor && this.client) {
|
||||||
|
await this.client.part(normalizedChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.monitors.delete(key);
|
||||||
|
this.webhookClients.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMonitors(guildId?: string): MonitorChannel[] {
|
||||||
|
if (guildId) {
|
||||||
|
return Array.from(this.monitors.values()).filter(m => m.guildId === guildId);
|
||||||
|
}
|
||||||
|
return Array.from(this.monitors.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendWebhook(webhookUrl: string, data: { title: string; description: string; color: number }, monitorKey?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
let webhook: WebhookClient;
|
||||||
|
|
||||||
|
if (monitorKey && this.webhookClients.has(monitorKey)) {
|
||||||
|
webhook = this.webhookClients.get(monitorKey)!;
|
||||||
|
} else {
|
||||||
|
webhook = new WebhookClient({ url: webhookUrl });
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setTitle(data.title)
|
||||||
|
.setDescription(data.description)
|
||||||
|
.setColor(data.color)
|
||||||
|
.setThumbnail(MOD_ICON)
|
||||||
|
.setTimestamp();
|
||||||
|
|
||||||
|
await webhook.send({ embeds: [embed] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[TWITCH] Webhook send error${monitorKey ? ` for ${monitorKey}` : ''}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
this.stopTokenValidation();
|
||||||
|
if (this.client) {
|
||||||
|
this.client.disconnect();
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
this.monitors.clear();
|
||||||
|
this.webhookClients.clear();
|
||||||
|
this.tokenFailureCount = 0;
|
||||||
|
this.isTokenInvalid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user