Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
table:
name: direct_message_reactions
schema: public
1 change: 1 addition & 0 deletions hasura/metadata/databases/default/tables/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- "!include public_custom_pages.yaml"
- "!include public_db_backups.yaml"
- "!include public_direct_conversations.yaml"
- "!include public_direct_message_reactions.yaml"
- "!include public_direct_messages.yaml"
- "!include public_draft_game_picks.yaml"
- "!include public_draft_game_players.yaml"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS public.direct_message_reactions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS public.direct_message_reactions (
message_id uuid NOT NULL REFERENCES public.direct_messages (id)
ON DELETE CASCADE,
steam_id bigint NOT NULL REFERENCES public.players (steam_id)
ON UPDATE CASCADE ON DELETE CASCADE,
reaction text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),

PRIMARY KEY (message_id, steam_id, reaction)
);

CREATE INDEX IF NOT EXISTS direct_message_reactions_steam_id_idx
ON public.direct_message_reactions (steam_id);
147 changes: 147 additions & 0 deletions src/chat/chat.gateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,150 @@ describe("ChatGateway lobby:edit", () => {
expect(chat.sendChatToServer).not.toHaveBeenCalled();
});
});

describe("ChatGateway lobby:react", () => {
const MESSAGE_ID = "3f0c1d2e-4b5a-4c6d-8e7f-9a0b1c2d3e4f";

let chat: { toggleReaction: jest.Mock; sendChatToServer: jest.Mock };
let gateway: ChatGateway;

const client = (user: any = { steam_id: "1", name: "Luke", role: "user" }) =>
({ id: "client-1", user, send: jest.fn() }) as any;

const sent = (socket: { send: jest.Mock }) =>
socket.send.mock.calls.map(([raw]) => JSON.parse(raw));

const reaction = (overrides: Record<string, unknown> = {}) => ({
id: "m-1",
type: ChatLobbyType.Match,
messageId: MESSAGE_ID,
reaction: "heart",
...overrides,
});

beforeEach(() => {
chat = {
toggleReaction: jest.fn().mockResolvedValue({
toggled: true,
reactions: { heart: ["1"] },
}),
sendChatToServer: jest.fn(),
};
gateway = new ChatGateway(chat as any);
});

it("ignores a socket that has not signed in", async () => {
const socket = client(null);

await gateway.react(reaction({ requestId: "r-1" }) as any, socket);

expect(chat.toggleReaction).not.toHaveBeenCalled();
expect(socket.send).not.toHaveBeenCalled();
});

it.each([
["an unknown lobby type", { type: "global" }],
["a room id that is not a string", { id: 1 }],
["a missing message id", { messageId: undefined }],
])("ignores %s", async (_, overrides) => {
const socket = client();

await gateway.react(reaction(overrides) as any, socket);

expect(chat.toggleReaction).not.toHaveBeenCalled();
expect(socket.send).not.toHaveBeenCalled();
});

it("ignores a missing payload", async () => {
await gateway.react(undefined as any, client());

expect(chat.toggleReaction).not.toHaveBeenCalled();
});

it.each([
["not on the list", "party"],
["not a string", 5],
["missing", undefined],
])(
"answers a reaction that is %s with invalid, without asking the service",
async (_, value) => {
const socket = client();

await gateway.react(
reaction({ reaction: value, requestId: "r-2" }) as any,
socket,
);

expect(chat.toggleReaction).not.toHaveBeenCalled();
expect(sent(socket)).toEqual([
{
event: "chat:error",
data: {
code: ChatErrorCode.Invalid,
action: "react",
requestId: "r-2",
},
},
]);
},
);

it("asks the service to toggle as the signed in player", async () => {
await gateway.react(reaction({ reaction: "laugh" }) as any, client());

expect(chat.toggleReaction).toHaveBeenCalledWith(
ChatLobbyType.Match,
"m-1",
MESSAGE_ID,
"laugh",
expect.objectContaining({ steam_id: "1" }),
);
});

it("acks a toggle under the requestId it came with", async () => {
const socket = client();

await gateway.react(reaction({ requestId: "r-3" }) as any, socket);

expect(sent(socket)).toEqual([
{
event: "chat:ack",
data: { requestId: "r-3", messageId: MESSAGE_ID, action: "react" },
},
]);
});

it("stays quiet for a toggle without a requestId", async () => {
const socket = client();

await gateway.react(reaction() as any, socket);

expect(socket.send).not.toHaveBeenCalled();
});

it.each([
ChatErrorCode.RateLimited,
ChatErrorCode.NotAllowed,
ChatErrorCode.NotFound,
ChatErrorCode.Gagged,
])("reports %s under the requestId it came with", async (code) => {
chat.toggleReaction.mockResolvedValue({ toggled: false, code });
const socket = client();

await gateway.react(reaction({ requestId: "r-4" }) as any, socket);

expect(sent(socket)).toEqual([
{
event: "chat:error",
data: { code, action: "react", requestId: "r-4" },
},
]);
});

it("never relays a reaction to the game server", async () => {
await gateway.react(reaction() as any, client());

expect(chat.toggleReaction).toHaveBeenCalled();
expect(chat.sendChatToServer).not.toHaveBeenCalled();
});
});
50 changes: 50 additions & 0 deletions src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,56 @@ export class ChatGateway {
}
}

@SubscribeMessage("lobby:react")
async react(
@MessageBody()
data: {
id: string;
type: ChatLobbyType;
messageId: string;
reaction: unknown;
requestId?: string;
},
@ConnectedSocket() client: FiveStackWebSocketClient,
) {
if (!client.user) {
return;
}

if (
!ChatGateway.isLobbyType(data?.type) ||
typeof data.id !== "string" ||
typeof data.messageId !== "string"
) {
return;
}

const requestId =
typeof data.requestId === "string" ? data.requestId : undefined;

if (!ChatService.isReaction(data.reaction)) {
this.sendError(client, "react", ChatErrorCode.Invalid, requestId);
return;
}

const result = await this.chat.toggleReaction(
data.type,
data.id,
data.messageId,
data.reaction,
client.user,
);

if (result.toggled === false) {
this.sendError(client, "react", result.code, requestId);
return;
}

if (requestId) {
this.sendAck(client, "react", requestId, data.messageId);
}
}

private static isLobbyType(value: unknown): value is ChatLobbyType {
return Object.values(ChatLobbyType).includes(value as ChatLobbyType);
}
Expand Down
Loading
Loading