@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
package/dist/src/db/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare function userFromActiveSessionKey(pgClient: QueryExecutor, sessio
|
|
|
21
21
|
user: DbUser;
|
|
22
22
|
sessionId: DbSession["id"];
|
|
23
23
|
isPlayground: boolean;
|
|
24
|
+
isExperienceOwner: boolean;
|
|
24
25
|
} | null>;
|
|
25
26
|
export declare class DatabaseNotifier extends stream.EventEmitter {
|
|
26
27
|
private pgClient;
|
package/dist/src/db/index.js
CHANGED
|
@@ -71,7 +71,7 @@ export async function withPgPoolTransaction(pool, callback, retryCount = 0, maxR
|
|
|
71
71
|
export async function userFromActiveSessionKey(pgClient, sessionKey) {
|
|
72
72
|
const result = await pgClient
|
|
73
73
|
.query(`
|
|
74
|
-
select u.id, u.uname, u.casino_id, s.experience_id, u.mp_user_id, s.id as session_id, c.is_playground
|
|
74
|
+
select u.id, u.uname, u.casino_id, s.experience_id, u.mp_user_id, s.id as session_id, c.is_playground, u.id = e.user_id as is_experience_owner
|
|
75
75
|
from hub.active_session s
|
|
76
76
|
join hub.user u on s.user_id = u.id
|
|
77
77
|
join hub.experience e on e.id = s.experience_id
|
|
@@ -82,11 +82,19 @@ export async function userFromActiveSessionKey(pgClient, sessionKey) {
|
|
|
82
82
|
if (!result) {
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
-
const
|
|
85
|
+
const user = {
|
|
86
|
+
id: result.id,
|
|
87
|
+
uname: result.uname,
|
|
88
|
+
casino_id: result.casino_id,
|
|
89
|
+
experience_id: result.experience_id,
|
|
90
|
+
mp_user_id: result.mp_user_id,
|
|
91
|
+
};
|
|
92
|
+
const { session_id, is_playground, is_experience_owner } = result;
|
|
86
93
|
return {
|
|
87
94
|
user,
|
|
88
95
|
sessionId: session_id,
|
|
89
96
|
isPlayground: is_playground,
|
|
97
|
+
isExperienceOwner: is_experience_owner,
|
|
90
98
|
};
|
|
91
99
|
}
|
|
92
100
|
export class DatabaseNotifier extends stream.EventEmitter {
|
package/dist/src/db/types.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export type DbExperience = {
|
|
|
49
49
|
id: string;
|
|
50
50
|
casino_id: string;
|
|
51
51
|
mp_experience_id: string;
|
|
52
|
+
user_id: string | null;
|
|
52
53
|
};
|
|
53
54
|
export type DbCasino = {
|
|
54
55
|
id: string;
|
|
@@ -172,3 +173,26 @@ export type DbAuditLogRecord = {
|
|
|
172
173
|
ref_type: string | null;
|
|
173
174
|
ref_id: string | null;
|
|
174
175
|
};
|
|
176
|
+
export type DbChatMute = {
|
|
177
|
+
id: string;
|
|
178
|
+
casino_id: DbCasino["id"];
|
|
179
|
+
experience_id: DbExperience["id"];
|
|
180
|
+
user_id: DbUser["id"];
|
|
181
|
+
expired_at: Date | null;
|
|
182
|
+
reason: string | null;
|
|
183
|
+
};
|
|
184
|
+
export type DbChatMod = {
|
|
185
|
+
id: string;
|
|
186
|
+
casino_id: DbCasino["id"];
|
|
187
|
+
experience_id: DbExperience["id"];
|
|
188
|
+
user_id: DbUser["id"];
|
|
189
|
+
};
|
|
190
|
+
export type DbChatMessage = {
|
|
191
|
+
id: string;
|
|
192
|
+
casino_id: DbCasino["id"];
|
|
193
|
+
experience_id: DbExperience["id"];
|
|
194
|
+
user_id: DbUser["id"];
|
|
195
|
+
client_id: string;
|
|
196
|
+
body: string;
|
|
197
|
+
hidden_at: Date | null;
|
|
198
|
+
};
|
package/dist/src/express.d.ts
CHANGED
package/dist/src/index.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ declare global {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
export { MakeOutcomeBetPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-make-outcome-bet.js";
|
|
21
|
-
export { HubCreatePlaygroundSessionPlugin } from "./plugins/hub-create-playground-session.js";
|
|
22
21
|
export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
|
|
23
22
|
export type PluginContext = Grafast.Context;
|
|
24
23
|
export { defaultPlugins, type PluginIdentity, type UserSessionContext, } from "./server/graphile.config.js";
|
|
@@ -32,6 +31,8 @@ export type ServerOptions = {
|
|
|
32
31
|
extraPgSchemas?: string[];
|
|
33
32
|
exportSchemaSDLPath?: string;
|
|
34
33
|
userDatabaseMigrationsPath?: string;
|
|
34
|
+
enableChat?: boolean;
|
|
35
|
+
enablePlayground?: boolean;
|
|
35
36
|
};
|
|
36
37
|
export declare function startAndListen(options: ServerOptions): Promise<{
|
|
37
38
|
port: number;
|
package/dist/src/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import { join } from "path";
|
|
|
7
7
|
import { logger } from "./logger.js";
|
|
8
8
|
import { createServerContext, closeServerContext, } from "./context.js";
|
|
9
9
|
export { MakeOutcomeBetPlugin, } from "./plugins/hub-make-outcome-bet.js";
|
|
10
|
-
export { HubCreatePlaygroundSessionPlugin } from "./plugins/hub-create-playground-session.js";
|
|
11
10
|
export { validateRisk, } from "./risk-policy.js";
|
|
12
11
|
export { defaultPlugins, } from "./server/graphile.config.js";
|
|
13
12
|
async function initialize(options) {
|
|
@@ -82,6 +81,8 @@ export async function startAndListen(options) {
|
|
|
82
81
|
extraPgSchemas: options.extraPgSchemas,
|
|
83
82
|
abortSignal: abortController.signal,
|
|
84
83
|
context,
|
|
84
|
+
enableChat: options.enableChat ?? true,
|
|
85
|
+
enablePlayground: options.enablePlayground ?? true,
|
|
85
86
|
});
|
|
86
87
|
const gracefulShutdown = async ({ exit = true } = {}) => {
|
|
87
88
|
if (isShuttingDown) {
|
|
@@ -10,4 +10,9 @@ export declare const PgAdvisoryLock: {
|
|
|
10
10
|
experienceId: DbExperience["id"];
|
|
11
11
|
casinoId: DbCasino["id"];
|
|
12
12
|
}) => Promise<void>;
|
|
13
|
+
forChatUserAction: (pgClient: PgClientInTransaction, params: {
|
|
14
|
+
userId: DbUser["id"];
|
|
15
|
+
experienceId: DbExperience["id"];
|
|
16
|
+
casinoId: DbCasino["id"];
|
|
17
|
+
}) => Promise<void>;
|
|
13
18
|
};
|
|
@@ -3,6 +3,7 @@ var LockNamespace;
|
|
|
3
3
|
(function (LockNamespace) {
|
|
4
4
|
LockNamespace[LockNamespace["MP_TAKE_REQUEST"] = 1] = "MP_TAKE_REQUEST";
|
|
5
5
|
LockNamespace[LockNamespace["NEW_HASH_CHAIN"] = 2] = "NEW_HASH_CHAIN";
|
|
6
|
+
LockNamespace[LockNamespace["CHAT_USER_ACTION"] = 3] = "CHAT_USER_ACTION";
|
|
6
7
|
})(LockNamespace || (LockNamespace = {}));
|
|
7
8
|
function simpleHash32(text) {
|
|
8
9
|
let hash = 0;
|
|
@@ -31,4 +32,8 @@ export const PgAdvisoryLock = {
|
|
|
31
32
|
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
32
33
|
await acquireAdvisoryLock(pgClient, LockNamespace.NEW_HASH_CHAIN, createHashKey(params.userId, params.experienceId, params.casinoId));
|
|
33
34
|
},
|
|
35
|
+
forChatUserAction: async (pgClient, params) => {
|
|
36
|
+
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
37
|
+
await acquireAdvisoryLock(pgClient, LockNamespace.CHAT_USER_ACTION, createHashKey(params.userId, params.experienceId, params.casinoId));
|
|
38
|
+
},
|
|
34
39
|
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
|
|
2
|
+
-- A simple single-room chat system
|
|
3
|
+
|
|
4
|
+
-- Quick revert:
|
|
5
|
+
drop table if exists hub.chat_message;
|
|
6
|
+
drop view if exists hub.active_chat_mute;
|
|
7
|
+
drop table if exists hub.chat_mute;
|
|
8
|
+
drop table if exists hub.chat_rate_bucket;
|
|
9
|
+
drop table if exists hub.chat_mod;
|
|
10
|
+
drop type if exists hub.chat_message_type;
|
|
11
|
+
alter table hub.experience drop column if exists user_id;
|
|
12
|
+
alter table hub.user drop column if exists client_id;
|
|
13
|
+
alter table hub.experience drop column if exists client_id;
|
|
14
|
+
|
|
15
|
+
-- New helper function for RLS so we don't have to do a subquery
|
|
16
|
+
create or replace function hub_hidden.is_experience_owner() returns boolean as $$
|
|
17
|
+
select nullif(current_setting('session.is_experience_owner', true), '') = '1';
|
|
18
|
+
$$ language sql stable;
|
|
19
|
+
|
|
20
|
+
-- We want to know which hub.user is the MP owner of the hub.experience
|
|
21
|
+
-- Ideally it would be not-null but we can't do that in this migration
|
|
22
|
+
-- since that info must be queried from MP.
|
|
23
|
+
alter table hub.experience add column user_id uuid null references hub.user(id);
|
|
24
|
+
|
|
25
|
+
create type hub.chat_message_type as enum ('user', 'system');
|
|
26
|
+
|
|
27
|
+
-- chat message type is 'user' or 'system'
|
|
28
|
+
-- system messages don't have a user_id
|
|
29
|
+
create table hub.chat_message (
|
|
30
|
+
-- uuidv7 has creation timestamp in it
|
|
31
|
+
id uuid primary key default hub_hidden.uuid_generate_v7(),
|
|
32
|
+
|
|
33
|
+
-- fks
|
|
34
|
+
casino_id uuid not null references hub.casino(id),
|
|
35
|
+
experience_id uuid not null references hub.experience(id),
|
|
36
|
+
-- system messages don't have a user_id
|
|
37
|
+
user_id uuid null references hub.user(id),
|
|
38
|
+
|
|
39
|
+
type hub.chat_message_type not null,
|
|
40
|
+
|
|
41
|
+
-- message info
|
|
42
|
+
client_id uuid not null, -- idempotent id
|
|
43
|
+
body text not null,
|
|
44
|
+
hidden_at timestamptz null,
|
|
45
|
+
|
|
46
|
+
constraint chat_message_user_id_check check (
|
|
47
|
+
(type = 'user' and user_id is not null) or (type = 'system' and user_id is null)
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- fk indexes
|
|
52
|
+
create index chat_message_casino_id_idx on hub.chat_message(casino_id);
|
|
53
|
+
create index chat_message_user_id_idx on hub.chat_message(user_id);
|
|
54
|
+
create index chat_message_experience_id_idx on hub.chat_message(experience_id);
|
|
55
|
+
|
|
56
|
+
-- support idempotency
|
|
57
|
+
create unique index chat_idempotent_user_message_idx
|
|
58
|
+
on hub.chat_message(casino_id, experience_id, user_id, client_id)
|
|
59
|
+
where type = 'user';
|
|
60
|
+
create unique index chat_idempotent_system_message_idx
|
|
61
|
+
on hub.chat_message(casino_id, experience_id, client_id)
|
|
62
|
+
where type = 'system';
|
|
63
|
+
|
|
64
|
+
-- append-only table
|
|
65
|
+
create table hub.chat_mute (
|
|
66
|
+
id uuid primary key default hub_hidden.uuid_generate_v7(),
|
|
67
|
+
casino_id uuid not null references hub.casino(id),
|
|
68
|
+
experience_id uuid not null references hub.experience(id),
|
|
69
|
+
user_id uuid not null references hub.user(id),
|
|
70
|
+
expired_at timestamptz null,
|
|
71
|
+
-- set on unmute
|
|
72
|
+
revoked_at timestamptz null,
|
|
73
|
+
reason text null
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
-- fk indexes
|
|
77
|
+
create index chat_mute_casino_id_idx on hub.chat_mute(casino_id);
|
|
78
|
+
create index chat_mute_user_id_idx on hub.chat_mute(user_id);
|
|
79
|
+
create index chat_mute_experience_id_idx on hub.chat_mute(experience_id);
|
|
80
|
+
|
|
81
|
+
-- One unrevoked row per key; expiry handled in code/view.
|
|
82
|
+
create unique index if not exists chat_mute_one_unrevoked_per_key
|
|
83
|
+
on hub.chat_mute (casino_id, experience_id, user_id)
|
|
84
|
+
where revoked_at is null;
|
|
85
|
+
|
|
86
|
+
-- "Active" means not revoked AND not expired (null = indefinite).
|
|
87
|
+
-- order by id desc determines which key is picked for `distinct on`: the latest one
|
|
88
|
+
create or replace view hub.active_chat_mute as
|
|
89
|
+
select distinct on (casino_id, experience_id, user_id) *
|
|
90
|
+
from hub.chat_mute
|
|
91
|
+
where revoked_at is null
|
|
92
|
+
and (expired_at is null or expired_at > now())
|
|
93
|
+
order by casino_id, experience_id, user_id, id desc; -- highest uuidv7 per key
|
|
94
|
+
|
|
95
|
+
-- for active chat mute lookup
|
|
96
|
+
create index if not exists chat_mute_lookup_idx
|
|
97
|
+
on hub.chat_mute (casino_id, experience_id, user_id)
|
|
98
|
+
where revoked_at is null;
|
|
99
|
+
|
|
100
|
+
-- not exposed to graphql
|
|
101
|
+
create table hub.chat_rate_bucket (
|
|
102
|
+
id uuid primary key default hub_hidden.uuid_generate_v7(),
|
|
103
|
+
casino_id uuid not null references hub.casino(id),
|
|
104
|
+
experience_id uuid not null references hub.experience(id),
|
|
105
|
+
user_id uuid not null references hub.user(id),
|
|
106
|
+
|
|
107
|
+
window_seconds int not null,
|
|
108
|
+
bucket_start timestamptz not null,
|
|
109
|
+
count int not null default 0
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
-- fk indexes
|
|
113
|
+
create index chat_rate_bucket_casino_id_idx on hub.chat_rate_bucket(casino_id);
|
|
114
|
+
create index chat_rate_bucket_user_id_idx on hub.chat_rate_bucket(user_id);
|
|
115
|
+
create index chat_rate_bucket_experience_id_idx on hub.chat_rate_bucket(experience_id);
|
|
116
|
+
|
|
117
|
+
create unique index chat_rate_bucket_window_idx on hub.chat_rate_bucket(casino_id, experience_id, user_id, window_seconds, bucket_start);
|
|
118
|
+
|
|
119
|
+
-- not exposed to graphql
|
|
120
|
+
create table hub.chat_mod (
|
|
121
|
+
id uuid primary key default hub_hidden.uuid_generate_v7(),
|
|
122
|
+
casino_id uuid not null references hub.casino(id),
|
|
123
|
+
experience_id uuid not null references hub.experience(id),
|
|
124
|
+
user_id uuid not null references hub.user(id)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
-- fk indexes
|
|
128
|
+
create index chat_mod_casino_id_idx on hub.chat_mod(casino_id);
|
|
129
|
+
create index chat_mod_user_id_idx on hub.chat_mod(user_id);
|
|
130
|
+
create index chat_mod_experience_id_idx on hub.chat_mod(experience_id);
|
|
131
|
+
-- a user can only be a chat mod once per experience
|
|
132
|
+
create unique index chat_mod_unique_idx on hub.chat_mod(casino_id, experience_id, user_id);
|
|
133
|
+
|
|
134
|
+
-- grant
|
|
135
|
+
grant select on hub.chat_message to app_postgraphile;
|
|
136
|
+
grant select on hub.chat_mute to app_postgraphile;
|
|
137
|
+
grant select on hub.chat_mod to app_postgraphile;
|
|
138
|
+
|
|
139
|
+
-- rls
|
|
140
|
+
alter table hub.chat_message enable row level security;
|
|
141
|
+
alter table hub.chat_mute enable row level security;
|
|
142
|
+
alter table hub.chat_mod enable row level security;
|
|
143
|
+
|
|
144
|
+
drop policy if exists select_chat_message on hub.chat_message;
|
|
145
|
+
create policy select_chat_message on hub.chat_message for select using (
|
|
146
|
+
-- operator can see all rows
|
|
147
|
+
hub_hidden.is_operator()
|
|
148
|
+
|
|
149
|
+
-- normal users can only see non-hidden rows in the current experience
|
|
150
|
+
or (
|
|
151
|
+
hidden_at is null
|
|
152
|
+
and experience_id = hub_hidden.current_experience_id()
|
|
153
|
+
and casino_id = hub_hidden.current_casino_id()
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
-- experience owner can see all messages no matter hidden status
|
|
157
|
+
or hub_hidden.is_experience_owner()
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
drop policy if exists select_chat_mute on hub.chat_mute;
|
|
161
|
+
create policy select_chat_mute on hub.chat_mute for select using (
|
|
162
|
+
-- operator can see all rows
|
|
163
|
+
hub_hidden.is_operator()
|
|
164
|
+
|
|
165
|
+
-- users can see their own mutes
|
|
166
|
+
or hub_hidden.current_user_id() = hub.chat_mute.user_id
|
|
167
|
+
|
|
168
|
+
-- experience owner can see all mutes
|
|
169
|
+
or hub_hidden.is_experience_owner()
|
|
170
|
+
|
|
171
|
+
-- anyone in chat_mod can see all mutes
|
|
172
|
+
or exists (
|
|
173
|
+
select 1
|
|
174
|
+
from hub.chat_mod
|
|
175
|
+
where user_id = hub_hidden.current_user_id()
|
|
176
|
+
and casino_id = hub.chat_mute.casino_id
|
|
177
|
+
and experience_id = hub.chat_mute.experience_id
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
create policy select_chat_mod on hub.chat_mod for select using (
|
|
182
|
+
-- operator can see all rows
|
|
183
|
+
hub_hidden.is_operator()
|
|
184
|
+
|
|
185
|
+
-- experience owner can see all mods
|
|
186
|
+
or hub_hidden.is_experience_owner()
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
-- relax the select_experience policy so that it's public
|
|
190
|
+
-- this lets us use hubCurrentExperience { hubChatMessages { ... } }
|
|
191
|
+
-- previously (in 001-schema.sql) this was scoped to operator only
|
|
192
|
+
alter policy select_experience on hub.experience using (true);
|
|
193
|
+
|
|
194
|
+
----
|
|
195
|
+
|
|
196
|
+
-- add client_id to hub.user and hub.experience for playground sessions
|
|
197
|
+
alter table hub.user add column client_id uuid null;
|
|
198
|
+
alter table hub.experience add column client_id uuid null;
|
|
199
|
+
|
|
200
|
+
-- fk indexes, ensure client_id is unique per casino
|
|
201
|
+
create unique index user_playground_client_id_idx on hub.user(casino_id, client_id)
|
|
202
|
+
where client_id is not null;
|
|
203
|
+
create unique index experience_playground_client_id_idx on hub.experience(casino_id, client_id)
|
|
204
|
+
where client_id is not null;
|
|
205
|
+
|
|
206
|
+
-- Update select_user policy so users can see other users
|
|
207
|
+
-- This way they can see user info {id, uname} on chat messages
|
|
208
|
+
drop policy if exists select_user on hub.user;
|
|
209
|
+
create policy select_user on hub.user for select using (
|
|
210
|
+
-- Old:
|
|
211
|
+
-- hub_hidden.is_operator() or (id = hub_hidden.current_user_id())
|
|
212
|
+
|
|
213
|
+
-- New:
|
|
214
|
+
true
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
-- Here's how we could clean up rate limit buckets periodically
|
|
218
|
+
-- create or replace function hub_hidden.gc_chat_buckets() returns void language sql as $$
|
|
219
|
+
-- delete from hub.chat_rate_bucket
|
|
220
|
+
-- where bucket_start < now() - interval '2 days';
|
|
221
|
+
-- $$;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubChatAfterIdConditionPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { sqlValueWithCodec, TYPES } from "postgraphile/@dataplan/pg";
|
|
2
|
+
import { sql } from "postgraphile/pg-sql2";
|
|
3
|
+
import { addPgTableCondition } from "postgraphile/utils";
|
|
4
|
+
export const HubChatAfterIdConditionPlugin = addPgTableCondition({
|
|
5
|
+
schemaName: "hub",
|
|
6
|
+
tableName: "chat_message",
|
|
7
|
+
}, "afterId", (build) => {
|
|
8
|
+
return {
|
|
9
|
+
description: "Get chat messages after a given ID",
|
|
10
|
+
type: build.getInputTypeByName("UUID"),
|
|
11
|
+
apply: (condition, value) => {
|
|
12
|
+
condition.where(sql `${condition.alias}.id > ${sqlValueWithCodec(value, TYPES.uuid)}`);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubChatCreateSystemMessagePlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,124 @@
|
|
|
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 { extractFirstZodErrorMessage } from "../../util.js";
|
|
6
|
+
import { exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../../db/index.js";
|
|
7
|
+
const InputSchema = z.object({
|
|
8
|
+
clientId: z.uuid("Invalid client ID"),
|
|
9
|
+
experienceId: z.uuid("Invalid experience ID"),
|
|
10
|
+
body: z
|
|
11
|
+
.string()
|
|
12
|
+
.transform(normalizeSystemMessageBody)
|
|
13
|
+
.refine((body) => body.length > 0, "Body is required")
|
|
14
|
+
.refine((body) => body.length <= 140, `Max body length: 140 chars`),
|
|
15
|
+
});
|
|
16
|
+
export const HubChatCreateSystemMessagePlugin = extendSchema((build) => {
|
|
17
|
+
const chatMessageTable = build.input.pgRegistry.pgResources.hub_chat_message;
|
|
18
|
+
return {
|
|
19
|
+
typeDefs: gql `
|
|
20
|
+
input HubChatCreateSystemMessageInput {
|
|
21
|
+
clientId: UUID!
|
|
22
|
+
body: String!
|
|
23
|
+
experienceId: UUID!
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type HubChatCreateSystemMessagePayload {
|
|
27
|
+
chatMessage: HubChatMessage!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
extend type Mutation {
|
|
31
|
+
hubChatCreateSystemMessage(
|
|
32
|
+
input: HubChatCreateSystemMessageInput!
|
|
33
|
+
): HubChatCreateSystemMessagePayload
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
objects: {
|
|
37
|
+
Mutation: {
|
|
38
|
+
plans: {
|
|
39
|
+
hubChatCreateSystemMessage(_, { $input }) {
|
|
40
|
+
const $identity = context().get("identity");
|
|
41
|
+
const $superuserPool = context().get("superuserPool");
|
|
42
|
+
const $chatMessageId = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
|
|
43
|
+
if (identity?.kind === "operator" ||
|
|
44
|
+
(identity?.kind === "user" &&
|
|
45
|
+
identity?.session.is_experience_owner)) {
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
throw new GraphQLError("Unauthorized");
|
|
49
|
+
}
|
|
50
|
+
let input;
|
|
51
|
+
try {
|
|
52
|
+
input = InputSchema.parse(rawInput);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
if (e instanceof z.ZodError) {
|
|
56
|
+
throw new GraphQLError(extractFirstZodErrorMessage(e));
|
|
57
|
+
}
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
const dbExperience = await superuserPool
|
|
61
|
+
.query({
|
|
62
|
+
text: `
|
|
63
|
+
SELECT id, casino_id
|
|
64
|
+
FROM hub.experience
|
|
65
|
+
WHERE id = $1`,
|
|
66
|
+
values: [input.experienceId],
|
|
67
|
+
})
|
|
68
|
+
.then(maybeOneRow);
|
|
69
|
+
if (!dbExperience) {
|
|
70
|
+
throw new GraphQLError("Experience not found");
|
|
71
|
+
}
|
|
72
|
+
return await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
73
|
+
const dbChatMessage = await pgClient
|
|
74
|
+
.query({
|
|
75
|
+
text: `
|
|
76
|
+
with ins as (
|
|
77
|
+
insert into hub.chat_message (casino_id, experience_id, client_id, body, type)
|
|
78
|
+
values ($1, $2, $3, $4, 'system')
|
|
79
|
+
on conflict (casino_id, experience_id, client_id) where type = 'system'
|
|
80
|
+
do nothing
|
|
81
|
+
returning id
|
|
82
|
+
)
|
|
83
|
+
select id from ins
|
|
84
|
+
union all
|
|
85
|
+
select id
|
|
86
|
+
from hub.chat_message
|
|
87
|
+
where casino_id = $1 and experience_id = $2 and client_id = $3
|
|
88
|
+
limit 1
|
|
89
|
+
`,
|
|
90
|
+
values: [
|
|
91
|
+
dbExperience.casino_id,
|
|
92
|
+
dbExperience.id,
|
|
93
|
+
input.clientId,
|
|
94
|
+
input.body,
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
.then(exactlyOneRow);
|
|
98
|
+
const notifyPayload = {
|
|
99
|
+
type: "message",
|
|
100
|
+
chat_message_id: dbChatMessage.id,
|
|
101
|
+
};
|
|
102
|
+
await pgClient.query({
|
|
103
|
+
text: `select pg_notify('hub:chat:${dbExperience.id}', $1::text)`,
|
|
104
|
+
values: [JSON.stringify(notifyPayload)],
|
|
105
|
+
});
|
|
106
|
+
return dbChatMessage.id;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return object({
|
|
110
|
+
chatMessage: chatMessageTable.get({ id: $chatMessageId }),
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
function normalizeSystemMessageBody(body) {
|
|
119
|
+
return body
|
|
120
|
+
.normalize("NFC")
|
|
121
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, "")
|
|
122
|
+
.replace(/\s+/g, " ")
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubChatCreateUserMessagePlugin: GraphileConfig.Plugin;
|