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
2 changes: 1 addition & 1 deletion hasura/enums/sanction-types.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
insert into e_sanction_types ("value", "description") values
('ban', 'Player is not able to participate in any activity'),
('mute', 'Player cannot use voice chat in game'),
('gag', 'Player cannot use text chat in game'),
('gag', 'Player cannot use text chat in game or in website lobby chat'),
('silence', 'Player muted and gagged'),
('warning', 'Informational note on the player''s record; never enforced, never expires')
on conflict(value) do update set "description" = EXCLUDED."description"
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
table:
name: chat_message_deletions
schema: public
object_relationships:
- name: author
using:
foreign_key_constraint_on: author_steam_id
- name: deleted_by
using:
foreign_key_constraint_on: deleted_by_steam_id
select_permissions:
- role: match_organizer
permission:
columns:
- id
- message_id
- room_type
- room_id
- author_steam_id
- message
- message_created_at
- source
- deleted_by_steam_id
- deleted_at
filter: {}
allow_aggregations: true
comment: Evidence of moderated website chat. Written only by the API.
- role: moderator
permission:
columns:
- id
- message_id
- room_type
- room_id
- author_steam_id
- message
- message_created_at
- source
- deleted_by_steam_id
- deleted_at
filter:
room_type:
_neq: organizers
allow_aggregations: true
comment: The organizers' room is closed to moderators, and so is its evidence.
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 @@ -5,6 +5,7 @@
- "!include public_award_recipients.yaml"
- "!include public_awards.yaml"
- "!include public_broadcast_huds.yaml"
- "!include public_chat_message_deletions.yaml"
- "!include public_chat_read_state.yaml"
- "!include public_clip_render_jobs.yaml"
- "!include public_custom_pages.yaml"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS public.notifications_message_id_idx;
DROP TABLE IF EXISTS public.chat_message_deletions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS public.chat_message_deletions (
id uuid NOT NULL DEFAULT gen_random_uuid(),
message_id uuid NOT NULL,
room_type text NOT NULL,
room_id text NOT NULL,
author_steam_id bigint REFERENCES public.players (steam_id)
ON UPDATE CASCADE ON DELETE SET NULL,
message text NOT NULL,
message_created_at timestamptz,
source text,
deleted_by_steam_id bigint REFERENCES public.players (steam_id)
ON UPDATE CASCADE ON DELETE SET NULL,
deleted_at timestamptz NOT NULL DEFAULT now(),

PRIMARY KEY (id),
UNIQUE (message_id, room_type, room_id)
);

CREATE INDEX IF NOT EXISTS chat_message_deletions_author_idx
ON public.chat_message_deletions (author_steam_id, deleted_at DESC);

CREATE INDEX IF NOT EXISTS notifications_message_id_idx
ON public.notifications ((data->>'messageId'))
WHERE data->>'messageId' IS NOT NULL;
193 changes: 189 additions & 4 deletions src/chat/chat.gateway.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ describe("ChatGateway lobby:chat", () => {
expect(sent(socket)).toEqual([
{
event: "chat:error",
data: { code: ChatErrorCode.TooLong, max: 2000, requestId: "r-1" },
data: {
code: ChatErrorCode.TooLong,
action: "send",
max: 2000,
requestId: "r-1",
},
},
]);
});
Expand All @@ -135,7 +140,10 @@ describe("ChatGateway lobby:chat", () => {
);

expect(sent(socket)).toEqual([
{ event: "chat:error", data: { code: "too_long", max: 2000 } },
{
event: "chat:error",
data: { code: "too_long", action: "send", max: 2000 },
},
]);
});

Expand Down Expand Up @@ -180,7 +188,37 @@ describe("ChatGateway lobby:chat", () => {
expect(sent(socket)).toEqual([
{
event: "chat:error",
data: { code: ChatErrorCode.NotAllowed, requestId: "r-2" },
data: {
code: ChatErrorCode.NotAllowed,
action: "send",
requestId: "r-2",
},
},
]);
});

it("tells a gagged sender why, and never relays them", async () => {
chat.sendMessageToChat.mockResolvedValue({
accepted: false,
code: ChatErrorCode.Gagged,
});
const socket = client();

await gateway.lobby(
{
id: "m-1",
type: ChatLobbyType.Match,
message: "gg",
requestId: "r-4",
},
socket,
);

expect(chat.sendChatToServer).not.toHaveBeenCalled();
expect(sent(socket)).toEqual([
{
event: "chat:error",
data: { code: "gagged", action: "send", requestId: "r-4" },
},
]);
});
Expand Down Expand Up @@ -260,7 +298,10 @@ describe("ChatGateway lobby:chat", () => {
);

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

Expand All @@ -276,3 +317,147 @@ describe("ChatGateway lobby:chat", () => {
});
});
});

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

let chat: { deleteMessage: jest.Mock };
let gateway: ChatGateway;

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

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

beforeEach(() => {
chat = { deleteMessage: jest.fn().mockResolvedValue({ deleted: true }) };
gateway = new ChatGateway(chat as any);
});

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

await gateway.deleteMessage(
{
id: "m-1",
type: ChatLobbyType.Match,
messageId: MESSAGE_ID,
requestId: "r-1",
},
socket,
);

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

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

await gateway.deleteMessage(data as any, socket);

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

it("asks the service to delete as the signed in player", async () => {
await gateway.deleteMessage(
{ id: "m-1", type: ChatLobbyType.Match, messageId: MESSAGE_ID },
client(),
);

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

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

await gateway.deleteMessage(
{
id: "m-1",
type: ChatLobbyType.Match,
messageId: MESSAGE_ID,
requestId: "r-2",
},
socket,
);

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

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

await gateway.deleteMessage(
{ id: "m-1", type: ChatLobbyType.Match, messageId: MESSAGE_ID },
socket,
);

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

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

await gateway.deleteMessage(
{
id: "m-1",
type: ChatLobbyType.Match,
messageId: MESSAGE_ID,
requestId: "r-3",
},
socket,
);

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

it("still reports a refusal without a requestId", async () => {
chat.deleteMessage.mockResolvedValue({
deleted: false,
code: ChatErrorCode.NotAllowed,
});
const socket = client();

await gateway.deleteMessage(
{ id: "x", type: ChatLobbyType.Direct, messageId: MESSAGE_ID },
socket,
);

expect(sent(socket)).toEqual([
{
event: "chat:error",
data: { code: "not_allowed", action: "delete" },
},
]);
});
});
Loading
Loading