Fixed Invite Accordion
This commit is contained in:
@@ -1049,7 +1049,36 @@ app.get('/api/servers/:guildId/invites', async (req, res) => {
|
||||
app.post('/api/servers/:guildId/invites', async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
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 });
|
||||
} 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' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,6 +94,14 @@
|
||||
- [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] 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
|
||||
- [x] Postgres support via `DATABASE_URL` (backend auto-creates `servers`, `invites`, `users`)
|
||||
|
||||
@@ -90,10 +90,24 @@ async function listInvites(guildId) {
|
||||
async function addInvite(guildId, invite) {
|
||||
const path = `/api/servers/${guildId}/invites`;
|
||||
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, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(invite),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return res && res.ok;
|
||||
} catch (e) {
|
||||
@@ -208,4 +222,44 @@ async function getAutoroleSettings(guildId) {
|
||||
return json || { enabled: false, roleId: '' };
|
||||
}
|
||||
|
||||
module.exports = { getServerSettings, upsertServerSettings, getCommands, toggleCommand, listInvites, addInvite, deleteInvite, getTwitchUsers, addTwitchUser, deleteTwitchUser, tryFetchTwitchStreams, _rawGetTwitchStreams, 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 };
|
||||
|
||||
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 deployCommands = require('../deploy-commands');
|
||||
const api = require('../api');
|
||||
|
||||
module.exports = {
|
||||
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 = [
|
||||
{ name: 'Watch EhChad Live!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
|
||||
{ name: 'Follow EhChad!', type: ActivityType.Streaming, url: 'https://twitch.tv/ehchad' },
|
||||
|
||||
@@ -269,6 +269,22 @@ const ServerSettings = () => {
|
||||
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('kickUsersUpdate', onKickUsers);
|
||||
eventTarget.addEventListener('liveNotificationsUpdate', onLiveNotifications);
|
||||
@@ -276,6 +292,8 @@ const ServerSettings = () => {
|
||||
eventTarget.addEventListener('adminLogAdded', onAdminLogAdded);
|
||||
eventTarget.addEventListener('adminLogDeleted', onAdminLogDeleted);
|
||||
eventTarget.addEventListener('adminLogsCleared', onAdminLogsCleared);
|
||||
eventTarget.addEventListener('inviteCreated', onInviteCreated);
|
||||
eventTarget.addEventListener('inviteDeleted', onInviteDeleted);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
@@ -286,6 +304,8 @@ const ServerSettings = () => {
|
||||
eventTarget.removeEventListener('adminLogAdded', onAdminLogAdded);
|
||||
eventTarget.removeEventListener('adminLogDeleted', onAdminLogDeleted);
|
||||
eventTarget.removeEventListener('adminLogsCleared', onAdminLogsCleared);
|
||||
eventTarget.removeEventListener('inviteCreated', onInviteCreated);
|
||||
eventTarget.removeEventListener('inviteDeleted', onInviteDeleted);
|
||||
} catch (err) {}
|
||||
};
|
||||
}, [eventTarget, guildId]);
|
||||
@@ -669,7 +689,9 @@ const ServerSettings = () => {
|
||||
<FormControl fullWidth>
|
||||
<Select value={inviteForm.channelId} onChange={(e) => setInviteForm(f => ({ ...f, channelId: e.target.value }))} displayEmpty>
|
||||
<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>
|
||||
</FormControl>
|
||||
</Box>
|
||||
@@ -778,7 +800,7 @@ const ServerSettings = () => {
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="" disabled>Select a channel</MenuItem>
|
||||
{channels.map(channel => (
|
||||
{channels.map((channel) => (
|
||||
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
@@ -819,7 +841,7 @@ const ServerSettings = () => {
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="" disabled>Select a channel</MenuItem>
|
||||
{channels.map(channel => (
|
||||
{channels.map((channel) => (
|
||||
<MenuItem key={channel.id} value={channel.id}>{channel.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
Reference in New Issue
Block a user