mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-25 13:03:59 -05:00
470 lines
12 KiB
TypeScript
470 lines
12 KiB
TypeScript
import type { Transaction } from "kysely";
|
|
import { jsonBuildObject } from "kysely/helpers/sqlite";
|
|
import { db } from "~/db/sql";
|
|
import type { DB, Tables } from "~/db/tables";
|
|
import { shortNanoid } from "~/utils/id";
|
|
import invariant from "~/utils/invariant";
|
|
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
|
|
import { errorIsSqliteForeignKeyConstraintFailure } from "~/utils/sql";
|
|
import { randomTeamName } from "~/utils/team-name";
|
|
|
|
export function startLooking(teamId: number) {
|
|
return db.transaction().execute(async (trx) => {
|
|
await trx
|
|
.updateTable("TournamentTeam")
|
|
.set({ isLooking: 1 })
|
|
.where("id", "=", teamId)
|
|
.execute();
|
|
|
|
return ensurePickupChatCode(teamId, trx);
|
|
});
|
|
}
|
|
|
|
type CreatePlaceholderTeamArgs = {
|
|
tournamentId: number;
|
|
userId: number;
|
|
isStayAsSub?: boolean;
|
|
lfgNote?: string;
|
|
};
|
|
export function createPlaceholderTeam(args: CreatePlaceholderTeamArgs) {
|
|
return db.transaction().execute(async (trx) => {
|
|
const createdTeam = await trx
|
|
.insertInto("TournamentTeam")
|
|
.values({
|
|
tournamentId: args.tournamentId,
|
|
name: randomTeamName(),
|
|
inviteCode: shortNanoid(),
|
|
isPlaceholder: 1,
|
|
isLooking: 1,
|
|
lfgNote: args.lfgNote ?? null,
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow();
|
|
|
|
await trx
|
|
.insertInto("TournamentTeamMember")
|
|
.values({
|
|
tournamentTeamId: createdTeam.id,
|
|
userId: args.userId,
|
|
role: "OWNER",
|
|
isStayAsSub: args.isStayAsSub ? 1 : 0,
|
|
})
|
|
.execute();
|
|
|
|
return createdTeam;
|
|
});
|
|
}
|
|
|
|
type TournamentLFGMemberObject = {
|
|
id: Tables["User"]["id"];
|
|
username: Tables["User"]["username"];
|
|
discordId: Tables["User"]["discordId"];
|
|
discordAvatar: Tables["User"]["discordAvatar"];
|
|
customUrl: Tables["User"]["customUrl"];
|
|
languages: Tables["User"]["languages"];
|
|
vc: Tables["User"]["vc"];
|
|
pronouns: Tables["User"]["pronouns"];
|
|
role: Tables["TournamentTeamMember"]["role"];
|
|
isStayAsSub: Tables["TournamentTeamMember"]["isStayAsSub"];
|
|
weapons: Tables["User"]["qWeaponPool"];
|
|
plusTier: Tables["PlusTier"]["tier"] | null;
|
|
};
|
|
|
|
export async function findLookingTeamsByTournamentId(tournamentId: number) {
|
|
return db
|
|
.selectFrom("TournamentTeam")
|
|
.innerJoin(
|
|
"TournamentTeamMember",
|
|
"TournamentTeamMember.tournamentTeamId",
|
|
"TournamentTeam.id",
|
|
)
|
|
.innerJoin("User", "User.id", "TournamentTeamMember.userId")
|
|
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
|
|
.leftJoin(
|
|
"UserSubmittedImage",
|
|
"UserSubmittedImage.id",
|
|
"TournamentTeam.avatarImgId",
|
|
)
|
|
.select(({ fn, eb }) => [
|
|
"TournamentTeam.id",
|
|
"TournamentTeam.isPlaceholder",
|
|
"TournamentTeam.lfgNote as note",
|
|
"TournamentTeam.name as teamName",
|
|
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
|
|
"teamAvatarUrl",
|
|
),
|
|
fn
|
|
.agg("json_group_array", [
|
|
jsonBuildObject({
|
|
id: eb.ref("User.id"),
|
|
username: eb.ref("User.username"),
|
|
discordId: eb.ref("User.discordId"),
|
|
discordAvatar: eb.ref("User.discordAvatar"),
|
|
customUrl: eb.ref("User.customUrl"),
|
|
languages: eb.ref("User.languages"),
|
|
vc: eb.ref("User.vc"),
|
|
pronouns: eb.ref("User.pronouns"),
|
|
role: eb.ref("TournamentTeamMember.role"),
|
|
isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"),
|
|
weapons: eb.ref("User.qWeaponPool"),
|
|
plusTier: eb.ref("PlusTier.tier"),
|
|
}),
|
|
])
|
|
.$castTo<TournamentLFGMemberObject[]>()
|
|
.as("members"),
|
|
])
|
|
.where("TournamentTeam.tournamentId", "=", tournamentId)
|
|
.where("TournamentTeam.isLooking", "=", 1)
|
|
.groupBy("TournamentTeam.id")
|
|
.execute();
|
|
}
|
|
|
|
export async function findSubGroups(tournamentId: number) {
|
|
const rows = await db
|
|
.selectFrom("TournamentTeam")
|
|
.innerJoin(
|
|
"TournamentTeamMember",
|
|
"TournamentTeamMember.tournamentTeamId",
|
|
"TournamentTeam.id",
|
|
)
|
|
.innerJoin("User", "User.id", "TournamentTeamMember.userId")
|
|
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
|
|
.select(({ fn, eb }) => [
|
|
"TournamentTeam.id",
|
|
"TournamentTeam.lfgNote as message",
|
|
fn
|
|
.agg("json_group_array", [
|
|
jsonBuildObject({
|
|
id: eb.ref("User.id"),
|
|
username: eb.ref("User.username"),
|
|
discordId: eb.ref("User.discordId"),
|
|
discordAvatar: eb.ref("User.discordAvatar"),
|
|
customUrl: eb.ref("User.customUrl"),
|
|
languages: eb.ref("User.languages"),
|
|
vc: eb.ref("User.vc"),
|
|
pronouns: eb.ref("User.pronouns"),
|
|
role: eb.ref("TournamentTeamMember.role"),
|
|
isStayAsSub: eb.ref("TournamentTeamMember.isStayAsSub"),
|
|
weapons: eb.ref("User.qWeaponPool"),
|
|
plusTier: eb.ref("PlusTier.tier"),
|
|
}),
|
|
])
|
|
.$castTo<TournamentLFGMemberObject[]>()
|
|
.as("members"),
|
|
])
|
|
.where("TournamentTeam.tournamentId", "=", tournamentId)
|
|
.where("TournamentTeam.isPlaceholder", "=", 1)
|
|
.where("TournamentTeamMember.isStayAsSub", "=", 1)
|
|
.groupBy("TournamentTeam.id")
|
|
.execute();
|
|
|
|
return rows;
|
|
}
|
|
|
|
export function mergeTeams({
|
|
survivingTeamId,
|
|
otherTeamId,
|
|
maxGroupSize,
|
|
}: {
|
|
survivingTeamId: number;
|
|
otherTeamId: number;
|
|
maxGroupSize: number;
|
|
}) {
|
|
return db.transaction().execute(async (trx) => {
|
|
const otherTeam = await trx
|
|
.selectFrom("TournamentTeam")
|
|
.select("chatCode")
|
|
.where("id", "=", otherTeamId)
|
|
.executeTakeFirst();
|
|
|
|
const otherMembers = await trx
|
|
.selectFrom("TournamentTeamMember")
|
|
.select(["TournamentTeamMember.userId", "TournamentTeamMember.role"])
|
|
.where("TournamentTeamMember.tournamentTeamId", "=", otherTeamId)
|
|
.execute();
|
|
|
|
for (const member of otherMembers) {
|
|
await trx
|
|
.updateTable("TournamentTeamMember")
|
|
.set({
|
|
role: member.role === "OWNER" ? "MANAGER" : member.role,
|
|
tournamentTeamId: survivingTeamId,
|
|
})
|
|
.where("TournamentTeamMember.tournamentTeamId", "=", otherTeamId)
|
|
.where("TournamentTeamMember.userId", "=", member.userId)
|
|
.execute();
|
|
}
|
|
|
|
await deleteLikesByTeamId(survivingTeamId, trx);
|
|
|
|
await trx
|
|
.deleteFrom("TournamentTeam")
|
|
.where("TournamentTeam.id", "=", otherTeamId)
|
|
.execute();
|
|
|
|
const memberCount = await getMemberCount(survivingTeamId, trx);
|
|
|
|
invariant(
|
|
memberCount <= maxGroupSize,
|
|
"Group has too many members after merge",
|
|
);
|
|
|
|
await trx
|
|
.updateTable("TournamentTeam")
|
|
.set({
|
|
isLooking: memberCount >= maxGroupSize ? 0 : undefined,
|
|
isPlaceholder: 0,
|
|
})
|
|
.where("id", "=", survivingTeamId)
|
|
.execute();
|
|
|
|
const survivor = await ensurePickupChatCode(survivingTeamId, trx);
|
|
|
|
return {
|
|
survivor,
|
|
removedChatCode: otherTeam?.chatCode ?? null,
|
|
};
|
|
});
|
|
}
|
|
|
|
export async function addLike({
|
|
likerTeamId,
|
|
targetTeamId,
|
|
}: {
|
|
likerTeamId: number;
|
|
targetTeamId: number;
|
|
}) {
|
|
try {
|
|
await db
|
|
.insertInto("TournamentLFGLike")
|
|
.values({ likerTeamId, targetTeamId })
|
|
.onConflict((oc) =>
|
|
oc.columns(["likerTeamId", "targetTeamId"]).doNothing(),
|
|
)
|
|
.execute();
|
|
} catch (error) {
|
|
if (errorIsSqliteForeignKeyConstraintFailure(error)) {
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function deleteLike({
|
|
likerTeamId,
|
|
targetTeamId,
|
|
}: {
|
|
likerTeamId: number;
|
|
targetTeamId: number;
|
|
}) {
|
|
return db
|
|
.deleteFrom("TournamentLFGLike")
|
|
.where("likerTeamId", "=", likerTeamId)
|
|
.where("targetTeamId", "=", targetTeamId)
|
|
.execute();
|
|
}
|
|
|
|
export async function allLikesByTeamId(teamId: number) {
|
|
const rows = await db
|
|
.selectFrom("TournamentLFGLike")
|
|
.select(["TournamentLFGLike.likerTeamId", "TournamentLFGLike.targetTeamId"])
|
|
.where((eb) =>
|
|
eb.or([
|
|
eb("TournamentLFGLike.likerTeamId", "=", teamId),
|
|
eb("TournamentLFGLike.targetTeamId", "=", teamId),
|
|
]),
|
|
)
|
|
.execute();
|
|
|
|
return {
|
|
given: rows
|
|
.filter((row) => row.likerTeamId === teamId)
|
|
.map((row) => ({ teamId: row.targetTeamId })),
|
|
received: rows
|
|
.filter((row) => row.targetTeamId === teamId)
|
|
.map((row) => ({ teamId: row.likerTeamId })),
|
|
};
|
|
}
|
|
|
|
export function updateTeamNote({
|
|
teamId,
|
|
value,
|
|
}: {
|
|
teamId: number;
|
|
value: string | null;
|
|
}) {
|
|
return db
|
|
.updateTable("TournamentTeam")
|
|
.set({ lfgNote: value })
|
|
.where("id", "=", teamId)
|
|
.execute();
|
|
}
|
|
|
|
export function updateMemberRole({
|
|
userId,
|
|
teamId,
|
|
role,
|
|
}: {
|
|
userId: number;
|
|
teamId: number;
|
|
role: Tables["TournamentTeamMember"]["role"];
|
|
}) {
|
|
if (role === "OWNER") {
|
|
throw new Error("Can't set role to OWNER with this function");
|
|
}
|
|
|
|
return db
|
|
.updateTable("TournamentTeamMember")
|
|
.set({ role })
|
|
.where("userId", "=", userId)
|
|
.where("tournamentTeamId", "=", teamId)
|
|
.execute();
|
|
}
|
|
|
|
export function updateStayAsSub({
|
|
teamId,
|
|
userId,
|
|
value,
|
|
}: {
|
|
teamId: number;
|
|
userId: number;
|
|
value: boolean;
|
|
}) {
|
|
return db
|
|
.updateTable("TournamentTeamMember")
|
|
.set({ isStayAsSub: value ? 1 : 0 })
|
|
.where("tournamentTeamId", "=", teamId)
|
|
.where("userId", "=", userId)
|
|
.execute();
|
|
}
|
|
|
|
export function leaveLfg({
|
|
userId,
|
|
tournamentId,
|
|
}: {
|
|
userId: number;
|
|
tournamentId: number;
|
|
}) {
|
|
return db.transaction().execute(async (trx) => {
|
|
const userTeam = await trx
|
|
.selectFrom("TournamentTeamMember")
|
|
.innerJoin(
|
|
"TournamentTeam",
|
|
"TournamentTeam.id",
|
|
"TournamentTeamMember.tournamentTeamId",
|
|
)
|
|
.select([
|
|
"TournamentTeamMember.tournamentTeamId",
|
|
"TournamentTeam.isPlaceholder",
|
|
])
|
|
.where("TournamentTeamMember.userId", "=", userId)
|
|
.where("TournamentTeam.tournamentId", "=", tournamentId)
|
|
.where("TournamentTeam.isLooking", "=", 1)
|
|
.executeTakeFirst();
|
|
|
|
if (!userTeam) return;
|
|
|
|
if (!userTeam.isPlaceholder) {
|
|
await trx
|
|
.updateTable("TournamentTeam")
|
|
.set({ isLooking: 0 })
|
|
.where("id", "=", userTeam.tournamentTeamId)
|
|
.execute();
|
|
await trx
|
|
.updateTable("TournamentTeamMember")
|
|
.set({ isStayAsSub: 0 })
|
|
.where("tournamentTeamId", "=", userTeam.tournamentTeamId)
|
|
.execute();
|
|
await deleteLikesByTeamId(userTeam.tournamentTeamId, trx);
|
|
return;
|
|
}
|
|
|
|
await trx
|
|
.deleteFrom("TournamentTeam")
|
|
.where("id", "=", userTeam.tournamentTeamId)
|
|
.execute();
|
|
});
|
|
}
|
|
|
|
export async function getSubsForTournament(tournamentId: number) {
|
|
const rows = await db
|
|
.selectFrom("TournamentTeamMember")
|
|
.innerJoin(
|
|
"TournamentTeam",
|
|
"TournamentTeam.id",
|
|
"TournamentTeamMember.tournamentTeamId",
|
|
)
|
|
.select("TournamentTeamMember.userId")
|
|
.where("TournamentTeam.tournamentId", "=", tournamentId)
|
|
.where("TournamentTeamMember.isStayAsSub", "=", 1)
|
|
.execute();
|
|
|
|
return rows.map((row) => row.userId);
|
|
}
|
|
|
|
function deleteLikesByTeamId(teamId: number, trx: Transaction<DB>) {
|
|
return trx
|
|
.deleteFrom("TournamentLFGLike")
|
|
.where((eb) =>
|
|
eb.or([
|
|
eb("TournamentLFGLike.likerTeamId", "=", teamId),
|
|
eb("TournamentLFGLike.targetTeamId", "=", teamId),
|
|
]),
|
|
)
|
|
.execute();
|
|
}
|
|
|
|
async function getMemberCount(
|
|
teamId: number,
|
|
trx: Transaction<DB>,
|
|
): Promise<number> {
|
|
const members = await trx
|
|
.selectFrom("TournamentTeamMember")
|
|
.select("TournamentTeamMember.userId")
|
|
.where("TournamentTeamMember.tournamentTeamId", "=", teamId)
|
|
.execute();
|
|
|
|
return members.length;
|
|
}
|
|
|
|
export type PickupChatTeam = {
|
|
chatCode: string;
|
|
name: string;
|
|
memberUserIds: number[];
|
|
};
|
|
|
|
async function ensurePickupChatCode(
|
|
teamId: number,
|
|
trx: Transaction<DB>,
|
|
): Promise<PickupChatTeam | null> {
|
|
const team = await trx
|
|
.selectFrom("TournamentTeam")
|
|
.select(["name", "chatCode"])
|
|
.where("id", "=", teamId)
|
|
.executeTakeFirstOrThrow();
|
|
|
|
const members = await trx
|
|
.selectFrom("TournamentTeamMember")
|
|
.select("userId")
|
|
.where("tournamentTeamId", "=", teamId)
|
|
.execute();
|
|
|
|
if (members.length < 2) return null;
|
|
|
|
let chatCode = team.chatCode;
|
|
if (!chatCode) {
|
|
chatCode = shortNanoid();
|
|
await trx
|
|
.updateTable("TournamentTeam")
|
|
.set({ chatCode })
|
|
.where("id", "=", teamId)
|
|
.execute();
|
|
}
|
|
|
|
return {
|
|
chatCode,
|
|
name: team.name,
|
|
memberUserIds: members.map((m) => m.userId),
|
|
};
|
|
}
|