From 3d61d284b1e553d9c008c9e17825839dc504ba03 Mon Sep 17 00:00:00 2001 From: Snazzah Date: Fri, 13 Dec 2024 21:28:10 -0600 Subject: [PATCH] feat: initial support for discord entitlements --- prisma/schema.prisma | 29 ++++++++ src/client.ts | 5 +- src/events.ts | 160 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 188 insertions(+), 6 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cf789ff..3810a9d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,6 +13,21 @@ model User { @@map("users") } +model Server { + id Int @id @default(autoincrement()) + serverID String @unique @db.VarChar(255) + bannedFromUse Boolean @default(false) + banReason String? @db.VarChar(255) + locale String? @db.VarChar(255) + trelloRole String? @db.VarChar(255) + prefix String? @db.VarChar(255) + maxWebhooks Int @default(5) + createdAt DateTime @db.Timestamptz(6) @default(now()) + updatedAt DateTime @db.Timestamptz(6) @updatedAt + + @@map("servers") +} + model Webhook { id Int @id @default(autoincrement()) active Boolean @default(true) @@ -22,3 +37,17 @@ model Webhook { @@map("webhooks") } + +model DiscordEntitlement { + id String @id + userId String? + guildId String? + skuId String + type Int + active Boolean @default(false) + createdAt DateTime @db.Timestamptz(6) @default(now()) + updatedAt DateTime @db.Timestamptz(6) @updatedAt + endsAt DateTime? @db.Timestamptz(6) + + @@map("discord_entitlement") +} diff --git a/src/client.ts b/src/client.ts index b9ae28d..3688803 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,6 @@ import Eris from 'eris'; -import { onChannelCreate, onChannelDelete, onChannelUpdate, onGuildJoin, onGuildLeave, onWebhooksUpdate } from './events'; +import { onChannelCreate, onChannelDelete, onChannelUpdate, onEntitlementCreate, onEntitlementDelete, onEntitlementUpdate, onGuildJoin, onGuildLeave, onWebhooksUpdate } from './events'; import { logger } from './logger'; import { start as startPoster } from './poster'; @@ -28,6 +28,9 @@ client.on('webhooksUpdate', onWebhooksUpdate); client.on('channelCreate', onChannelCreate); client.on('channelUpdate', onChannelUpdate); client.on('channelDelete', onChannelDelete); +client.on('entitlementCreate', onEntitlementCreate); +client.on('entitlementUpdate', onEntitlementUpdate); +client.on('entitlementDelete', onEntitlementDelete); // Shard Events client.on('connect', (id) => logger.info(`Shard ${id} connected.`)); diff --git a/src/events.ts b/src/events.ts index 4f95dc1..71a1a9a 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,3 +1,4 @@ +import { CronJob } from 'cron'; import Eris from 'eris'; import { logger } from './logger'; @@ -8,10 +9,10 @@ export function onGuildJoin(guild: Eris.Guild) { logger.info(`Joined guild ${guild.name} (${guild.id})`); } -export function onGuildLeave(guild: Eris.Guild) { +export async function onGuildLeave(guild: Eris.Guild) { logger.info(`Left guild ${guild.name} (${guild.id})`); // deactivate guild webhooks - prisma.webhook.updateMany({ + await prisma.webhook.updateMany({ where: { guildID: guild.id }, data: { active: false } }); @@ -29,18 +30,167 @@ export function onWebhooksUpdate({ channelID, guildID }: WebhooksUpdateEvent) { export function onChannelCreate(channel: Eris.AnyChannel) { if (channel.type !== 0 && channel.type !== 5) return; - logger.info(`Channel ${channel.id} created in ${channel.guild.id}`); + logger.debug(`Channel ${channel.id} created in ${channel.guild.id}`); redisClient.del(`discord.channels:${channel.guild.id}`); } export function onChannelUpdate(channel: Eris.AnyChannel) { if (channel.type !== 0 && channel.type !== 5) return; - logger.info(`Channel ${channel.id} updated in ${channel.guild.id}`); + logger.debug(`Channel ${channel.id} updated in ${channel.guild.id}`); redisClient.del(`discord.channels:${channel.guild.id}`); } export function onChannelDelete(channel: Eris.AnyChannel) { if (channel.type !== 0 && channel.type !== 5) return; - logger.info(`Channel ${channel.id} deleted in ${channel.guild.id}`); + logger.debug(`Channel ${channel.id} deleted in ${channel.guild.id}`); redisClient.del(`discord.channels:${channel.guild.id}`); } + +export async function onEntitlementCreate(entitlement: Eris.Entitlement) { + logger.info(`Entitlement ${entitlement.id} created (guild=${entitlement.guildID}, user=${entitlement.userID}, sku=${entitlement.skuID})`); + + const dbEntitlement = await prisma.discordEntitlement.create({ + data: { + id: entitlement.id, + skuId: entitlement.skuID, + type: entitlement.type, + guildId: entitlement.guildID, + userId: entitlement.userID, + active: entitlement.endsAt ? Date.now() < entitlement.endsAt : true, + createdAt: new Date(entitlement.startsAt), + endsAt: entitlement.endsAt ? new Date(entitlement.endsAt) : null + } + }); + + // Apply entitlement + if ((entitlement.skuID === process.env.DISCORD_SKU_TIER_1 || entitlement.skuID === process.env.DISCORD_SKU_TIER_2) && entitlement.guildID && dbEntitlement.active) { + const maxWebhooks = entitlement.skuID === process.env.DISCORD_SKU_TIER_2 ? 200 : 20; + logger.info(`Benefits for ${entitlement.guildID} updated (maxWebhooks=${maxWebhooks})`); + await prisma.server.upsert({ + where: { + serverID: entitlement.guildID + }, + create: { + serverID: entitlement.guildID, + maxWebhooks + }, + update: { + maxWebhooks + } + }); + } +} + +export async function onEntitlementUpdate(entitlement: Eris.Entitlement) { + logger.info(`Entitlement ${entitlement.id} updated (guild=${entitlement.guildID}, user=${entitlement.userID}, sku=${entitlement.skuID})`); + + const dbEntitlement = await prisma.discordEntitlement.upsert({ + where: { + id: entitlement.id + }, + update: { + active: entitlement.endsAt ? Date.now() < entitlement.endsAt : true, + endsAt: entitlement.endsAt ? new Date(entitlement.endsAt) : null + }, + create: { + id: entitlement.id, + skuId: entitlement.skuID, + type: entitlement.type, + guildId: entitlement.guildID, + userId: entitlement.userID, + active: entitlement.endsAt ? Date.now() < entitlement.endsAt : true, + createdAt: new Date(entitlement.startsAt), + endsAt: entitlement.endsAt ? new Date(entitlement.endsAt) : null + } + }); + + if (!dbEntitlement.active && dbEntitlement.guildId) await updateGuildBenefits(dbEntitlement.guildId); +} + +export async function onEntitlementDelete(entitlement: Eris.Entitlement) { + logger.info(`Entitlement ${entitlement.id} deleted (guild=${entitlement.guildID}, user=${entitlement.userID}, sku=${entitlement.skuID})`); + + const dbEntitlement = await prisma.discordEntitlement.delete({ + where: { + id: entitlement.id + } + }); + + if (dbEntitlement.guildId) await updateGuildBenefits(dbEntitlement.guildId); +} + +async function updateGuildBenefits(guildId: string) { + const now = new Date(); + const otherEntitlements = await prisma.discordEntitlement.findMany({ + where: { + OR: [ + { + guildId, + active: true, + endsAt: { lt: now } + }, + { + guildId, + active: true, + endsAt: null + } + ] + } + }); + + const maxWebhooks = + otherEntitlements.find((e) => e.skuId === process.env.DISCORD_SKU_TIER_2) ? 200 : + otherEntitlements.find((e) => e.skuId === process.env.DISCORD_SKU_TIER_1) ? 20 : + 5; + + logger.info(`Benefits for ${guildId} updated (maxWebhooks=${maxWebhooks})`); + await prisma.server.upsert({ + where: { + serverID: guildId + }, + create: { + serverID: guildId, + maxWebhooks + }, + update: { + maxWebhooks + } + }); + + // Restrict webhooks + const webhooks = await prisma.webhook.findMany({ + take: maxWebhooks, + where: { guildID: guildId }, + orderBy: [{ createdAt: 'asc' }] + }); + await prisma.webhook.updateMany({ + where: { + guildID: guildId, + id: { notIn: webhooks.map((w) => w.id) } + }, + data: { active: false } + }); +} + +const entitlementCron = new CronJob('*/5 * * * *', onEntitlementCron, null, true, 'America/New_York'); + +async function onEntitlementCron() { + const expiredEntitlements = await prisma.discordEntitlement.findMany({ + where: { + active: true, + endsAt: { gte: new Date() } + } + }); + await prisma.discordEntitlement.updateMany({ + where: { + active: true, + id: { in: expiredEntitlements.map((e) => e.id) } + }, + data: { active: false } + }); + const guildsToUpdate: string[] = [...new Set(expiredEntitlements.map((e) => e.guildId))]; + + for (const guildId of guildsToUpdate) { + if (guildId) await updateGuildBenefits(guildId); + } +}