Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ CONFIG_DISCORD_COMPONENTS_PAGE_SIZE='10' # The number of channels to display per
# Postgres URLs
POSTGRES_URL='postgresql://user:password@server:port/database'
POSTGRES_DEV_URL='postgresql://user:password@server:port/database'
POSTGRES_STAGING_URL='postgresql://user:password@server:port/database'
4 changes: 3 additions & 1 deletion drizzle.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { defineConfig } from "drizzle-kit";
const databaseUrl =
process.env.NODE_ENV === "development"
? process.env.POSTGRES_DEV_URL
: process.env.POSTGRES_URL;
: process.env.NODE_ENV === "staging"
? process.env.POSTGRES_STAGING_URL
: process.env.POSTGRES_URL;

console.log("Using database URL:", databaseUrl);

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"scripts": {
"db:generate": "bun drizzle-kit generate",
"db:push:dev": "cross-env NODE_ENV=development bun drizzle-kit push",
"db:push:staging": "cross-env NODE_ENV=staging bun drizzle-kit push",
"db:push:prod": "bun drizzle-kit push",
"db:migrate:staging": "bun src/db/migratedb.ts --staging",
"db:migrate:prod": "bun src/db/migratedb.ts",
"db:staging:reset": "bun src/db/resetStagingDatabase.ts --staging",
"dev": "concurrently --names \"WEB,API,BOT\" --prefix-colors \"blue,green,magenta\" \"cd web && bun dev\" \"cd api && cargo watch -x \\\"run -- --dev\\\"\" \"bun --watch . --dev\"",
"dev:bot": "bun --watch . --dev",
"test:jetstream": "bun --watch src/utils/bluesky/jetstream.ts",
Expand Down
179 changes: 177 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
GuildMember,
InteractionContextType,
MessageFlags,
TextChannel,
type ApplicationCommandOptionData,
type CacheType,
type CommandInteraction,
Expand All @@ -37,6 +38,9 @@ import {
discordCheckIfDmChannelExists,
discordGetAllTrackedInGuild,
discordRemoveGuildTrackingChannel,
discordUpdateSubscriptionAddChannel,
discordUpdateSubscriptionCheckGuild,
discordUpdateSubscriptionRemoveChannel,
} from "./db/discord";
import {
Platform,
Expand Down Expand Up @@ -69,7 +73,7 @@ interface Command {
}

// Context 2: Interaction can be used within Group DMs and DMs other than the app's bot user
// /track, /tracked and /untracked can't be used in these contexts
// /track, /tracked, /untracked and /updates can't be used in these contexts
const commands: Record<string, Command> = {
ping: {
data: {
Expand Down Expand Up @@ -708,7 +712,7 @@ const commands: Record<string, Command> = {
) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content: `Started tracking the streamer ${platformUserId} (${platformUserId}) in <#${targetChannel?.id}>!`,
content: `Started tracking the streamer ${streamerName} in <#${targetChannel?.id}>!`,
});
} else {
await interaction.reply({
Expand Down Expand Up @@ -1174,6 +1178,177 @@ const commands: Record<string, Command> = {
});
},
},
updates: {
data: {
name: "updates",
description: "Enable or disable updates for Feedr in this channel",
integration_types: [0, 1],
contexts: [0, 1],
options: [
{
name: "state",
description: "Choose whether to enable or disable updates",
type: ApplicationCommandOptionType.String,
required: true,
choices: [
{
name: "Enable",
value: "enable",
},
{
name: "Disable",
value: "disable",
},
],
},
],
},
execute: async (interaction: CommandInteraction) => {
const isDm = !interaction.inGuild();

const channelId = interaction.channelId;
const guildId = isDm ? channelId : interaction.guildId;

if (!isDm && !guildId) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content: "An error occurred! Please report",
});

return;
}

// Check type of the channel
const targetChannel = await client.channels.fetch(channelId);

if (
targetChannel &&
(targetChannel.type === ChannelType.GuildText ||
targetChannel.type === ChannelType.GuildAnnouncement)
) {
if (
!isDm &&
!interaction.memberPermissions?.has(
PermissionFlagsBits.ManageChannels,
)
) {
// Check the permissions of the user
await interaction.reply({
flags: MessageFlags.Ephemeral,
content:
"You do not have the permission to manage channels!",
});

return;
}
}

// Check the permissions of the bot in the channel
const botMember = isDm
? null
: await interaction.guild?.members.fetchMe();

if (
botMember &&
!botMember
.permissionsIn(channelId)
.has(PermissionFlagsBits.SendMessages)
) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content:
"I do not have permission to send messages in that channel!",
});

return;
}

// Get the current state from the database
const currentDatabaseState =
await discordUpdateSubscriptionCheckGuild(guildId);

if (!currentDatabaseState || !currentDatabaseState.success) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content:
"An error occurred while trying to get the current update state from the database! Please report this error!",
});

return;
}

const currentState = Boolean(
currentDatabaseState.data[0].feedrUpdatesChannelId,
);
const desiredState = Boolean(
interaction.options.get("state")?.value === "enable",
);

if (currentState === desiredState) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content: `Updates are already ${
desiredState ? "enabled" : "disabled"
} in this channel!`,
});

return;
}

if (desiredState) {
// Enable updates
const updateSuccess = await discordUpdateSubscriptionAddChannel(
guildId,
channelId,
);

if (!updateSuccess || !updateSuccess.success) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content:
"An error occurred while trying to enable updates in this channel! Please report this error!",
});

return;
}

await client.channels
.fetch(channelId)
.then(async (channel) => {
if (channel?.isTextBased()) {
await (channel as TextChannel).send({
content: `Updates have been successfully enabled in this channel!`,
});
}
})
.catch(console.error);

await interaction.reply({
flags: MessageFlags.Ephemeral,
content:
"If a test message was sent, updates are enabled! If not, please report this as an error!",
});
} else {
// Disable updates
const updateSuccess =
await discordUpdateSubscriptionRemoveChannel(guildId);

if (!updateSuccess || !updateSuccess.success) {
await interaction.reply({
flags: MessageFlags.Ephemeral,
content:
"An error occurred while trying to disable updates in this channel! Please report this error!",
});

return;
}

await interaction.reply({
content: `Successfully disabled updates in <#${channelId}>!`,
});
}
},
},
};

// Convert commands to a Map
Expand Down
14 changes: 13 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
// FILL IN THIS INFORMATION IN .ENV
export const runningInDevMode: boolean = process.argv.includes("--dev");

// Staging mode is for only testing the production database before breaking the actual production bot
// Run `bun db:migrate:staging` to migrate the staging database and check for any mistakes before running `bun db:migrate:prod`
// Do NOT use this mode for regular testing, use --dev for that
export const runningInStagingMode: boolean = process.argv.includes("--staging");

if (runningInDevMode && runningInStagingMode) {
throw new Error("Cannot run in both dev and staging mode!");
}

export interface Config {
youtubeInnertubeProxyUrl: string | null;
updateIntervalYouTube: number;
Expand All @@ -20,7 +30,9 @@ export const config: Config = {
: 60_000,
databaseUrl: runningInDevMode
? process.env?.POSTGRES_DEV_URL
: process.env?.POSTGRES_URL,
: runningInStagingMode
? process.env?.POSTGRES_STAGING_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
Expand Down
61 changes: 61 additions & 0 deletions src/db/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,3 +401,64 @@ export async function discordCheckIfDmChannelExists(
return { success: false, data: [] };
}
}

// Bot updates
export async function discordUpdateSubscriptionAddChannel(
guildId: string,
channelId: string,
): Promise<{ success: boolean; data: [] }> {
console.log(
`Updating guild ${guildId} to set subscription channel to ${channelId}`,
);

try {
await db
.update(dbDiscordTable)
.set({ feedrUpdatesChannelId: channelId })
.where(eq(dbDiscordTable.guildId, guildId));

return { success: true, data: [] };
} catch (error) {
console.error("Error updating subscription channel:", error);

return { success: false, data: [] };
}
}

export async function discordUpdateSubscriptionRemoveChannel(
guildId: string,
): Promise<{ success: boolean; data: [] }> {
console.log(`Removing subscription channel for guild ${guildId}`);

try {
await db
.update(dbDiscordTable)
.set({ feedrUpdatesChannelId: null })
.where(eq(dbDiscordTable.guildId, guildId));

return { success: true, data: [] };
} catch (error) {
console.error("Error removing subscription channel:", error);

return { success: false, data: [] };
}
}

export async function discordUpdateSubscriptionCheckGuild(
guildId: string,
): Promise<{ success: boolean; data: (typeof dbDiscordTable.$inferSelect)[] }> {
console.log(`Checking subscription settings for guild ${guildId}`);

try {
const result = await db
.select()
.from(dbDiscordTable)
.where(eq(dbDiscordTable.guildId, guildId));

return { success: true, data: result };
} catch (error) {
console.error("Error checking subscription settings for guild:", error);

return { success: false, data: [] };
}
}
Loading