Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Represent season as Discord webhook content #33

Merged
merged 10 commits into from
Sep 30, 2023
24 changes: 21 additions & 3 deletions api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import * as lc from "lc-dailies/lib/lc/mod.ts";
import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts";
import * as router from "lc-dailies/lib/router/mod.ts";
import * as discord_app from "./discord_app/mod.ts";
import { makeDailyWebhookPostHandler } from "./dailies.ts";
import {
makeDailyWebhookPostHandler,
makeManualDailyWebhookPostHandler,
} from "./dailies.ts";
import { makeSeasonGetHandler, makeSeasonsGetHandler } from "./seasons.ts";

/**
Expand All @@ -30,10 +33,18 @@ export function makeAPIRouter(
),
),
)
.post(
new URLPattern({ pathname: "/webhook" }),
makeManualDailyWebhookPostHandler(
lcClient,
leaderboardClient,
),
)
.post(
new URLPattern({ pathname: "/webhook/:token" }),
makeDailyWebhookPostHandler(
lcClient,
leaderboardClient,
webhookURL,
webhookToken,
),
Expand Down Expand Up @@ -85,15 +96,22 @@ export function makeOnListen(
);
console.log(
"- Invite LC-Dailies to your server:",
`http://127.0.0.1:${port}/invite/`,
`http://127.0.0.1:${port}/invite`,
);
console.log(
"- Latest season:",
`http://127.0.0.1:${port}/seasons/latest/`,
`http://127.0.0.1:${port}/seasons/latest`,
);
};
}

function makeInviteURL(applicationID: string) {
return `https://discord.com/api/oauth2/authorize?client_id=${applicationID}&scope=applications.commands`;
}

fetch(
"http://127.0.0.1:8080/webhook?season_id=01H8T4MM00BQHHK7VTTEJE1WAS&webhook_url=https://discord.com/api/webhooks/1128189891847667712/jUq5uK9dzM4V99fsj8Y6b8tBiZx_idMB5DAMKAPJQ4rTbEiqoy7ah2qUpORWyfaHGl4l",
{
method: "POST",
},
).then((res) => res.json()).then(console.log).catch(console.error);
194 changes: 181 additions & 13 deletions api/dailies.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { type APIEmbed } from "lc-dailies/deps.ts";
import * as discord from "lc-dailies/lib/discord/mod.ts";
import * as router from "lc-dailies/lib/router/mod.ts";
import * as lc from "lc-dailies/lib/lc/mod.ts";
import * as leaderboard from "lc-dailies/lib/leaderboard/mod.ts";
import * as snacks from "./snacks.ts";

/**
* makeDailyWebhookPostHandler creates a handler for daily webhook POST requests.
*/
export function makeDailyWebhookPostHandler(
lcClient: lc.LCClient,
leaderboardClient: leaderboard.LeaderboardClient,
webhookURL: string,
webhookToken?: string,
) {
Expand All @@ -17,37 +20,202 @@ export function makeDailyWebhookPostHandler(
return async function handlePostDailyWebhook(
request: router.RouterRequest,
): Promise<Response> {
// Override the webhook URL if applicable.
const overrideWebhookURL = request.url.searchParams.get("webhook_url");
if (overrideWebhookURL) {
webhookURL = overrideWebhookURL;
}

// Check the webhook token.
const token = request.params["token"];
if (webhookToken && token !== webhookToken) {
if (!overrideWebhookURL && webhookToken && token !== webhookToken) {
return new Response("Invalid token", { status: 401 });
}

return await handleExecuteDailyWebhook(lcClient, webhookURL);
// Get the season ID if applicable.
const seasonID = request.url.searchParams.get("season_id");

// Execute the webhook.
return await executeDailyWebhook(
lcClient,
leaderboardClient,
webhookURL,
seasonID,
);
};
}

/**
* makeManualDailyWebhookPostHandler creates a handler for any variable
* webhook URL POST requests.
*/
export function makeManualDailyWebhookPostHandler(
lcClient: lc.LCClient,
leaderboardClient: leaderboard.LeaderboardClient,
) {
return async function handleManualPostDailyWebhook(
request: router.RouterRequest,
): Promise<Response> {
const seasonID = request.url.searchParams.get("season_id");
const webhookURL = request.url.searchParams.get("webhook_url");
if (!webhookURL) {
return new Response("Missing webhook_url", { status: 400 });
}

return await executeDailyWebhook(
lcClient,
leaderboardClient,
webhookURL,
seasonID,
);
};
}

async function handleExecuteDailyWebhook(
async function executeDailyWebhook(
lcClient: lc.LCClient,
leaderboardClient: leaderboard.LeaderboardClient,
webhookURL: string,
seasonID: string | null,
): Promise<Response> {
// Get the daily question.
const question = await lcClient.getDailyQuestion();
const content = formatLCDailyQuestion(question);

// 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
? await leaderboardClient.getSeason(seasonID)
: isSunday
? await leaderboardClient.getCurrentSeason()
: null;

// Format the webhook embed.
const embeds = makeDailyWebhookEmbeds({ question, season });

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

// Acknowledge the request.
return new Response("OK");
}

function formatLCDailyQuestion(question: lc.DailyQuestion): string {
/**
* DailyWebhookOptions are the options for makeDailyWebhookEmbeds.
*/
export interface DailyWebhookOptions {
/**
* question is the daily question.
*/
question: lc.DailyQuestion;

/**
* season is the season to recap.
*/
season: leaderboard.Season | null;
}

/**
* makeDailyWebhookEmbeds formats a daily webhook.
*/
export function makeDailyWebhookEmbeds(
options: DailyWebhookOptions,
): APIEmbed[] {
const embed: APIEmbed = {
title: options.question.title,
url: options.question.url,
description: `Daily Leetcode question for ${options.question.date}.`,
fields: [
{
name: "Difficulty",
value: options.question.difficulty,
inline: true,
},
{
name: "Here is a snack to get your brain working!",
value: snacks.pickRandom(),
inline: true,
},
{
name:
"Submit your solution by typing `/lc submit YOUR_SUBMISSION_URL` below!",
value: "[See more…](https://acmcsuf.com/lc-dailies-handbook)",
},
],
};

if (options.season) {
embed.fields?.push({
name: `Leaderboard for week of ${options.season.start_date}`,
value: formatScores(options.season),
});
}

return [embed];
}

/**
* formatScores formats the scores of all players in a season.
*/
export function formatScores(season: leaderboard.Season): string {
const scores = leaderboard.calculateSeasonScores(
leaderboard.makeDefaultCalculateScoresOptions(season),
);
return [
`## Daily Leetcode Question for ${question.date}`,
`**Question**: ${question.title}`,
`**Difficulty**: ${question.difficulty}`,
`**Link**: <${question.url}>`,
`**Snack**: Here is a snack to get your brain working: ${snacks.pickRandom()}`,
"",
"Submit your solution by typing `/lc submit YOUR_SUBMISSION_URL` below! ([more info](https://acmcsuf.com/lc-dailies-handbook))",
"```",
...Object.entries(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`;
}
}
}
10 changes: 8 additions & 2 deletions cf/dailies/dailies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export default {
return new Response("Unexpected cron expression", { status: 400 });
}

return await execute(env.WEBHOOK_URL);
const seasonID = url.searchParams.get("season_id");
return await execute(env.WEBHOOK_URL, seasonID);
},

/**
Expand All @@ -56,7 +57,12 @@ export default {
},
};

function execute(webhookURL: string) {
function execute(webhookURL: string | URL, seasonID?: string | null) {
if (seasonID) {
webhookURL = new URL(webhookURL);
webhookURL.searchParams.set("season_id", seasonID);
}

return fetch(webhookURL, { method: "POST" });
}

Expand Down
Loading