Skip to content

Commit

Permalink
Replace submit subcommand with sync (#41)
Browse files Browse the repository at this point in the history
* replace `submit` subcommand with `sync`

TODO: Implement leaderboard sync function.

* wip

* wip

* passing test "DenoKvLeaderboardClient" step "sync"

WIP. TODO: resolve failing tests
```
DenoKvLeaderboardClient ... getLatestSeason => ./lib/leaderboard/denokv/denokv_leaderboard_client_test.ts:67:11
DenoKvLeaderboardClient ... listSeasons => ./lib/leaderboard/denokv/denokv_leaderboard_client_test.ts:73:11
DenoKvLeaderboardClient ... getSeason => ./lib/leaderboard/denokv/denokv_leaderboard_client_test.ts:79:11
```

* fix test steps

Fix test steps `getLatestSeason`, `listSeasons , and `getSeason`.

* set up sync in daily webhook

* move `formatScores` to `lib/leaderboard/scores.ts`

* run `deno task all`

* Update main.ts

* sub sync: allow season_id to be optional

* register.ts: aesthetic fix to match prev commit

Prev commit SHA: `80aee9e5aeff07b59e186a2d91b901ac6246993e`.

* Update sync.ts

* Update denokv_leaderboard_client.ts

* Update denokv_leaderboard_client.ts

* Change `sync` subcommand response to embeds

Resolves <#41 (comment)>.

* Add `synced_at` property to `api.Season`

Resolves #44, <#41 (comment)>.

* sync: Overwrite to empty season for new seasons

Resolves <#41 (comment)>.

* Update instructions sent in daily webhook embed

Resolves <#41 (comment)>.

* Update HANDBOOK.md

Resolves <#41 (comment)>.

* refactor `executeDailyWebhook` logic

* `updateLatestSeason`: forceful kv set op
  • Loading branch information
EthanThatOneKid authored Oct 16, 2023
1 parent 1f68435 commit 3ac6b70
Show file tree
Hide file tree
Showing 18 changed files with 662 additions and 486 deletions.
99 changes: 28 additions & 71 deletions api/dailies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,24 +80,43 @@ async function executeDailyWebhook(
): Promise<Response> {
// Get the daily question.
const question = await lcClient.getDailyQuestion();
const questionDate = new Date(`${question.date} GMT`);
const isSunday = questionDate.getDay() === 0;

// Get the season data if a season ID is provided or if it is Sunday.
const isSunday = new Date(question.date).getDay() === 0;
const season = seasonID
// Get the stored season.
const storedSeason = seasonID
? await leaderboardClient.getSeason(seasonID)
: isSunday
? await leaderboardClient.getLatestSeason()
: await leaderboardClient.getLatestSeason();

// If the season is ongoing, then sync it.
const referenceDate = new Date();
const isLatestSeason = storedSeason && leaderboard.checkDateInWeek(
new Date(storedSeason.start_date).getTime(),
referenceDate.getTime(),
);
const syncedSeason = isLatestSeason
? await leaderboardClient
.sync(storedSeason.id)
.then((response) => response.season)
: null;

// Format the webhook embed.
const embeds = makeDailyWebhookEmbeds({ question, season });
const embeds = makeDailyWebhookEmbeds({
question,
season: isSunday ? (syncedSeason ?? storedSeason) : null,
});

// Execute the webhook.
await discord.executeWebhook({
url: webhookURL,
data: { embeds },
});

// If the season is not synced, then sync it to set up the next season.
if (!syncedSeason) {
await leaderboardClient.sync(undefined, referenceDate);
}

// Acknowledge the request.
return new Response("OK");
}
Expand All @@ -109,7 +128,7 @@ export interface DailyWebhookOptions {
/**
* question is the daily question.
*/
question: api.LCQuestion;
question: api.Question;

/**
* season is the season to recap.
Expand Down Expand Up @@ -140,7 +159,7 @@ export function makeDailyWebhookEmbeds(
},
{
name:
"Submit your solution by typing `/lc submit YOUR_SUBMISSION_URL` below!",
"Register to play by typing `/lc register YOUR_LC_USERNAME` below!",
value: "[See more…](https://acmcsuf.com/lc-dailies-handbook)",
},
],
Expand All @@ -149,71 +168,9 @@ export function makeDailyWebhookEmbeds(
if (options.season) {
embed.fields?.push({
name: `Leaderboard for week of ${options.season.start_date}`,
value: formatScores(options.season),
value: leaderboard.formatScores(options.season),
});
}

return [embed];
}

/**
* formatScores formats the scores of all players in a season.
*/
export function formatScores(season: api.Season): string {
return [
"```",
...Object.entries(season.scores)
.sort(({ 1: scoreA }, { 1: scoreB }) => scoreB - scoreA)
.map(([playerID, score], i) => {
const player = season.players[playerID];
const formattedScore = String(score).padStart(3, " ");
const formattedRank = formatRank(i + 1);
return `${formattedScore} ${player.lc_username} (${formattedRank})`;
}),
"```",
].join("\n");
}

/**
* formatRank formats the rank of a player in a season.
*/
export function formatRank(rank: number): string {
switch (rank) {
case 1: {
return "🥇";
}

case 2: {
return "🥈";
}

case 3: {
return "🥉";
}

case 11:
case 12:
case 13: {
return `${rank}th`;
}
}

const lastDigit = rank % 10;
switch (lastDigit) {
case 1: {
return `${rank}st`;
}

case 2: {
return `${rank}nd`;
}

case 3: {
return `${rank}rd`;
}

default: {
return `${rank}th`;
}
}
}
108 changes: 47 additions & 61 deletions api/discord_app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from "lc-dailies/deps.ts";
import * as router from "lc-dailies/lib/router/mod.ts";
import * as discord from "lc-dailies/lib/discord/mod.ts";
import * as lc from "lc-dailies/lib/lc/mod.ts";
import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts";
import {
makeRegisterInteractionResponse,
Expand All @@ -23,11 +22,11 @@ import {
SUB_REGISTER,
} from "./sub/register.ts";
import {
makeSubmitInteractionResponse,
parseSubmitOptions,
SUB_SUBMIT,
SUBMIT,
} from "./sub/submit.ts";
makeSyncInteractionResponse,
parseSyncOptions,
SUB_SYNC,
SYNC,
} from "./sub/sync.ts";

export const LC = "lc";
export const LC_DESCRIPTION =
Expand All @@ -39,7 +38,7 @@ export const LC_DESCRIPTION =
export const APP_LC: RESTPostAPIApplicationCommandsJSONBody = {
name: LC,
description: LC_DESCRIPTION,
options: [SUB_REGISTER, SUB_SUBMIT],
options: [SUB_REGISTER, SUB_SYNC],
};

/**
Expand Down Expand Up @@ -108,39 +107,34 @@ export function makeDiscordAppHandler(
// Handle the subcommand.
switch (name) {
case REGISTER: {
const handleRegisterSubcommand = makeRegisterSubcommandHandler(
const registerResponse = await handleRegisterSubcommand(
leaderboardClient,
interaction.member.user,
parseRegisterOptions(interaction.data.options),
);
return Response.json(
await handleRegisterSubcommand(
interaction.member.user,
parseRegisterOptions(interaction.data.options),
),
);

return Response.json(registerResponse);
}

case SUBMIT: {
const handleSubmitSubcommand = makeSubmitSubcommandHandler(
case SYNC: {
const syncResponse = await handleSyncSubcommand(
leaderboardClient,
parseSyncOptions(interaction.data.options),
);

return Response.json(syncResponse);
}

default: {
// Acknowledge the interaction.
return Response.json(
await handleSubmitSubcommand(
interaction.member.user,
parseSubmitOptions(interaction.data.options),
),
{
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: { flags: MessageFlags.Ephemeral },
} satisfies APIInteractionResponseDeferredChannelMessageWithSource,
);
}
}

// Acknowledge the interaction.
return Response.json(
{
type: InteractionResponseType.DeferredChannelMessageWithSource,
data: {
flags: MessageFlags.Ephemeral,
},
} satisfies APIInteractionResponseDeferredChannelMessageWithSource,
);
}

default: {
Expand All @@ -150,42 +144,34 @@ export function makeDiscordAppHandler(
};
}

function makeRegisterSubcommandHandler(
/**
* handleRegisterSubcommand handles the register subcommand.
*/
async function handleRegisterSubcommand(
leaderboardClient: leaderboard.LeaderboardClient,
) {
/**
* handleRegisterSubcommand handles the register subcommand.
*/
return async function handleRegisterSubcommand(
user: APIUser,
options: ReturnType<typeof parseRegisterOptions>,
): Promise<APIInteractionResponse> {
const registerResponse = await leaderboardClient.register(
user.id,
options.lc_username,
);

return makeRegisterInteractionResponse(registerResponse);
};
user: APIUser,
options: ReturnType<typeof parseRegisterOptions>,
): Promise<APIInteractionResponse> {
const registerResponse = await leaderboardClient.register(
user.id,
options.lc_username,
);

return makeRegisterInteractionResponse(registerResponse);
}

function makeSubmitSubcommandHandler(
/**
* handleSyncSubcommand handles the sync subcommand.
*/
async function handleSyncSubcommand(
leaderboardClient: leaderboard.LeaderboardClient,
) {
/**
* handleSubmitSubcommand handles the submit subcommand.
*/
return async function handleSubmitSubcommand(
user: APIUser,
options: ReturnType<typeof parseSubmitOptions>,
): Promise<APIInteractionResponse> {
const submitResponse = await leaderboardClient.submit(
user.id,
lc.parseSubmissionID(options.submission_url),
);
options: ReturnType<typeof parseSyncOptions>,
): Promise<APIInteractionResponse> {
const syncResponse = await leaderboardClient.sync(
options.season_id,
);

return makeSubmitInteractionResponse(submitResponse);
};
return makeSyncInteractionResponse(syncResponse);
}

/**
Expand Down
8 changes: 2 additions & 6 deletions api/discord_app/sub/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ export const SUB_REGISTER: APIApplicationCommandOption = {
*/
export function parseRegisterOptions(
options: APIApplicationCommandInteractionDataOption[],
): {
[REGISTER_LC_USERNAME]: string;
} {
) {
const registerOption = options.find((option) => option.name === REGISTER);
if (!registerOption) {
throw new Error("No options provided");
Expand All @@ -59,9 +57,7 @@ export function parseRegisterOptions(
throw new Error("Expected a string for the username option.");
}

return {
[REGISTER_LC_USERNAME]: usernameOption.value,
};
return { [REGISTER_LC_USERNAME]: usernameOption.value };
}

/**
Expand Down
Loading

0 comments on commit 3ac6b70

Please sign in to comment.