mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
326 lines
8.6 KiB
TypeScript
326 lines
8.6 KiB
TypeScript
import type { ActionFunction } from "react-router";
|
|
import { redirect } from "react-router";
|
|
import { requireUser } from "~/features/auth/core/user.server";
|
|
import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
|
|
import { notify } from "~/features/notifications/core/notify.server";
|
|
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
|
|
import {
|
|
createMatchMemento,
|
|
matchMapList,
|
|
} from "~/features/sendouq-match/core/match.server";
|
|
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
|
|
import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server";
|
|
import { errorToastIfFalsy, parseRequestPayload } from "~/utils/remix.server";
|
|
import { assertUnreachable } from "~/utils/types";
|
|
import { navIconUrl, SENDOUQ_PAGE, sendouQMatchPage } from "~/utils/urls";
|
|
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, setGroupChatMetadata } from "../q-utils.server";
|
|
|
|
// this function doesn't throw normally because we are assuming
|
|
// if there is a validation error the user saw stale data
|
|
// and when we return null we just force a refresh
|
|
export const action: ActionFunction = async ({ request }) => {
|
|
const user = requireUser();
|
|
const data = await parseRequestPayload({
|
|
request,
|
|
schema: lookingSchema,
|
|
});
|
|
const currentGroup = SendouQ.findOwnGroup(user.id);
|
|
if (!currentGroup) return null;
|
|
|
|
try {
|
|
// this throws because there should normally be no way user loses ownership by the action of some other user
|
|
const validateIsGroupOwner = () =>
|
|
errorToastIfFalsy(currentGroup.usersRole === "OWNER", "Not owner");
|
|
const isGroupManager = () =>
|
|
currentGroup.usersRole === "MANAGER" ||
|
|
currentGroup.usersRole === "OWNER";
|
|
|
|
switch (data._action) {
|
|
case "LIKE": {
|
|
if (!isGroupManager()) return null;
|
|
|
|
await SQGroupRepository.addLike({
|
|
likerGroupId: currentGroup.id,
|
|
targetGroupId: data.targetGroupId,
|
|
});
|
|
|
|
const targetChatCode = SendouQ.findUncensoredGroupById(
|
|
data.targetGroupId,
|
|
)?.chatCode;
|
|
if (targetChatCode) {
|
|
ChatSystemMessage.send({
|
|
room: targetChatCode,
|
|
type: "LIKE_RECEIVED",
|
|
revalidateOnly: true,
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "RECHALLENGE": {
|
|
if (!isGroupManager()) return null;
|
|
|
|
await SQGroupRepository.rechallenge({
|
|
likerGroupId: currentGroup.id,
|
|
targetGroupId: data.targetGroupId,
|
|
});
|
|
|
|
const targetChatCode = SendouQ.findUncensoredGroupById(
|
|
data.targetGroupId,
|
|
)?.chatCode;
|
|
if (targetChatCode) {
|
|
ChatSystemMessage.send({
|
|
room: targetChatCode,
|
|
type: "LIKE_RECEIVED",
|
|
revalidateOnly: true,
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
case "UNLIKE": {
|
|
if (!isGroupManager()) return null;
|
|
|
|
await SQGroupRepository.deleteLike({
|
|
likerGroupId: currentGroup.id,
|
|
targetGroupId: data.targetGroupId,
|
|
});
|
|
|
|
break;
|
|
}
|
|
case "GROUP_UP": {
|
|
if (!isGroupManager()) return null;
|
|
|
|
const allLikes = await SQGroupRepository.allLikesByGroupId(
|
|
data.targetGroupId,
|
|
);
|
|
if (!allLikes.given.some((like) => like.groupId === currentGroup.id)) {
|
|
return null;
|
|
}
|
|
|
|
const ourGroup = SendouQ.findOwnGroup(user.id);
|
|
const theirGroup = SendouQ.findUncensoredGroupById(data.targetGroupId);
|
|
if (!ourGroup || !theirGroup) return null;
|
|
|
|
const { id: survivingGroupId } = groupAfterMorph({
|
|
liker: "THEM",
|
|
ourGroup,
|
|
theirGroup,
|
|
});
|
|
|
|
const otherGroup =
|
|
ourGroup.id === survivingGroupId ? theirGroup : ourGroup;
|
|
|
|
await SQGroupRepository.morphGroups({
|
|
survivingGroupId,
|
|
otherGroupId: otherGroup.id,
|
|
});
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
if (ourGroup.chatCode) {
|
|
ChatSystemMessage.removeRoom(ourGroup.chatCode);
|
|
}
|
|
if (theirGroup.chatCode) {
|
|
ChatSystemMessage.removeRoom(theirGroup.chatCode);
|
|
}
|
|
|
|
const survivingGroup =
|
|
SendouQ.findUncensoredGroupById(survivingGroupId);
|
|
if (survivingGroup?.chatCode) {
|
|
setGroupChatMetadata({
|
|
chatCode: survivingGroup.chatCode,
|
|
members: survivingGroup.members,
|
|
});
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "MATCH_UP": {
|
|
if (!isGroupManager()) return null;
|
|
|
|
const ownGroup = SendouQ.findOwnGroup(user.id);
|
|
const theirGroup = SendouQ.findUncensoredGroupById(data.targetGroupId);
|
|
if (!ownGroup || !theirGroup) return null;
|
|
|
|
const ownGroupPreferences =
|
|
await SQGroupRepository.mapModePreferencesByGroupId(ownGroup.id);
|
|
const theirGroupPreferences =
|
|
await SQGroupRepository.mapModePreferencesByGroupId(theirGroup.id);
|
|
|
|
const modesIncluded = resolveFutureMatchModes(ownGroup, theirGroup);
|
|
|
|
const mapList = await matchMapList(
|
|
{
|
|
id: ownGroup.id,
|
|
preferences: ownGroupPreferences,
|
|
},
|
|
{
|
|
id: theirGroup.id,
|
|
preferences: theirGroupPreferences,
|
|
},
|
|
modesIncluded,
|
|
);
|
|
|
|
const createdMatch = await SQMatchRepository.create({
|
|
alphaGroupId: ownGroup.id,
|
|
bravoGroupId: theirGroup.id,
|
|
mapList,
|
|
memento: createMatchMemento({
|
|
own: { group: ownGroup, preferences: ownGroupPreferences },
|
|
their: { group: theirGroup, preferences: theirGroupPreferences },
|
|
mapList,
|
|
}),
|
|
});
|
|
|
|
await refreshSendouQInstance();
|
|
refreshStreamsCache();
|
|
|
|
if (createdMatch.chatCode) {
|
|
ChatSystemMessage.setMetadata({
|
|
chatCode: createdMatch.chatCode,
|
|
header: `Match #${createdMatch.id}`,
|
|
subtitle: "SendouQ",
|
|
url: sendouQMatchPage(createdMatch.id),
|
|
imageUrl: `${navIconUrl("sendouq")}.avif`,
|
|
participantUserIds: [
|
|
...ownGroup.members.map((m) => m.id),
|
|
...theirGroup.members.map((m) => m.id),
|
|
],
|
|
expiresAfter: { hours: 2 },
|
|
});
|
|
}
|
|
|
|
if (ownGroup.chatCode && theirGroup.chatCode) {
|
|
ChatSystemMessage.send([
|
|
{
|
|
room: ownGroup.chatCode,
|
|
type: "MATCH_STARTED",
|
|
revalidateOnly: true,
|
|
},
|
|
{
|
|
room: theirGroup.chatCode,
|
|
type: "MATCH_STARTED",
|
|
revalidateOnly: true,
|
|
},
|
|
]);
|
|
}
|
|
|
|
notify({
|
|
userIds: [
|
|
...ownGroup.members.map((m) => m.id),
|
|
...theirGroup.members.map((m) => m.id),
|
|
],
|
|
defaultSeenUserIds: [user.id],
|
|
notification: {
|
|
type: "SQ_NEW_MATCH",
|
|
meta: {
|
|
matchId: createdMatch.id,
|
|
},
|
|
},
|
|
});
|
|
|
|
throw redirect(sendouQMatchPage(createdMatch.id));
|
|
}
|
|
case "GIVE_MANAGER": {
|
|
validateIsGroupOwner();
|
|
|
|
await SQGroupRepository.updateMemberRole({
|
|
groupId: currentGroup.id,
|
|
userId: data.userId,
|
|
role: "MANAGER",
|
|
});
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
break;
|
|
}
|
|
case "REMOVE_MANAGER": {
|
|
validateIsGroupOwner();
|
|
|
|
await SQGroupRepository.updateMemberRole({
|
|
groupId: currentGroup.id,
|
|
userId: data.userId,
|
|
role: "REGULAR",
|
|
});
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
break;
|
|
}
|
|
case "LEAVE_GROUP": {
|
|
await SQGroupRepository.leaveGroup(user.id);
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
const targetChatCode = SendouQ.findUncensoredGroupById(
|
|
currentGroup.id,
|
|
)?.chatCode;
|
|
if (targetChatCode) {
|
|
ChatSystemMessage.send({
|
|
room: targetChatCode,
|
|
type: "USER_LEFT",
|
|
context: { name: user.username },
|
|
});
|
|
}
|
|
|
|
throw redirect(SENDOUQ_PAGE);
|
|
}
|
|
case "KICK_FROM_GROUP": {
|
|
validateIsGroupOwner();
|
|
errorToastIfFalsy(data.userId !== user.id, "Can't kick yourself");
|
|
|
|
await SQGroupRepository.leaveGroup(data.userId);
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
break;
|
|
}
|
|
case "REFRESH_GROUP": {
|
|
await SQGroupRepository.refreshGroup(currentGroup.id);
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
break;
|
|
}
|
|
case "UPDATE_NOTE": {
|
|
await SQGroupRepository.updateMemberNote({
|
|
groupId: currentGroup.id,
|
|
userId: user.id,
|
|
value: data.value,
|
|
});
|
|
|
|
await refreshSendouQInstance();
|
|
|
|
break;
|
|
}
|
|
case "DELETE_PRIVATE_USER_NOTE": {
|
|
await PrivateUserNoteRepository.del({
|
|
authorId: user.id,
|
|
targetId: data.targetId,
|
|
});
|
|
|
|
break;
|
|
}
|
|
default: {
|
|
assertUnreachable(data);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
// some errors are expected to happen, for example they might request two groups at the same time
|
|
// then after morphing one group the other request fails because the group no longer exists
|
|
// return null causes loaders to run and they see the fresh state again instead of error page
|
|
if (error instanceof SendouQError) {
|
|
return null;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|