@helyx/bot 0.4.24 → 0.4.25

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.25
4
+
5
+ ### Patch Changes
6
+
7
+ - Launch the shared gaming statistics foundation and the Achievements, Counting and Trivia module integrations, while removing retired Access Control and Counting storage through guarded migrations.
8
+
3
9
  ## 0.4.24
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -23,6 +23,12 @@ resume secure weighted-selection checkpoints without exposing the selection
23
23
  key to a module. Keep it stable across deployments; do not rotate it while a
24
24
  giveaway is drawing.
25
25
 
26
+ Trivia uses the operator-only `HELYX_TRIVIA_API_KEY`. Commercial deployments
27
+ need a paid The Trivia API subscription. Guess reuses saved Multiple Choice
28
+ questions and checks answers locally, so it does not require the provider's
29
+ Complete subscription. Local development may exercise Multiple Choice without
30
+ a key; production never enables the provider adapter without one.
31
+
26
32
  The always-available guild-only `/bug` command can be used by any guild member.
27
33
  Hosted deployments submit through the dedicated `HELYX_SUPPORT_SERVICE_SECRET`;
28
34
  paired self-hosted deployments use their authenticated Cloud Connect session
@@ -4,7 +4,7 @@ import { type CloudConnectorConsentRecord, type DatabasePool } from "@helyx/data
4
4
  import type { Client } from "discord.js";
5
5
  import type { Logger } from "pino";
6
6
  import type { ConfiguredModulePackage } from "./modules.js";
7
- import type { BotControlHandler } from "./control-server.js";
7
+ import { type BotControlHandler } from "./control-server.js";
8
8
  type ConnectorState = "disabled" | "unpaired" | "connecting" | "online" | "offline" | "recovery_required" | "revoked" | "stopped";
9
9
  export interface CloudConnectorStatus {
10
10
  state: ConnectorState;
@@ -80,5 +80,7 @@ export declare class CloudConnectorController implements CloudConnector {
80
80
  export declare class CloudConnectorUserError extends Error {
81
81
  constructor(message: string);
82
82
  }
83
+ export declare function mapControlError(error: unknown): "not_found" | "conflict" | "bad_request" | "forbidden" | "temporarily_unavailable" | "internal_error";
84
+ export declare function safeControlMessage(error: unknown): string;
83
85
  export {};
84
86
  //# sourceMappingURL=cloud-connector.d.ts.map
@@ -6,6 +6,7 @@ import { resolve } from "node:path";
6
6
  import WebSocket from "ws";
7
7
  import { z } from "zod";
8
8
  import { CloudControlDeniedError, CloudControlDispatcher, } from "./cloud-control-dispatcher.js";
9
+ import { BotControlTransportError, } from "./control-server.js";
9
10
  const enrolmentFileSchema = z
10
11
  .object({
11
12
  schemaVersion: z.literal(1),
@@ -786,7 +787,19 @@ function connectedMessage(input) {
786
787
  payload: input.payload,
787
788
  });
788
789
  }
789
- function mapControlError(error) {
790
+ export function mapControlError(error) {
791
+ if (error instanceof BotControlTransportError) {
792
+ if (error.status === 400)
793
+ return "bad_request";
794
+ if (error.status === 403)
795
+ return "forbidden";
796
+ if (error.status === 404)
797
+ return "not_found";
798
+ if (error.status === 409)
799
+ return "conflict";
800
+ if (error.status >= 502)
801
+ return "temporarily_unavailable";
802
+ }
790
803
  const name = error instanceof Error ? error.name : "";
791
804
  if (name.includes("NotFound"))
792
805
  return "not_found";
@@ -798,7 +811,7 @@ function mapControlError(error) {
798
811
  return "temporarily_unavailable";
799
812
  return "internal_error";
800
813
  }
801
- function safeControlMessage(error) {
814
+ export function safeControlMessage(error) {
802
815
  const code = mapControlError(error);
803
816
  if (code === "not_found")
804
817
  return "The requested Helyx resource was not found";
@@ -808,6 +821,8 @@ function safeControlMessage(error) {
808
821
  return error instanceof Error
809
822
  ? error.message.slice(0, 300)
810
823
  : "The requested change was not valid";
824
+ if (code === "forbidden")
825
+ return "You do not have permission to make this change";
811
826
  if (code === "temporarily_unavailable")
812
827
  return "The self-hosted bot is temporarily unavailable";
813
828
  return "The self-hosted bot could not complete the request";
package/dist/events.js CHANGED
@@ -14,6 +14,10 @@ export function installModuleEvents(client, modules, logger, services) {
14
14
  false,
15
15
  contribution,
16
16
  })));
17
+ const guildVoiceSessionContributions = modules.flatMap((module) => (module.events?.guildVoiceSession ?? []).map((contribution) => ({
18
+ moduleId: module.manifest.id,
19
+ contribution,
20
+ })));
17
21
  const messageClassificationRequested = modules.some((module) => module.requiredDiscordCapabilities?.includes("message-content"));
18
22
  const messageClassificationAvailable = messageClassificationRequested &&
19
23
  client.options.intents.has(GatewayIntentBits.MessageContent);
@@ -63,6 +67,7 @@ export function installModuleEvents(client, modules, logger, services) {
63
67
  })));
64
68
  if (privilegedMessageContributions.length === 0 &&
65
69
  guildMessageContributions.length === 0 &&
70
+ guildVoiceSessionContributions.length === 0 &&
66
71
  reactionContributions.length === 0 &&
67
72
  reactionRemoveContributions.length === 0 &&
68
73
  messageDeleteContributions.length === 0 &&
@@ -76,6 +81,7 @@ export function installModuleEvents(client, modules, logger, services) {
76
81
  guildEmojiUpdateContributions.length === 0)
77
82
  return { stop: () => Promise.resolve(), queueDepth: () => 0 };
78
83
  const queue = new BoundedGuildEventQueue(logger);
84
+ const voiceSessions = new Map();
79
85
  const privilegedEligibilityQueue = privilegedMessageContributions.length > 0
80
86
  ? new BoundedGuildEventQueue(logger)
81
87
  : null;
@@ -129,6 +135,13 @@ export function installModuleEvents(client, modules, logger, services) {
129
135
  guildMessageQueue.enqueue(message.guildId, () => handleGuildMessageCreate(message));
130
136
  });
131
137
  }
138
+ if (guildVoiceSessionContributions.length > 0) {
139
+ client.on(Events.VoiceStateUpdate, (before, after) => {
140
+ if ((after.member ?? before.member)?.user.bot)
141
+ return;
142
+ queue.enqueue(after.guild.id, () => handleVoiceState(before, after));
143
+ });
144
+ }
132
145
  if (reactionContributions.length > 0) {
133
146
  client.on(Events.MessageReactionAdd, (reaction, user, details) => {
134
147
  const guildId = reaction.message.guildId;
@@ -271,6 +284,8 @@ export function installModuleEvents(client, modules, logger, services) {
271
284
  channelId: message.channelId,
272
285
  ...(parentChannelId ? { parentChannelId } : {}),
273
286
  messageId: message.id,
287
+ isReply: message.reference?.messageId !== null &&
288
+ message.reference?.messageId !== undefined,
274
289
  authorId: message.author.id,
275
290
  authorDisplayName,
276
291
  authorRoleIds,
@@ -514,11 +529,64 @@ export function installModuleEvents(client, modules, logger, services) {
514
529
  }
515
530
  }
516
531
  }
532
+ async function handleVoiceState(before, after) {
533
+ const memberId = after.id;
534
+ const key = `${after.guild.id}:${memberId}`;
535
+ const previous = voiceSessions.get(key);
536
+ const channelId = eligibleVoiceChannel(after);
537
+ if (previous && previous.channelId !== channelId) {
538
+ voiceSessions.delete(key);
539
+ const endedAt = new Date();
540
+ const durationSeconds = Math.max(1, Math.floor((endedAt.getTime() - previous.startedAt.getTime()) / 1_000));
541
+ const installations = services.get(HELYX_SERVICE_NAMES.installations);
542
+ for (const { moduleId, contribution } of guildVoiceSessionContributions) {
543
+ try {
544
+ if (!(await installations.isModuleEnabled(previous.guildId, moduleId)))
545
+ continue;
546
+ await contribution.execute({
547
+ serverId: previous.guildId,
548
+ channelId: previous.channelId,
549
+ memberId,
550
+ startedAt: previous.startedAt,
551
+ endedAt,
552
+ durationSeconds,
553
+ services,
554
+ });
555
+ }
556
+ catch (error) {
557
+ logger.warn({
558
+ event: "module.guild_voice_session_failed",
559
+ moduleId,
560
+ guildId: previous.guildId,
561
+ error: safeError(error),
562
+ }, "Module guild voice session event failed");
563
+ }
564
+ }
565
+ }
566
+ if (channelId && (!previous || previous.channelId !== channelId)) {
567
+ voiceSessions.set(key, {
568
+ guildId: after.guild.id,
569
+ memberId,
570
+ channelId,
571
+ startedAt: new Date(),
572
+ });
573
+ }
574
+ }
575
+ function eligibleVoiceChannel(state) {
576
+ if (!state.channelId || !state.channel)
577
+ return null;
578
+ if (state.channel.type !== ChannelType.GuildVoice)
579
+ return null;
580
+ if (state.guild.afkChannelId === state.channelId)
581
+ return null;
582
+ return state.channelId;
583
+ }
517
584
  return {
518
585
  stop: async () => {
519
586
  await privilegedEligibilityQueue?.stop();
520
587
  await guildMessageQueue?.stop();
521
588
  await queue.stop();
589
+ voiceSessions.clear();
522
590
  },
523
591
  queueDepth: () => queue.queueDepth() +
524
592
  (privilegedEligibilityQueue?.queueDepth() ?? 0) +
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { parseBotEnvironment } from "@helyx/config";
2
2
  import { safeError, ServiceRegistry } from "@helyx/core";
3
3
  import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
4
4
  import { createRuntime } from "@helyx/framework";
5
- import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
5
+ import { createPlatformServices, gamingStatisticsRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
6
6
  import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
7
7
  import { Client, Events, GatewayIntentBits, Partials } from "discord.js";
8
8
  import pino from "pino";
@@ -292,11 +292,11 @@ async function startUnshardedBot(processEnvironment) {
292
292
  ]),
293
293
  }, bootstrapLogger, serviceAccess);
294
294
  const moduleEvents = installModuleEvents(client, publicModules, bootstrapLogger, serviceAccess);
295
- const controlService = new BotControlService(database, publicModules, guildResources, serviceAccess, privacyDatabase
295
+ const controlService = new BotControlService(database, [...publicModules, gamingStatisticsRecordOwner], guildResources, serviceAccess, privacyDatabase
296
296
  ? new PostgresPrivacyProviderExecutor({
297
297
  readPool: database,
298
298
  executorPool: privacyDatabase,
299
- modules: featureModules,
299
+ modules: [...featureModules, gamingStatisticsRecordOwner],
300
300
  })
301
301
  : undefined, environment.DISCORD_INTERNAL_GUILD_ID &&
302
302
  environment.HELYX_SUPPORT_STAFF_CHANNEL_ID
@@ -484,6 +484,9 @@ export function gatewayIntentsForModules(modules, messageContentEnabled = false)
484
484
  module.events?.guildMemberRemove?.length)) {
485
485
  intents.push(GatewayIntentBits.GuildMembers);
486
486
  }
487
+ if (modules.some((module) => module.events?.guildVoiceSession?.length)) {
488
+ intents.push(GatewayIntentBits.GuildVoiceStates);
489
+ }
487
490
  if (modules.some((module) => module.events?.guildEmojiDelete?.length ||
488
491
  module.events?.guildEmojiUpdate?.length)) {
489
492
  intents.push(GatewayIntentBits.GuildExpressions);
@@ -277,6 +277,20 @@ function addCommandOption(builder, option) {
277
277
  .setRequired(option.required ?? false));
278
278
  return;
279
279
  }
280
+ if (option.type === "integer") {
281
+ builder.addIntegerOption((optionBuilder) => {
282
+ optionBuilder
283
+ .setName(option.name)
284
+ .setDescription(option.description)
285
+ .setRequired(option.required ?? false);
286
+ if (option.minimum !== undefined)
287
+ optionBuilder.setMinValue(option.minimum);
288
+ if (option.maximum !== undefined)
289
+ optionBuilder.setMaxValue(option.maximum);
290
+ return optionBuilder;
291
+ });
292
+ return;
293
+ }
280
294
  if (option.type === "user") {
281
295
  builder.addUserOption((optionBuilder) => optionBuilder
282
296
  .setName(option.name)
@@ -399,6 +413,7 @@ function createCommandContext(interaction, services) {
399
413
  ...createContext(interaction, services),
400
414
  getStringOption: (name) => interaction.options.getString(name),
401
415
  getBooleanOption: (name) => interaction.options.getBoolean(name),
416
+ getIntegerOption: (name) => interaction.options.getInteger(name),
402
417
  getUserOption: (name) => {
403
418
  const user = interaction.options.getUser(name);
404
419
  return user
package/dist/modules.js CHANGED
@@ -100,6 +100,7 @@ export function validateContributionParity(definition) {
100
100
  const executableEvents = [
101
101
  ...(definition.events?.privilegedMessageCreate ?? []),
102
102
  ...(definition.events?.guildMessageCreate ?? []),
103
+ ...(definition.events?.guildVoiceSession ?? []),
103
104
  ...(definition.events?.messageReactionAdd ?? []),
104
105
  ...(definition.events?.messageReactionRemove ?? []),
105
106
  ...(definition.events?.messageDelete ?? []),
@@ -275,8 +275,10 @@ function encodeRecord(input) {
275
275
  result[field] = { type: "string", value };
276
276
  else if (typeof value === "boolean")
277
277
  result[field] = { type: "boolean", value };
278
- else
278
+ else if (typeof value === "number")
279
279
  result[field] = { type: "number", value };
280
+ else
281
+ throw new Error("Numeric write-behind values must be scalar");
280
282
  }
281
283
  return result;
282
284
  }
@@ -1,6 +1,6 @@
1
1
  import { safeError, ServiceRegistry } from "@helyx/core";
2
2
  import { checkDatabaseConnection, createDatabasePool, AuditRepository, InteractionSessionRepository, RateLimitRepository, runConfiguredModuleMigrations, runCoreMigrations, RuntimeStateRepository, } from "@helyx/database";
3
- import { createPlatformServices, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
3
+ import { createPlatformServices, gamingStatisticsRecordOwner, PostgresPrivacyProviderExecutor, } from "@helyx/platform";
4
4
  import { HELYX_SERVICE_NAMES } from "@helyx/sdk";
5
5
  import { Client, ShardingManager } from "discord.js";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -230,11 +230,11 @@ export async function startShardSupervisor(environment, processEnvironment, work
230
230
  const services = new Map(createPlatformServices(database, publicModules, platformServiceOptions(environment, publicModules)));
231
231
  if (numericWriteBehind)
232
232
  services.set(HELYX_SERVICE_NAMES.numericWriteBehind, numericWriteBehind);
233
- privacyService = new BotControlService(database, publicModules, unavailableResources, new ServiceRegistry(services), privacyDatabase
233
+ privacyService = new BotControlService(database, [...publicModules, gamingStatisticsRecordOwner], unavailableResources, new ServiceRegistry(services), privacyDatabase
234
234
  ? new PostgresPrivacyProviderExecutor({
235
235
  readPool: database,
236
236
  executorPool: privacyDatabase,
237
- modules: featureModules,
237
+ modules: [...featureModules, gamingStatisticsRecordOwner],
238
238
  })
239
239
  : undefined);
240
240
  cloudConnector?.setControlHandler(new ShardControlRouter({
@@ -41,6 +41,7 @@ export declare function parseShardWorkerEnvironment(processEnvironment: NodeJS.P
41
41
  HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS: number;
42
42
  HELYX_PRIVACY_ENABLED: boolean;
43
43
  HELYX_MESSAGE_CONTENT_ENABLED: boolean;
44
+ HELYX_TRIVIA_API_KEY: string | undefined;
44
45
  HELYX_CLOUD_CONNECT_ENABLED: boolean;
45
46
  HELYX_CLOUD_ENROLMENT_TOKEN: string | undefined;
46
47
  HELYX_CLOUD_ENROLMENT_FILE: string;
@@ -10,6 +10,7 @@ import pino from "pino";
10
10
  import { createCloudManagementModule } from "./cloud-management-module.js";
11
11
  import { BotControlService, DiscordGuildResourceDirectory, mapBotControlError, } from "./control-server.js";
12
12
  import { HttpVerificationService } from "./verification-service.js";
13
+ import { HttpTriviaQuestionProvider } from "./trivia-provider.js";
13
14
  import { DiscordMessageDeliveryService, DiscordMemberEnforcementService, DiscordMemberRoleService, DiscordVerificationMemberService, DiscordResourceInspection, installModuleEvents, } from "./events.js";
14
15
  import { installGuildTracking, } from "./guilds.js";
15
16
  import { installInteractions, } from "./interactions.js";
@@ -182,6 +183,14 @@ export async function startShardWorker(processEnvironment = process.env, identit
182
183
  }));
183
184
  platformServices.set(HELYX_SERVICE_NAMES.verificationMembers, new DiscordVerificationMemberService(client));
184
185
  }
186
+ if (environment.HELYX_TRIVIA_API_KEY ||
187
+ environment.NODE_ENV !== "production") {
188
+ platformServices.set(HELYX_SERVICE_NAMES.triviaQuestions, new HttpTriviaQuestionProvider({
189
+ ...(environment.HELYX_TRIVIA_API_KEY
190
+ ? { apiKey: environment.HELYX_TRIVIA_API_KEY }
191
+ : {}),
192
+ }));
193
+ }
185
194
  numericWriteBehind =
186
195
  identity.scope === "coordinated"
187
196
  ? new WorkerNumericWriteBehindProxy(identity)
@@ -0,0 +1,15 @@
1
+ import { type TriviaProviderFailureCode, type TriviaQuestion, type TriviaQuestionProvider } from "@helyx/sdk";
2
+ export interface HttpTriviaQuestionProviderOptions {
3
+ apiKey?: string;
4
+ fetchImplementation?: typeof fetch;
5
+ }
6
+ export declare class HttpTriviaQuestionProvider implements TriviaQuestionProvider {
7
+ #private;
8
+ constructor(options: HttpTriviaQuestionProviderOptions);
9
+ getQuestions(input: Parameters<TriviaQuestionProvider["getQuestions"]>[0]): Promise<readonly TriviaQuestion[]>;
10
+ health(): Promise<{
11
+ status: "healthy" | "degraded" | "unavailable";
12
+ safeCode?: TriviaProviderFailureCode;
13
+ }>;
14
+ }
15
+ //# sourceMappingURL=trivia-provider.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { TRIVIA_CATEGORIES, TriviaProviderError, } from "@helyx/sdk";
2
+ import { z } from "zod";
3
+ const BASE_URL = "https://the-trivia-api.com/v2";
4
+ const MAX_RESPONSE_BYTES = 256 * 1_024;
5
+ const REQUEST_TIMEOUT_MS = 8_000;
6
+ const questionSchema = z.object({
7
+ category: z.enum(TRIVIA_CATEGORIES),
8
+ id: z.string().regex(/^[A-Za-z0-9_-]{1,100}$/u),
9
+ tags: z.array(z.string().trim().min(1).max(100)).max(100),
10
+ difficulty: z.enum(["easy", "medium", "hard"]),
11
+ regions: z.array(z.string().trim().min(1).max(20)).max(100),
12
+ isNiche: z.boolean(),
13
+ question: z.object({ text: z.string().trim().min(1).max(2_000) }),
14
+ correctAnswer: z.string().trim().min(1).max(500),
15
+ incorrectAnswers: z.array(z.string().trim().min(1).max(500)).max(10),
16
+ type: z.literal("text_choice"),
17
+ });
18
+ export class HttpTriviaQuestionProvider {
19
+ #apiKey;
20
+ #fetch;
21
+ #lastFailure;
22
+ constructor(options) {
23
+ this.#apiKey = options.apiKey;
24
+ this.#fetch = options.fetchImplementation ?? fetch;
25
+ }
26
+ async getQuestions(input) {
27
+ if (!Number.isSafeInteger(input.limit) ||
28
+ input.limit < 1 ||
29
+ input.limit > 50)
30
+ this.#fail("invalid_response");
31
+ const query = new URLSearchParams({
32
+ limit: input.isNiche === undefined ? String(input.limit) : "50",
33
+ types: input.type,
34
+ contentFilter: input.familyContentOnly ? "family" : "all",
35
+ });
36
+ if (input.category)
37
+ query.set("categories", input.category);
38
+ if (input.difficulty)
39
+ query.set("difficulties", input.difficulty);
40
+ const raw = await this.#request(`/questions?${query.toString()}`, "GET");
41
+ const parsed = z.array(questionSchema).max(50).safeParse(raw);
42
+ if (!parsed.success) {
43
+ this.#fail("invalid_response");
44
+ }
45
+ const matching = parsed.data.filter((question) => question.type === input.type &&
46
+ (input.isNiche === undefined || question.isNiche === input.isNiche));
47
+ this.#lastFailure = undefined;
48
+ return matching.slice(0, input.limit);
49
+ }
50
+ health() {
51
+ return Promise.resolve(this.#lastFailure
52
+ ? { status: "degraded", safeCode: this.#lastFailure }
53
+ : { status: "healthy" });
54
+ }
55
+ async #request(path, method) {
56
+ let response;
57
+ try {
58
+ response = await this.#fetch(`${BASE_URL}${path}`, {
59
+ method,
60
+ headers: {
61
+ accept: "application/json",
62
+ ...(this.#apiKey ? { "x-api-key": this.#apiKey } : {}),
63
+ },
64
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
65
+ });
66
+ }
67
+ catch (error) {
68
+ this.#fail(error instanceof DOMException && error.name === "TimeoutError"
69
+ ? "timeout"
70
+ : "unavailable");
71
+ }
72
+ if (!response.ok) {
73
+ if (response.status === 429)
74
+ this.#fail("rate_limited");
75
+ if (response.status === 401)
76
+ this.#fail("authentication");
77
+ if (response.status === 403)
78
+ this.#fail("subscription");
79
+ this.#fail(response.status >= 500 ? "unavailable" : "invalid_response");
80
+ }
81
+ try {
82
+ return JSON.parse(await readBoundedResponse(response));
83
+ }
84
+ catch (error) {
85
+ if (error instanceof TriviaProviderError)
86
+ throw error;
87
+ this.#fail("invalid_response");
88
+ }
89
+ }
90
+ #fail(code) {
91
+ this.#lastFailure = code;
92
+ throw new TriviaProviderError(code);
93
+ }
94
+ }
95
+ async function readBoundedResponse(response) {
96
+ const declared = Number(response.headers.get("content-length"));
97
+ if (Number.isFinite(declared) && declared > MAX_RESPONSE_BYTES)
98
+ throw new TriviaProviderError("invalid_response");
99
+ if (!response.body)
100
+ return "";
101
+ const reader = response.body.getReader();
102
+ const chunks = [];
103
+ let total = 0;
104
+ try {
105
+ for (;;) {
106
+ const chunk = await reader.read();
107
+ if (chunk.done)
108
+ break;
109
+ const boundedValue = chunk.value;
110
+ if (!(boundedValue instanceof Uint8Array))
111
+ throw new TriviaProviderError("invalid_response");
112
+ total += boundedValue.byteLength;
113
+ if (total > MAX_RESPONSE_BYTES)
114
+ throw new TriviaProviderError("invalid_response");
115
+ chunks.push(boundedValue);
116
+ }
117
+ }
118
+ finally {
119
+ reader.releaseLock();
120
+ }
121
+ return Buffer.concat(chunks, total).toString("utf8");
122
+ }
123
+ //# sourceMappingURL=trivia-provider.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",