@moneypot/hub 1.15.0-dev.3 → 1.16.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/__generated__/gql.d.ts +2 -2
- package/dist/src/__generated__/gql.js +1 -1
- package/dist/src/__generated__/graphql.d.ts +240 -27
- package/dist/src/__generated__/graphql.js +30 -1
- package/dist/src/db/index.d.ts +1 -0
- package/dist/src/db/index.js +10 -2
- package/dist/src/db/types.d.ts +24 -0
- package/dist/src/express.d.ts +1 -0
- package/dist/src/graphql-queries.js +4 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/pg-advisory-lock.d.ts +5 -0
- package/dist/src/pg-advisory-lock.js +5 -0
- package/dist/src/pg-versions/013-chat.sql +221 -0
- package/dist/src/plugins/chat/hub-chat-after-id-condition.d.ts +1 -0
- package/dist/src/plugins/chat/hub-chat-after-id-condition.js +15 -0
- package/dist/src/plugins/chat/hub-chat-create-system-message.d.ts +1 -0
- package/dist/src/plugins/chat/hub-chat-create-system-message.js +124 -0
- package/dist/src/plugins/chat/hub-chat-create-user-message.d.ts +1 -0
- package/dist/src/plugins/chat/hub-chat-create-user-message.js +231 -0
- package/dist/src/plugins/chat/hub-chat-mute-user.d.ts +1 -0
- package/dist/src/plugins/chat/hub-chat-mute-user.js +186 -0
- package/dist/src/plugins/chat/hub-chat-subscription.d.ts +14 -0
- package/dist/src/plugins/chat/hub-chat-subscription.js +133 -0
- package/dist/src/plugins/chat/hub-chat-unmute-user.d.ts +1 -0
- package/dist/src/plugins/chat/hub-chat-unmute-user.js +146 -0
- package/dist/src/plugins/hub-authenticate.js +40 -18
- package/dist/src/plugins/hub-create-playground-session.js +44 -13
- package/dist/src/server/graphile.config.d.ts +13 -1
- package/dist/src/server/graphile.config.js +38 -12
- package/dist/src/server/index.d.ts +3 -1
- package/dist/src/server/index.js +3 -1
- package/dist/src/server/middleware/authentication.js +1 -0
- package/dist/src/util.d.ts +3 -0
- package/dist/src/util.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { GraphQLError } from "graphql";
|
|
2
|
+
import { access, context, object, ObjectStep, sideEffect, } from "postgraphile/grafast";
|
|
3
|
+
import { extendSchema, gql } from "postgraphile/utils";
|
|
4
|
+
import z from "zod/v4";
|
|
5
|
+
import { exactlyOneRow, maybeOneRow } from "../../db/util.js";
|
|
6
|
+
import { extractFirstZodErrorMessage } from "../../util.js";
|
|
7
|
+
import { withPgPoolTransaction } from "../../db/index.js";
|
|
8
|
+
import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
|
|
9
|
+
const InputSchema = z.object({
|
|
10
|
+
clientId: z.uuid("Invalid client ID"),
|
|
11
|
+
body: z
|
|
12
|
+
.string()
|
|
13
|
+
.transform(normalizeUserMessageBody)
|
|
14
|
+
.refine((body) => body.length > 0, "Body is required")
|
|
15
|
+
.refine((body) => body.length <= 140, `Max body length: 140 chars`),
|
|
16
|
+
});
|
|
17
|
+
const RATE_LIMITS = [
|
|
18
|
+
{
|
|
19
|
+
seconds: 10,
|
|
20
|
+
cap: 3,
|
|
21
|
+
},
|
|
22
|
+
{ seconds: 60, cap: 20 },
|
|
23
|
+
];
|
|
24
|
+
const RL_WINDOWS = RATE_LIMITS.map((r) => r.seconds);
|
|
25
|
+
const RL_CAPS = RATE_LIMITS.map((r) => r.cap);
|
|
26
|
+
export const HubChatCreateUserMessagePlugin = extendSchema((build) => {
|
|
27
|
+
const chatMessageTable = build.input.pgRegistry.pgResources.hub_chat_message;
|
|
28
|
+
return {
|
|
29
|
+
typeDefs: gql `
|
|
30
|
+
input HubChatCreateUserMessageInput {
|
|
31
|
+
clientId: UUID!
|
|
32
|
+
body: String!
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type HubChatCreateUserMessageSuccess {
|
|
36
|
+
chatMessage: HubChatMessage!
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type HubChatUserRateLimited {
|
|
40
|
+
message: String
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type HubChatUserMuted {
|
|
44
|
+
message: String
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
union HubChatCreateUserMessageResult =
|
|
48
|
+
HubChatCreateUserMessageSuccess
|
|
49
|
+
| HubChatUserRateLimited
|
|
50
|
+
| HubChatUserMuted
|
|
51
|
+
|
|
52
|
+
type HubChatCreateUserMessagePayload {
|
|
53
|
+
result: HubChatCreateUserMessageResult!
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
extend type Mutation {
|
|
57
|
+
hubChatCreateUserMessage(
|
|
58
|
+
input: HubChatCreateUserMessageInput!
|
|
59
|
+
): HubChatCreateUserMessagePayload
|
|
60
|
+
}
|
|
61
|
+
`,
|
|
62
|
+
objects: {
|
|
63
|
+
Mutation: {
|
|
64
|
+
plans: {
|
|
65
|
+
hubChatCreateUserMessage(_, { $input }) {
|
|
66
|
+
const $identity = context().get("identity");
|
|
67
|
+
const $superuserPool = context().get("superuserPool");
|
|
68
|
+
const $payload = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
|
|
69
|
+
if (identity?.kind !== "user") {
|
|
70
|
+
throw new GraphQLError("Unauthorized");
|
|
71
|
+
}
|
|
72
|
+
let input;
|
|
73
|
+
try {
|
|
74
|
+
input = InputSchema.parse(rawInput);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
if (e instanceof z.ZodError) {
|
|
78
|
+
throw new GraphQLError(extractFirstZodErrorMessage(e));
|
|
79
|
+
}
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
return await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
83
|
+
await PgAdvisoryLock.forChatUserAction(pgClient, {
|
|
84
|
+
userId: identity.session.user_id,
|
|
85
|
+
experienceId: identity.session.experience_id,
|
|
86
|
+
casinoId: identity.session.casino_id,
|
|
87
|
+
});
|
|
88
|
+
const dbMute = await pgClient
|
|
89
|
+
.query({
|
|
90
|
+
text: `
|
|
91
|
+
SELECT id
|
|
92
|
+
FROM hub.active_chat_mute
|
|
93
|
+
WHERE casino_id = $1
|
|
94
|
+
AND experience_id = $2
|
|
95
|
+
AND user_id = $3
|
|
96
|
+
`,
|
|
97
|
+
values: [
|
|
98
|
+
identity.session.casino_id,
|
|
99
|
+
identity.session.experience_id,
|
|
100
|
+
identity.session.user_id,
|
|
101
|
+
],
|
|
102
|
+
})
|
|
103
|
+
.then(maybeOneRow);
|
|
104
|
+
if (dbMute) {
|
|
105
|
+
return {
|
|
106
|
+
__typename: "HubChatUserMuted",
|
|
107
|
+
message: "You are muted",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const dbChatMessage = await pgClient
|
|
111
|
+
.query({
|
|
112
|
+
text: `
|
|
113
|
+
with ins as (
|
|
114
|
+
insert into hub.chat_message (casino_id, experience_id, user_id, client_id, body, type)
|
|
115
|
+
values ($1, $2, $3, $4, $5, 'user')
|
|
116
|
+
on conflict (casino_id, experience_id, user_id, client_id) where type = 'user'
|
|
117
|
+
do nothing
|
|
118
|
+
returning id
|
|
119
|
+
)
|
|
120
|
+
select id from ins
|
|
121
|
+
union all
|
|
122
|
+
select id
|
|
123
|
+
from hub.chat_message
|
|
124
|
+
where casino_id = $1 and experience_id = $2 and user_id = $3 and client_id = $4
|
|
125
|
+
limit 1
|
|
126
|
+
`,
|
|
127
|
+
values: [
|
|
128
|
+
identity.session.casino_id,
|
|
129
|
+
identity.session.experience_id,
|
|
130
|
+
identity.session.user_id,
|
|
131
|
+
input.clientId,
|
|
132
|
+
input.body,
|
|
133
|
+
],
|
|
134
|
+
})
|
|
135
|
+
.then(exactlyOneRow);
|
|
136
|
+
const dbRateLimit = await pgClient
|
|
137
|
+
.query({
|
|
138
|
+
text: `
|
|
139
|
+
with wins as (
|
|
140
|
+
select *
|
|
141
|
+
from unnest($4::int[], $5::int[]) as t(window_seconds, cap)
|
|
142
|
+
),
|
|
143
|
+
bumped as (
|
|
144
|
+
insert into hub.chat_rate_bucket
|
|
145
|
+
(casino_id, experience_id, user_id, window_seconds, bucket_start, count)
|
|
146
|
+
select
|
|
147
|
+
$1, $2, $3,
|
|
148
|
+
w.window_seconds,
|
|
149
|
+
to_timestamp(floor(extract(epoch from now())/w.window_seconds)*w.window_seconds),
|
|
150
|
+
1
|
|
151
|
+
from wins w
|
|
152
|
+
on conflict (casino_id, experience_id, user_id, window_seconds, bucket_start)
|
|
153
|
+
do update set count = hub.chat_rate_bucket.count + 1
|
|
154
|
+
returning window_seconds, count
|
|
155
|
+
)
|
|
156
|
+
select bool_or(b.count > w.cap) as exceeded
|
|
157
|
+
from bumped b
|
|
158
|
+
join wins w using (window_seconds)
|
|
159
|
+
`,
|
|
160
|
+
values: [
|
|
161
|
+
identity.session.casino_id,
|
|
162
|
+
identity.session.experience_id,
|
|
163
|
+
identity.session.user_id,
|
|
164
|
+
RL_WINDOWS,
|
|
165
|
+
RL_CAPS,
|
|
166
|
+
],
|
|
167
|
+
})
|
|
168
|
+
.then(maybeOneRow);
|
|
169
|
+
if (dbRateLimit?.exceeded) {
|
|
170
|
+
return {
|
|
171
|
+
__typename: "HubChatUserRateLimited",
|
|
172
|
+
message: "Rate limit exceeded",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const notifyPayload = {
|
|
176
|
+
type: "message",
|
|
177
|
+
chat_message_id: dbChatMessage.id,
|
|
178
|
+
};
|
|
179
|
+
await pgClient.query({
|
|
180
|
+
text: `
|
|
181
|
+
select pg_notify('hub:chat:${identity.session.experience_id}', $1::text)
|
|
182
|
+
`,
|
|
183
|
+
values: [JSON.stringify(notifyPayload)],
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
__typename: "HubChatCreateUserMessageSuccess",
|
|
187
|
+
chatMessageId: dbChatMessage.id,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
return object({
|
|
192
|
+
result: $payload,
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
HubChatUserMuted: {
|
|
198
|
+
assertStep: ObjectStep,
|
|
199
|
+
},
|
|
200
|
+
HubChatUserRateLimited: {
|
|
201
|
+
assertStep: ObjectStep,
|
|
202
|
+
},
|
|
203
|
+
HubChatCreateUserMessageSuccess: {
|
|
204
|
+
assertStep: ObjectStep,
|
|
205
|
+
plans: {
|
|
206
|
+
chatMessage($data) {
|
|
207
|
+
const $id = access($data, "chatMessageId");
|
|
208
|
+
return chatMessageTable.get({ id: $id });
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
HubChatCreateUserMessagePayload: {
|
|
213
|
+
assertStep: ObjectStep,
|
|
214
|
+
plans: {
|
|
215
|
+
result($data) {
|
|
216
|
+
const $result = $data.get("result");
|
|
217
|
+
return $result;
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
});
|
|
224
|
+
function normalizeUserMessageBody(body) {
|
|
225
|
+
return (body
|
|
226
|
+
.normalize("NFC")
|
|
227
|
+
.replace(/(\p{M}{1,2})\p{M}+/gu, "$1")
|
|
228
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, "")
|
|
229
|
+
.replace(/\s+/g, " ")
|
|
230
|
+
.trim());
|
|
231
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubChatMuteUserPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { GraphQLError } from "graphql";
|
|
2
|
+
import { context, object, sideEffect } from "postgraphile/grafast";
|
|
3
|
+
import { extendSchema, gql } from "postgraphile/utils";
|
|
4
|
+
import z from "zod/v4";
|
|
5
|
+
import { exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../../db/index.js";
|
|
6
|
+
import { extractFirstZodErrorMessage, uuidEqual } from "../../util.js";
|
|
7
|
+
import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
|
|
8
|
+
const InputSchema = z.object({
|
|
9
|
+
userId: z.uuid("Invalid user ID"),
|
|
10
|
+
expiredAt: z.iso
|
|
11
|
+
.datetime({ offset: true, error: "Invalid expired at" })
|
|
12
|
+
.optional()
|
|
13
|
+
.refine((value) => typeof value === "undefined" ||
|
|
14
|
+
(typeof value === "string" && new Date(value).getTime() > Date.now()), { error: "Expired at must be in the future" })
|
|
15
|
+
.transform((value) => (value ? new Date(value) : null)),
|
|
16
|
+
reason: z
|
|
17
|
+
.string("Invalid reason")
|
|
18
|
+
.trim()
|
|
19
|
+
.optional()
|
|
20
|
+
.refine((value) => typeof value === "undefined" ||
|
|
21
|
+
(typeof value === "string" && value.length > 0), { error: "Reason must be at least 1 character" })
|
|
22
|
+
.refine((value) => typeof value === "undefined" ||
|
|
23
|
+
(typeof value === "string" && value.length <= 140), { error: "Reason must be at most 140 characters" })
|
|
24
|
+
.transform((value) => (value ? value : null)),
|
|
25
|
+
});
|
|
26
|
+
export const HubChatMuteUserPlugin = extendSchema((build) => {
|
|
27
|
+
const chatMuteTable = build.input.pgRegistry.pgResources.hub_chat_mute;
|
|
28
|
+
return {
|
|
29
|
+
typeDefs: gql `
|
|
30
|
+
input HubChatMuteUserInput {
|
|
31
|
+
userId: UUID!
|
|
32
|
+
expiredAt: Datetime
|
|
33
|
+
reason: String
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type HubChatMuteUserPayload {
|
|
37
|
+
chatMute: HubChatMute!
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
extend type Mutation {
|
|
41
|
+
hubChatMuteUser(input: HubChatMuteUserInput!): HubChatMuteUserPayload
|
|
42
|
+
}
|
|
43
|
+
`,
|
|
44
|
+
objects: {
|
|
45
|
+
Mutation: {
|
|
46
|
+
plans: {
|
|
47
|
+
hubChatMuteUser(_, { $input }) {
|
|
48
|
+
const $identity = context().get("identity");
|
|
49
|
+
const $superuserPool = context().get("superuserPool");
|
|
50
|
+
const $muteId = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
|
|
51
|
+
if (identity?.kind !== "user") {
|
|
52
|
+
throw new GraphQLError("Unauthorized");
|
|
53
|
+
}
|
|
54
|
+
let input;
|
|
55
|
+
try {
|
|
56
|
+
input = InputSchema.parse(rawInput);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e instanceof z.ZodError) {
|
|
60
|
+
throw new GraphQLError(extractFirstZodErrorMessage(e));
|
|
61
|
+
}
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
const dbTargetUser = await superuserPool
|
|
65
|
+
.query({
|
|
66
|
+
text: `
|
|
67
|
+
SELECT id
|
|
68
|
+
FROM hub.user
|
|
69
|
+
WHERE casino_id = $1 AND id = $2
|
|
70
|
+
`,
|
|
71
|
+
values: [identity.session.casino_id, input.userId],
|
|
72
|
+
})
|
|
73
|
+
.then(maybeOneRow);
|
|
74
|
+
if (!dbTargetUser) {
|
|
75
|
+
throw new GraphQLError("User not found");
|
|
76
|
+
}
|
|
77
|
+
const dbCurrentUserChatMod = await superuserPool
|
|
78
|
+
.query({
|
|
79
|
+
text: `
|
|
80
|
+
SELECT id, user_id
|
|
81
|
+
FROM hub.chat_mod
|
|
82
|
+
WHERE casino_id = $1 AND experience_id = $2 AND user_id = $3
|
|
83
|
+
`,
|
|
84
|
+
values: [
|
|
85
|
+
identity.session.casino_id,
|
|
86
|
+
identity.session.experience_id,
|
|
87
|
+
identity.session.user_id,
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
.then(maybeOneRow);
|
|
91
|
+
if (!dbCurrentUserChatMod &&
|
|
92
|
+
!identity.session.is_experience_owner) {
|
|
93
|
+
throw new GraphQLError("Unauthorized");
|
|
94
|
+
}
|
|
95
|
+
const dbExperience = await superuserPool
|
|
96
|
+
.query({
|
|
97
|
+
text: `
|
|
98
|
+
SELECT id, user_id
|
|
99
|
+
FROM hub.experience
|
|
100
|
+
WHERE id = $1 AND casino_id = $2
|
|
101
|
+
`,
|
|
102
|
+
values: [
|
|
103
|
+
identity.session.experience_id,
|
|
104
|
+
identity.session.casino_id,
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
.then(exactlyOneRow);
|
|
108
|
+
const targetIsChatMod = await superuserPool
|
|
109
|
+
.query({
|
|
110
|
+
text: `
|
|
111
|
+
select 1
|
|
112
|
+
from hub.chat_mod
|
|
113
|
+
where casino_id = $1 and experience_id = $2 and user_id = $3
|
|
114
|
+
`,
|
|
115
|
+
values: [
|
|
116
|
+
identity.session.casino_id,
|
|
117
|
+
identity.session.experience_id,
|
|
118
|
+
dbTargetUser.id,
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
.then(maybeOneRow);
|
|
122
|
+
const targetIsExperienceOwner = dbExperience.user_id &&
|
|
123
|
+
uuidEqual(dbExperience.user_id, dbTargetUser.id);
|
|
124
|
+
if (targetIsExperienceOwner || targetIsChatMod) {
|
|
125
|
+
throw new GraphQLError("Cannot mute staff");
|
|
126
|
+
}
|
|
127
|
+
if (uuidEqual(identity.session.user_id, dbTargetUser.id)) {
|
|
128
|
+
throw new GraphQLError("Cannot mute yourself");
|
|
129
|
+
}
|
|
130
|
+
const muteId = await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
131
|
+
await PgAdvisoryLock.forChatUserAction(pgClient, {
|
|
132
|
+
userId: dbTargetUser.id,
|
|
133
|
+
experienceId: identity.session.experience_id,
|
|
134
|
+
casinoId: identity.session.casino_id,
|
|
135
|
+
});
|
|
136
|
+
await pgClient.query(`
|
|
137
|
+
UPDATE hub.chat_mute
|
|
138
|
+
SET revoked_at = now()
|
|
139
|
+
WHERE user_id = $1
|
|
140
|
+
AND experience_id = $2
|
|
141
|
+
AND casino_id = $3
|
|
142
|
+
AND revoked_at IS NULL
|
|
143
|
+
`, [
|
|
144
|
+
dbTargetUser.id,
|
|
145
|
+
identity.session.experience_id,
|
|
146
|
+
identity.session.casino_id,
|
|
147
|
+
]);
|
|
148
|
+
const dbMute = await pgClient
|
|
149
|
+
.query({
|
|
150
|
+
text: `
|
|
151
|
+
INSERT INTO hub.chat_mute (casino_id, experience_id, user_id, reason, expired_at)
|
|
152
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
153
|
+
RETURNING id
|
|
154
|
+
`,
|
|
155
|
+
values: [
|
|
156
|
+
identity.session.casino_id,
|
|
157
|
+
identity.session.experience_id,
|
|
158
|
+
dbTargetUser.id,
|
|
159
|
+
input.reason,
|
|
160
|
+
input.expiredAt,
|
|
161
|
+
],
|
|
162
|
+
})
|
|
163
|
+
.then(exactlyOneRow);
|
|
164
|
+
const notifyPayload = {
|
|
165
|
+
type: "mute",
|
|
166
|
+
user_id: dbTargetUser.id,
|
|
167
|
+
};
|
|
168
|
+
await pgClient.query({
|
|
169
|
+
text: `
|
|
170
|
+
SELECT pg_notify('hub:chat:${identity.session.experience_id}', $1::text)
|
|
171
|
+
`,
|
|
172
|
+
values: [JSON.stringify(notifyPayload)],
|
|
173
|
+
});
|
|
174
|
+
return dbMute.id;
|
|
175
|
+
});
|
|
176
|
+
return muteId;
|
|
177
|
+
});
|
|
178
|
+
return object({
|
|
179
|
+
chatMute: chatMuteTable.get({ id: $muteId }),
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import z from "zod/v4";
|
|
2
|
+
declare const ChatPgNotifyPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3
|
+
type: z.ZodLiteral<"message">;
|
|
4
|
+
chat_message_id: z.ZodString;
|
|
5
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
6
|
+
type: z.ZodLiteral<"mute">;
|
|
7
|
+
user_id: z.ZodString;
|
|
8
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
9
|
+
type: z.ZodLiteral<"unmute">;
|
|
10
|
+
user_id: z.ZodString;
|
|
11
|
+
}, z.core.$strip>]>;
|
|
12
|
+
export type ChatPgNotifyPayload = z.infer<typeof ChatPgNotifyPayloadSchema>;
|
|
13
|
+
export declare const HubChatSubscriptionPlugin: GraphileConfig.Plugin;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { access, context, lambda, ObjectStep } from "postgraphile/grafast";
|
|
2
|
+
import { extendSchema, gql } from "postgraphile/utils";
|
|
3
|
+
import { jsonParse } from "postgraphile/@dataplan/json";
|
|
4
|
+
import { listenWithFilter } from "../listen-with-filter.js";
|
|
5
|
+
import { logger } from "../../logger.js";
|
|
6
|
+
import z, { prettifyError } from "zod/v4";
|
|
7
|
+
const ChatPgNotifyPayloadSchema = z.discriminatedUnion("type", [
|
|
8
|
+
z.object({
|
|
9
|
+
type: z.literal("message"),
|
|
10
|
+
chat_message_id: z.string(),
|
|
11
|
+
}),
|
|
12
|
+
z.object({
|
|
13
|
+
type: z.literal("mute"),
|
|
14
|
+
user_id: z.string(),
|
|
15
|
+
}),
|
|
16
|
+
z.object({
|
|
17
|
+
type: z.literal("unmute"),
|
|
18
|
+
user_id: z.string(),
|
|
19
|
+
}),
|
|
20
|
+
]);
|
|
21
|
+
export const HubChatSubscriptionPlugin = extendSchema((build) => {
|
|
22
|
+
const chatMessageTable = build.input.pgRegistry.pgResources.hub_chat_message;
|
|
23
|
+
return {
|
|
24
|
+
typeDefs: gql `
|
|
25
|
+
type HubChatSubscriptionNewMessage {
|
|
26
|
+
chatMessage: HubChatMessage!
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type HubChatSubscriptionMuted {
|
|
30
|
+
_: Boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type HubChatSubscriptionUnmuted {
|
|
34
|
+
_: Boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
union HubChatSubscriptionPayload =
|
|
38
|
+
HubChatSubscriptionNewMessage
|
|
39
|
+
| HubChatSubscriptionMuted
|
|
40
|
+
| HubChatSubscriptionUnmuted
|
|
41
|
+
|
|
42
|
+
extend type Subscription {
|
|
43
|
+
hubChatAlert: HubChatSubscriptionPayload
|
|
44
|
+
}
|
|
45
|
+
`,
|
|
46
|
+
objects: {
|
|
47
|
+
HubChatSubscriptionMuted: {
|
|
48
|
+
assertStep: ObjectStep,
|
|
49
|
+
},
|
|
50
|
+
HubChatSubscriptionUnmuted: {
|
|
51
|
+
assertStep: ObjectStep,
|
|
52
|
+
},
|
|
53
|
+
HubChatSubscriptionNewMessage: {
|
|
54
|
+
assertStep: ObjectStep,
|
|
55
|
+
plans: {
|
|
56
|
+
chatMessage($data) {
|
|
57
|
+
const $id = access($data, "chatMessageId");
|
|
58
|
+
return chatMessageTable.get({ id: $id });
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
Subscription: {
|
|
63
|
+
plans: {
|
|
64
|
+
hubChatAlert: {
|
|
65
|
+
subscribePlan(_$root) {
|
|
66
|
+
const $pgSubscriber = context().get("pgSubscriber");
|
|
67
|
+
const $identity = context().get("identity");
|
|
68
|
+
const $channelKey = lambda($identity, (identity) => {
|
|
69
|
+
if (identity?.kind !== "user") {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
return `hub:chat:${identity.session.experience_id}`;
|
|
73
|
+
});
|
|
74
|
+
return listenWithFilter($pgSubscriber, $channelKey, jsonParse, $identity, (item, identity) => {
|
|
75
|
+
if (identity?.kind !== "user") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (typeof item !== "string") {
|
|
79
|
+
logger.warn(item, `hubChatAlert: item is not a string`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const payload = JSON.parse(item);
|
|
83
|
+
switch (payload.type) {
|
|
84
|
+
case "message":
|
|
85
|
+
break;
|
|
86
|
+
case "mute":
|
|
87
|
+
case "unmute":
|
|
88
|
+
return payload.user_id === identity.session.user_id;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
plan($event) {
|
|
94
|
+
return lambda($event, (event) => {
|
|
95
|
+
let payload;
|
|
96
|
+
try {
|
|
97
|
+
payload = ChatPgNotifyPayloadSchema.parse(event);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
if (e instanceof z.ZodError) {
|
|
101
|
+
throw new Error(`Unexpected payload shape: ${prettifyError(e)}`);
|
|
102
|
+
}
|
|
103
|
+
throw e;
|
|
104
|
+
}
|
|
105
|
+
switch (payload.type) {
|
|
106
|
+
case "message":
|
|
107
|
+
return {
|
|
108
|
+
__typename: "HubChatSubscriptionNewMessage",
|
|
109
|
+
chatMessageId: payload.chat_message_id,
|
|
110
|
+
};
|
|
111
|
+
case "mute":
|
|
112
|
+
return {
|
|
113
|
+
__typename: "HubChatSubscriptionMuted",
|
|
114
|
+
_: true,
|
|
115
|
+
};
|
|
116
|
+
case "unmute":
|
|
117
|
+
return {
|
|
118
|
+
__typename: "HubChatSubscriptionUnmuted",
|
|
119
|
+
_: true,
|
|
120
|
+
};
|
|
121
|
+
default: {
|
|
122
|
+
const _exhaustiveCheck = payload;
|
|
123
|
+
throw new Error(`Unexpected payload type: ${_exhaustiveCheck}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubChatUnmuteUserPlugin: GraphileConfig.Plugin;
|