diff --git a/.env.example b/.env.example index b203af7..cd48a5d 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ TWITCH_CLIENT_SECRET='YOUR_TWITCH_CLIENT_SECRET' CONFIG_UPDATE_INTERVAL_YOUTUBE='10' CONFIG_UPDATE_INTERVAL_TWITCH='2' CONFIG_DISCORD_LOGS_CHANNEL='YOUR_DISCORD_LOGS_CHANNEL' +CONFIG_DISCORD_WAIT_FOR_GUILD_CACHE_TIME='YOUR_TIME_IN_SECONDS' # Postgres URLs POSTGRES_URL='postgresql://user:password@server:port/database' diff --git a/src/config.ts b/src/config.ts index 40dd781..4e827fd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,7 @@ export interface Config { updateIntervalYouTube: number; updateIntervalTwitch: number; databaseUrl: string | undefined; + discordWaitForGuildCacheTime: number; } export const config: Config = { @@ -16,6 +17,10 @@ export const config: Config = { databaseUrl: runningInDevMode ? process.env?.POSTGRES_DEV_URL : process.env?.POSTGRES_URL, + discordWaitForGuildCacheTime: process.env + ?.CONFIG_DISCORD_WAIT_FOR_GUILD_CACHE_TIME + ? parseInt(process.env?.CONFIG_DISCORD_WAIT_FOR_GUILD_CACHE_TIME) * 1000 + : 10_000, }; interface Env { diff --git a/src/db/discord.ts b/src/db/discord.ts index f954df9..52be8e5 100644 --- a/src/db/discord.ts +++ b/src/db/discord.ts @@ -6,6 +6,7 @@ import { db } from "./db"; import { dbGuildYouTubeSubscriptionsTable, dbGuildTwitchSubscriptionsTable, + dbDiscordTable, } from "./schema"; export async function checkIfGuildIsTrackingUserAlready( @@ -280,3 +281,46 @@ export async function discordRemoveGuildTrackingChannel( return { success: false, data: [] }; } } + +// Add a new guild to track +export async function discordAddNewGuild( + guildId: string, +): Promise<{ success: boolean; data: [] }> { + console.log(`Adding new guild to track: ${guildId}`); + + try { + await db.insert(dbDiscordTable).values({ + guildId, + allowedPublicSharing: false, + isInServer: true, + memberCount: 0, + }); + + return { success: true, data: [] }; + } catch (error) { + console.error("Error adding new guild to track:", error); + + return { success: false, data: [] }; + } +} + +// "Remove" a guild from tracking +// Basically just set isInServer to false for archival purposes +export async function discordRemoveGuildFromTracking( + guildId: string, +): Promise<{ success: boolean; data: [] }> { + console.log(`Removing guild from tracking: ${guildId}`); + + try { + await db + .update(dbDiscordTable) + .set({ isInServer: false }) + .where(eq(dbDiscordTable.guildId, guildId)); + + return { success: true, data: [] }; + } catch (error) { + console.error("Error removing guild from tracking:", error); + + return { success: false, data: [] }; + } +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 9cde7bf..1617f8a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -8,6 +8,7 @@ export const dbDiscordTable = pgTable("discord", { allowedPublicSharing: boolean("allowed_public_sharing").notNull().default(false), feedrUpdatesChannelId: text("feedr_updates_channel_id"), isInServer: boolean("is_in_server").notNull().default(true), + memberCount: integer("member_count").notNull().default(0), }); export const dbBlueskyTable = pgTable("bluesky", { diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts new file mode 100644 index 0000000..1a3697b --- /dev/null +++ b/src/events/guildCreate.ts @@ -0,0 +1,17 @@ +import { Events } from "discord.js"; + +import client from ".."; +import { discordAddNewGuild } from "../db/discord"; + +client.on(Events.GuildCreate, async (guild) => { + console.log(`Joined new guild: ${guild.name} (ID: ${guild.id})`); + + // Add the new guild to tracking + const result = await discordAddNewGuild(guild.id); + + if (result.success) { + console.log(`Successfully added guild ${guild.id} to tracking.`); + } else { + console.error(`Failed to add guild ${guild.id} to tracking.`); + } +}); diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts new file mode 100644 index 0000000..02d878b --- /dev/null +++ b/src/events/guildDelete.ts @@ -0,0 +1,17 @@ +import { Events } from "discord.js"; + +import client from ".."; +import { discordRemoveGuildFromTracking } from "../db/discord"; + +client.on(Events.GuildDelete, async (guild) => { + console.log(`Left guild: ${guild.name} (ID: ${guild.id})`); + + // Remove the guild from tracking + const result = await discordRemoveGuildFromTracking(guild.id); + + if (result.success) { + console.log(`Successfully removed guild ${guild.id} from tracking.`); + } else { + console.error(`Failed to remove guild ${guild.id} from tracking.`); + } +}); diff --git a/src/index.ts b/src/index.ts index 5c9d22e..1f21bb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { import { env } from "./config.ts"; import commandsMap from "./commands.ts"; import { getTwitchToken } from "./utils/twitch/auth.ts"; +import updateGuildsOnStartup from "./utils/discord/updateGuildsOnStartup.ts"; if (!env.discordToken || env.discordToken === "YOUR_DISCORD_TOKEN") { throw new Error("You MUST provide a discord token in .env!"); @@ -75,6 +76,9 @@ await Promise.all( }), ); +// Update the guilds on startup +await updateGuildsOnStartup(); + // Attempt the garbage collection every hour setInterval( () => { diff --git a/src/utils/discord/updateGuildsOnStartup.ts b/src/utils/discord/updateGuildsOnStartup.ts new file mode 100644 index 0000000..ba0c37a --- /dev/null +++ b/src/utils/discord/updateGuildsOnStartup.ts @@ -0,0 +1,97 @@ +// Checks for any guilds that may have been added/removed while the bot was offline +import { eq } from "drizzle-orm"; + +import { dbDiscordTable } from "../../db/schema"; +import client from "../.."; +import { db } from "../../db/db"; +import { config } from "../../config"; + +export default async function () { + console.log("Checking for guilds to update on startup..."); + + let currentGuilds: string[] = []; + + // Keep checking every 10 seconds until currentGuilds is not empty + while (currentGuilds.length === 0) { + console.log("Waiting for guilds to load..."); + currentGuilds = client.guilds.cache.map((guild) => guild.id); + if (currentGuilds.length === 0) { + await new Promise((resolve) => + setTimeout(resolve, config.discordWaitForGuildCacheTime), + ); + } + } + + // Get all the guilds from the database + const data = await db.select().from(dbDiscordTable); + + console.log( + `Currently in ${currentGuilds.length} guilds, checking against ${data.length} in the database.`, + ); + + // Find any guilds that are in the database but not in the current guilds + const missingGuilds = data.filter( + (guild) => !currentGuilds.includes(guild.guildId), + ); + + // Find any guilds that are in the current guilds but not in the database + const newGuilds = currentGuilds.filter( + (id) => !data.some((guild) => guild.guildId === id), + ); + + // Update the database for missing guilds + try { + await Promise.all( + missingGuilds.map(async (guild) => { + console.log(`Removing guild from tracking: ${guild.guildId}`); + const result = await db + .update(dbDiscordTable) + .set({ isInServer: false }) + .where(eq(dbDiscordTable.guildId, guild.guildId)) + .returning(); + + if (result.length > 0) { + console.log( + `Successfully removed guild ${guild.guildId} from tracking.`, + ); + } else { + console.error( + `Failed to remove guild ${guild.guildId} from tracking.`, + ); + } + }), + ); + } catch (error) { + console.error("Error while removing missing guilds:", error); + } + + try { + await Promise.all( + newGuilds.map(async (guildId) => { + console.log(`Adding new guild to tracking: ${guildId}`); + const result = await db + .insert(dbDiscordTable) + .values({ + guildId, + allowedPublicSharing: false, + feedrUpdatesChannelId: null, + isInServer: true, + memberCount: 0, + }) + .returning(); + + if (result.length > 0) { + console.log( + `Successfully added guild ${guildId} to tracking.`, + ); + } else { + console.error( + `Failed to add guild ${guildId} to tracking.`, + ); + } + }), + ); + } catch (error) { + console.error("Error while adding new guilds:", error); + } +} diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index b684cea..d1f32e4 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -1,5 +1,4 @@ -import type { Platform } from "../../types/types.d.ts"; - +import { Platform } from "../../types/types.d"; import { dbGuildYouTubeSubscriptionsTable, dbYouTubeTable,