Codex commited on
Commit ·
ec4cf41
1
Parent(s): 7da65ca
Add bulk resolve command
Browse files- README.md +1 -0
- src/commands.js +20 -0
- src/embeds.js +20 -0
- src/index.js +47 -0
- src/resolve-bulk.js +39 -0
- test/resolve-bulk.test.js +24 -0
README.md
CHANGED
|
@@ -16,6 +16,7 @@ A simple Discord bot for manually tracking bets and ROI per user. Each Discord u
|
|
| 16 |
|
| 17 |
- `/bet` opens a modal so users can log bets privately
|
| 18 |
- `/resolve` grades a bet as `win`, `loss`, or `void`
|
|
|
|
| 19 |
- `/roi` shows net profit, ROI percent, and a cumulative profit chart
|
| 20 |
- `/bets` shows total bet count and the latest 5 bets
|
| 21 |
- `/summary` shows win/loss totals plus a chart
|
|
|
|
| 16 |
|
| 17 |
- `/bet` opens a modal so users can log bets privately
|
| 18 |
- `/resolve` grades a bet as `win`, `loss`, or `void`
|
| 19 |
+
- `/resolveall` grades multiple bet IDs to the same result
|
| 20 |
- `/roi` shows net profit, ROI percent, and a cumulative profit chart
|
| 21 |
- `/bets` shows total bet count and the latest 5 bets
|
| 22 |
- `/summary` shows win/loss totals plus a chart
|
src/commands.js
CHANGED
|
@@ -34,6 +34,26 @@ export const commands = [
|
|
| 34 |
{ name: 'void', value: 'void' }
|
| 35 |
)
|
| 36 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
new SlashCommandBuilder()
|
| 38 |
.setName('roi')
|
| 39 |
.setDescription('Show your ROI, net profit, and cumulative profit chart.'),
|
|
|
|
| 34 |
{ name: 'void', value: 'void' }
|
| 35 |
)
|
| 36 |
),
|
| 37 |
+
new SlashCommandBuilder()
|
| 38 |
+
.setName('resolveall')
|
| 39 |
+
.setDescription('Resolve multiple of your tracked bets with the same result.')
|
| 40 |
+
.addStringOption((option) =>
|
| 41 |
+
option
|
| 42 |
+
.setName('bet_ids')
|
| 43 |
+
.setDescription('Comma-separated bet IDs, for example: 12, 13, 14')
|
| 44 |
+
.setRequired(true)
|
| 45 |
+
)
|
| 46 |
+
.addStringOption((option) =>
|
| 47 |
+
option
|
| 48 |
+
.setName('result')
|
| 49 |
+
.setDescription('The final result for all listed bets.')
|
| 50 |
+
.setRequired(true)
|
| 51 |
+
.addChoices(
|
| 52 |
+
{ name: 'win', value: 'win' },
|
| 53 |
+
{ name: 'loss', value: 'loss' },
|
| 54 |
+
{ name: 'void', value: 'void' }
|
| 55 |
+
)
|
| 56 |
+
),
|
| 57 |
new SlashCommandBuilder()
|
| 58 |
.setName('roi')
|
| 59 |
.setDescription('Show your ROI, net profit, and cumulative profit chart.'),
|
src/embeds.js
CHANGED
|
@@ -117,6 +117,7 @@ export function buildCommandsEmbed() {
|
|
| 117 |
.addFields(
|
| 118 |
{ name: '/bet', value: 'Open a private modal to log a new bet.' },
|
| 119 |
{ name: '/resolve', value: 'Grade one of your bet IDs as win, loss, or void.' },
|
|
|
|
| 120 |
{ name: '/roi', value: 'Show your net profit, ROI %, and cumulative chart.' },
|
| 121 |
{ name: '/bets', value: 'Show your total tracked bets and latest 5 bets.' },
|
| 122 |
{ name: '/summary', value: 'Show full totals, record, ROI %, and chart.' },
|
|
@@ -172,3 +173,22 @@ export function buildBooksEmbed(bookRows) {
|
|
| 172 |
|
| 173 |
return embed;
|
| 174 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
.addFields(
|
| 118 |
{ name: '/bet', value: 'Open a private modal to log a new bet.' },
|
| 119 |
{ name: '/resolve', value: 'Grade one of your bet IDs as win, loss, or void.' },
|
| 120 |
+
{ name: '/resolveall', value: 'Grade multiple bet IDs to the same result in one command.' },
|
| 121 |
{ name: '/roi', value: 'Show your net profit, ROI %, and cumulative chart.' },
|
| 122 |
{ name: '/bets', value: 'Show your total tracked bets and latest 5 bets.' },
|
| 123 |
{ name: '/summary', value: 'Show full totals, record, ROI %, and chart.' },
|
|
|
|
| 173 |
|
| 174 |
return embed;
|
| 175 |
}
|
| 176 |
+
|
| 177 |
+
export function buildResolveAllEmbed(result, summary) {
|
| 178 |
+
const colorMap = {
|
| 179 |
+
win: 0x15803d,
|
| 180 |
+
loss: 0xb91c1c,
|
| 181 |
+
void: 0x475569,
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
const embed = new EmbedBuilder()
|
| 185 |
+
.setColor(colorMap[result] ?? 0x475569)
|
| 186 |
+
.setTitle(`Bulk resolve complete: ${result.toUpperCase()}`)
|
| 187 |
+
.addFields(
|
| 188 |
+
{ name: 'Resolved', value: summary.resolved.length > 0 ? summary.resolved.map((id) => `#${id}`).join(', ') : 'None' },
|
| 189 |
+
{ name: 'Already settled', value: summary.alreadyResolved.length > 0 ? summary.alreadyResolved.map((id) => `#${id}`).join(', ') : 'None', inline: true },
|
| 190 |
+
{ name: 'Not found', value: summary.missing.length > 0 ? summary.missing.map((id) => `#${id}`).join(', ') : 'None', inline: true }
|
| 191 |
+
);
|
| 192 |
+
|
| 193 |
+
return embed;
|
| 194 |
+
}
|
src/index.js
CHANGED
|
@@ -21,10 +21,12 @@ import {
|
|
| 21 |
buildChartCaption,
|
| 22 |
buildCommandsEmbed,
|
| 23 |
buildErrorEmbed,
|
|
|
|
| 24 |
buildResolveEmbed,
|
| 25 |
buildSummaryEmbed,
|
| 26 |
buildWelcomeEmbed,
|
| 27 |
} from './embeds.js';
|
|
|
|
| 28 |
|
| 29 |
const BET_MODAL_PREFIX = 'bet-entry-modal';
|
| 30 |
const BET_PROP_INPUT_ID = 'bet-prop';
|
|
@@ -119,6 +121,11 @@ async function handleChatInput(interaction, store, config) {
|
|
| 119 |
return;
|
| 120 |
}
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
if (commandName === 'roi') {
|
| 123 |
await handleRoi(interaction, store);
|
| 124 |
return;
|
|
@@ -266,6 +273,46 @@ async function handleResolve(interaction, store) {
|
|
| 266 |
});
|
| 267 |
}
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
async function handleRoi(interaction, store) {
|
| 270 |
const summary = await store.getUserSummary(interaction.user.id);
|
| 271 |
const points = await store.getChartPoints(interaction.user.id);
|
|
|
|
| 21 |
buildChartCaption,
|
| 22 |
buildCommandsEmbed,
|
| 23 |
buildErrorEmbed,
|
| 24 |
+
buildResolveAllEmbed,
|
| 25 |
buildResolveEmbed,
|
| 26 |
buildSummaryEmbed,
|
| 27 |
buildWelcomeEmbed,
|
| 28 |
} from './embeds.js';
|
| 29 |
+
import { parseBetIdList } from './resolve-bulk.js';
|
| 30 |
|
| 31 |
const BET_MODAL_PREFIX = 'bet-entry-modal';
|
| 32 |
const BET_PROP_INPUT_ID = 'bet-prop';
|
|
|
|
| 121 |
return;
|
| 122 |
}
|
| 123 |
|
| 124 |
+
if (commandName === 'resolveall') {
|
| 125 |
+
await handleResolveAll(interaction, store);
|
| 126 |
+
return;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
if (commandName === 'roi') {
|
| 130 |
await handleRoi(interaction, store);
|
| 131 |
return;
|
|
|
|
| 273 |
});
|
| 274 |
}
|
| 275 |
|
| 276 |
+
async function handleResolveAll(interaction, store) {
|
| 277 |
+
const betIdsRaw = interaction.options.getString('bet_ids', true);
|
| 278 |
+
const result = interaction.options.getString('result', true);
|
| 279 |
+
const parsedIds = parseBetIdList(betIdsRaw);
|
| 280 |
+
|
| 281 |
+
if (!parsedIds.ok) {
|
| 282 |
+
await interaction.reply({
|
| 283 |
+
embeds: [buildErrorEmbed('Invalid bet IDs', parsedIds.error)],
|
| 284 |
+
flags: MessageFlags.Ephemeral,
|
| 285 |
+
});
|
| 286 |
+
return;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
const summary = {
|
| 290 |
+
resolved: [],
|
| 291 |
+
alreadyResolved: [],
|
| 292 |
+
missing: [],
|
| 293 |
+
};
|
| 294 |
+
|
| 295 |
+
for (const betId of parsedIds.ids) {
|
| 296 |
+
const resolution = await store.resolveBet(interaction.user.id, betId, result);
|
| 297 |
+
if (resolution.type === 'resolved') {
|
| 298 |
+
summary.resolved.push(betId);
|
| 299 |
+
continue;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
if (resolution.type === 'already_resolved') {
|
| 303 |
+
summary.alreadyResolved.push(betId);
|
| 304 |
+
continue;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
summary.missing.push(betId);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
await interaction.reply({
|
| 311 |
+
embeds: [buildResolveAllEmbed(result, summary)],
|
| 312 |
+
flags: MessageFlags.Ephemeral,
|
| 313 |
+
});
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
async function handleRoi(interaction, store) {
|
| 317 |
const summary = await store.getUserSummary(interaction.user.id);
|
| 318 |
const points = await store.getChartPoints(interaction.user.id);
|
src/resolve-bulk.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export function parseBetIdList(input) {
|
| 2 |
+
const tokens = String(input ?? '')
|
| 3 |
+
.split(/[,\s]+/)
|
| 4 |
+
.map((token) => token.trim())
|
| 5 |
+
.filter(Boolean);
|
| 6 |
+
|
| 7 |
+
if (tokens.length === 0) {
|
| 8 |
+
return { ok: false, error: 'Please provide at least one bet ID.' };
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const ids = [];
|
| 12 |
+
const invalid = [];
|
| 13 |
+
|
| 14 |
+
for (const token of tokens) {
|
| 15 |
+
if (!/^\d+$/.test(token)) {
|
| 16 |
+
invalid.push(token);
|
| 17 |
+
continue;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
const value = Number(token);
|
| 21 |
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
| 22 |
+
invalid.push(token);
|
| 23 |
+
continue;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
if (!ids.includes(value)) {
|
| 27 |
+
ids.push(value);
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
if (invalid.length > 0) {
|
| 32 |
+
return {
|
| 33 |
+
ok: false,
|
| 34 |
+
error: `Invalid bet ID value(s): ${invalid.join(', ')}`,
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return { ok: true, ids };
|
| 39 |
+
}
|
test/resolve-bulk.test.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import test from 'node:test';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import { parseBetIdList } from '../src/resolve-bulk.js';
|
| 4 |
+
|
| 5 |
+
test('parses comma-separated bet ids', () => {
|
| 6 |
+
const parsed = parseBetIdList('12, 13,14');
|
| 7 |
+
|
| 8 |
+
assert.equal(parsed.ok, true);
|
| 9 |
+
assert.deepEqual(parsed.ids, [12, 13, 14]);
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
test('deduplicates repeated ids', () => {
|
| 13 |
+
const parsed = parseBetIdList('7 7 8');
|
| 14 |
+
|
| 15 |
+
assert.equal(parsed.ok, true);
|
| 16 |
+
assert.deepEqual(parsed.ids, [7, 8]);
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
test('rejects invalid ids', () => {
|
| 20 |
+
const parsed = parseBetIdList('4, nope, 6');
|
| 21 |
+
|
| 22 |
+
assert.equal(parsed.ok, false);
|
| 23 |
+
assert.match(parsed.error, /nope/);
|
| 24 |
+
});
|