Compare commits
2 Commits
ff10bb3183
...
8236c1e0e7
| Author | SHA1 | Date | |
|---|---|---|---|
| 8236c1e0e7 | |||
| 900ce85e2c |
@@ -1049,7 +1049,36 @@ app.get('/api/servers/:guildId/invites', async (req, res) => {
|
|||||||
app.post('/api/servers/:guildId/invites', async (req, res) => {
|
app.post('/api/servers/:guildId/invites', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { guildId } = req.params;
|
const { guildId } = req.params;
|
||||||
const { channelId, maxAge, maxUses, temporary } = req.body || {};
|
const { code, url, channelId, maxAge, maxUses, temporary, createdAt } = req.body || {};
|
||||||
|
|
||||||
|
// If code is provided, this is an existing invite to store (from Discord events)
|
||||||
|
if (code) {
|
||||||
|
const item = {
|
||||||
|
code,
|
||||||
|
url: url || `https://discord.gg/${code}`,
|
||||||
|
channelId: channelId || '',
|
||||||
|
createdAt: createdAt || new Date().toISOString(),
|
||||||
|
maxUses: maxUses || 0,
|
||||||
|
maxAge: maxAge || 0,
|
||||||
|
temporary: !!temporary,
|
||||||
|
};
|
||||||
|
|
||||||
|
await pgClient.addInvite({
|
||||||
|
code: item.code,
|
||||||
|
guildId,
|
||||||
|
url: item.url,
|
||||||
|
channelId: item.channelId,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
maxUses: item.maxUses,
|
||||||
|
maxAge: item.maxAge,
|
||||||
|
temporary: item.temporary
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, invite: item });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, create a new invite
|
||||||
const guild = bot.client.guilds.cache.get(guildId);
|
const guild = bot.client.guilds.cache.get(guildId);
|
||||||
if (!guild) return res.status(404).json({ success: false, message: 'Guild not found' });
|
if (!guild) return res.status(404).json({ success: false, message: 'Guild not found' });
|
||||||
|
|
||||||
@@ -1089,7 +1118,7 @@ app.post('/api/servers/:guildId/invites', async (req, res) => {
|
|||||||
|
|
||||||
res.json({ success: true, invite: item });
|
res.json({ success: true, invite: item });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating invite:', error);
|
console.error('Error creating/storing invite:', error);
|
||||||
res.status(500).json({ success: false, message: 'Internal Server Error' });
|
res.status(500).json({ success: false, message: 'Internal Server Error' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
16
checklist.md
16
checklist.md
@@ -51,6 +51,12 @@
|
|||||||
- [x] Live Notifications: bot posts rich embed to channel when a watched Twitch user goes live (thumbnail, clickable title, bio/description, category/game, viewers, footer with "ehchadservices" and start datetime)
|
- [x] Live Notifications: bot posts rich embed to channel when a watched Twitch user goes live (thumbnail, clickable title, bio/description, category/game, viewers, footer with "ehchadservices" and start datetime)
|
||||||
- [x] Live Notifications polling frequency set to 5 seconds (configurable via `TWITCH_POLL_INTERVAL_MS`)
|
- [x] Live Notifications polling frequency set to 5 seconds (configurable via `TWITCH_POLL_INTERVAL_MS`)
|
||||||
- [x] On bot restart, sends messages for currently live watched users; then sends for new streams once per session
|
- [x] On bot restart, sends messages for currently live watched users; then sends for new streams once per session
|
||||||
|
- [x] Twitch Watcher Debug Logging: comprehensive debug mode added (enable with `TWITCH_WATCHER_DEBUG=true`) to track guild checks, settings retrieval, stream fetching, channel permissions, and message sending for troubleshooting live notification issues
|
||||||
|
- [x] Twitch API Functions Export Fix: added missing `tryFetchTwitchStreams` and `_rawGetTwitchStreams` to api.js module exports to resolve "is not a function" errors
|
||||||
|
- [x] Twitch Streams Array Safety: added `Array.isArray()` checks in twitch-watcher.js to prevent "filter is not a function" errors when API returns unexpected data types
|
||||||
|
- [x] Twitch Commands Postgres Integration: updated all Discord bot Twitch commands (`/add-twitchuser`, `/remove-twitchuser`) to use api.js functions for consistent Postgres backend communication
|
||||||
|
- [x] Twitch Message Template Variables: added support for `{user}`, `{title}`, `{category}`, and `{viewers}` template variables in custom live notification messages for dynamic content insertion
|
||||||
|
- [x] Frontend JSX Syntax Fix: fixed React Fragment wrapping for admin logs map to resolve build compilation errors
|
||||||
- [x] Frontend: show "Watch Live" button next to watched user when they are live (links to Twitch)
|
- [x] Frontend: show "Watch Live" button next to watched user when they are live (links to Twitch)
|
||||||
- [x] Bi-directional sync: backend POST/DELETE for twitch-users now also pushes new settings to bot process (when `BOT_PUSH_URL` configured)
|
- [x] Bi-directional sync: backend POST/DELETE for twitch-users now also pushes new settings to bot process (when `BOT_PUSH_URL` configured)
|
||||||
- [x] Bot adds/removes users via backend endpoints ensuring single source of truth (Postgres)
|
- [x] Bot adds/removes users via backend endpoints ensuring single source of truth (Postgres)
|
||||||
@@ -86,7 +92,16 @@
|
|||||||
- [x] Frontend delete buttons for individual logs and delete all logs with confirm dialogs
|
- [x] Frontend delete buttons for individual logs and delete all logs with confirm dialogs
|
||||||
- [x] Live updates between bot and frontend using SSE events for real-time log synchronization
|
- [x] Live updates between bot and frontend using SSE events for real-time log synchronization
|
||||||
- [x] Admin logs properly display the username who called the command and the user they called it on for both bot slash commands and frontend moderation actions
|
- [x] Admin logs properly display the username who called the command and the user they called it on for both bot slash commands and frontend moderation actions
|
||||||
|
- [x] Bot command username logging fixed: uses correct Discord user properties (username/global_name instead of deprecated tag)
|
||||||
- [x] Bot event handlers: added guildCreate and guildDelete events to publish SSE notifications for live dashboard updates
|
- [x] Bot event handlers: added guildCreate and guildDelete events to publish SSE notifications for live dashboard updates
|
||||||
|
- [x] Invite synchronization: real-time sync between Discord server events and frontend
|
||||||
|
- [x] Discord event handlers for inviteCreate and inviteDelete events
|
||||||
|
- [x] Only bot-created invites are tracked and synchronized
|
||||||
|
- [x] Frontend SSE event listeners for inviteCreated and inviteDeleted events
|
||||||
|
- [x] Backend API updated to store existing invites from Discord events
|
||||||
|
- [x] Invite deletions from Discord server are immediately reflected in frontend
|
||||||
|
- [x] Offline reconciliation: bot detects and removes invites deleted while offline on startup
|
||||||
|
- [x] Automatic cleanup of stale invites from database and frontend when bot comes back online
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
- [x] Postgres support via `DATABASE_URL` (backend auto-creates `servers`, `invites`, `users`)
|
- [x] Postgres support via `DATABASE_URL` (backend auto-creates `servers`, `invites`, `users`)
|
||||||
@@ -155,4 +170,3 @@
|
|||||||
- [x] Fixed ESLint warnings: removed unused imports and handlers, added proper dependency management
|
- [x] Fixed ESLint warnings: removed unused imports and handlers, added proper dependency management
|
||||||
- [x] Fixed compilation errors: added missing MUI imports and Snackbar component
|
- [x] Fixed compilation errors: added missing MUI imports and Snackbar component
|
||||||
- [x] Navbar visibility: enhanced with solid background and stronger border for better visibility across all themes
|
- [x] Navbar visibility: enhanced with solid background and stronger border for better visibility across all themes
|
||||||
|
|
||||||
@@ -90,10 +90,24 @@ async function listInvites(guildId) {
|
|||||||
async function addInvite(guildId, invite) {
|
async function addInvite(guildId, invite) {
|
||||||
const path = `/api/servers/${guildId}/invites`;
|
const path = `/api/servers/${guildId}/invites`;
|
||||||
try {
|
try {
|
||||||
|
// If invite is an object with code property, it's already created - send full data
|
||||||
|
// If it's just channelId/maxAge/etc, it's for creation
|
||||||
|
const isExistingInvite = invite && typeof invite === 'object' && invite.code;
|
||||||
|
|
||||||
|
const body = isExistingInvite ? {
|
||||||
|
code: invite.code,
|
||||||
|
url: invite.url,
|
||||||
|
channelId: invite.channelId,
|
||||||
|
maxUses: invite.maxUses,
|
||||||
|
maxAge: invite.maxAge,
|
||||||
|
temporary: invite.temporary,
|
||||||
|
createdAt: invite.createdAt
|
||||||
|
} : invite;
|
||||||
|
|
||||||
const res = await tryFetch(path, {
|
const res = await tryFetch(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(invite),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
return res && res.ok;
|
return res && res.ok;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -208,4 +222,44 @@ async function getAutoroleSettings(guildId) {
|
|||||||
return json || { enabled: false, roleId: '' };
|
return json || { enabled: false, roleId: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, getTwitchUsers, addTwitchUser, deleteTwitchUser, getKickUsers, addKickUser, deleteKickUser, getWelcomeLeaveSettings, getAutoroleSettings };
|
async function reconcileInvites(guildId, currentDiscordInvites) {
|
||||||
|
try {
|
||||||
|
// Get invites from database
|
||||||
|
const dbInvites = await listInvites(guildId) || [];
|
||||||
|
|
||||||
|
// Find invites in database that no longer exist in Discord
|
||||||
|
const discordInviteCodes = new Set(currentDiscordInvites.map(inv => inv.code));
|
||||||
|
const deletedInvites = dbInvites.filter(dbInv => !discordInviteCodes.has(dbInv.code));
|
||||||
|
|
||||||
|
// Delete each invite that no longer exists
|
||||||
|
for (const invite of deletedInvites) {
|
||||||
|
console.log(`🗑️ Reconciling deleted invite ${invite.code} for guild ${guildId}`);
|
||||||
|
await deleteInvite(guildId, invite.code);
|
||||||
|
|
||||||
|
// Publish SSE event for frontend update
|
||||||
|
try {
|
||||||
|
await tryFetch('/api/events/publish', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
event: 'inviteDeleted',
|
||||||
|
data: { code: invite.code, guildId }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
} catch (sseErr) {
|
||||||
|
console.error('Failed to publish SSE event for reconciled invite deletion:', sseErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deletedInvites.length > 0) {
|
||||||
|
console.log(`✅ Reconciled ${deletedInvites.length} deleted invites for guild ${guildId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedInvites.length;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to reconcile invites for guild ${guildId}:`, e && e.message ? e.message : e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, getTwitchUsers, addTwitchUser, deleteTwitchUser, tryFetchTwitchStreams, _rawGetTwitchStreams, getKickUsers, addKickUser, deleteKickUser, getWelcomeLeaveSettings, getAutoroleSettings, reconcileInvites };
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
|
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
|
||||||
const fetch = require('node-fetch');
|
const api = require('../api');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'add-twitchuser',
|
name: 'add-twitchuser',
|
||||||
@@ -16,20 +16,14 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
const username = interaction.options.getString('username').toLowerCase().trim();
|
const username = interaction.options.getString('username').toLowerCase().trim();
|
||||||
try {
|
try {
|
||||||
const backendBase = process.env.BACKEND_BASE || `http://${process.env.HOST || '127.0.0.1'}:${process.env.PORT || 3002}`;
|
const success = await api.addTwitchUser(interaction.guildId, username);
|
||||||
const resp = await fetch(`${backendBase}/api/servers/${interaction.guildId}/twitch-users`, {
|
if (success) {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username })
|
|
||||||
});
|
|
||||||
if (resp.ok) {
|
|
||||||
await interaction.reply({ content: `Added ${username} to watch list.`, flags: 64 });
|
await interaction.reply({ content: `Added ${username} to watch list.`, flags: 64 });
|
||||||
// Refresh cached settings from backend so watcher sees new user immediately
|
// Refresh cached settings from backend so watcher sees new user immediately
|
||||||
try {
|
try {
|
||||||
const settingsResp = await fetch(`${backendBase}/api/servers/${interaction.guildId}/settings`);
|
const settings = await api.getServerSettings(interaction.guildId);
|
||||||
if (settingsResp.ok) {
|
|
||||||
const json = await settingsResp.json();
|
|
||||||
const bot = require('..');
|
const bot = require('..');
|
||||||
if (bot && bot.setGuildSettings) bot.setGuildSettings(interaction.guildId, json);
|
if (bot && bot.setGuildSettings) bot.setGuildSettings(interaction.guildId, settings);
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: 'Failed to add user via backend.', flags: 64 });
|
await interaction.reply({ content: 'Failed to add user via backend.', flags: 64 });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
|
const { SlashCommandBuilder, PermissionsBitField } = require('discord.js');
|
||||||
const fetch = require('node-fetch');
|
const api = require('../api');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'remove-twitchuser',
|
name: 'remove-twitchuser',
|
||||||
@@ -16,18 +16,14 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
const username = interaction.options.getString('username').toLowerCase().trim();
|
const username = interaction.options.getString('username').toLowerCase().trim();
|
||||||
try {
|
try {
|
||||||
const backendBase = process.env.BACKEND_BASE || `http://${process.env.HOST || '127.0.0.1'}:${process.env.PORT || 3002}`;
|
const success = await api.deleteTwitchUser(interaction.guildId, username);
|
||||||
const resp = await fetch(`${backendBase}/api/servers/${interaction.guildId}/twitch-users/${encodeURIComponent(username)}`, { method: 'DELETE' });
|
if (success) {
|
||||||
if (resp.ok) {
|
|
||||||
await interaction.reply({ content: `Removed ${username} from watch list.`, flags: 64 });
|
await interaction.reply({ content: `Removed ${username} from watch list.`, flags: 64 });
|
||||||
// Refresh cached settings from backend
|
// Refresh cached settings from backend
|
||||||
try {
|
try {
|
||||||
const settingsResp = await fetch(`${backendBase}/api/servers/${interaction.guildId}/settings`);
|
const settings = await api.getServerSettings(interaction.guildId);
|
||||||
if (settingsResp.ok) {
|
|
||||||
const json = await settingsResp.json();
|
|
||||||
const bot = require('..');
|
const bot = require('..');
|
||||||
if (bot && bot.setGuildSettings) bot.setGuildSettings(interaction.guildId, json);
|
if (bot && bot.setGuildSettings) bot.setGuildSettings(interaction.guildId, settings);
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
} else {
|
} else {
|
||||||
await interaction.reply({ content: 'Failed to remove user via backend.', flags: 64 });
|
await interaction.reply({ content: 'Failed to remove user via backend.', flags: 64 });
|
||||||
|
|||||||
49
discord-bot/events/inviteCreate.js
Normal file
49
discord-bot/events/inviteCreate.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const api = require('../api');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'inviteCreate',
|
||||||
|
async execute(invite) {
|
||||||
|
try {
|
||||||
|
// Only track invites created by the bot or in channels the bot can access
|
||||||
|
const guildId = invite.guild.id;
|
||||||
|
|
||||||
|
// Check if this invite was created by our bot
|
||||||
|
const isBotCreated = invite.inviter && invite.inviter.id === invite.client.user.id;
|
||||||
|
|
||||||
|
if (isBotCreated) {
|
||||||
|
// Add to database if created by bot
|
||||||
|
const inviteData = {
|
||||||
|
code: invite.code,
|
||||||
|
guildId: guildId,
|
||||||
|
url: invite.url,
|
||||||
|
channelId: invite.channel.id,
|
||||||
|
createdAt: invite.createdAt ? invite.createdAt.toISOString() : new Date().toISOString(),
|
||||||
|
maxUses: invite.maxUses || 0,
|
||||||
|
maxAge: invite.maxAge || 0,
|
||||||
|
temporary: invite.temporary || false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the API to add the invite to database
|
||||||
|
await api.addInvite(inviteData);
|
||||||
|
|
||||||
|
// Publish SSE event for real-time frontend updates
|
||||||
|
const bot = require('..');
|
||||||
|
if (bot && bot.publishEvent) {
|
||||||
|
bot.publishEvent(guildId, 'inviteCreated', {
|
||||||
|
code: invite.code,
|
||||||
|
url: invite.url,
|
||||||
|
channelId: invite.channel.id,
|
||||||
|
maxUses: invite.maxUses || 0,
|
||||||
|
maxAge: invite.maxAge || 0,
|
||||||
|
temporary: invite.temporary || false,
|
||||||
|
createdAt: invite.createdAt ? invite.createdAt.toISOString() : new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Note: We don't automatically add invites created by other users to avoid spam
|
||||||
|
// Only bot-created invites are tracked for the web interface
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling inviteCreate:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
24
discord-bot/events/inviteDelete.js
Normal file
24
discord-bot/events/inviteDelete.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
const api = require('../api');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'inviteDelete',
|
||||||
|
async execute(invite) {
|
||||||
|
try {
|
||||||
|
const guildId = invite.guild.id;
|
||||||
|
const code = invite.code;
|
||||||
|
|
||||||
|
// Remove from database
|
||||||
|
await api.deleteInvite(guildId, code);
|
||||||
|
|
||||||
|
// Publish SSE event for real-time frontend updates
|
||||||
|
const bot = require('..');
|
||||||
|
if (bot && bot.publishEvent) {
|
||||||
|
bot.publishEvent(guildId, 'inviteDeleted', {
|
||||||
|
code: code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling inviteDelete:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
const { ActivityType } = require('discord.js');
|
const { ActivityType } = require('discord.js');
|
||||||
const deployCommands = require('../deploy-commands');
|
const deployCommands = require('../deploy-commands');
|
||||||
|
const api = require('../api');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'clientReady',
|
name: 'clientReady',
|
||||||
@@ -16,6 +17,31 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reconcile invites for all guilds to detect invites deleted while bot was offline
|
||||||
|
console.log('🔄 Reconciling invites for offline changes...');
|
||||||
|
let totalReconciled = 0;
|
||||||
|
for (const guildId of guildIds) {
|
||||||
|
try {
|
||||||
|
const guild = client.guilds.cache.get(guildId);
|
||||||
|
if (!guild) continue;
|
||||||
|
|
||||||
|
// Fetch current invites from Discord
|
||||||
|
const discordInvites = await guild.invites.fetch();
|
||||||
|
const currentInvites = Array.from(discordInvites.values());
|
||||||
|
|
||||||
|
// Reconcile with database
|
||||||
|
const reconciled = await api.reconcileInvites(guildId, currentInvites);
|
||||||
|
totalReconciled += reconciled;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to reconcile invites for guild ${guildId}:`, e && e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (totalReconciled > 0) {
|
||||||
|
console.log(`✅ Invite reconciliation complete: removed ${totalReconciled} stale invites`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Invite reconciliation complete: no stale invites found');
|
||||||
|
}
|
||||||
|
|
||||||
const activities = [
|
const activities = [
|
||||||
{ name: 'Watch EhChad Live!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
|
{ name: 'Watch EhChad Live!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
|
||||||
{ name: 'Follow EhChad!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
|
{ name: 'Follow EhChad!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
|
||||||
|
|||||||
@@ -60,41 +60,86 @@ async function fetchUserInfo(login) {
|
|||||||
|
|
||||||
let polling = false;
|
let polling = false;
|
||||||
const pollIntervalMs = Number(process.env.TWITCH_POLL_INTERVAL_MS || 5000); // 5s default
|
const pollIntervalMs = Number(process.env.TWITCH_POLL_INTERVAL_MS || 5000); // 5s default
|
||||||
|
const debugMode = false; // Debug logging disabled
|
||||||
|
|
||||||
// Keep track of which streams we've already announced per guild:user -> { started_at }
|
// Keep track of which streams we've already announced per guild:user -> { started_at }
|
||||||
const announced = new Map(); // key: `${guildId}:${user}` -> { started_at }
|
const announced = new Map(); // key: `${guildId}:${user}` -> { started_at }
|
||||||
|
|
||||||
async function checkGuild(client, guild) {
|
async function checkGuild(client, guild) {
|
||||||
|
const guildId = guild.id;
|
||||||
|
const guildName = guild.name;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Intentionally quiet: per-guild checking logs are suppressed to avoid spam
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Checking guild ${guildName} (${guildId})`);
|
||||||
const settings = await api.getServerSettings(guild.id) || {};
|
|
||||||
|
const settings = await api.getServerSettings(guildId) || {};
|
||||||
const liveSettings = settings.liveNotifications || {};
|
const liveSettings = settings.liveNotifications || {};
|
||||||
if (!liveSettings.enabled) return;
|
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: Guild ${guildName} settings:`, {
|
||||||
|
enabled: liveSettings.enabled,
|
||||||
|
channelId: liveSettings.channelId,
|
||||||
|
usersCount: (liveSettings.users || []).length,
|
||||||
|
hasCustomMessage: !!liveSettings.customMessage,
|
||||||
|
hasDefaultMessage: !!liveSettings.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!liveSettings.enabled) {
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Live notifications disabled for ${guildName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const channelId = liveSettings.channelId;
|
const channelId = liveSettings.channelId;
|
||||||
const users = (liveSettings.users || []).map(u => u.toLowerCase()).filter(Boolean);
|
const users = (liveSettings.users || []).map(u => u.toLowerCase()).filter(Boolean);
|
||||||
if (!channelId || users.length === 0) return;
|
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: Guild ${guildName} - Channel: ${channelId}, Users: [${users.join(', ')}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channelId || users.length === 0) {
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Skipping ${guildName} - ${!channelId ? 'No channel configured' : 'No users configured'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ask backend for current live streams
|
// ask backend for current live streams
|
||||||
const query = users.join(',');
|
const query = users.join(',');
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Fetching streams for query: ${query}`);
|
||||||
|
|
||||||
const streams = await api._rawGetTwitchStreams ? api._rawGetTwitchStreams(query) : null;
|
const streams = await api._rawGetTwitchStreams ? api._rawGetTwitchStreams(query) : null;
|
||||||
// If the helper isn't available, try backend proxy
|
// If the helper isn't available, try backend proxy
|
||||||
let live = [];
|
let live = [];
|
||||||
if (streams) live = streams.filter(s => s.is_live);
|
if (streams && Array.isArray(streams)) {
|
||||||
else {
|
live = streams.filter(s => s.is_live);
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Found ${live.length} live streams via _rawGetTwitchStreams`);
|
||||||
|
} else {
|
||||||
|
if (debugMode && streams) {
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: _rawGetTwitchStreams returned non-array:`, typeof streams, streams);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const resp = await api.tryFetchTwitchStreams(query);
|
const resp = await api.tryFetchTwitchStreams(query);
|
||||||
live = (resp || []).filter(s => s.is_live);
|
live = (Array.isArray(resp) ? resp : []).filter(s => s.is_live);
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Found ${live.length} live streams via tryFetchTwitchStreams`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(`❌ TwitchWatcher: Failed to fetch streams for ${guildName}:`, e && e.message ? e.message : e);
|
||||||
live = [];
|
live = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debugMode && live.length > 0) {
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: Live streams:`, live.map(s => `${s.user_login} (${s.viewer_count} viewers)`));
|
||||||
|
}
|
||||||
|
|
||||||
if (!live || live.length === 0) {
|
if (!live || live.length === 0) {
|
||||||
// No live streams: ensure any announced keys for these users are cleared so they can be re-announced later
|
// No live streams: ensure any announced keys for these users are cleared so they can be re-announced later
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
const key = `${guild.id}:${u}`;
|
const key = `${guildId}:${u}`;
|
||||||
if (announced.has(key)) {
|
if (announced.has(key)) {
|
||||||
announced.delete(key);
|
announced.delete(key);
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Cleared announcement for ${u} in ${guildName} (no longer live)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: No live streams found for ${guildName}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,16 +148,28 @@ async function checkGuild(client, guild) {
|
|||||||
let channel = null;
|
let channel = null;
|
||||||
try {
|
try {
|
||||||
channel = await client.channels.fetch(channelId);
|
channel = await client.channels.fetch(channelId);
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Successfully fetched channel ${channel.name} (${channelId}) in ${guildName}`);
|
||||||
|
|
||||||
if (channel.type !== 0) { // 0 is text channel
|
if (channel.type !== 0) { // 0 is text channel
|
||||||
console.error(`TwitchWatcher: channel ${channelId} is not a text channel (type: ${channel.type})`);
|
console.error(`❌ TwitchWatcher: Channel ${channelId} in ${guildName} is not a text channel (type: ${channel.type})`);
|
||||||
channel = null;
|
channel = null;
|
||||||
|
} else {
|
||||||
|
// Check if bot has permission to send messages
|
||||||
|
const permissions = channel.permissionsFor(client.user);
|
||||||
|
if (!permissions || !permissions.has('SendMessages')) {
|
||||||
|
console.error(`❌ TwitchWatcher: Bot lacks SendMessages permission in channel ${channel.name} (${channelId}) for ${guildName}`);
|
||||||
|
channel = null;
|
||||||
|
} else if (debugMode) {
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: Bot has SendMessages permission in ${channel.name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`TwitchWatcher: failed to fetch channel ${channelId}:`, e && e.message ? e.message : e);
|
console.error(`❌ TwitchWatcher: Failed to fetch channel ${channelId} for ${guildName}:`, e && e.message ? e.message : e);
|
||||||
channel = null;
|
channel = null;
|
||||||
}
|
}
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
// Channel not found or inaccessible; skip
|
// Channel not found or inaccessible; skip
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Skipping announcements for ${guildName} - channel unavailable`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,40 +178,51 @@ async function checkGuild(client, guild) {
|
|||||||
|
|
||||||
// Clear announced entries for users that are no longer live
|
// Clear announced entries for users that are no longer live
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
const key = `${guild.id}:${u}`;
|
const key = `${guildId}:${u}`;
|
||||||
if (!liveLogins.has(u) && announced.has(key)) {
|
if (!liveLogins.has(u) && announced.has(key)) {
|
||||||
announced.delete(key);
|
announced.delete(key);
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Cleared announcement for ${u} in ${guildName} (stream ended)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Announce each live once per live session
|
// Announce each live once per live session
|
||||||
for (const s of live) {
|
for (const s of live) {
|
||||||
const login = (s.user_login || '').toLowerCase();
|
const login = (s.user_login || '').toLowerCase();
|
||||||
const key = `${guild.id}:${login}`;
|
const key = `${guildId}:${login}`;
|
||||||
if (announced.has(key)) continue; // already announced for this live session
|
if (announced.has(key)) {
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Skipping ${login} in ${guildName} - already announced`);
|
||||||
|
continue; // already announced for this live session
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Preparing announcement for ${login} in ${guildName}`);
|
||||||
|
|
||||||
// mark announced for this session
|
// mark announced for this session
|
||||||
announced.set(key, { started_at: s.started_at || new Date().toISOString() });
|
announced.set(key, { started_at: s.started_at || new Date().toISOString() });
|
||||||
|
|
||||||
// Build and send embed (standardized layout)
|
// Build and send embed (standardized layout)
|
||||||
try {
|
try {
|
||||||
// Announce without per-guild log spam
|
|
||||||
const { EmbedBuilder } = require('discord.js');
|
const { EmbedBuilder } = require('discord.js');
|
||||||
// Attempt to enrich with user bio (description) if available
|
// Attempt to enrich with user bio (description) if available
|
||||||
let bio = '';
|
let bio = '';
|
||||||
try {
|
try {
|
||||||
const info = await fetchUserInfo(login);
|
const info = await fetchUserInfo(login);
|
||||||
if (info && info.description) bio = info.description.slice(0, 200);
|
if (info && info.description) bio = info.description.slice(0, 200);
|
||||||
} catch (_) {}
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Fetched user info for ${login} - bio length: ${bio.length}`);
|
||||||
|
} catch (e) {
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Failed to fetch user info for ${login}:`, e && e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setColor(0x9146FF)
|
.setColor('#6441A5') // Twitch purple
|
||||||
|
.setAuthor({ name: s.user_name, iconURL: s.profile_image_url || undefined, url: s.url })
|
||||||
.setTitle(s.title || `${s.user_name} is live`)
|
.setTitle(s.title || `${s.user_name} is live`)
|
||||||
.setURL(s.url)
|
.setURL(s.url)
|
||||||
.setAuthor({ name: s.user_name, iconURL: s.profile_image_url || undefined, url: s.url })
|
.setThumbnail(s.profile_image_url || undefined)
|
||||||
.setThumbnail(s.thumbnail_url || s.profile_image_url || undefined)
|
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: 'Category', value: s.game_name || 'Unknown', inline: true },
|
{ name: 'Category', value: s.game_name || 'Unknown', inline: true },
|
||||||
{ name: 'Viewers', value: String(s.viewer_count || 0), inline: true }
|
{ name: 'Viewers', value: String(s.viewer_count || 0), inline: true }
|
||||||
)
|
)
|
||||||
|
.setImage(s.thumbnail_url ? s.thumbnail_url.replace('{width}', '640').replace('{height}', '360') + `?t=${Date.now()}` : null)
|
||||||
.setDescription(bio || (s.description || '').slice(0, 200))
|
.setDescription(bio || (s.description || '').slice(0, 200))
|
||||||
.setFooter({ text: `ehchadservices • Started: ${s.started_at ? new Date(s.started_at).toLocaleString() : 'unknown'}` });
|
.setFooter({ text: `ehchadservices • Started: ${s.started_at ? new Date(s.started_at).toLocaleString() : 'unknown'}` });
|
||||||
|
|
||||||
@@ -167,43 +235,75 @@ async function checkGuild(client, guild) {
|
|||||||
} else {
|
} else {
|
||||||
prefixMsg = `🔴 ${s.user_name} is now live!`;
|
prefixMsg = `🔴 ${s.user_name} is now live!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace template variables in custom messages
|
||||||
|
prefixMsg = prefixMsg
|
||||||
|
.replace(/\{user\}/g, s.user_name || login)
|
||||||
|
.replace(/\{title\}/g, s.title || 'Untitled Stream')
|
||||||
|
.replace(/\{category\}/g, s.game_name || 'Unknown')
|
||||||
|
.replace(/\{viewers\}/g, String(s.viewer_count || 0));
|
||||||
|
|
||||||
|
if (debugMode) {
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: Sending announcement for ${login} in ${guildName} to #${channel.name}`);
|
||||||
|
console.log(`🔍 [DEBUG] TwitchWatcher: Message content: "${prefixMsg}"`);
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure we always hyperlink the title via embed; prefix is optional add above embed
|
// Ensure we always hyperlink the title via embed; prefix is optional add above embed
|
||||||
const payload = prefixMsg ? { content: prefixMsg, embeds: [embed] } : { embeds: [embed] };
|
const payload = prefixMsg ? { content: prefixMsg, embeds: [embed] } : { embeds: [embed] };
|
||||||
await channel.send(payload);
|
await channel.send(payload);
|
||||||
console.log(`🔔 Announced live: ${login} - ${(s.title || '').slice(0, 80)}`);
|
console.log(`🔔 TwitchWatcher: Successfully announced ${login} in ${guildName} - "${(s.title || '').slice(0, 80)}"`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`TwitchWatcher: failed to send announcement for ${login}:`, e && e.message ? e.message : e);
|
console.error(`❌ TwitchWatcher: Failed to send announcement for ${login} in ${guildName}:`, e && e.message ? e.message : e);
|
||||||
// fallback
|
// fallback
|
||||||
const msg = `🔴 ${s.user_name} is live: **${s.title}**\nWatch: ${s.url}`;
|
const msg = `🔴 ${s.user_name} is live: **${s.title}**\nWatch: ${s.url}`;
|
||||||
try { await channel.send({ content: msg }); console.log('TwitchWatcher: fallback message sent'); } catch (err) { console.error('TwitchWatcher: fallback send failed:', err && err.message ? err.message : err); }
|
try {
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Attempting fallback message for ${login} in ${guildName}`);
|
||||||
|
await channel.send({ content: msg });
|
||||||
|
console.log(`🔔 TwitchWatcher: Fallback message sent for ${login} in ${guildName}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`❌ TwitchWatcher: Fallback send failed for ${login} in ${guildName}:`, err && err.message ? err.message : err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error checking guild for live streams:', e && e.message ? e.message : e);
|
console.error(`❌ TwitchWatcher: Error checking guild ${guildName} (${guildId}) for live streams:`, e && e.message ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function poll(client) {
|
async function poll(client) {
|
||||||
if (polling) return;
|
if (polling) return;
|
||||||
polling = true;
|
polling = true;
|
||||||
console.log(`🔁 TwitchWatcher started, polling every ${Math.round(pollIntervalMs/1000)}s`);
|
console.log(`🔁 TwitchWatcher started, polling every ${Math.round(pollIntervalMs/1000)}s${debugMode ? ' (DEBUG MODE ENABLED)' : ''}`);
|
||||||
|
|
||||||
// Initial check on restart: send messages for currently live users
|
// Initial check on restart: send messages for currently live users
|
||||||
try {
|
try {
|
||||||
const guilds = Array.from(client.guilds.cache.values());
|
const guilds = Array.from(client.guilds.cache.values());
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Initial check for ${guilds.length} guilds`);
|
||||||
|
|
||||||
for (const g of guilds) {
|
for (const g of guilds) {
|
||||||
await checkGuild(client, g).catch(err => { console.error('TwitchWatcher: initial checkGuild error', err && err.message ? err.message : err); });
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Initial check for guild ${g.name} (${g.id})`);
|
||||||
|
await checkGuild(client, g).catch(err => {
|
||||||
|
console.error(`❌ TwitchWatcher: Initial checkGuild error for ${g.name}:`, err && err.message ? err.message : err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error during initial twitch check:', e && e.message ? e.message : e);
|
console.error('❌ TwitchWatcher: Error during initial twitch check:', e && e.message ? e.message : e);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (polling) {
|
while (polling) {
|
||||||
try {
|
try {
|
||||||
const guilds = Array.from(client.guilds.cache.values());
|
const guilds = Array.from(client.guilds.cache.values());
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Polling cycle starting for ${guilds.length} guilds`);
|
||||||
|
|
||||||
for (const g of guilds) {
|
for (const g of guilds) {
|
||||||
await checkGuild(client, g).catch(err => { console.error('TwitchWatcher: checkGuild error', err && err.message ? err.message : err); });
|
await checkGuild(client, g).catch(err => {
|
||||||
|
console.error(`❌ TwitchWatcher: checkGuild error for ${g.name}:`, err && err.message ? err.message : err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (debugMode) console.log(`🔍 [DEBUG] TwitchWatcher: Polling cycle completed, waiting ${Math.round(pollIntervalMs/1000)}s`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error during twitch poll loop:', e && e.message ? e.message : e);
|
console.error('❌ TwitchWatcher: Error during twitch poll loop:', e && e.message ? e.message : e);
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, pollIntervalMs));
|
await new Promise(r => setTimeout(r, pollIntervalMs));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,6 +269,22 @@ const ServerSettings = () => {
|
|||||||
setAdminLogs([]);
|
setAdminLogs([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onInviteCreated = (e) => {
|
||||||
|
const data = e.detail || {};
|
||||||
|
if (!data) return;
|
||||||
|
if (data.guildId && data.guildId !== guildId) return;
|
||||||
|
// Add the new invite to the list
|
||||||
|
setInvites(prev => [...prev, data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInviteDeleted = (e) => {
|
||||||
|
const data = e.detail || {};
|
||||||
|
if (!data) return;
|
||||||
|
if (data.guildId && data.guildId !== guildId) return;
|
||||||
|
// Remove the deleted invite from the list
|
||||||
|
setInvites(prev => prev.filter(invite => invite.code !== data.code));
|
||||||
|
};
|
||||||
|
|
||||||
eventTarget.addEventListener('twitchUsersUpdate', onTwitchUsers);
|
eventTarget.addEventListener('twitchUsersUpdate', onTwitchUsers);
|
||||||
eventTarget.addEventListener('kickUsersUpdate', onKickUsers);
|
eventTarget.addEventListener('kickUsersUpdate', onKickUsers);
|
||||||
eventTarget.addEventListener('liveNotificationsUpdate', onLiveNotifications);
|
eventTarget.addEventListener('liveNotificationsUpdate', onLiveNotifications);
|
||||||
@@ -276,6 +292,8 @@ const ServerSettings = () => {
|
|||||||
eventTarget.addEventListener('adminLogAdded', onAdminLogAdded);
|
eventTarget.addEventListener('adminLogAdded', onAdminLogAdded);
|
||||||
eventTarget.addEventListener('adminLogDeleted', onAdminLogDeleted);
|
eventTarget.addEventListener('adminLogDeleted', onAdminLogDeleted);
|
||||||
eventTarget.addEventListener('adminLogsCleared', onAdminLogsCleared);
|
eventTarget.addEventListener('adminLogsCleared', onAdminLogsCleared);
|
||||||
|
eventTarget.addEventListener('inviteCreated', onInviteCreated);
|
||||||
|
eventTarget.addEventListener('inviteDeleted', onInviteDeleted);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
try {
|
||||||
@@ -286,6 +304,8 @@ const ServerSettings = () => {
|
|||||||
eventTarget.removeEventListener('adminLogAdded', onAdminLogAdded);
|
eventTarget.removeEventListener('adminLogAdded', onAdminLogAdded);
|
||||||
eventTarget.removeEventListener('adminLogDeleted', onAdminLogDeleted);
|
eventTarget.removeEventListener('adminLogDeleted', onAdminLogDeleted);
|
||||||
eventTarget.removeEventListener('adminLogsCleared', onAdminLogsCleared);
|
eventTarget.removeEventListener('adminLogsCleared', onAdminLogsCleared);
|
||||||
|
eventTarget.removeEventListener('inviteCreated', onInviteCreated);
|
||||||
|
eventTarget.removeEventListener('inviteDeleted', onInviteDeleted);
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
};
|
};
|
||||||
}, [eventTarget, guildId]);
|
}, [eventTarget, guildId]);
|
||||||
@@ -669,7 +689,9 @@ const ServerSettings = () => {
|
|||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<Select value={inviteForm.channelId} onChange={(e) => setInviteForm(f => ({ ...f, channelId: e.target.value }))} displayEmpty>
|
<Select value={inviteForm.channelId} onChange={(e) => setInviteForm(f => ({ ...f, channelId: e.target.value }))} displayEmpty>
|
||||||
<MenuItem value="">(Any channel)</MenuItem>
|
<MenuItem value="">(Any channel)</MenuItem>
|
||||||
{channels.map(ch => (<MenuItem key={ch.id} value={ch.id}>{ch.name}</MenuItem>))}
|
{channels.map((channel) => (
|
||||||
|
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -778,7 +800,7 @@ const ServerSettings = () => {
|
|||||||
displayEmpty
|
displayEmpty
|
||||||
>
|
>
|
||||||
<MenuItem value="" disabled>Select a channel</MenuItem>
|
<MenuItem value="" disabled>Select a channel</MenuItem>
|
||||||
{channels.map(channel => (
|
{channels.map((channel) => (
|
||||||
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -819,7 +841,7 @@ const ServerSettings = () => {
|
|||||||
displayEmpty
|
displayEmpty
|
||||||
>
|
>
|
||||||
<MenuItem value="" disabled>Select a channel</MenuItem>
|
<MenuItem value="" disabled>Select a channel</MenuItem>
|
||||||
{channels.map(channel => (
|
{channels.map((channel) => (
|
||||||
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -1151,7 +1173,8 @@ const ServerSettings = () => {
|
|||||||
{adminLogs.length === 0 ? (
|
{adminLogs.length === 0 ? (
|
||||||
<Typography>No logs available.</Typography>
|
<Typography>No logs available.</Typography>
|
||||||
) : (
|
) : (
|
||||||
adminLogs.map(log => (
|
<React.Fragment>
|
||||||
|
{adminLogs.map((log) => (
|
||||||
<Box key={log.id} sx={{ p: 1, border: '1px solid #eee', mb: 1, borderRadius: 1, bgcolor: 'background.paper' }}>
|
<Box key={log.id} sx={{ p: 1, border: '1px solid #eee', mb: 1, borderRadius: 1, bgcolor: 'background.paper' }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
@@ -1174,7 +1197,8 @@ const ServerSettings = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
))
|
))}
|
||||||
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user