Two feature batches plus a deploy fix: Moderation & engagement: persistent ModCase history (retrofits ban/kick/ mute/timeout/tempban to record cases), /warn, /watch watchlist with automatic alerts on deletions/tickets/automod hits, /case file lookup, achievement badges (/badges, /profile) with message/streak/ticket/ birthday/event/booster triggers, /weekplan RSVP boards, /poll with anonymous mode and scheduled auto-close, /suggest with vote/comment/ decide flow. Branding & growth: /branding (per-guild embed color/logo/footer/bot name/theme, applied across logging/ticket/register/embed-builder embeds and the dashboard sidebar/accent color at runtime), two new /setup server-type presets (Roleplay, Creator), /rules generate (template-based rule sets), /partner apply/list/config with invite validation and showcase posting, alt-account suspicion scoring on join (account age, avatar, name pattern, join bursts, ban-list similarity), /gallery submit with voting and weekly-winner scheduler, invite-attributed join/leave tracking with a new Wachstum dashboard page, and an expanded Admin dashboard (guild list, feature-usage counters). Also fixes production: the Docker container never ran `prisma migrate deploy`, so schema changes never reached the live database. The compose command now applies pending migrations on every start. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
72 lines
3.2 KiB
TypeScript
72 lines
3.2 KiB
TypeScript
import { GuildMember, EmbedBuilder } from 'discord.js';
|
|
import { EventHandler } from '../utils/types';
|
|
import { context } from '../config/context';
|
|
import { settingsStore } from '../config/state';
|
|
|
|
const event: EventHandler = {
|
|
name: 'guildMemberAdd',
|
|
execute(member: GuildMember) {
|
|
if (context.lockdown.isActive(member.guild.id)) {
|
|
member.kick('Lockdown aktiv - neue Mitglieder werden abgewiesen').catch(() => undefined);
|
|
context.logging.logMemberJoin(member);
|
|
return;
|
|
}
|
|
|
|
context.altAccounts.check(member).then(({ score, reasons }) => {
|
|
if (context.altAccounts.isSuspicious(score)) context.logging.logSuspiciousJoin(member, reasons);
|
|
}).catch(() => undefined);
|
|
|
|
context.growth.resolveUsedInvite(member.guild).then((inviteCode) => {
|
|
context.growth.recordJoin(member.guild.id, member.id, inviteCode);
|
|
}).catch(() => undefined);
|
|
|
|
const guildConfig = settingsStore.get(member.guild.id);
|
|
const welcomeCfg = guildConfig?.welcomeConfig || guildConfig?.automodConfig?.welcomeConfig;
|
|
if (welcomeCfg?.enabled && welcomeCfg.channelId) {
|
|
const channel = member.guild.channels.cache.get(welcomeCfg.channelId);
|
|
if (channel && channel.isTextBased()) {
|
|
const files: any[] = [];
|
|
const colorVal = parseInt((welcomeCfg.embedColor || '00ff99').replace('#', ''), 16);
|
|
const embed = new EmbedBuilder()
|
|
.setTitle(welcomeCfg.embedTitle || 'Willkommen!')
|
|
.setDescription(welcomeCfg.embedDescription || `${member} ist beigetreten.`)
|
|
.setColor(isNaN(colorVal) ? 0x00ff99 : colorVal);
|
|
const footerText = (welcomeCfg.embedFooter || '').trim();
|
|
if (footerText) {
|
|
embed.setFooter({ text: footerText });
|
|
}
|
|
if (welcomeCfg.embedThumbnailData && welcomeCfg.embedThumbnailData.startsWith('data:')) {
|
|
const [meta, b64] = welcomeCfg.embedThumbnailData.split(',');
|
|
const ext = meta.includes('gif') ? 'gif' : 'png';
|
|
const buf = Buffer.from(b64, 'base64');
|
|
const name = `welcome-thumb.${ext}`;
|
|
files.push({ attachment: buf, name });
|
|
embed.setThumbnail(`attachment://${name}`);
|
|
} else if (welcomeCfg.embedThumbnail) {
|
|
embed.setThumbnail(welcomeCfg.embedThumbnail);
|
|
}
|
|
if (welcomeCfg.embedImageData && welcomeCfg.embedImageData.startsWith('data:')) {
|
|
const [meta, b64] = welcomeCfg.embedImageData.split(',');
|
|
const ext = meta.includes('gif') ? 'gif' : 'png';
|
|
const buf = Buffer.from(b64, 'base64');
|
|
const name = `welcome-image.${ext}`;
|
|
files.push({ attachment: buf, name });
|
|
embed.setImage(`attachment://${name}`);
|
|
} else if (welcomeCfg.embedImage) {
|
|
embed.setImage(welcomeCfg.embedImage);
|
|
}
|
|
channel.send({ embeds: [embed], files }).catch(() => undefined);
|
|
}
|
|
} else if (guildConfig?.welcomeChannelId) {
|
|
const channel = member.guild.channels.cache.get(guildConfig.welcomeChannelId);
|
|
if (channel && channel.isTextBased()) {
|
|
channel.send({ content: `Willkommen ${member}!` }).catch(() => undefined);
|
|
}
|
|
}
|
|
context.logging.logMemberJoin(member);
|
|
context.stats.refreshGuild(member.guild.id).catch(() => undefined);
|
|
}
|
|
};
|
|
|
|
export default event;
|