Skip to content

Commit

Permalink
feat: war win streak announcer (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
r-priyam authored Nov 24, 2024
1 parent b993cf2 commit 02dee28
Show file tree
Hide file tree
Showing 8 changed files with 235 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
- POSTGRES_PASSWORD=goblin
- POSTGRES_DB=goblin
ports:
- '5454:5432'
- '5555:5432'
restart: always
logging:
options:
Expand Down
18 changes: 18 additions & 0 deletions migrations/1732452486-war-win-streak-announcement.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* @param {import('postgres').Sql} sql
*/
export async function up(sql) {
await sql.unsafe(`
CREATE TABLE war_win_streak_announcement
(
id SERIAL PRIMARY KEY,
clan_tag TEXT NOT NULL,
current_win_streak INTEGER NOT NULL DEFAULT 0,
guild_id TEXT NOT NULL,
channel_id TEXT NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`);
}
2 changes: 1 addition & 1 deletion scripts/dbMigration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ley from 'ley';
setup(new URL('../src/.env', import.meta.url));

process.env.PGHOST = 'localhost';
process.env.PGPORT = '5454';
process.env.PGPORT = '5555';

const result = await ley.up({ dir: 'migrations', driver: 'postgres' });

Expand Down
40 changes: 37 additions & 3 deletions src/commands/Automation/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import {
bold
} from 'discord.js';
import { ValidateTag } from '#lib/decorators/ValidateTag';
import type { GoblinCommandOptions } from '#lib/extensions/GoblinCommand';
import { GoblinCommand } from '#lib/extensions/GoblinCommand';
import type { GoblinCommandOptions } from '#lib/extensions/GoblinCommand';
import { Colors, Emotes, ErrorIdentifiers, ModalCustomIds, ModalInputCustomIds } from '#utils/constants';
import { automationMemberCheck } from '#utils/functions/automationMemberCheck';
import { addTagOption } from '#utils/functions/commandOptions';
Expand All @@ -26,7 +26,11 @@ import { addTagOption } from '#utils/functions/commandOptions';
option
.setName('type')
.setDescription('The type of automation to start')
.addChoices({ name: 'Clan Embed', value: 'clanEmbed' }, { name: 'War Image', value: 'warImage' })
.addChoices(
{ name: 'Clan Embed', value: 'clanEmbed' },
{ name: 'War Image', value: 'warImage' },
{ name: 'War Streak Announcement', value: 'warStreakAnnouncement' }
)
.setRequired(true)
)
.addStringOption((option) =>
Expand All @@ -41,7 +45,10 @@ export class StartCommand extends GoblinCommand {
public override async chatInputRun(interaction: ChatInputCommandInteraction<'cached'>) {
automationMemberCheck(interaction.guildId, interaction.member);

const startType = interaction.options.getString('type', true) as 'clanEmbed' | 'warImage';
const startType = interaction.options.getString('type', true) as
| 'clanEmbed'
| 'warImage'
| 'warStreakAnnouncement';
return this[startType](interaction);
}

Expand Down Expand Up @@ -109,4 +116,31 @@ export class StartCommand extends GoblinCommand {
]
});
}

private async warStreakAnnouncement(interaction: ChatInputCommandInteraction<'cached'>) {
await interaction.deferReply({ ephemeral: true });

const clan = await this.coc.clanHelper.info(interaction, interaction.options.getString('tag', true));

try {
await this.sql`INSERT INTO war_win_streak_announcement (clan_tag, current_win_streak, guild_id, channel_id)
VALUES (${clan.tag}, ${clan.warWinStreak}, ${interaction.guildId}, ${interaction.channelId})`;
} catch (error) {
if (error instanceof this.sql.PostgresError && error.code === '23505') {
throw new UserError({
identifier: ErrorIdentifiers.DatabaseError,
message: `War Streak Announcement for **${clan.name} (${clan.tag})** is already running in this server`
});
}
}

return interaction.editReply({
embeds: [
new EmbedBuilder() //
.setTitle(`${Emotes.Success} Success`)
.setDescription(`Successfully started War Streak Announcement for ${clan.name} (${clan.tag})`)
.setColor(Colors.Green)
]
});
}
}
40 changes: 37 additions & 3 deletions src/commands/Automation/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Util } from 'clashofclans.js';
import { PermissionFlagsBits } from 'discord-api-types/v10';
import { bold, EmbedBuilder, ChatInputCommandInteraction } from 'discord.js';
import { ValidateTag } from '#lib/decorators/ValidateTag';
import type { GoblinCommandOptions } from '#lib/extensions/GoblinCommand';
import { GoblinCommand } from '#lib/extensions/GoblinCommand';
import type { GoblinCommandOptions } from '#lib/extensions/GoblinCommand';
import { Colors, Emotes, ErrorIdentifiers } from '#utils/constants';
import { automationMemberCheck } from '#utils/functions/automationMemberCheck';
import { addTagOption } from '#utils/functions/commandOptions';
Expand All @@ -20,7 +20,11 @@ import { addTagOption } from '#utils/functions/commandOptions';
option //
.setName('type')
.setDescription('The type of automation to stop')
.addChoices({ name: 'Clan Embed', value: 'clanEmbed' }, { name: 'War Image', value: 'warImage' })
.addChoices(
{ name: 'Clan Embed', value: 'clanEmbed' },
{ name: 'War Image', value: 'warImage' },
{ name: 'War Streak Announcement', value: 'warStreakAnnouncement' }
)
.setRequired(true)
)
.addStringOption((option) =>
Expand All @@ -35,7 +39,10 @@ export class StopCommand extends GoblinCommand {
public override async chatInputRun(interaction: ChatInputCommandInteraction<'cached'>) {
automationMemberCheck(interaction.guildId, interaction.member);

const stopType = interaction.options.getString('type', true) as 'clanEmbed' | 'warImage';
const stopType = interaction.options.getString('type', true) as
| 'clanEmbed'
| 'warImage'
| 'warStreakAnnouncement';
return this[stopType](interaction);
}

Expand Down Expand Up @@ -94,4 +101,31 @@ export class StopCommand extends GoblinCommand {
]
});
}

private async warStreakAnnouncement(interaction: ChatInputCommand.Interaction) {
await interaction.deferReply({ ephemeral: true });
const clanTag = Util.formatTag(interaction.options.getString('tag', true));

const [result] = await this.sql<[{ clanName?: string }]>`DELETE
FROM war_win_streak_announcement
WHERE clan_tag = ${clanTag}
AND guild_id = ${interaction.guildId}
RETURNING clan_tag`;

if (!result) {
throw new UserError({
identifier: ErrorIdentifiers.DatabaseError,
message: `Can't find any War Streak Announcement running for ${bold(clanTag)} in this server`
});
}

return interaction.editReply({
embeds: [
new EmbedBuilder()
.setTitle(`${Emotes.Success} Success`)
.setDescription(`Successfully stopped ${bold(clanTag)} War Streak Announcement in this server`)
.setColor(Colors.Green)
]
});
}
}
2 changes: 1 addition & 1 deletion src/scheduled-tasks/coc-tasks/eygWarEndImagePoster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { MetaDir } from '#utils/constants';
import { logInfo, logWarning } from '#utils/functions/logging';

@ApplyOptions<ScheduledTask.Options>({
pattern: '*/5 * * * *',
pattern: '*/2 * * * *',
customJobOptions: {
removeOnComplete: true,
removeOnFail: true
Expand Down
3 changes: 2 additions & 1 deletion src/scheduled-tasks/coc-tasks/syncClanEmbed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { logInfo, logWarning } from '#utils/functions/logging';
@ApplyOptions<ScheduledTask.Options>({
pattern: '00 */2 * * *',
customJobOptions: {
removeOnComplete: true
removeOnComplete: true,
removeOnFail: true
}
})
export class SyncClanEmbedTask extends ScheduledTask {
Expand Down
138 changes: 138 additions & 0 deletions src/scheduled-tasks/coc-tasks/warStreakAnnouncer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ApplyOptions } from '@sapphire/decorators';
import { ScheduledTask } from '@sapphire/plugin-scheduled-tasks';
import { Result } from '@sapphire/result';
import type { Clan, HTTPError as COCHttpError } from 'clashofclans.js';
import { RESTJSONErrorCodes, Routes } from 'discord-api-types/v10';
import { bold, EmbedBuilder, Status } from 'discord.js';
import type { HTTPError } from 'discord.js';
import { logInfo, logWarning } from '#utils/functions/logging';

@ApplyOptions<ScheduledTask.Options>({
pattern: '*/2 * * * *',
customJobOptions: {
removeOnComplete: true,
removeOnFail: true
}
})
export class WarStreakAnnouncer extends ScheduledTask {
#winStreakMessages = [
'What a powerhouse! #NAME is on fire with a #STREAK win streak. Keep the wins rolling in!',
'Incredible teamwork pays off! #NAME is celebrating a #STREAK war win streak. GG!',
'Talk about domination! #NAME secures a #STREAK win streak. Next stop: greatness!',
"Victory dance time! #NAME just locked in a #STREAK streak. Let's go!"
];

#winStreakColors = [0xff4500, 0x32cd32, 0x1e90ff, 0xffd700];

public override async run() {
if (this.container.client.ws.status !== Status.Ready) {
return;
}

const data = await this.sql<WarStreakAnnouncerData[]>`SELECT clan_tag,
channel_id,
current_win_streak
FROM war_win_streak_announcement
WHERE enabled = true`;

if (!data) {
return;
}

for (const x of data) {
await this.announceWarStreak(x);
}
}

private async announceWarStreak(data: WarStreakAnnouncerData) {
const clan = await this.getClan(data.clanTag, data.channelId);
if (!clan) {
return;
}

// Don't want to send the message if the streak is 0 or the same as the last streak
// Streak is incremental, can never be the same value again without going down first
if (clan.warWinStreak === 0 || clan.warWinStreak === data.currentWinStreak) {
return;
}

const streakMessage = this.#winStreakMessages[Math.floor(Math.random() * this.#winStreakMessages.length)]
.replace('#NAME', bold(clan.name))
.replace('#STREAK', bold(`${clan.warWinStreak}`));

const streakEmbed = new EmbedBuilder()
.setDescription(streakMessage)
.setColor(this.#winStreakColors[Math.floor(Math.random() * this.#winStreakColors.length)])
.setThumbnail(clan.badge.medium);

const result = await Result.fromAsync<unknown, HTTPError>(async () =>
this.client.rest.post(Routes.channelMessages(data.channelId), {
body: { embeds: [streakEmbed.toJSON()] }
})
);

if (result.isErr()) {
const error = result.unwrapErr();
if (
[
RESTJSONErrorCodes.MissingAccess,
RESTJSONErrorCodes.MissingPermissions,
RESTJSONErrorCodes.UnknownMessage
].includes(error.status)
) {
await this.stopWarStreakAnnouncer(data.clanTag, data.channelId);
this.logger.info(
logInfo(
'War Streak Announcer',
`Stopping war streak announcement for ${data.clanTag} with reason ${error.message}`
)
);
return;
}

this.logger.warn(
logWarning(
'War Streak Announcer',
`Failed to announce war streak for ${data.clanTag} with reason ${error.message}`
)
);
} else {
await this.sql`UPDATE war_win_streak_announcement
SET current_win_streak = ${clan.warWinStreak}
WHERE clan_tag = ${data.clanTag} AND channel_id = ${data.channelId}`;
}
}

/**
* Get clan information from the clan tag.
* Stops the clan embed if clan not found and logs it
*
* @param clanTag - Clan Tag to get information for
* @param channelId - Channel ID where the clan board is running
*/
private async getClan(clanTag: string, channelId: string) {
const result = await Result.fromAsync<Clan, COCHttpError>(async () => this.coc.getClan(clanTag));

if (result.isErr() && result.unwrapErr().status === 404) {
await this.stopWarStreakAnnouncer(clanTag, channelId);
this.logger.info(
logInfo('ClanEmbed Syncer', `Stopping clan embed for ${clanTag} with reason Clan not found`)
);
return;
}

return result.unwrap();
}

private async stopWarStreakAnnouncer(clanTag: string, channelId: string) {
await this.sql`UPDATE war_win_streak_announcement
SET enabled = false
WHERE clan_tag = ${clanTag} AND channel_id = ${channelId}`;
}
}

type WarStreakAnnouncerData = {
channelId: string;
clanTag: string;
currentWinStreak: number;
};

0 comments on commit 02dee28

Please sign in to comment.