Skip to content

Commit

Permalink
Merge pull request #27 from moonstar-x/bugfix/command-defer
Browse files Browse the repository at this point in the history
Version 3.0.1
  • Loading branch information
moonstar-x authored Aug 13, 2024
2 parents bb65479 + 2f835c5 commit 8832230
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 42 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ services:
>
> Make sure to replace `SOMETHING_SECRET` with a password for your database and `YOUR_DISCORD_TOKEN_HERE` with your bot's token.

And start it up:

```bash
docker compose up -d
```

Once you have it, you should deploy the commands. To do this, run:

```bash
docker compose run bot npm run deploy:prod
```

### With Node.js

Make sure to have at least Node.js 20.
Expand Down Expand Up @@ -106,6 +118,12 @@ POSTGRES_USER=dev
POSTGRES_PASSWORD=password
```

Deploy the commands:

```bash
npm run deploy:prod
```

And start the bot:

```bash
Expand All @@ -123,7 +141,7 @@ You can configure the bot with the following environment variables.
| DISCORD_TOKEN | Yes | | The token to connect your bot to Discord. |
| DISCORD_SHARDING_ENABLED | No | false | Whether the bot should start in sharded mode or not. This is necessary if your bot is in more than 2000 servers. |
| DISCORD_SHARDING_COUNT | No | auto | The amount of shards to spawn if sharding is enabled. It should be a number greater than 1. You can leave this as `auto` to use an automatic value generated for your own needs. |
| DISCORD_PRESENCE_INTERVAL | No | 30000 | The amount of milliseconds to wait before the bot changes its presence or activity. |
| DISCORD_PRESENCE_INTERVAL | No | 300000 | The amount of milliseconds to wait before the bot changes its presence or activity. |
| REDIS_URI | Yes | | The Redis URI shared with the crawler service. |
| POSTGRES_HOST | Yes | | The database host to connect to. |
| POSTGRES_PORT | No | 5432 | The port to use to connect to the database. |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "discord-free-games-notifier",
"version": "3.0.0",
"version": "3.0.1",
"description": "A Discord bot that will notify you when games on various storefronts become free.",
"private": true,
"scripts": {
Expand Down
27 changes: 23 additions & 4 deletions src/base/presence/PresenceResolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import dayjs from 'dayjs';
dayjs.tz.setDefault('America/Guayaquil');
const dateGetTimeSpy = jest.spyOn(Date.prototype, 'getTime', undefined as never);

jest.mock('../../features/gameOffers/functions/getStorefronts', () => {
return {
getStorefronts: jest.fn().mockResolvedValue([1, 2])
};
});

jest.mock('../../features/gameOffers/functions/getCurrentGameOffers', () => {
return {
getCurrentGameOffers: jest.fn().mockResolvedValue([1, 2])
};
});

describe('Base > Presence > PresenceResolver', () => {
beforeAll(() => {
(dateGetTimeSpy as jest.Mock).mockReturnValue(1723224679000);
Expand Down Expand Up @@ -85,10 +97,17 @@ describe('Base > Presence > PresenceResolver', () => {
});
});

describe('n_commands', () => {
it('should return the number of commands.', async () => {
const result = await resolver.get('n_commands');
expect(result).toBe('with 5 commands!');
describe('n_storefronts', () => {
it('should return the number of storefronts.', async () => {
const result = await resolver.get('n_storefronts');
expect(result).toBe('on 2 storefronts!');
});
});

describe('n_offers', () => {
it('should return the number of storefronts.', async () => {
const result = await resolver.get('n_offers');
expect(result).toBe('with 2 offers!');
});
});

Expand Down
22 changes: 21 additions & 1 deletion src/base/presence/PresenceResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import timezone from 'dayjs/plugin/timezone';
import humanizeDuration from 'humanize-duration';
import { Collection, Guild, Snowflake } from 'discord.js';
import { randomItem } from '../../utils/array';
import { getStorefronts } from '../../features/gameOffers/functions/getStorefronts';
import { getCurrentGameOffers } from '../../features/gameOffers/functions/getCurrentGameOffers';

dayjs.extend(utc);
dayjs.extend(timezone);

const PRESENCE_NAMES = ['n_guilds', 'n_members', 'n_commands', 'time_cur', 'time_ready', 'uptime'] as const;
const PRESENCE_NAMES = ['n_guilds', 'n_members', 'n_commands', 'n_storefronts', 'n_offers', 'time_cur', 'time_ready', 'uptime'] as const;
type PresenceName = typeof PRESENCE_NAMES[number];

export class PresenceResolver {
Expand All @@ -29,6 +31,10 @@ export class PresenceResolver {
return `with ${value} users!`;
case 'n_commands':
return `with ${value} commands!`;
case 'n_storefronts':
return `on ${value} storefronts!`;
case 'n_offers':
return `with ${value} offers!`;
case 'time_cur':
return `Current time: ${value}`;
case 'time_ready':
Expand All @@ -52,6 +58,10 @@ export class PresenceResolver {
return this.getNumberOfMembers();
case 'n_commands':
return this.getNumberOfCommands();
case 'n_storefronts':
return this.getNumberOfStorefronts();
case 'n_offers':
return this.getNumberOfOffers();
case 'time_cur':
return this.getCurrentTime();
case 'time_ready':
Expand Down Expand Up @@ -88,6 +98,16 @@ export class PresenceResolver {
return this.client.registry.size().toString();
}

private async getNumberOfStorefronts(): Promise<string> {
const storefronts = await getStorefronts();
return storefronts.length.toString();
}

private async getNumberOfOffers(): Promise<string> {
const offers = await getCurrentGameOffers();
return offers.length.toString();
}

private async getCurrentTime(): Promise<string> {
const now = new Date().getTime();
return dayjs(now).tz().format('hh:mm:ss A');
Expand Down
46 changes: 35 additions & 11 deletions src/commands/ConfigureCommand.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ describe('Commands > ConfigureCommand', () => {

describe('runChannel()', () => {
const interaction = {
reply: jest.fn(),
deferReply: jest.fn(),
editReply: jest.fn(),
locale: 'en-US',
guildId: '1267881983548063785',
options: {
Expand All @@ -63,11 +64,16 @@ describe('Commands > ConfigureCommand', () => {
}
} as unknown as GuildChatInputCommandInteraction;

it('should defer the reply.', async () => {
await command.run(interaction);
expect(interaction.deferReply).toHaveBeenCalled();
});

it('should reply with pre check message if no channel is provided.', async () => {
(interaction.options.getChannel as jest.Mock).mockReturnValueOnce(null);
await command.run(interaction);

expect(interaction.reply).toHaveBeenCalledWith({ content: 'No channel provided.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No channel provided.' });
});

it('should update guild channel.', async () => {
Expand All @@ -77,7 +83,7 @@ describe('Commands > ConfigureCommand', () => {

it('should reply with channel update message.', async () => {
await command.run(interaction);
expect(interaction.reply).toHaveBeenCalledWith({ content: 'Successfully updated notifications channel to Channel.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Successfully updated notifications channel to Channel.' });
});
});

Expand All @@ -90,7 +96,8 @@ describe('Commands > ConfigureCommand', () => {
awaitMessageComponent: jest.fn().mockResolvedValue(userResponseMock)
};
const interaction = {
reply: jest.fn(),
deferReply: jest.fn(),
editReply: jest.fn(),
followUp: jest.fn().mockResolvedValue(followUpResponseMock),
locale: 'en-US',
guildId: '1267881983548063785',
Expand All @@ -99,16 +106,21 @@ describe('Commands > ConfigureCommand', () => {
}
} as unknown as GuildChatInputCommandInteraction;

it('should defer the reply.', async () => {
await command.run(interaction);
expect(interaction.deferReply).toHaveBeenCalled();
});

it('should reply with empty storefronts message if no storefronts exist.', async () => {
(getStorefronts as jest.Mock).mockResolvedValueOnce([]);
await command.run(interaction);

expect(interaction.reply).toHaveBeenCalledWith({ content: 'No storefronts are available right now.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No storefronts are available right now.' });
});

it('should reply with start message.', async () => {
await command.run(interaction);
expect(interaction.reply).toHaveBeenCalledWith({ content: 'You will receive a follow up for each storefront available. Please, click on the buttons as they appear to enable or disable notifications for each storefront.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'You will receive a follow up for each storefront available. Please, click on the buttons as they appear to enable or disable notifications for each storefront.' });
});

it('should send follow up with correct components for each storefront.', async () => {
Expand Down Expand Up @@ -169,7 +181,8 @@ describe('Commands > ConfigureCommand', () => {

describe('runLanguage()', () => {
const interaction = {
reply: jest.fn(),
deferReply: jest.fn(),
editReply: jest.fn(),
locale: 'en-US',
guildId: '1267881983548063785',
options: {
Expand All @@ -178,11 +191,16 @@ describe('Commands > ConfigureCommand', () => {
}
} as unknown as GuildChatInputCommandInteraction;

it('should defer the reply.', async () => {
await command.run(interaction);
expect(interaction.deferReply).toHaveBeenCalled();
});

it('should reply with pre check message if no locale is provided.', async () => {
(interaction.options.getString as jest.Mock).mockReturnValueOnce(null);
await command.run(interaction);

expect(interaction.reply).toHaveBeenCalledWith({ content: 'No language provided.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'No language provided.' });
});

it('should update guild locale.', async () => {
Expand All @@ -192,23 +210,29 @@ describe('Commands > ConfigureCommand', () => {

it('should reply with language update message.', async () => {
await command.run(interaction);
expect(interaction.reply).toHaveBeenCalledWith({ content: 'Successfully updated notifications language to **English**.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Successfully updated notifications language to **English**.' });
});
});

describe('runDefault()', () => {
const interaction = {
reply: jest.fn(),
deferReply: jest.fn(),
editReply: jest.fn(),
locale: 'en-US',
guildId: '1267881983548063785',
options: {
getSubcommand: jest.fn().mockReturnValue('unknown')
}
} as unknown as GuildChatInputCommandInteraction;

it('should defer the reply.', async () => {
await command.run(interaction);
expect(interaction.deferReply).toHaveBeenCalled();
});

it('should reply with unknown subcommand message.', async () => {
await command.run(interaction);
expect(interaction.reply).toHaveBeenCalledWith({ content: 'Unknown subcommand received.' });
expect(interaction.editReply).toHaveBeenCalledWith({ content: 'Unknown subcommand received.' });
});
});
});
Expand Down
16 changes: 9 additions & 7 deletions src/commands/ConfigureCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export default class ConfigureCommand extends Command {
}

public override async run(interaction: GuildChatInputCommandInteraction): Promise<void> {
await interaction.deferReply();

const subCommand = interaction.options.getSubcommand();

switch (subCommand) {
Expand All @@ -87,24 +89,24 @@ export default class ConfigureCommand extends Command {
const channel = interaction.options.getChannel('channel');

if (!channel) {
await interaction.reply({ content: t('commands.configure.run.channel.pre_check.text') });
await interaction.editReply({ content: t('commands.configure.run.channel.pre_check.text') });
return;
}

await updateOrCreateGuildChannel(interaction.guildId, channel.id);
await interaction.reply({ content: t('commands.configure.run.channel.success.text', { channel: channel.toString() }) });
await interaction.editReply({ content: t('commands.configure.run.channel.success.text', { channel: channel.toString() }) });
}

private async runStorefronts(interaction: GuildChatInputCommandInteraction): Promise<void> {
const t = getInteractionTranslator(interaction);
const storefronts = await getStorefronts();

if (!storefronts.length) {
await interaction.reply({ content: t('commands.configure.run.storefronts.empty.text') });
await interaction.editReply({ content: t('commands.configure.run.storefronts.empty.text') });
return;
}

await interaction.reply({ content: t('commands.configure.run.storefronts.start.text') });
await interaction.editReply({ content: t('commands.configure.run.storefronts.start.text') });

const buttonIds = {
enable: 'configure-storefronts-enable',
Expand Down Expand Up @@ -151,19 +153,19 @@ export default class ConfigureCommand extends Command {
const locale = interaction.options.getString('language') as Locale | null;

if (!locale) {
await interaction.reply({ content: t('commands.configure.run.language.pre_check.text') });
await interaction.editReply({ content: t('commands.configure.run.language.pre_check.text') });
return;
}

const language = t(AVAILABLE_LOCALES[locale]);
await updateOrCreateGuildLocale(interaction.guildId, locale);

await interaction.reply({ content: t('commands.configure.run.language.success.text', { language }) });
await interaction.editReply({ content: t('commands.configure.run.language.success.text', { language }) });
}

private async runDefault(interaction: ChatInputCommandInteraction): Promise<void> {
const t = getInteractionTranslator(interaction);

await interaction.reply({ content: t('commands.configure.run.default.response.text') });
await interaction.editReply({ content: t('commands.configure.run.default.response.text') });
}
}
10 changes: 8 additions & 2 deletions src/commands/HelpCommand.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,16 @@ describe('Commands > HelpCommand', () => {
describe('run()', () => {
const command = new HelpCommand(client);
const interaction = {
reply: jest.fn(),
deferReply: jest.fn(),
editReply: jest.fn(),
locale: 'en-US'
} as unknown as ChatInputCommandInteraction;

it('should defer the reply.', async () => {
await command.run(interaction);
expect(interaction.deferReply).toHaveBeenCalled();
});

it('should reply with the embed.', async () => {
await command.run(interaction);

Expand All @@ -54,7 +60,7 @@ describe('Commands > HelpCommand', () => {
)
];

expect(interaction.reply).toHaveBeenCalledWith({ embeds: expectedEmbeds, components: expectedComponents });
expect(interaction.editReply).toHaveBeenCalledWith({ embeds: expectedEmbeds, components: expectedComponents });
});
});
});
3 changes: 2 additions & 1 deletion src/commands/HelpCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default class HelpCommand extends Command {
}

public override async run(interaction: ChatInputCommandInteraction): Promise<void> {
await interaction.deferReply();
const t = getInteractionTranslator(interaction);

const embed = new EmbedBuilder()
Expand All @@ -39,6 +40,6 @@ export default class HelpCommand extends Command {
new ButtonBuilder().setEmoji('🌎').setStyle(ButtonStyle.Link).setURL(BOT_WEBSITE_URL).setLabel(t('commands.help.run.buttons.bot_website.label')),
);

await interaction.reply({ embeds: [embed], components: [row1, row2] });
await interaction.editReply({ embeds: [embed], components: [row1, row2] });
}
}
Loading

0 comments on commit 8832230

Please sign in to comment.