Skip to content

Commit

Permalink
Show correct discord avatar on leaderboard (#1955)
Browse files Browse the repository at this point in the history
  • Loading branch information
AbdBarho committed Mar 4, 2023
1 parent 3cf7156 commit bcb2d44
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 41 deletions.
4 changes: 2 additions & 2 deletions website/src/components/LeaderboardTable/LeaderboardTable.tsx
Expand Up @@ -4,16 +4,16 @@ import { MoreHorizontal } from "lucide-react";
import NextLink from "next/link";
import { useTranslation } from "next-i18next";
import React, { useMemo } from "react";
import Image from "next/image";
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";

import { DataTable, DataTableColumnDef } from "../DataTable/DataTable";
import { createJsonExpandRowModel } from "../DataTable/jsonExpandRowModel";
import { UserAvatar } from "../UserAvatar";
import { useBoardPagination } from "./useBoardPagination";
import { useBoardRowProps } from "./useBoardRowProps";
import { useFetchBoard } from "./useFetchBoard";
import { UserAvatar } from "../UserAvatar";

type WindowLeaderboardEntity = LeaderboardEntity & { isSpaceRow?: boolean };

const columnHelper = createColumnHelper<WindowLeaderboardEntity>();
Expand Down
1 change: 1 addition & 0 deletions website/src/components/UserAvatar.tsx
Expand Up @@ -12,6 +12,7 @@ export function UserAvatar(options: { displayName: string; avatarUrl: string | n
alt={`${displayName}'s avatar`}
width={30}
height={30}
className="rounded-full"
/>
</>
);
Expand Down
20 changes: 20 additions & 0 deletions website/src/lib/leaderboard_utilities.ts
@@ -0,0 +1,20 @@
import { getValidDisplayName } from "src/lib/display_name_validation";
import { getBatchFrontendUserIdFromBackendUser } from "src/lib/users";

export const updateUsersDisplayNames = <T extends { display_name: string; username: string }>(entries: T[]) => {
return entries.map((entry) => ({
...entry,
display_name: getValidDisplayName(entry.display_name, entry.username),
}));
};

export const updateUsersProfilePictures = async <T extends { auth_method: string; username: string }>(entires: T[]) => {
const frontendUserIds = await getBatchFrontendUserIdFromBackendUser(entires);

const items = await prisma.user.findMany({
where: { id: { in: frontendUserIds } },
select: { image: true },
});

return entires.map((entry, idx) => ({ ...entry, image: items[idx].image }));
};
60 changes: 56 additions & 4 deletions website/src/lib/users.ts
Expand Up @@ -25,17 +25,15 @@ export const getBackendUserCore = async (id: string): Promise<BackendUserCore> =
if (user.accounts.length === 0) {
return {
id: user.id,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
display_name: user.name!,
display_name: user.name,
auth_method: "local",
};
}

// Otherwise, use the first linked account that the user created.
return {
id: user.accounts[0].providerAccountId,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
display_name: user.name!,
display_name: user.name,
auth_method: user.accounts[0].provider,
};
};
Expand All @@ -55,3 +53,57 @@ export const getFrontendUserIdForDiscordUser = async (id: string) => {
const { userId } = await prisma.account.findFirst({ where: { provider: "discord", providerAccountId: id } });
return userId;
};

/**
*
* @param {string} username the id of the user, this field is called 'username' in the python backend's user table
* not to be confused with the user's UUID
* @param auth_method either "local" or "discord"
*/
export const getFrontendUserIdFromBackendUser = async (username: string, auth_method: string) => {
if (auth_method === "local") {
return username;
} else if (auth_method === "discord") {
return getFrontendUserIdForDiscordUser(username);
}
throw new Error(`Unexpected auth_method: ${auth_method}`);
};

/**
* this function is similar to `getFrontendUserIdFromBackendUser`, but optimized for reducing the
* number of database calls if fetching the data for many users (i.e. leaderboard)
*/
export const getBatchFrontendUserIdFromBackendUser = async (users: { username: string; auth_method: string }[]) => {
// for users signed up with email, the 'username' field from the backend is the id of the user in the frontend db
// we initialize the output for all users with the username for now:
const outputIds = users.map((user) => user.username);

// handle discord users a bit differently
const indicesOfDiscordUsers = users
.map((user, idx) => ({ idx, isDiscord: user.auth_method === "discord" }))
.filter((x) => x.isDiscord)
.map((x) => x.idx);

if (indicesOfDiscordUsers.length === 0) {
// no discord users, save a database call
return outputIds;
}

// get the frontendUserIds for the discord users
// the `username` field for discord users is the id of the discord account
const discordAccountIds = indicesOfDiscordUsers.map((idx) => users[idx].username);
const discordAccounts = await prisma.account.findMany({
where: {
provider: "discord",
providerAccountId: { in: discordAccountIds },
},
select: { userId: true },
});

console.assert(discordAccountIds.length === discordAccounts.length);
discordAccounts.forEach(({ userId }, index) => {
const outputIndex = indicesOfDiscordUsers[index];
outputIds[outputIndex] = userId;
});
return outputIds;
};
17 changes: 9 additions & 8 deletions website/src/pages/api/admin/trollboard.ts
@@ -1,19 +1,20 @@
import { withAnyRole } from "src/lib/auth";
import { updateUsersDisplayNames, updateUsersProfilePictures } from "src/lib/leaderboard_utilities";
import { createApiClient } from "src/lib/oasst_client_factory";
import { TrollboardTimeFrame } from "src/types/Trollboard";
import { getValidDisplayName } from "src/lib/display_name_validation";

export default withAnyRole(["admin", "moderator"], async (req, res, token) => {
const client = await createApiClient(token);
const limit = parseInt(req.query.limit as string);
const enabled = req.query.enabled === "true";

const trollboard = await client.fetch_trollboard(req.query.time_frame as TrollboardTimeFrame, {
limit: req.query.limit as unknown as number,
enabled: req.query.enabled as unknown as boolean,
const trollboardReply = await client.fetch_trollboard(req.query.time_frame as TrollboardTimeFrame, {
limit,
enabled,
});

trollboard.trollboard.forEach((troll) => {
troll.display_name = getValidDisplayName(troll.display_name, troll.username);
});
trollboardReply.trollboard = updateUsersDisplayNames(trollboardReply.trollboard);
trollboardReply.trollboard = await updateUsersProfilePictures(trollboardReply.trollboard);

return res.status(200).json(trollboard);
return res.status(200).json(trollboardReply);
});
41 changes: 14 additions & 27 deletions website/src/pages/api/leaderboard.ts
@@ -1,47 +1,34 @@
import { withoutRole } from "src/lib/auth";
import { updateUsersDisplayNames, updateUsersProfilePictures } from "src/lib/leaderboard_utilities";
import { createApiClient } from "src/lib/oasst_client_factory";
import { getBackendUserCore } from "src/lib/users";
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
import { getValidDisplayName } from "src/lib/display_name_validation";

/**
* Returns the set of valid labels that can be applied to messages.
*/
const handler = withoutRole("banned", async (req, res, token) => {
export default withoutRole("banned", async (req, res, token) => {
const oasstApiClient = await createApiClient(token);
const backendUser = await getBackendUserCore(token.sub);
const time_frame = (req.query.time_frame as LeaderboardTimeFrame) ?? LeaderboardTimeFrame.day;
const includeUserStats = req.query.includeUserStats;
const limit = parseInt(req.query.limit as string);
const includeUserStats = req.query.includeUserStats === "true";

if (includeUserStats !== "true") {
let leaderboard = await oasstApiClient.fetch_leaderboard(time_frame, {
limit: req.query.limit as unknown as number,
});
leaderboard = getValidLeaderboard(leaderboard);
return res.status(200).json(leaderboard);
if (!includeUserStats) {
const leaderboardReply = await oasstApiClient.fetch_leaderboard(time_frame, { limit });
leaderboardReply.leaderboard = updateUsersDisplayNames(leaderboardReply.leaderboard);
leaderboardReply.leaderboard = await updateUsersProfilePictures(leaderboardReply.leaderboard);
return res.status(200).json(leaderboardReply);
}
const user = await oasstApiClient.fetch_frontend_user(backendUser);

const [leaderboard, user_stats] = await Promise.all([
oasstApiClient.fetch_leaderboard(time_frame, {
limit: req.query.limit as unknown as number,
}),
const [leaderboardReply, user_stats] = await Promise.all([
oasstApiClient.fetch_leaderboard(time_frame, { limit }),
oasstApiClient.fetch_user_stats_window(user.user_id, time_frame, 3),
]);

const validLeaderboard = getValidLeaderboard(leaderboard);
leaderboardReply.leaderboard = updateUsersDisplayNames(leaderboardReply.leaderboard);
leaderboardReply.leaderboard = await updateUsersProfilePictures(leaderboardReply.leaderboard);

res.status(200).json({
...validLeaderboard,
...leaderboardReply,
user_stats_window: user_stats?.leaderboard.map((stats) => ({ ...stats, is_window: true })),
});
});

const getValidLeaderboard = (leaderboard) => {
leaderboard.leaderboard.forEach((user) => {
user.display_name = getValidDisplayName(user.display_name, user.username);
});
return leaderboard;
};

export default handler;

0 comments on commit bcb2d44

Please sign in to comment.