@notidotbot/noti-api-client 1.4.23 → 1.5.0
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/classes/games.d.ts +106 -1
- package/dist/classes/games.js +19 -0
- package/dist/other/zod/gameConfig.zod.d.ts +26 -0
- package/dist/other/zod/gameConfig.zod.js +15 -0
- package/dist/other/zod/schema.zod.d.ts +5 -0
- package/dist/other/zod/schema.zod.js +6 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/generated/ts-prisma.d.ts +417 -66
- package/prisma/schema/models/gameConfig.prisma +30 -0
- package/prisma/schema/schema.prisma +9 -0
package/dist/classes/games.d.ts
CHANGED
|
@@ -84,6 +84,55 @@ export interface ChangelogEntry {
|
|
|
84
84
|
publishedAt: string;
|
|
85
85
|
feedLabel: string;
|
|
86
86
|
}
|
|
87
|
+
export type RobloxSignal = 'update' | 'event';
|
|
88
|
+
export interface RobloxEntry {
|
|
89
|
+
id: string;
|
|
90
|
+
signal: RobloxSignal;
|
|
91
|
+
universeId: number;
|
|
92
|
+
/** canonical root place — the game URL is roblox.com/games/<placeId> */
|
|
93
|
+
placeId: number;
|
|
94
|
+
gameTitle: string;
|
|
95
|
+
/** update: "<name> was updated" · event: the event title */
|
|
96
|
+
title: string;
|
|
97
|
+
/** event description; null for updates (no notes text exists) */
|
|
98
|
+
description: string | null;
|
|
99
|
+
url: string;
|
|
100
|
+
thumbnail: string | null;
|
|
101
|
+
/** event window (null for updates) */
|
|
102
|
+
startsAt: string | null;
|
|
103
|
+
endsAt: string | null;
|
|
104
|
+
/** event status: 'active' | 'upcoming' | … (null for updates) */
|
|
105
|
+
status: string | null;
|
|
106
|
+
/** live players (update signal); null for events */
|
|
107
|
+
playing: number | null;
|
|
108
|
+
publishedAt: string;
|
|
109
|
+
}
|
|
110
|
+
/** A followable Roblox discovery card (popular grid + name search). `following` reflects THIS
|
|
111
|
+
* guild's follows — the bot recomputes gameApi's GLOBAL flag against the guild before returning. */
|
|
112
|
+
export interface RobloxPopularGame {
|
|
113
|
+
universeId: number;
|
|
114
|
+
placeId: number;
|
|
115
|
+
name: string;
|
|
116
|
+
playing: number | null;
|
|
117
|
+
thumbnail: string | null;
|
|
118
|
+
following: boolean;
|
|
119
|
+
}
|
|
120
|
+
/** Live preview of a pasted `roblox.com/games/<id>` URL or bare place id before following.
|
|
121
|
+
* `inWatchlist` is recomputed against THIS guild's follows (not the global gameApi set). */
|
|
122
|
+
export interface RobloxLookupResult {
|
|
123
|
+
placeId: number;
|
|
124
|
+
universeId: number;
|
|
125
|
+
rootPlaceId: number | null;
|
|
126
|
+
title: string | null;
|
|
127
|
+
playing: number | null;
|
|
128
|
+
thumbnail: string | null;
|
|
129
|
+
inWatchlist: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** Followable-card list — shared by the popular grid and the name search box. */
|
|
132
|
+
export interface RobloxDiscoverResponse {
|
|
133
|
+
items: RobloxPopularGame[];
|
|
134
|
+
total: number;
|
|
135
|
+
}
|
|
87
136
|
export interface DealsCatalogResponse {
|
|
88
137
|
items: DealItem[];
|
|
89
138
|
nextCursor: string | null;
|
|
@@ -188,6 +237,20 @@ export interface ChangelogGame {
|
|
|
188
237
|
channelId?: string | null;
|
|
189
238
|
pingRoleId?: string | null;
|
|
190
239
|
}
|
|
240
|
+
export type RobloxKind = 'updates' | 'events' | 'both';
|
|
241
|
+
/** A followed Roblox experience. Keyed by canonical root `placeId` (paste a URL or id); ids are
|
|
242
|
+
* strings because Roblox ids overflow a 32-bit number column. `universeId` is resolved by the gameApi. */
|
|
243
|
+
export interface RobloxGame {
|
|
244
|
+
dbId?: string;
|
|
245
|
+
placeId: string;
|
|
246
|
+
universeId?: string | null;
|
|
247
|
+
title?: string | null;
|
|
248
|
+
/** Which signal(s) to notify on. Default `both`. */
|
|
249
|
+
kinds?: RobloxKind;
|
|
250
|
+
/** Optional per-game routing override (else uses the guild's default Roblox channel/role). */
|
|
251
|
+
channelId?: string | null;
|
|
252
|
+
pingRoleId?: string | null;
|
|
253
|
+
}
|
|
191
254
|
export interface GameConfig {
|
|
192
255
|
guildId?: string;
|
|
193
256
|
enabled: boolean;
|
|
@@ -217,9 +280,12 @@ export interface GameConfig {
|
|
|
217
280
|
allTimeLowMaxPriceCents: number | null;
|
|
218
281
|
changelogChannelId: string | null;
|
|
219
282
|
changelogPingRoleId: string | null;
|
|
283
|
+
robloxChannelId: string | null;
|
|
284
|
+
robloxPingRoleId: string | null;
|
|
220
285
|
categoryRoutes: GameCategoryRoute[];
|
|
221
286
|
trackedGames: TrackedGame[];
|
|
222
287
|
changelogGames: ChangelogGame[];
|
|
288
|
+
robloxGames: RobloxGame[];
|
|
223
289
|
}
|
|
224
290
|
/** Resolved premium entitlements + limits for the guild (gating is visual, never hidden). */
|
|
225
291
|
export interface GameEntitlements {
|
|
@@ -228,6 +294,7 @@ export interface GameEntitlements {
|
|
|
228
294
|
allTimeLow: boolean;
|
|
229
295
|
customWebhooks: boolean;
|
|
230
296
|
changelogs: boolean;
|
|
297
|
+
roblox: boolean;
|
|
231
298
|
trackedGameLimit: number;
|
|
232
299
|
}
|
|
233
300
|
export interface GameConfigResponse {
|
|
@@ -274,8 +341,14 @@ export type GamesWsEvent = {
|
|
|
274
341
|
type: 'changelog.new';
|
|
275
342
|
at: string;
|
|
276
343
|
changelog: ChangelogEntry;
|
|
344
|
+
} | {
|
|
345
|
+
op: 'event';
|
|
346
|
+
type: GameRobloxEventType;
|
|
347
|
+
at: string;
|
|
348
|
+
roblox: RobloxEntry;
|
|
277
349
|
};
|
|
278
350
|
export type GameDealEventType = 'deal.new' | 'deal.updated' | 'deal.expired' | 'deal.enriched' | 'free.new' | 'free.upcoming' | 'free.expired' | 'free.weekend' | 'deal.lowest';
|
|
351
|
+
export type GameRobloxEventType = 'roblox.update' | 'roblox.event';
|
|
279
352
|
export declare class APIGames {
|
|
280
353
|
private web;
|
|
281
354
|
constructor(web: WebDataManager);
|
|
@@ -287,6 +360,11 @@ export declare class APIGames {
|
|
|
287
360
|
updateChangelogGames({ auth, guildId, games }: GamesFunctionsInput['updateChangelogGames']): Promise<WebResponse<{
|
|
288
361
|
games: ChangelogGame[];
|
|
289
362
|
}>>;
|
|
363
|
+
/** Replace the guild's followed Roblox experiences (replace-semantics). `placeId` accepts a game
|
|
364
|
+
* URL, a bare id, or a numeric string — the bot extracts the place id. Capped at 500. */
|
|
365
|
+
updateRobloxGames({ auth, guildId, games }: GamesFunctionsInput['updateRobloxGames']): Promise<WebResponse<{
|
|
366
|
+
games: RobloxGame[];
|
|
367
|
+
}>>;
|
|
290
368
|
updateCategoryRoutes({ auth, guildId, routes }: GamesFunctionsInput['updateCategoryRoutes']): Promise<WebResponse<{
|
|
291
369
|
routes: GameCategoryRoute[];
|
|
292
370
|
}>>;
|
|
@@ -301,6 +379,13 @@ export declare class APIGames {
|
|
|
301
379
|
getHealth({ auth, guildId }: GamesFunctionsInput['getHealth']): Promise<WebResponse<GameHealthBundle>>;
|
|
302
380
|
/** Full Steam catalog search by name (proxied from the gameApi) — find ANY Steam game, not just catalog deals. */
|
|
303
381
|
searchSteam({ auth, guildId, q, limit }: GamesFunctionsInput['searchSteam']): Promise<WebResponse<SteamSearchResponse>>;
|
|
382
|
+
/** Discover grid — top ~100 Roblox games by players. `following` reflects THIS guild's follows. */
|
|
383
|
+
getRobloxPopular({ auth, guildId }: GamesFunctionsInput['getRobloxPopular']): Promise<WebResponse<RobloxDiscoverResponse>>;
|
|
384
|
+
/** Search Roblox games by name (live proxy). Same followable-card shape as the popular grid. */
|
|
385
|
+
searchRoblox({ auth, guildId, q, limit }: GamesFunctionsInput['searchRoblox']): Promise<WebResponse<RobloxDiscoverResponse>>;
|
|
386
|
+
/** Preview a pasted `roblox.com/games/<id>` URL or bare place id before following — resolves the
|
|
387
|
+
* title/thumbnail/players. `placeId` accepts a URL or id; the bot extracts the numeric id. */
|
|
388
|
+
lookupRoblox({ auth, guildId, placeId }: GamesFunctionsInput['lookupRoblox']): Promise<WebResponse<RobloxLookupResult>>;
|
|
304
389
|
}
|
|
305
390
|
export type GameDealsQuery = {
|
|
306
391
|
kind?: string;
|
|
@@ -331,7 +416,7 @@ export type GamesFunctionsInput = {
|
|
|
331
416
|
updateConfig: {
|
|
332
417
|
auth: string;
|
|
333
418
|
guildId: string;
|
|
334
|
-
} & Partial<Omit<GameConfig, 'guildId' | 'categoryRoutes' | 'trackedGames' | 'changelogGames'>>;
|
|
419
|
+
} & Partial<Omit<GameConfig, 'guildId' | 'categoryRoutes' | 'trackedGames' | 'changelogGames' | 'robloxGames'>>;
|
|
335
420
|
updateTrackedGames: {
|
|
336
421
|
auth: string;
|
|
337
422
|
guildId: string;
|
|
@@ -342,6 +427,11 @@ export type GamesFunctionsInput = {
|
|
|
342
427
|
guildId: string;
|
|
343
428
|
games: ChangelogGame[];
|
|
344
429
|
};
|
|
430
|
+
updateRobloxGames: {
|
|
431
|
+
auth: string;
|
|
432
|
+
guildId: string;
|
|
433
|
+
games: RobloxGame[];
|
|
434
|
+
};
|
|
345
435
|
updateCategoryRoutes: {
|
|
346
436
|
auth: string;
|
|
347
437
|
guildId: string;
|
|
@@ -394,6 +484,21 @@ export type GamesFunctionsInput = {
|
|
|
394
484
|
q: string;
|
|
395
485
|
limit?: number;
|
|
396
486
|
};
|
|
487
|
+
getRobloxPopular: {
|
|
488
|
+
auth: string;
|
|
489
|
+
guildId: string;
|
|
490
|
+
};
|
|
491
|
+
searchRoblox: {
|
|
492
|
+
auth: string;
|
|
493
|
+
guildId: string;
|
|
494
|
+
q: string;
|
|
495
|
+
limit?: number;
|
|
496
|
+
};
|
|
497
|
+
lookupRoblox: {
|
|
498
|
+
auth: string;
|
|
499
|
+
guildId: string;
|
|
500
|
+
placeId: string | number;
|
|
501
|
+
};
|
|
397
502
|
};
|
|
398
503
|
export type GamesReturnTypes = {
|
|
399
504
|
getConfigRaw: Awaited<ReturnType<APIGames['getConfig']>>;
|
package/dist/classes/games.js
CHANGED
|
@@ -20,6 +20,11 @@ class APIGames {
|
|
|
20
20
|
async updateChangelogGames({ auth, guildId, games }) {
|
|
21
21
|
return this.web.request({ method: 'PUT', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/changelogs`), body: { games } });
|
|
22
22
|
}
|
|
23
|
+
/** Replace the guild's followed Roblox experiences (replace-semantics). `placeId` accepts a game
|
|
24
|
+
* URL, a bare id, or a numeric string — the bot extracts the place id. Capped at 500. */
|
|
25
|
+
async updateRobloxGames({ auth, guildId, games }) {
|
|
26
|
+
return this.web.request({ method: 'PUT', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/roblox`), body: { games } });
|
|
27
|
+
}
|
|
23
28
|
async updateCategoryRoutes({ auth, guildId, routes }) {
|
|
24
29
|
return this.web.request({ method: 'PUT', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/routes`), body: { routes } });
|
|
25
30
|
}
|
|
@@ -53,5 +58,19 @@ class APIGames {
|
|
|
53
58
|
async searchSteam({ auth, guildId, q, limit }) {
|
|
54
59
|
return this.web.request({ method: 'GET', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/steam/search`, { q, limit }) });
|
|
55
60
|
}
|
|
61
|
+
// ── Roblox discovery (three ways to find a followable game) ───────────────
|
|
62
|
+
/** Discover grid — top ~100 Roblox games by players. `following` reflects THIS guild's follows. */
|
|
63
|
+
async getRobloxPopular({ auth, guildId }) {
|
|
64
|
+
return this.web.request({ method: 'GET', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/roblox/popular`) });
|
|
65
|
+
}
|
|
66
|
+
/** Search Roblox games by name (live proxy). Same followable-card shape as the popular grid. */
|
|
67
|
+
async searchRoblox({ auth, guildId, q, limit }) {
|
|
68
|
+
return this.web.request({ method: 'GET', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/roblox/search`, { q, limit }) });
|
|
69
|
+
}
|
|
70
|
+
/** Preview a pasted `roblox.com/games/<id>` URL or bare place id before following — resolves the
|
|
71
|
+
* title/thumbnail/players. `placeId` accepts a URL or id; the bot extracts the numeric id. */
|
|
72
|
+
async lookupRoblox({ auth, guildId, placeId }) {
|
|
73
|
+
return this.web.request({ method: 'GET', auth, endpoint: this.web.qp(`/data/guild/${guildId}/games/roblox/lookup`, { placeId }) });
|
|
74
|
+
}
|
|
56
75
|
}
|
|
57
76
|
exports.APIGames = APIGames;
|
|
@@ -67,6 +67,8 @@ export declare const GameConfigZod: {
|
|
|
67
67
|
allTimeLowMaxPriceCents: z.ZodNullable<z.ZodNumber>;
|
|
68
68
|
changelogChannelId: z.ZodNullable<z.ZodString>;
|
|
69
69
|
changelogPingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
70
|
+
robloxChannelId: z.ZodNullable<z.ZodString>;
|
|
71
|
+
robloxPingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
70
72
|
categoryRoutes: z.ZodArray<z.ZodLazy<z.ZodObject<{
|
|
71
73
|
kinds: z.ZodArray<z.ZodEnum<{
|
|
72
74
|
free_game: "free_game";
|
|
@@ -118,6 +120,18 @@ export declare const GameConfigZod: {
|
|
|
118
120
|
channelId: z.ZodNullable<z.ZodString>;
|
|
119
121
|
pingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
120
122
|
}, z.core.$strip>>>;
|
|
123
|
+
robloxGames: z.ZodArray<z.ZodLazy<z.ZodObject<{
|
|
124
|
+
placeId: z.ZodString;
|
|
125
|
+
universeId: z.ZodNullable<z.ZodString>;
|
|
126
|
+
title: z.ZodNullable<z.ZodString>;
|
|
127
|
+
kinds: z.ZodEnum<{
|
|
128
|
+
updates: "updates";
|
|
129
|
+
events: "events";
|
|
130
|
+
both: "both";
|
|
131
|
+
}>;
|
|
132
|
+
channelId: z.ZodNullable<z.ZodString>;
|
|
133
|
+
pingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
134
|
+
}, z.core.$strip>>>;
|
|
121
135
|
}, z.core.$strip>;
|
|
122
136
|
readonly GuildGameCategoryRouteSchema: z.ZodObject<{
|
|
123
137
|
kinds: z.ZodArray<z.ZodEnum<{
|
|
@@ -170,5 +184,17 @@ export declare const GameConfigZod: {
|
|
|
170
184
|
channelId: z.ZodNullable<z.ZodString>;
|
|
171
185
|
pingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
172
186
|
}, z.core.$strip>;
|
|
187
|
+
readonly GuildRobloxGameSchema: z.ZodObject<{
|
|
188
|
+
placeId: z.ZodString;
|
|
189
|
+
universeId: z.ZodNullable<z.ZodString>;
|
|
190
|
+
title: z.ZodNullable<z.ZodString>;
|
|
191
|
+
kinds: z.ZodEnum<{
|
|
192
|
+
updates: "updates";
|
|
193
|
+
events: "events";
|
|
194
|
+
both: "both";
|
|
195
|
+
}>;
|
|
196
|
+
channelId: z.ZodNullable<z.ZodString>;
|
|
197
|
+
pingRoleId: z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"everyone">, z.ZodLiteral<"here">]>>;
|
|
198
|
+
}, z.core.$strip>;
|
|
173
199
|
};
|
|
174
200
|
export declare const GameConfigZodPartial: PartialZodObject<typeof GameConfigZod>;
|
|
@@ -42,6 +42,16 @@ const GuildGameChangelogSchema = zod_2.z.object({
|
|
|
42
42
|
channelId: zod_1.SnowFlake.nullable(),
|
|
43
43
|
pingRoleId: zod_1.SnowFlakeOrEveryone.nullable(),
|
|
44
44
|
});
|
|
45
|
+
// A Roblox experience the guild follows (updates and/or events). Keyed by canonical root placeId;
|
|
46
|
+
// ids are strings because Roblox ids overflow a 32-bit Int. `universeId` is resolved by the gameApi.
|
|
47
|
+
const GuildRobloxGameSchema = zod_2.z.object({
|
|
48
|
+
placeId: zod_2.z.string(),
|
|
49
|
+
universeId: zod_2.z.string().nullable(),
|
|
50
|
+
title: zod_2.z.string().max(200).nullable(),
|
|
51
|
+
kinds: schema_zod_1.GameRobloxKindEnum,
|
|
52
|
+
channelId: zod_1.SnowFlake.nullable(),
|
|
53
|
+
pingRoleId: zod_1.SnowFlakeOrEveryone.nullable(),
|
|
54
|
+
});
|
|
45
55
|
const GuildGameConfigSchema = zod_2.z.object({
|
|
46
56
|
guildId: zod_1.SnowFlake,
|
|
47
57
|
enabled: zod_2.z.boolean(),
|
|
@@ -79,14 +89,19 @@ const GuildGameConfigSchema = zod_2.z.object({
|
|
|
79
89
|
// Game changelogs routing (PatchBot-style).
|
|
80
90
|
changelogChannelId: zod_1.SnowFlake.nullable(),
|
|
81
91
|
changelogPingRoleId: zod_1.SnowFlakeOrEveryone.nullable(),
|
|
92
|
+
// Roblox experience tracking routing.
|
|
93
|
+
robloxChannelId: zod_1.SnowFlake.nullable(),
|
|
94
|
+
robloxPingRoleId: zod_1.SnowFlakeOrEveryone.nullable(),
|
|
82
95
|
categoryRoutes: (0, zod_1.NoDuplicate)(zod_2.z.array(zod_2.z.lazy(() => GuildGameCategoryRouteSchema)), ['priority']),
|
|
83
96
|
trackedGames: zod_2.z.array(zod_2.z.lazy(() => GuildTrackedGameSchema)),
|
|
84
97
|
changelogGames: (0, zod_1.NoDuplicate)(zod_2.z.array(zod_2.z.lazy(() => GuildGameChangelogSchema)), ['steamAppId']),
|
|
98
|
+
robloxGames: (0, zod_1.NoDuplicate)(zod_2.z.array(zod_2.z.lazy(() => GuildRobloxGameSchema)), ['placeId']),
|
|
85
99
|
});
|
|
86
100
|
exports.GameConfigZod = {
|
|
87
101
|
GuildGameConfigSchema,
|
|
88
102
|
GuildGameCategoryRouteSchema,
|
|
89
103
|
GuildTrackedGameSchema,
|
|
90
104
|
GuildGameChangelogSchema,
|
|
105
|
+
GuildRobloxGameSchema,
|
|
91
106
|
};
|
|
92
107
|
exports.GameConfigZodPartial = Object.fromEntries(Object.entries(exports.GameConfigZod).map(([key, value]) => [key, (0, schema_zod_1.deepPartialify)(value)]));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GameDeliveryEnum = exports.GameFlagEnum = exports.GameKindEnum = exports.GameStoreEnum = exports.UserNotificationSeverityEnum = exports.RoleBonusEnum = exports.MediaShareCommandsEnum = exports.MediaShareTypesEnum = exports.FilterEnum = exports.QueryInEnum = exports.RumbleTypeEnum = exports.PlatformsWithVideosEnum = exports.UserMediaSharePermissionsEnum = exports.ClipperCategoryEnum = exports.PlatformEnum = exports.OverridesEnum = exports.GuildSingleMessageTypeEnum = exports.GuildMessageTypeEnum = exports.StreamerMessageTypeEnum = exports.SingleMessageTypeEnum = exports.LeaderBoardTypesEnum = void 0;
|
|
3
|
+
exports.GameRobloxKindEnum = exports.GameDeliveryEnum = exports.GameFlagEnum = exports.GameKindEnum = exports.GameStoreEnum = exports.UserNotificationSeverityEnum = exports.RoleBonusEnum = exports.MediaShareCommandsEnum = exports.MediaShareTypesEnum = exports.FilterEnum = exports.QueryInEnum = exports.RumbleTypeEnum = exports.PlatformsWithVideosEnum = exports.UserMediaSharePermissionsEnum = exports.ClipperCategoryEnum = exports.PlatformEnum = exports.OverridesEnum = exports.GuildSingleMessageTypeEnum = exports.GuildMessageTypeEnum = exports.StreamerMessageTypeEnum = exports.SingleMessageTypeEnum = exports.LeaderBoardTypesEnum = void 0;
|
|
4
4
|
exports.deepPartialify = deepPartialify;
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
function deepPartialify(schema) {
|
|
@@ -214,3 +214,8 @@ exports.GameDeliveryEnum = zod_1.z.enum([
|
|
|
214
214
|
'daily',
|
|
215
215
|
'weekly',
|
|
216
216
|
]);
|
|
217
|
+
exports.GameRobloxKindEnum = zod_1.z.enum([
|
|
218
|
+
'updates',
|
|
219
|
+
'events',
|
|
220
|
+
'both',
|
|
221
|
+
]);
|