@moneypot/hub 1.16.0-dev.5 → 1.16.0-dev.6
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/README.md +9 -2
- package/dist/src/pg-advisory-lock.d.ts +5 -1
- package/dist/src/pg-advisory-lock.js +7 -3
- package/dist/src/plugins/chat/hub-chat-create-user-message.js +1 -1
- package/dist/src/plugins/chat/hub-chat-mod-management.d.ts +1 -0
- package/dist/src/plugins/chat/hub-chat-mod-management.js +227 -0
- package/dist/src/plugins/chat/hub-chat-mute-user.js +1 -1
- package/dist/src/plugins/chat/hub-chat-unmute-user.js +1 -1
- package/dist/src/server/graphile.config.js +2 -1
- package/dist/src/util.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Example implementations:
|
|
10
10
|
|
|
11
|
-
1. <https://github.com/moneypot/
|
|
12
|
-
2. <https://github.com/moneypot/dice-controller/>: A dice game ([frontend](https://github.com/moneypot/dice-experience/))
|
|
11
|
+
1. <https://github.com/moneypot/controller-template/>: An template hub server that comes with an example of a custom game implementation.
|
|
13
12
|
|
|
14
13
|
## Manual
|
|
15
14
|
|
|
@@ -76,11 +75,19 @@ insert into hub.api_key default values returning key;
|
|
|
76
75
|
|
|
77
76
|
## Changelog
|
|
78
77
|
|
|
78
|
+
You should always keep your hub server up to date as soon as possible.
|
|
79
|
+
|
|
80
|
+
### 1.16.x
|
|
81
|
+
|
|
82
|
+
- Added a simple built-in chat system.
|
|
83
|
+
- You can opt-out of the chat system by passing `startAndListen({ ..., enableChat: false })`.
|
|
84
|
+
|
|
79
85
|
### 1.15.x
|
|
80
86
|
|
|
81
87
|
- Added a "playground" casino (`hub.casino.is_playground = true`) that lets skin devs create ephemeral sessions for testing.
|
|
82
88
|
- Added `hubCreatePlaygroundSession` mutation to create a playground session.
|
|
83
89
|
- It's a drop-in replacement for `hubAuthenticate({ userToken, baseCasinoUrl })` when you're testing your experience outside of the MoneyPot.com iframe (thus you don't have a userToken).
|
|
90
|
+
- You can opt-out of allowing playground sessions by passing `startAndListen({ ..., enablePlayground: false })`, but remember that the objective of this system is to make it easier for developers to build experience frontends that bet against your server.
|
|
84
91
|
|
|
85
92
|
### 1.14.x
|
|
86
93
|
|
|
@@ -10,9 +10,13 @@ export declare const PgAdvisoryLock: {
|
|
|
10
10
|
experienceId: DbExperience["id"];
|
|
11
11
|
casinoId: DbCasino["id"];
|
|
12
12
|
}) => Promise<void>;
|
|
13
|
-
|
|
13
|
+
forChatPlayerAction: (pgClient: PgClientInTransaction, params: {
|
|
14
14
|
userId: DbUser["id"];
|
|
15
15
|
experienceId: DbExperience["id"];
|
|
16
16
|
casinoId: DbCasino["id"];
|
|
17
17
|
}) => Promise<void>;
|
|
18
|
+
forChatModManagement: (pgClient: PgClientInTransaction, params: {
|
|
19
|
+
experienceId: DbExperience["id"];
|
|
20
|
+
casinoId: DbCasino["id"];
|
|
21
|
+
}) => Promise<void>;
|
|
18
22
|
};
|
|
@@ -3,7 +3,8 @@ 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["
|
|
6
|
+
LockNamespace[LockNamespace["CHAT_PLAYER_ACTION"] = 3] = "CHAT_PLAYER_ACTION";
|
|
7
|
+
LockNamespace[LockNamespace["CHAT_MOD_MANAGEMENT"] = 4] = "CHAT_MOD_MANAGEMENT";
|
|
7
8
|
})(LockNamespace || (LockNamespace = {}));
|
|
8
9
|
function simpleHash32(text) {
|
|
9
10
|
let hash = 0;
|
|
@@ -32,8 +33,11 @@ export const PgAdvisoryLock = {
|
|
|
32
33
|
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
33
34
|
await acquireAdvisoryLock(pgClient, LockNamespace.NEW_HASH_CHAIN, createHashKey(params.userId, params.experienceId, params.casinoId));
|
|
34
35
|
},
|
|
35
|
-
|
|
36
|
+
forChatPlayerAction: async (pgClient, params) => {
|
|
36
37
|
assert(pgClient._inTransaction, "pgClient must be in a transaction");
|
|
37
|
-
await acquireAdvisoryLock(pgClient, LockNamespace.
|
|
38
|
+
await acquireAdvisoryLock(pgClient, LockNamespace.CHAT_PLAYER_ACTION, createHashKey(params.userId, params.experienceId, params.casinoId));
|
|
39
|
+
},
|
|
40
|
+
forChatModManagement: async (pgClient, params) => {
|
|
41
|
+
await acquireAdvisoryLock(pgClient, LockNamespace.CHAT_MOD_MANAGEMENT, createHashKey(params.experienceId, params.casinoId));
|
|
38
42
|
},
|
|
39
43
|
};
|
|
@@ -81,7 +81,7 @@ export const HubChatCreateUserMessagePlugin = extendSchema((build) => {
|
|
|
81
81
|
throw e;
|
|
82
82
|
}
|
|
83
83
|
return await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
84
|
-
await PgAdvisoryLock.
|
|
84
|
+
await PgAdvisoryLock.forChatPlayerAction(pgClient, {
|
|
85
85
|
userId: identity.session.user_id,
|
|
86
86
|
experienceId: identity.session.experience_id,
|
|
87
87
|
casinoId: identity.session.casino_id,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const HubChatModManagementPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { GraphQLError } from "graphql";
|
|
2
|
+
import { constant, context, object, sideEffect } from "postgraphile/grafast";
|
|
3
|
+
import { gql, extendSchema } from "postgraphile/utils";
|
|
4
|
+
import z from "zod/v4";
|
|
5
|
+
import { extractFirstZodErrorMessage, uuidEqual } from "../../util.js";
|
|
6
|
+
import { exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../../db/index.js";
|
|
7
|
+
import { PgAdvisoryLock } from "../../pg-advisory-lock.js";
|
|
8
|
+
const AddModInputSchema = z.object({
|
|
9
|
+
userId: z.uuid("Invalid user ID"),
|
|
10
|
+
experienceId: z.uuid("Invalid experience ID"),
|
|
11
|
+
});
|
|
12
|
+
const RemoveModInputSchema = z.object({
|
|
13
|
+
userId: z.uuid("Invalid user ID"),
|
|
14
|
+
experienceId: z.uuid("Invalid experience ID"),
|
|
15
|
+
});
|
|
16
|
+
export const HubChatModManagementPlugin = extendSchema((build) => {
|
|
17
|
+
const chatModTable = build.input.pgRegistry.pgResources.hub_chat_mod;
|
|
18
|
+
return {
|
|
19
|
+
typeDefs: gql `
|
|
20
|
+
input HubChatAddModInput {
|
|
21
|
+
userId: UUID!
|
|
22
|
+
experienceId: UUID!
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
input HubChatRemoveModInput {
|
|
26
|
+
userId: UUID!
|
|
27
|
+
experienceId: UUID!
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type HubChatAddModPayload {
|
|
31
|
+
chatMod: HubChatMod!
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type HubChatRemoveModPayload {
|
|
35
|
+
_: Boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
extend type Mutation {
|
|
39
|
+
hubChatAddMod(input: HubChatAddModInput!): HubChatAddModPayload
|
|
40
|
+
hubChatRemoveMod(input: HubChatRemoveModInput!): HubChatRemoveModPayload
|
|
41
|
+
}
|
|
42
|
+
`,
|
|
43
|
+
objects: {
|
|
44
|
+
Mutation: {
|
|
45
|
+
plans: {
|
|
46
|
+
hubChatRemoveMod(_, { $input }) {
|
|
47
|
+
const $identity = context().get("identity");
|
|
48
|
+
const $superuserPool = context().get("superuserPool");
|
|
49
|
+
const _$result = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
|
|
50
|
+
if (identity?.kind === "operator" ||
|
|
51
|
+
identity?.session.is_experience_owner) {
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new GraphQLError("Unauthorized");
|
|
55
|
+
}
|
|
56
|
+
let input;
|
|
57
|
+
try {
|
|
58
|
+
input = RemoveModInputSchema.parse(rawInput);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e instanceof z.ZodError) {
|
|
62
|
+
throw new GraphQLError(extractFirstZodErrorMessage(e));
|
|
63
|
+
}
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
const dbTargetExperience = await superuserPool
|
|
67
|
+
.query({
|
|
68
|
+
text: `
|
|
69
|
+
SELECT id, user_id, casino_id
|
|
70
|
+
FROM hub.experience
|
|
71
|
+
WHERE id = $1
|
|
72
|
+
`,
|
|
73
|
+
values: [input.experienceId],
|
|
74
|
+
})
|
|
75
|
+
.then(maybeOneRow);
|
|
76
|
+
if (!dbTargetExperience) {
|
|
77
|
+
throw new GraphQLError("Experience not found");
|
|
78
|
+
}
|
|
79
|
+
if (identity?.kind === "user" &&
|
|
80
|
+
!uuidEqual(identity?.session.experience_id, dbTargetExperience.id)) {
|
|
81
|
+
throw new GraphQLError("Your session does not match the experienceId");
|
|
82
|
+
}
|
|
83
|
+
const dbTargetUser = await superuserPool
|
|
84
|
+
.query({
|
|
85
|
+
text: `
|
|
86
|
+
SELECT id
|
|
87
|
+
FROM hub.user
|
|
88
|
+
WHERE id = $1 AND experience_id = $2
|
|
89
|
+
`,
|
|
90
|
+
values: [input.userId, input.experienceId],
|
|
91
|
+
})
|
|
92
|
+
.then(maybeOneRow);
|
|
93
|
+
if (!dbTargetUser) {
|
|
94
|
+
throw new GraphQLError("User not found");
|
|
95
|
+
}
|
|
96
|
+
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
97
|
+
await PgAdvisoryLock.forChatModManagement(pgClient, {
|
|
98
|
+
experienceId: dbTargetExperience.id,
|
|
99
|
+
casinoId: dbTargetExperience.casino_id,
|
|
100
|
+
});
|
|
101
|
+
const dbChatMod = await pgClient
|
|
102
|
+
.query({
|
|
103
|
+
text: `
|
|
104
|
+
SELECT id
|
|
105
|
+
FROM hub.chat_mod
|
|
106
|
+
WHERE experience_id = $1 AND user_id = $2 AND casino_id = $3
|
|
107
|
+
`,
|
|
108
|
+
values: [
|
|
109
|
+
dbTargetExperience.id,
|
|
110
|
+
dbTargetUser.id,
|
|
111
|
+
dbTargetExperience.casino_id,
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
.then(maybeOneRow);
|
|
115
|
+
if (!dbChatMod) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await pgClient.query(`
|
|
119
|
+
DELETE FROM hub.chat_mod
|
|
120
|
+
WHERE id = $1
|
|
121
|
+
`, [dbChatMod.id]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
return object({
|
|
125
|
+
_: constant(true),
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
hubChatAddMod(_, { $input }) {
|
|
129
|
+
const $identity = context().get("identity");
|
|
130
|
+
const $superuserPool = context().get("superuserPool");
|
|
131
|
+
const $chatModId = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
|
|
132
|
+
if (identity?.kind === "operator" ||
|
|
133
|
+
identity?.session.is_experience_owner) {
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
throw new GraphQLError("Unauthorized");
|
|
137
|
+
}
|
|
138
|
+
let input;
|
|
139
|
+
try {
|
|
140
|
+
input = AddModInputSchema.parse(rawInput);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
if (e instanceof z.ZodError) {
|
|
144
|
+
throw new GraphQLError(extractFirstZodErrorMessage(e));
|
|
145
|
+
}
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
const dbTargetExperience = await superuserPool
|
|
149
|
+
.query({
|
|
150
|
+
text: `
|
|
151
|
+
SELECT id, user_id, casino_id
|
|
152
|
+
FROM hub.experience
|
|
153
|
+
WHERE id = $1
|
|
154
|
+
`,
|
|
155
|
+
values: [input.experienceId],
|
|
156
|
+
})
|
|
157
|
+
.then(maybeOneRow);
|
|
158
|
+
if (!dbTargetExperience) {
|
|
159
|
+
throw new GraphQLError("Experience not found");
|
|
160
|
+
}
|
|
161
|
+
if (identity?.kind === "user" &&
|
|
162
|
+
!uuidEqual(identity?.session.experience_id, dbTargetExperience.id)) {
|
|
163
|
+
throw new GraphQLError("Your session does not match the experienceId");
|
|
164
|
+
}
|
|
165
|
+
const dbTargetUser = await superuserPool
|
|
166
|
+
.query({
|
|
167
|
+
text: `
|
|
168
|
+
SELECT id
|
|
169
|
+
FROM hub.user
|
|
170
|
+
WHERE id = $1 AND experience_id = $2
|
|
171
|
+
`,
|
|
172
|
+
values: [input.userId, input.experienceId],
|
|
173
|
+
})
|
|
174
|
+
.then(maybeOneRow);
|
|
175
|
+
if (!dbTargetUser) {
|
|
176
|
+
throw new GraphQLError("User not found");
|
|
177
|
+
}
|
|
178
|
+
if (dbTargetExperience.user_id &&
|
|
179
|
+
uuidEqual(dbTargetUser.id, dbTargetExperience.user_id)) {
|
|
180
|
+
throw new GraphQLError("User cannot be a chat mod");
|
|
181
|
+
}
|
|
182
|
+
const chatModId = await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
183
|
+
await PgAdvisoryLock.forChatModManagement(pgClient, {
|
|
184
|
+
experienceId: dbTargetExperience.id,
|
|
185
|
+
casinoId: dbTargetExperience.casino_id,
|
|
186
|
+
});
|
|
187
|
+
const dbExistingChatMod = await pgClient
|
|
188
|
+
.query({
|
|
189
|
+
text: `
|
|
190
|
+
SELECT id
|
|
191
|
+
FROM hub.chat_mod
|
|
192
|
+
WHERE experience_id = $1 AND user_id = $2 AND casino_id = $3
|
|
193
|
+
`,
|
|
194
|
+
values: [
|
|
195
|
+
dbTargetExperience.id,
|
|
196
|
+
dbTargetUser.id,
|
|
197
|
+
dbTargetExperience.casino_id,
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
.then(maybeOneRow);
|
|
201
|
+
if (dbExistingChatMod) {
|
|
202
|
+
return dbExistingChatMod.id;
|
|
203
|
+
}
|
|
204
|
+
const dbCreatedChatMod = await pgClient
|
|
205
|
+
.query(`
|
|
206
|
+
INSERT INTO hub.chat_mod (experience_id, user_id, casino_id)
|
|
207
|
+
VALUES ($1, $2, $3)
|
|
208
|
+
RETURNING id
|
|
209
|
+
`, [
|
|
210
|
+
dbTargetExperience.id,
|
|
211
|
+
dbTargetUser.id,
|
|
212
|
+
dbTargetExperience.casino_id,
|
|
213
|
+
])
|
|
214
|
+
.then(exactlyOneRow);
|
|
215
|
+
return dbCreatedChatMod.id;
|
|
216
|
+
});
|
|
217
|
+
return chatModId;
|
|
218
|
+
});
|
|
219
|
+
return object({
|
|
220
|
+
chatMod: chatModTable.get({ id: $chatModId }),
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
});
|
|
@@ -128,7 +128,7 @@ export const HubChatMuteUserPlugin = extendSchema((build) => {
|
|
|
128
128
|
throw new GraphQLError("Cannot mute yourself");
|
|
129
129
|
}
|
|
130
130
|
const muteId = await withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
131
|
-
await PgAdvisoryLock.
|
|
131
|
+
await PgAdvisoryLock.forChatPlayerAction(pgClient, {
|
|
132
132
|
userId: dbTargetUser.id,
|
|
133
133
|
experienceId: identity.session.experience_id,
|
|
134
134
|
casinoId: identity.session.casino_id,
|
|
@@ -79,7 +79,7 @@ export const HubChatUnmuteUserPlugin = extendSchema((build) => {
|
|
|
79
79
|
throw new GraphQLError("User not found");
|
|
80
80
|
}
|
|
81
81
|
return withPgPoolTransaction(superuserPool, async (pgClient) => {
|
|
82
|
-
await PgAdvisoryLock.
|
|
82
|
+
await PgAdvisoryLock.forChatPlayerAction(pgClient, {
|
|
83
83
|
userId: dbTargetUser.id,
|
|
84
84
|
experienceId: identity.session.experience_id,
|
|
85
85
|
casinoId: identity.session.casino_id,
|
|
@@ -32,6 +32,7 @@ import { HubChatUnmuteUserPlugin } from "../plugins/chat/hub-chat-unmute-user.js
|
|
|
32
32
|
import { HubChatMuteUserPlugin } from "../plugins/chat/hub-chat-mute-user.js";
|
|
33
33
|
import { HubChatCreateSystemMessagePlugin } from "../plugins/chat/hub-chat-create-system-message.js";
|
|
34
34
|
import { HubChatAfterIdConditionPlugin } from "../plugins/chat/hub-chat-after-id-condition.js";
|
|
35
|
+
import { HubChatModManagementPlugin } from "../plugins/chat/hub-chat-mod-management.js";
|
|
35
36
|
export const requiredPlugins = [
|
|
36
37
|
SmartTagsPlugin,
|
|
37
38
|
IdToNodeIdPlugin,
|
|
@@ -76,7 +77,7 @@ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, abo
|
|
|
76
77
|
mutablePlugins.push(HubCreatePlaygroundSessionPlugin);
|
|
77
78
|
}
|
|
78
79
|
if (enableChat) {
|
|
79
|
-
mutablePlugins.push(HubChatCreateUserMessagePlugin, HubChatCreateSystemMessagePlugin, HubChatSubscriptionPlugin, HubChatMuteUserPlugin, HubChatUnmuteUserPlugin, HubChatAfterIdConditionPlugin);
|
|
80
|
+
mutablePlugins.push(HubChatCreateUserMessagePlugin, HubChatCreateSystemMessagePlugin, HubChatSubscriptionPlugin, HubChatMuteUserPlugin, HubChatUnmuteUserPlugin, HubChatAfterIdConditionPlugin, HubChatModManagementPlugin);
|
|
80
81
|
}
|
|
81
82
|
const preset = {
|
|
82
83
|
extends: [PostGraphileAmberPreset],
|
package/dist/src/util.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assert } from "tsafe";
|
|
1
2
|
const UUID_REGEX = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
|
|
2
3
|
export function isUuid(input) {
|
|
3
4
|
return typeof input === "string" && UUID_REGEX.test(input);
|
|
@@ -6,8 +7,7 @@ export function extractFirstZodErrorMessage(e) {
|
|
|
6
7
|
return e.issues[0].message;
|
|
7
8
|
}
|
|
8
9
|
export function uuidEqual(a, b) {
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return uuid.toLowerCase();
|
|
10
|
+
assert(isUuid(a), `Expected valid uuid string: "${a}"`);
|
|
11
|
+
assert(isUuid(b), `Expected valid uuid string: "${b}"`);
|
|
12
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
13
13
|
}
|