diff --git a/docs/GAMEPLAY.md b/docs/GAMEPLAY.md index 209eb3f83..106c20ab4 100644 --- a/docs/GAMEPLAY.md +++ b/docs/GAMEPLAY.md @@ -258,5 +258,6 @@ Use `,help [command_name]` for more details for any of the following commands: - `,include`: Specify which artists to forcefully include, regardless of other game options - `,reset`: Reset all options to the default settings +- `,default`: Sets the current game options as the default settings - `,add`: Add groups to `,groups`, `,exclude`, or `,include` - `,remove`: Remove groups to `,groups`, `,exclude`, or `,include` diff --git a/src/commands/game_options/cutoff.ts b/src/commands/game_options/cutoff.ts index 7c4f291ab..3fd5fc1cd 100644 --- a/src/commands/game_options/cutoff.ts +++ b/src/commands/game_options/cutoff.ts @@ -89,10 +89,7 @@ export default class CutoffCommand implements BaseCommand { call = async ({ message, parsedMessage }: CommandArgs): Promise => { const guildPreference = await getGuildPreference(message.guildID); if (parsedMessage.components.length === 0) { - await guildPreference.setBeginningCutoffYear( - DEFAULT_BEGINNING_SEARCH_YEAR - ); - await guildPreference.setEndCutoffYear(DEFAULT_ENDING_SEARCH_YEAR); + await guildPreference.reset(GameOption.CUTOFF); await sendOptionsMessage( MessageContext.fromMessage(message), guildPreference, @@ -110,8 +107,10 @@ export default class CutoffCommand implements BaseCommand { const yearRange = parsedMessage.components; const startYear = yearRange[0]; if (yearRange.length === 1) { - await guildPreference.setBeginningCutoffYear(parseInt(startYear)); - await guildPreference.setEndCutoffYear(DEFAULT_ENDING_SEARCH_YEAR); + await guildPreference.setCutoff( + parseInt(startYear), + DEFAULT_ENDING_SEARCH_YEAR + ); } else if (yearRange.length === 2) { const endYear = yearRange[1]; if (endYear < startYear) { @@ -128,8 +127,10 @@ export default class CutoffCommand implements BaseCommand { return; } - await guildPreference.setBeginningCutoffYear(parseInt(startYear)); - await guildPreference.setEndCutoffYear(parseInt(endYear)); + await guildPreference.setCutoff( + parseInt(startYear), + parseInt(endYear) + ); } await sendOptionsMessage( diff --git a/src/commands/game_options/default.ts b/src/commands/game_options/default.ts new file mode 100644 index 000000000..9320ce66b --- /dev/null +++ b/src/commands/game_options/default.ts @@ -0,0 +1,48 @@ +import BaseCommand, { CommandArgs } from "../interfaces/base_command"; +import { IPCLogger } from "../../logger"; +import { getGuildPreference } from "../../helpers/game_utils"; +import { + getDebugLogHeader, + sendInfoMessage, +} from "../../helpers/discord_utils"; +import MessageContext from "../../structures/message_context"; +import CommandPrechecks from "../../command_prechecks"; + +const logger = new IPCLogger("default"); + +export default class DefaultCommand implements BaseCommand { + preRunChecks = [{ checkFn: CommandPrechecks.competitionPrecheck }]; + + validations = { + minArgCount: 0, + maxArgCount: 0, + arguments: [], + }; + + aliases = ["setdefault", "setdefaults", "defaults"]; + + help = { + name: "default", + description: `Sets the current game option as the defaults (for ${process.env.BOT_PREFIX}reset or per-option resets). This should only be used by experienced users!`, + usage: ",default", + examples: [ + { + example: "`,default`", + explanation: "Sets the current game option as the defaults.", + }, + ], + priority: 130, + }; + + call = async ({ message }: CommandArgs): Promise => { + const guildPreference = await getGuildPreference(message.guildID); + await guildPreference.setAsDefault(); + logger.info(`${getDebugLogHeader(message)} | Set default game options`); + + await sendInfoMessage(MessageContext.fromMessage(message), { + title: "Success!", + description: + "Default game options has been set to the current options!", + }); + }; +} diff --git a/src/migrations/20211221024206_game_option_defaults.js b/src/migrations/20211221024206_game_option_defaults.js new file mode 100644 index 000000000..f0dafab3e --- /dev/null +++ b/src/migrations/20211221024206_game_option_defaults.js @@ -0,0 +1,12 @@ +exports.up = function (knex) { + return knex.schema.createTable("game_option_defaults", (table) => { + table.string("guild_id").notNullable(); + table.string("option_name").notNullable(); + table.json("option_value"); + table.unique(["guild_id", "option_name"]); + }); +}; + +exports.down = function (knex) { + return knex.schema.dropTableIfExists("game_option_defaults"); +}; diff --git a/src/structures/guild_preference.ts b/src/structures/guild_preference.ts index f3b5bf668..44e37e996 100644 --- a/src/structures/guild_preference.ts +++ b/src/structures/guild_preference.ts @@ -181,6 +181,13 @@ export default class GuildPreference { default: [0, DEFAULT_LIMIT], setter: this.setLimit, }, + [GameOption.CUTOFF]: { + default: [ + DEFAULT_BEGINNING_SEARCH_YEAR, + DEFAULT_ENDING_SEARCH_YEAR, + ], + setter: this.setCutoff, + }, [GameOption.GROUPS]: { default: [null], setter: this.setGroups }, [GameOption.EXCLUDE]: { default: [null], setter: this.setExcludes }, [GameOption.INCLUDE]: { default: [null], setter: this.setIncludes }, @@ -470,32 +477,51 @@ export default class GuildPreference { ]); } + /** + * Resets a specific game option to the default value + * @param gameOption - The game option to reset + */ async reset(gameOption: GameOption): Promise { if (gameOption in this.resetArgs) { const resetArg = this.resetArgs[gameOption]; - resetArg.setter.bind(this)(...resetArg.default); + const defaultGameOptionOverride = + await this.getGameOptionDefaultOverrides(); + + if (!defaultGameOptionOverride) { + resetArg.setter.bind(this)(...resetArg.default); + return; + } + + // get game optional internal keys to reset + const gameOptionsDefaultsToReset = Object.keys( + GameOptionInternalToGameOption + ).filter( + (key) => GameOptionInternalToGameOption[key] === gameOption + ); + + // associated override values + const gameOptionDefaultResets = gameOptionsDefaultsToReset.map( + (key) => defaultGameOptionOverride[key] + ); + + resetArg.setter.bind(this)( + ...(gameOptionDefaultResets || resetArg.default) + ); } } /** - * Sets the beginning cutoff year option value - * @param year - The beginning cutoff year + * Sets the cutoff option value + * @param beginningYear - The beginning cutoff year + * @param endingYear - The ending cutoff year */ - async setBeginningCutoffYear(year: number): Promise { - this.gameOptions.beginningYear = year; - await this.updateGuildPreferences([ - { name: GameOptionInternal.BEGINNING_YEAR, value: year }, - ]); - } + async setCutoff(beginningYear: number, endingYear?: number): Promise { + this.gameOptions.beginningYear = beginningYear; + this.gameOptions.endYear = endingYear ?? DEFAULT_ENDING_SEARCH_YEAR; - /** - * Sets the end cutoff year option value - * @param year - The end cutoff year - */ - async setEndCutoffYear(year: number): Promise { - this.gameOptions.endYear = year; await this.updateGuildPreferences([ - { name: GameOptionInternal.END_YEAR, value: year }, + { name: GameOptionInternal.BEGINNING_YEAR, value: beginningYear }, + { name: GameOptionInternal.END_YEAR, value: endingYear }, ]); } @@ -909,6 +935,17 @@ export default class GuildPreference { async resetToDefault(): Promise> { const oldOptions = this.gameOptions; this.gameOptions = { ...GuildPreference.DEFAULT_OPTIONS }; + + // apply per-server game option default overrides + const gameOptionOverrides = await this.getGameOptionDefaultOverrides(); + if (gameOptionOverrides) { + for (const [option, value] of Object.entries(gameOptionOverrides)) { + if (value) { + this.gameOptions[option] = value; + } + } + } + const options = Object.entries(this.gameOptions).map((x) => { const optionName = x[0]; const optionValue = x[1]; @@ -926,4 +963,42 @@ export default class GuildPreference { "option" ); } + + /** Sets the current game options as the default */ + async setAsDefault(): Promise { + const options = Object.entries(this.gameOptions).map((x) => ({ + guild_id: this.guildID, + option_name: x[0], + option_value: JSON.stringify(x[1]), + })); + + await dbContext.kmq.transaction(async (trx) => { + await dbContext + .kmq("game_option_defaults") + .where("guild_id", "=", this.guildID) + .del() + .transacting(trx); + + await dbContext + .kmq("game_option_defaults") + .insert(options) + .transacting(trx); + }); + } + + /* Gets the per-server game option default overrides */ + async getGameOptionDefaultOverrides(): Promise { + const defaultOverrides = await dbContext + .kmq("game_option_defaults") + .where("guild_id", "=", this.guildID); + + if (defaultOverrides.length === 0) return null; + + const gameOptions = defaultOverrides.reduce((prev, curr) => { + prev[curr["option_name"]] = JSON.parse(curr["option_value"]); + return prev; + }, {}); + + return gameOptions; + } } diff --git a/src/test/ci/song_selector.test.ts b/src/test/ci/song_selector.test.ts index b4cd30554..096d827aa 100644 --- a/src/test/ci/song_selector.test.ts +++ b/src/test/ci/song_selector.test.ts @@ -19,6 +19,7 @@ import { LanguageType, } from "../../commands/game_options/language"; import { ShuffleType } from "../../commands/game_options/shuffle"; +import { DEFAULT_BEGINNING_SEARCH_YEAR } from "../../commands/game_options/cutoff"; async function getMockGuildPreference(): Promise { const guildPreference = new GuildPreference("test"); @@ -244,7 +245,7 @@ describe("getFilteredSongList", () => { (song) => song.publishedon >= new Date("2016-01-01") ).length; - await guildPreference.setBeginningCutoffYear(2016); + await guildPreference.setCutoff(2016); const { songs } = await SongSelector.getFilteredSongList( guildPreference ); @@ -259,7 +260,10 @@ describe("getFilteredSongList", () => { (song) => song.publishedon <= new Date("2015-12-31") ).length; - await guildPreference.setEndCutoffYear(2015); + await guildPreference.setCutoff( + DEFAULT_BEGINNING_SEARCH_YEAR, + 2015 + ); const { songs } = await SongSelector.getFilteredSongList( guildPreference ); @@ -276,8 +280,7 @@ describe("getFilteredSongList", () => { song.publishedon <= new Date("2018-12-31") ).length; - await guildPreference.setBeginningCutoffYear(2008); - await guildPreference.setEndCutoffYear(2018); + await guildPreference.setCutoff(2008, 2018); const { songs } = await SongSelector.getFilteredSongList( guildPreference ); @@ -294,8 +297,7 @@ describe("getFilteredSongList", () => { song.publishedon <= new Date("2017-12-31") ).length; - await guildPreference.setBeginningCutoffYear(2017); - await guildPreference.setEndCutoffYear(2017); + await guildPreference.setCutoff(2017, 2017); const { songs } = await SongSelector.getFilteredSongList( guildPreference );