diff --git a/src/commands/elite-early-bird.ts b/src/commands/elite-early-bird.ts new file mode 100644 index 0000000..df8f4a0 --- /dev/null +++ b/src/commands/elite-early-bird.ts @@ -0,0 +1,199 @@ + +import { CommandInteraction, EmbedBuilder, SlashCommandBuilder } from 'discord.js' +import LogManager from '../logger/logger' +import { EliteGameDataStorage } from '../service/eliteGameDataStorage' +import { getTodayAsDate } from '../service/promotionService' + +function parse(dateToParse: string | number | boolean): undefined | number { + if (typeof dateToParse !== 'string') { + return undefined + } + const now = new Date() + const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).getTime() + if (/^\d{1,2}:\d{1,2}:\d{1,2}\.\d{1,3}$/g.test(dateToParse)) { + const splitByColon = dateToParse.split(':') + if (splitByColon.length !== 3) { + return undefined + } + const hours = parseInt(splitByColon[0]) + const minutes = parseInt(splitByColon[1]) + const splitByDot = splitByColon[2].split('.') + if (splitByDot.length !== 2) { + return undefined + } + const seconds = parseInt(splitByDot[0]) + const milliseconds = parseInt(splitByDot[1]) + if (hours < 0 || hours > 23) { + return undefined + } + if (minutes < 0 || minutes > 59) { + return undefined + } + if (seconds < 0 || seconds > 59) { + return undefined + } + if (milliseconds < 0 || milliseconds > 999) { + return undefined + } + return midnight + milliseconds + (seconds * 1000) + (minutes * 60 * 1000) + (hours * 60 * 60 * 1000) + } + if (/^\d{1,2}:\d{1,2}:\d{1,2}$/g.test(dateToParse)) { + const splitByColon = dateToParse.split(':') + if (splitByColon.length !== 3) { + return undefined + } + const hours = parseInt(splitByColon[0]) + const minutes = parseInt(splitByColon[1]) + const seconds = parseInt(splitByColon[2]) + if (hours < 0 || hours > 23) { + return undefined + } + if (minutes < 0 || minutes > 59) { + return undefined + } + if (seconds < 0 || seconds > 59) { + return undefined + } + return midnight + (seconds * 1000) + (minutes * 60 * 1000) + (hours * 60 * 60 * 1000) + } + if (/^\d{1,2}:\d{1,2}$/g.test(dateToParse)) { + const splitByColon = dateToParse.split(':') + if (splitByColon.length !== 2) { + return undefined + } + const minutes = parseInt(splitByColon[0]) + const seconds = parseInt(splitByColon[1]) + if (minutes < 0 || minutes > 59) { + return undefined + } + if (seconds < 0 || seconds > 59) { + return undefined + } + return midnight + (seconds * 1000) + (minutes * 60 * 1000) + (13 * 60 * 60 * 1000) + } + if (/^\d{1,2}\.\d{1,3}$/g.test(dateToParse)) { + const splitByDot = dateToParse.split('.') + if (splitByDot.length !== 2) { + return undefined + } + const seconds = parseInt(splitByDot[0]) + const milliseconds = parseInt(splitByDot[1]) + if (seconds < 0 || seconds > 59) { + return undefined + } + if (milliseconds < 0 || milliseconds > 999) { + return undefined + } + return midnight + milliseconds + (seconds * 1000) + (37 * 60 * 1000) + (13 * 60 * 60 * 1000) + } + if (/^\d{1,2}$/g.test(dateToParse)) { + const seconds = parseInt(dateToParse) + if (seconds < 0 || seconds > 59) { + return undefined + } + return midnight + seconds * 1000 + (37 * 60 * 1000) + (13 * 60 * 60 * 1000) + } + return undefined +} + +function calcValidFrom(playTime: number): number { + const now = new Date() + let offset = 999 + // rules: Mitternacht tippt sind es 65s, wenn man um 8 Uhr tippt 30s, um 12 Uhr -> 7s, 13 Uhr -> 3s, ab 13:37 dann 0s + if (now.getHours() >= 0 && now.getHours() <= 7) { + offset = 65 + } else if (now.getHours() >= 8 && now.getHours() <= 11) { + offset = 30 + } else if (now.getHours() === 12) { + offset = 7 + } else if (now.getHours() === 13) { + offset = 3 + } + return playTime + offset * 1000 +} + +export default { + data: new SlashCommandBuilder() + .setName('1337-early-bird') + .setDescription('Plays the 1337 game as an early bird.') + .addStringOption((opt) => opt.setName('time').setDescription('The time (HH:mm:ss.sss) you would have /1337`ed').setRequired(true)), + async execute(interaction: CommandInteraction) { + const logger = LogManager.getInstance().logger('EliteEarlyBirdCommand') + if (!interaction.isRepliable()) { + logger.logSync('ERROR', 'Gegebene interaction kann nicht beantwortet werden.') + return + } + const berlinDate = getTodayAsDate() + + const userId = interaction.member?.user.id ?? '' + const time = interaction.options.get('time', true).value + if (!time) { + throw new Error('No time provided') + } + + const rows = await EliteGameDataStorage.instance().loadGamePlayForUser(userId, berlinDate) + try { + if (rows.length > 0) { + if (rows[0].player === '\uFFFF') { + // the player '\uFFFF' indicates that the game has been played for this day + await interaction.reply({ + embeds: [new EmbedBuilder() + .setColor(0x0099ff) + .setTitle('Too late!') + .setDescription( + 'Es ist nach 13:37. Morgen kannst du wieder mitmachen.' + ) + .setTimestamp()], + ephemeral: true + }) + } else { + await interaction.reply({ + embeds: [new EmbedBuilder() + .setColor(0x0099ff) + .setTitle('That`s not going to happen!') + .setDescription( + 'Du hast heute bereits deinen Tipp abgegeben. Warte auf das Ergebnis.' + ) + .setTimestamp()], + ephemeral: true + }) + } + } else { + const playTime = parse(time) + if (!playTime) { + await interaction.reply({ + embeds: [new EmbedBuilder() + .setColor(0x0099ff) + .setTitle('Invalid time!') + .setDescription( + 'Die Zeit muss im Format `HH:mm:ss.sss` sein.' + ) + .setTimestamp()], + ephemeral: true + }) + return + } + const validFrom = calcValidFrom(playTime) + await EliteGameDataStorage.instance().writeGamePlayEarlyBird( + userId, // discord-id user + berlinDate, // german format date of play e.g. 17.06.2024 + playTime, // submission time (the guess) in millis since epoc e.g. 1710419768333 for 13.03.2024 23:05 + validFrom // valid starting time in millis since epoc e.g. 1710419768333 for 13.03.2024 23:05 + ) + await interaction.reply({ + embeds: [new EmbedBuilder() + .setColor(0x0099ff) + .setTitle('Got it early bird!') + .setDescription( + 'Dein Tipp wurde registriert. Warte auf das Ergebnis.' + ) + .setTimestamp()], + ephemeral: true + }) + } + } catch (err) { + console.log(err) + logger.logSync('ERROR', `Reply to elite command konnte nicht gesendet werden. ${JSON.stringify(err)}`) + } + } +} diff --git a/src/service/eliteGameDataStorage.ts b/src/service/eliteGameDataStorage.ts index 8c18027..0e3102b 100644 --- a/src/service/eliteGameDataStorage.ts +++ b/src/service/eliteGameDataStorage.ts @@ -42,6 +42,11 @@ export class EliteGameDataStorage { register_date TEXT NOT NULL, play_timestamp INTEGER NOT NULL )`) + const rowST = await this.db.getAsyncT<{ count: number }>(`SELECT 1 as count FROM sqlite_master WHERE type='table' AND name='elite_game' AND sql LIKE '%submission_time%';`, []) + if (rowST?.count !== 1) { + await this.db.runAsync(`ALTER TABLE elite_game ADD COLUMN submission_time INTEGER NOT NULL DEFAULT 0`) + await this.db.runAsync(`ALTER TABLE elite_game ADD COLUMN valid_starting_at INTEGER NOT NULL DEFAULT 0`) + } await this.db.runAsync(`CREATE TABLE IF NOT EXISTS elite_game_winner ( identifier INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE, player TEXT NOT NULL, @@ -84,7 +89,15 @@ export class EliteGameDataStorage { if (!this.db) { throw Error('initEliteGame was not called!') } - await this.db.runAsync(`INSERT INTO elite_game(register_date, player, play_timestamp) VALUES(?,?,?)`, [berlinDate, userId, Date.now()]) + const now = Date.now() + await this.db.runAsync(`INSERT INTO elite_game(register_date, player, play_timestamp, submission_time, valid_starting_at) VALUES(?,?,?,?,0)`, [berlinDate, userId, now, now]) + } + + public async writeGamePlayEarlyBird(userId: string, berlinDate: string, tippTime: number, validFrom: number): Promise { + if (!this.db) { + throw Error('initEliteGame was not called!') + } + await this.db.runAsync(`INSERT INTO elite_game(register_date, player, play_timestamp, submission_time, valid_starting_at) VALUES(?,?,?,?,?)`, [berlinDate, userId, tippTime, Date.now(), validFrom]) } public async loadGamePlayForUser(userId: string, berlinDate: string): Promise { @@ -98,7 +111,8 @@ export class EliteGameDataStorage { if (!this.db) { throw Error('initEliteGame was not called!') } - return await this.db.allAsyncT(`select * from elite_game where register_date = ? and player != "\uFFFF" order by play_timestamp desc`, [berlinDate]) + const now = Date.now() + return await this.db.allAsyncT(`select * from elite_game where register_date = ? and valid_starting_at <= ? and player != "\uFFFF" order by play_timestamp desc`, [berlinDate, now]) } public async writeGameWinner(userId: string, berlinDate: string, playerTimestamp: number): Promise { @@ -132,6 +146,10 @@ export class EliteGameDataStorage { if (!this.db) { throw Error('initEliteGame was not called!') } - return await this.db.allAsyncT<{ userId: string }>(`select player as userId from elite_game where register_date = (select register_date from elite_game where identifier = (select max(identifier) from elite_game where player = "\uFFFF")) and player != "\uFFFF" order by play_timestamp desc`, []) + return await this.db.allAsyncT<{ userId: string }>(`select player as userId from elite_game where register_date = ` + + `(select register_date from elite_game where identifier = (select max(identifier) from elite_game where player = "\uFFFF")) ` + + `and player != "\uFFFF" and valid_starting_at <= ` + + `(select play_timestamp from elite_game where identifier = (select max(identifier) from elite_game where player = "\uFFFF")) ` + + `order by play_timestamp desc`, []) } }