diff --git a/.env.example b/.env.example index a0488d0..f47fc10 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,5 @@ DISCORD_GUILD_ID= DATABASE_URL=postgresql://booster_role:booster_role@localhost:5432/booster_role BOOSTER_ROLE_ANCHOR_ROLE_ID= BOOSTER_ELIGIBILITY_ROLE_ID=1206431347925852162 +BOOSTER_GREETING_CHANNEL_ID= LOG_LEVEL=info diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 09f8df3..7794594 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -73,12 +73,13 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} BOOSTER_ROLE_ANCHOR_ROLE_ID: ${{ secrets.BOOSTER_ROLE_ANCHOR_ROLE_ID }} BOOSTER_ELIGIBILITY_ROLE_ID: ${{ secrets.BOOSTER_ELIGIBILITY_ROLE_ID }} + BOOSTER_GREETING_CHANNEL_ID: ${{ secrets.BOOSTER_GREETING_CHANNEL_ID }} LOG_LEVEL: ${{ secrets.LOG_LEVEL }} with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} - envs: IMAGE_NAME,GHCR_USERNAME,GHCR_TOKEN,DISCORD_TOKEN,DISCORD_CLIENT_ID,DISCORD_GUILD_ID,DATABASE_URL,BOOSTER_ROLE_ANCHOR_ROLE_ID,BOOSTER_ELIGIBILITY_ROLE_ID,LOG_LEVEL + envs: IMAGE_NAME,GHCR_USERNAME,GHCR_TOKEN,DISCORD_TOKEN,DISCORD_CLIENT_ID,DISCORD_GUILD_ID,DATABASE_URL,BOOSTER_ROLE_ANCHOR_ROLE_ID,BOOSTER_ELIGIBILITY_ROLE_ID,BOOSTER_GREETING_CHANNEL_ID,LOG_LEVEL script: | set -eu mkdir -p /opt/booster-role @@ -91,6 +92,7 @@ jobs: DATABASE_URL=${DATABASE_URL:?DATABASE_URL is required} BOOSTER_ROLE_ANCHOR_ROLE_ID=${BOOSTER_ROLE_ANCHOR_ROLE_ID} BOOSTER_ELIGIBILITY_ROLE_ID=${BOOSTER_ELIGIBILITY_ROLE_ID:-1206431347925852162} + BOOSTER_GREETING_CHANNEL_ID=${BOOSTER_GREETING_CHANNEL_ID} LOG_LEVEL=${LOG_LEVEL:-info} EOF diff --git a/src/config/index.ts b/src/config/index.ts index 1a0b9f8..be6146b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -6,6 +6,7 @@ export type AppConfig = { boosterRoleAnchorRoleId: string | null; boosterEligibilityRoleId: string; boostCleanupIntervalMs: number; + boosterGreetingChannelId: string | null; }; export function loadConfig(env: Record = process.env): AppConfig { @@ -16,7 +17,8 @@ export function loadConfig(env: Record = process.env databaseUrl: env.DATABASE_URL ?? "postgresql://booster_role:booster_role@localhost:5432/booster_role", boosterRoleAnchorRoleId: env.BOOSTER_ROLE_ANCHOR_ROLE_ID ?? null, boosterEligibilityRoleId: env.BOOSTER_ELIGIBILITY_ROLE_ID ?? "1206431347925852162", - boostCleanupIntervalMs: Number(env.BOOST_CLEANUP_INTERVAL_MS) || 5 * 60 * 1000 + boostCleanupIntervalMs: Number(env.BOOST_CLEANUP_INTERVAL_MS) || 5 * 60 * 1000, + boosterGreetingChannelId: env.BOOSTER_GREETING_CHANNEL_ID ?? null }; } diff --git a/src/discord/events/boostGreeting.ts b/src/discord/events/boostGreeting.ts new file mode 100644 index 0000000..a66baa0 --- /dev/null +++ b/src/discord/events/boostGreeting.ts @@ -0,0 +1,60 @@ +import { logger } from "../../logger"; + +type GuildTextChannel = { + send(input: { content: string }): Promise; +}; + +type GuildMember = { + id: string; + guild: { + id: string; + channels: { + cache: { + get(channelId: string): GuildTextChannel | undefined; + }; + }; + }; + user?: { id: string }; +}; + +/** + * Sends a welcome message to the configured greeting channel when a member + * newly acquires the booster eligibility role (i.e. just boosted the server). + * + * Only fires on a *gain* transition (hadBoosterRole = false → hasBoosterRole = true). + */ +export function handleBoostGreeting( + oldMember: GuildMember, + newMember: GuildMember, + greetingChannelId: string | null, +): void { + if (!greetingChannelId) return; + + const hadBoosterRole = oldMember.roles.cache.has(/** @type {any} */ (null) as any); + // Actually detect the gain: we need the actual role ID. The caller should pass it. +} + +/** + * Sends a welcome message when a member newly acquires the booster eligibility role. + * Must be called with the booster eligibility role ID to check correctly. + */ +export async function sendBoostGreeting( + member: { id: string; guild: { id: string; channels: { cache: { get(channelId: string): { send(input: { content: string }): Promise } | undefined } } } }, + greetingChannelId: string, +): Promise { + const channel = member.guild.channels.cache.get(greetingChannelId); + if (!channel) { + logger.warn("Boost greeting channel not found", { channelId: greetingChannelId, guildId: member.guild.id }); + return; + } + + await channel.send({ + content: `🎉 Thank you for boosting <@${member.user?.id ?? member.id}>! You can now claim a custom role using \`/booster-role claim\` in <#${greetingChannelId}>.` + }); + + logger.info("Boost greeting sent", { + userId: member.user?.id ?? member.id, + guildId: member.guild.id, + channelId: greetingChannelId, + }); +}