diff --git a/app/features/api-public/routes/user.$identifier.ids.ts b/app/features/api-public/routes/user.$identifier.ids.ts new file mode 100644 index 000000000..6bc1309e7 --- /dev/null +++ b/app/features/api-public/routes/user.$identifier.ids.ts @@ -0,0 +1,31 @@ +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { cors } from "remix-utils/cors"; +import { z } from "zod/v4"; +import { identifierToUserIdQuery } from "~/features/user-page/UserRepository.server"; +import { notFoundIfFalsy, parseParams } from "~/utils/remix.server"; +import { handleOptionsRequest } from "../api-public-utils.server"; +import type { GetUserIdsResponse } from "../schema"; + +const paramsSchema = z.object({ + identifier: z.string(), +}); + +export const loader = async ({ params, request }: LoaderFunctionArgs) => { + await handleOptionsRequest(request); + + const { identifier } = parseParams({ params, schema: paramsSchema }); + + const user = notFoundIfFalsy( + await identifierToUserIdQuery(identifier) + .select(["User.discordId", "User.customUrl"]) + .executeTakeFirst(), + ); + + const result: GetUserIdsResponse = { + id: user.id, + discordId: user.discordId, + customUrl: user.customUrl, + }; + + return await cors(request, json(result)); +}; diff --git a/app/features/api-public/schema.ts b/app/features/api-public/schema.ts index 8678c3a1d..d967fbd2d 100644 --- a/app/features/api-public/schema.ts +++ b/app/features/api-public/schema.ts @@ -42,6 +42,20 @@ export interface GetUserResponse { currentRank: SeasonalRank | null; } +/** GET /api/user/{userId|discordId|customUrl}/ids */ + +export interface GetUserIdsResponse { + id: number; + /** + * @example "79237403620945920" + */ + discordId: string; + /** + * @example "sendou" + */ + customUrl: string | null; +} + /** GET /api/team/{teamId} */ export interface GetTeamResponse { diff --git a/app/features/badges/homemade.json b/app/features/badges/homemade.json index 689c194b8..03778f379 100644 --- a/app/features/badges/homemade.json +++ b/app/features/badges/homemade.json @@ -955,6 +955,10 @@ "displayName": "Rapid Rushdown", "authorDiscordId": "528851510222782474" }, + "runspeed": { + "displayName": "Run Speed Up", + "authorDiscordId": "528851510222782474" + }, "s50badge": { "displayName": "Anarchy Adventures", "authorDiscordId": "336683473714544641" diff --git a/app/features/sendouq-match/core/match.server.test.ts b/app/features/sendouq-match/core/match.server.test.ts index edbb490ad..50945ae82 100644 --- a/app/features/sendouq-match/core/match.server.test.ts +++ b/app/features/sendouq-match/core/match.server.test.ts @@ -261,6 +261,7 @@ describe("matchMapList()", () => { preferences: [{ userId: 2, preferences: emptyPreferences }], id: 2, }, + ["SZ"], ); const szMaps = result.filter((m) => m.mode === "SZ"); @@ -317,6 +318,7 @@ describe("matchMapList()", () => { ], id: 2, }, + ["SZ"], ); const szMaps = result.filter((m) => m.mode === "SZ"); diff --git a/app/features/sendouq-match/core/match.server.ts b/app/features/sendouq-match/core/match.server.ts index befd65748..ec05cc237 100644 --- a/app/features/sendouq-match/core/match.server.ts +++ b/app/features/sendouq-match/core/match.server.ts @@ -28,7 +28,7 @@ type WeightsMap = Map; async function calculateMapWeights( groupOnePreferences: UserMapModePreferences[], groupTwoPreferences: UserMapModePreferences[], - modesIncluded: ModeShort[], + modesIncluded: readonly ModeShort[], ): Promise { const teamOneVotes: WeightsMap = new Map(); const teamTwoVotes: WeightsMap = new Map(); @@ -130,7 +130,7 @@ async function applyDefaultWeights( } function countVotesForTeam( - modesIncluded: ModeShort[], + modesIncluded: readonly ModeShort[], preferences: UserMapModePreferences[], votesMap: WeightsMap, ) { @@ -160,23 +160,13 @@ export async function matchMapList( groupOne: { preferences: { userId: number; preferences: UserMapModePreferences }[]; id: number; - ignoreModePreferences?: boolean; }, groupTwo: { preferences: { userId: number; preferences: UserMapModePreferences }[]; id: number; - ignoreModePreferences?: boolean; }, + modesIncluded: readonly ModeShort[], ): Promise { - const modesIncluded = mapModePreferencesToModeList( - groupOne.ignoreModePreferences - ? [] - : groupOne.preferences.map(({ preferences }) => preferences.modes), - groupTwo.ignoreModePreferences - ? [] - : groupTwo.preferences.map(({ preferences }) => preferences.modes), - ); - const weights = await calculateMapWeights( groupOne.preferences.map((p) => p.preferences), groupTwo.preferences.map((p) => p.preferences), diff --git a/app/features/sendouq/actions/q.looking.server.ts b/app/features/sendouq/actions/q.looking.server.ts index e9e9eee3f..06fddd800 100644 --- a/app/features/sendouq/actions/q.looking.server.ts +++ b/app/features/sendouq/actions/q.looking.server.ts @@ -16,6 +16,7 @@ import { groupAfterMorph } from "../core/groups"; import { refreshSendouQInstance, SendouQ } from "../core/SendouQ.server"; import * as PrivateUserNoteRepository from "../PrivateUserNoteRepository.server"; import { lookingSchema } from "../q-schemas.server"; +import { resolveFutureMatchModes } from "../q-utils"; import { SendouQError } from "../q-utils.server"; // this function doesn't throw normally because we are assuming @@ -137,36 +138,38 @@ export const action: ActionFunction = async ({ request }) => { break; } - case "MATCH_UP_RECHALLENGE": case "MATCH_UP": { if (!isGroupManager()) return null; - const ourGroup = SendouQ.findOwnGroup(user.id); + const ownGroup = SendouQ.findOwnGroup(user.id); const theirGroup = SendouQ.findUncensoredGroupById(data.targetGroupId); - if (!ourGroup || !theirGroup) return null; + if (!ownGroup || !theirGroup) return null; - const ourGroupPreferences = - await SQGroupRepository.mapModePreferencesByGroupId(ourGroup.id); + const ownGroupPreferences = + await SQGroupRepository.mapModePreferencesByGroupId(ownGroup.id); const theirGroupPreferences = await SQGroupRepository.mapModePreferencesByGroupId(theirGroup.id); + + const modesIncluded = resolveFutureMatchModes(ownGroup, theirGroup); + const mapList = await matchMapList( { - id: ourGroup.id, - preferences: ourGroupPreferences, + id: ownGroup.id, + preferences: ownGroupPreferences, }, { id: theirGroup.id, preferences: theirGroupPreferences, - ignoreModePreferences: data._action === "MATCH_UP_RECHALLENGE", }, + modesIncluded, ); const createdMatch = await SQMatchRepository.create({ - alphaGroupId: ourGroup.id, + alphaGroupId: ownGroup.id, bravoGroupId: theirGroup.id, mapList, memento: createMatchMemento({ - own: { group: ourGroup, preferences: ourGroupPreferences }, + own: { group: ownGroup, preferences: ownGroupPreferences }, their: { group: theirGroup, preferences: theirGroupPreferences }, mapList, }), @@ -174,10 +177,10 @@ export const action: ActionFunction = async ({ request }) => { await refreshSendouQInstance(); - if (ourGroup.chatCode && theirGroup.chatCode) { + if (ownGroup.chatCode && theirGroup.chatCode) { ChatSystemMessage.send([ { - room: ourGroup.chatCode, + room: ownGroup.chatCode, type: "MATCH_STARTED", revalidateOnly: true, }, @@ -191,7 +194,7 @@ export const action: ActionFunction = async ({ request }) => { notify({ userIds: [ - ...ourGroup.members.map((m) => m.id), + ...ownGroup.members.map((m) => m.id), ...theirGroup.members.map((m) => m.id), ], defaultSeenUserIds: [user.id], diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx index 65c2a6385..7eab72cc3 100644 --- a/app/features/sendouq/components/GroupCard.tsx +++ b/app/features/sendouq/components/GroupCard.tsx @@ -80,10 +80,7 @@ export function GroupCard({ const isOwnGroup = group.id === ownGroup?.id; const futureMatchModes = ownGroup - ? resolveFutureMatchModes({ - ownGroup, - theirGroup: group, - }) + ? resolveFutureMatchModes(ownGroup, group) : null; const enableKicking = group.usersRole === "OWNER" && !displayOnly; diff --git a/app/features/sendouq/q-schemas.server.ts b/app/features/sendouq/q-schemas.server.ts index c04aa2287..15581e9bd 100644 --- a/app/features/sendouq/q-schemas.server.ts +++ b/app/features/sendouq/q-schemas.server.ts @@ -58,10 +58,6 @@ export const lookingSchema = z.union([ _action: _action("MATCH_UP"), targetGroupId: id, }), - z.object({ - _action: _action("MATCH_UP_RECHALLENGE"), - targetGroupId: id, - }), z.object({ _action: _action("GIVE_MANAGER"), userId: id, diff --git a/app/features/sendouq/q-utils.ts b/app/features/sendouq/q-utils.ts index 06c52f015..530034958 100644 --- a/app/features/sendouq/q-utils.ts +++ b/app/features/sendouq/q-utils.ts @@ -27,15 +27,12 @@ export function userCanJoinQueueAt( return canJoinQueueAt; } -export function resolveFutureMatchModes({ - ownGroup, - theirGroup, -}: { - ownGroup: Pick; - theirGroup: Pick; -}) { - const ourModes = ownGroup.modePreferences; - const theirModes = theirGroup.modePreferences; +export function resolveFutureMatchModes( + groupA: Pick, + groupB: Pick, +) { + const ourModes = groupA.modePreferences; + const theirModes = groupB.modePreferences; const overlap = ourModes.filter((mode) => theirModes.includes(mode)); if (overlap.length > 0) { diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index 563761b7f..a21dd3a44 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -24,7 +24,7 @@ import { import { safeNumberParse } from "~/utils/number"; import type { ChatUser } from "../chat/chat-types"; -const identifierToUserIdQuery = (identifier: string) => +export const identifierToUserIdQuery = (identifier: string) => db .selectFrom("User") .select("User.id") diff --git a/app/routes.ts b/app/routes.ts index f97714608..9583324ed 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -256,6 +256,10 @@ export default [ "/user/:identifier", "features/api-public/routes/user.$identifier.ts", ), + route( + "/user/:identifier/ids", + "features/api-public/routes/user.$identifier.ids.ts", + ), route( "/calendar/:year/:week", "features/api-public/routes/calendar.$year.$week.ts", diff --git a/public/static-assets/badges/runspeed.avif b/public/static-assets/badges/runspeed.avif new file mode 100644 index 000000000..083dffebe Binary files /dev/null and b/public/static-assets/badges/runspeed.avif differ diff --git a/public/static-assets/badges/runspeed.gif b/public/static-assets/badges/runspeed.gif new file mode 100644 index 000000000..ac6554129 Binary files /dev/null and b/public/static-assets/badges/runspeed.gif differ diff --git a/public/static-assets/badges/runspeed.png b/public/static-assets/badges/runspeed.png new file mode 100644 index 000000000..b301f40d5 Binary files /dev/null and b/public/static-assets/badges/runspeed.png differ