@helyx/bot 0.5.0 → 0.6.2
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 +57 -0
- package/README.md +4 -4
- package/dist/cloud-management-module.js +1 -5
- package/dist/command-registration-policy.js +1 -0
- package/dist/control-server.d.ts +1 -33
- package/dist/control-server.js +23 -41
- package/dist/dashboard-commands.d.ts +1 -0
- package/dist/dashboard-commands.js +1 -0
- package/dist/dashboard-module-projection.d.ts +39 -0
- package/dist/dashboard-module-projection.js +28 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +29 -20
- package/dist/guilds.js +1 -4
- package/dist/index.js +38 -5
- package/dist/interactions.d.ts +1 -1
- package/dist/interactions.js +41 -34
- package/dist/module-logging.js +1 -4
- package/dist/module-status-control.js +1 -1
- package/dist/premium-access.d.ts +7 -0
- package/dist/premium-access.js +38 -0
- package/dist/premium-activation-module.d.ts +20 -0
- package/dist/premium-activation-module.js +542 -0
- package/dist/premium-catalogue-enforcement.d.ts +5 -0
- package/dist/premium-catalogue-enforcement.js +29 -0
- package/dist/premium-guild-authority.d.ts +12 -0
- package/dist/premium-guild-authority.js +25 -0
- package/dist/scheduled-task-runner.js +14 -1
- package/dist/shard-supervisor.js +30 -1
- package/dist/shard-worker.d.ts +2 -0
- package/dist/shard-worker.js +25 -1
- package/documentation/examples/.env.example +2 -2
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseBotEnvironment } from "@helyx/config";
|
|
2
2
|
import { StatusHeartbeatReporter } from "@helyx/status-client";
|
|
3
3
|
import { safeError, ServiceRegistry } from "@helyx/core";
|
|
4
|
-
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
4
|
+
import { checkDatabaseConnection, createDatabasePool, AuditRepository, GuildCommandRepository, InteractionSessionRepository, PremiumAllocationRepository, RateLimitRepository, runCoreMigrations, runConfiguredModuleMigrations, } from "@helyx/database";
|
|
5
5
|
import { createRuntime } from "@helyx/framework";
|
|
6
6
|
import { createPlatformServices, gamingStatisticsRecordOwner, achievementContributionRecordOwner, PostgresPrivacyProviderExecutor, PlatformMemberRoleGrantOwnershipService, PostgresMemberRoleGrantOwnershipStore, } from "@helyx/platform";
|
|
7
7
|
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
@@ -30,6 +30,9 @@ import { shardHealthSnapshotSchema, } from "./shard-contracts.js";
|
|
|
30
30
|
import { createSupportModule } from "./support-module.js";
|
|
31
31
|
import { createOAuthModule } from "./oauth-module.js";
|
|
32
32
|
import { createPrivacyModule } from "./privacy-module.js";
|
|
33
|
+
import { REVIEWED_PREMIUM_MODULE_IDS } from "@helyx/module-manifest";
|
|
34
|
+
import { createPremiumActivationModule } from "./premium-activation-module.js";
|
|
35
|
+
import { DiscordPremiumGuildAuthority } from "./premium-guild-authority.js";
|
|
33
36
|
import { HttpOAuthService } from "./oauth-service.js";
|
|
34
37
|
import { DiscordOwnedMessageDeletionService } from "./discord-owned-message-service.js";
|
|
35
38
|
import { DiscordGuildHealthInspection } from "./discord-guild-health.js";
|
|
@@ -45,6 +48,8 @@ import { createLocalScheduledTaskRoute, createScheduledTaskRunner, schedulerOper
|
|
|
45
48
|
import { createLocalNumericWriteBehind } from "./numeric-write-behind.js";
|
|
46
49
|
import { discordRestOptions } from "./discord-rest.js";
|
|
47
50
|
import { discordBotStatusService } from "./status-producer.js";
|
|
51
|
+
import { assertReviewedPremiumCatalogue, requirePremiumProviderEnvironment, } from "./premium-access.js";
|
|
52
|
+
import { enforcePremiumCatalogue, PREMIUM_CATALOGUE_ENFORCEMENT_INTERVAL_MS, } from "./premium-catalogue-enforcement.js";
|
|
48
53
|
export { loadConfiguredModulePackages } from "./modules.js";
|
|
49
54
|
export { resolveModulePackageSpecifiers } from "./module-configuration.js";
|
|
50
55
|
export * from "./control-server.js";
|
|
@@ -83,9 +88,7 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
83
88
|
},
|
|
84
89
|
{
|
|
85
90
|
key: "creator-connections",
|
|
86
|
-
state:
|
|
87
|
-
? "operational"
|
|
88
|
-
: "major_outage",
|
|
91
|
+
state: "operational",
|
|
89
92
|
reasonCode: environment.HELYX_OAUTH_ENABLED
|
|
90
93
|
? null
|
|
91
94
|
: "feature_disabled",
|
|
@@ -95,7 +98,7 @@ export async function startBot(processEnvironment = process.env) {
|
|
|
95
98
|
];
|
|
96
99
|
},
|
|
97
100
|
onError(error) {
|
|
98
|
-
bootstrapLogger.warn({ event: "status.heartbeat_failed",
|
|
101
|
+
bootstrapLogger.warn({ event: "status.heartbeat_failed", error: safeError(error) }, "Status heartbeat could not be delivered");
|
|
99
102
|
},
|
|
100
103
|
});
|
|
101
104
|
reporter.start();
|
|
@@ -128,6 +131,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
128
131
|
: undefined;
|
|
129
132
|
let databaseClosed = false;
|
|
130
133
|
let retentionCleanupTimer;
|
|
134
|
+
let premiumCatalogueTimer;
|
|
131
135
|
async function closeDatabase() {
|
|
132
136
|
if (databaseClosed)
|
|
133
137
|
return;
|
|
@@ -198,6 +202,15 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
198
202
|
})),
|
|
199
203
|
}, "Module databases ready");
|
|
200
204
|
const featureModules = modulePackages.map(({ definition }) => definition);
|
|
205
|
+
assertReviewedPremiumCatalogue(featureModules);
|
|
206
|
+
await enforcePremiumCatalogue(database, bootstrapLogger);
|
|
207
|
+
premiumCatalogueTimer = setInterval(() => void enforcePremiumCatalogue(database, bootstrapLogger).catch((error) => {
|
|
208
|
+
bootstrapLogger.warn({
|
|
209
|
+
event: "premium.catalogue_enforcement_failed",
|
|
210
|
+
error: safeError(error),
|
|
211
|
+
}, "Premium module catalogue enforcement failed");
|
|
212
|
+
}), PREMIUM_CATALOGUE_ENFORCEMENT_INTERVAL_MS);
|
|
213
|
+
premiumCatalogueTimer.unref();
|
|
201
214
|
const client = new Client({
|
|
202
215
|
rest: discordRestOptions,
|
|
203
216
|
intents: gatewayIntentsForModules(featureModules, environment.HELYX_MESSAGE_CONTENT_ENABLED, environment.HELYX_GUILD_MEMBERS_ENABLED),
|
|
@@ -224,6 +237,16 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
224
237
|
: undefined;
|
|
225
238
|
const supportModule = createSupportModule(cloudConnector);
|
|
226
239
|
const privacyModule = createPrivacyModule();
|
|
240
|
+
const premiumEnvironment = requirePremiumProviderEnvironment(featureModules, environment.STRIPE_ENVIRONMENT);
|
|
241
|
+
const premiumActivationModule = !premiumEnvironment
|
|
242
|
+
? undefined
|
|
243
|
+
: createPremiumActivationModule({
|
|
244
|
+
allocations: new PremiumAllocationRepository(database),
|
|
245
|
+
authority: new DiscordPremiumGuildAuthority(client),
|
|
246
|
+
provider: "stripe",
|
|
247
|
+
providerEnvironment: premiumEnvironment,
|
|
248
|
+
reviewedPremiumModuleIds: REVIEWED_PREMIUM_MODULE_IDS,
|
|
249
|
+
});
|
|
227
250
|
const oauthModule = environment.HELYX_OAUTH_ENABLED
|
|
228
251
|
? createOAuthModule()
|
|
229
252
|
: undefined;
|
|
@@ -232,6 +255,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
232
255
|
permissionsModule,
|
|
233
256
|
supportModule,
|
|
234
257
|
privacyModule,
|
|
258
|
+
...(premiumActivationModule ? [premiumActivationModule] : []),
|
|
235
259
|
...(oauthModule ? [oauthModule] : []),
|
|
236
260
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
237
261
|
...featureModules,
|
|
@@ -305,6 +329,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
305
329
|
permissionsModule,
|
|
306
330
|
supportModule,
|
|
307
331
|
privacyModule,
|
|
332
|
+
...(premiumActivationModule ? [premiumActivationModule] : []),
|
|
308
333
|
...(oauthModule ? [oauthModule] : []),
|
|
309
334
|
...(cloudManagementModule ? [cloudManagementModule] : []),
|
|
310
335
|
].map(({ manifest }) => manifest),
|
|
@@ -372,6 +397,7 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
372
397
|
managementModule.manifest.id,
|
|
373
398
|
permissionsModule.manifest.id,
|
|
374
399
|
supportModule.manifest.id,
|
|
400
|
+
...(premiumActivationModule ? [premiumActivationModule.manifest.id] : []),
|
|
375
401
|
...(oauthModule ? [oauthModule.manifest.id] : []),
|
|
376
402
|
...(cloudManagementModule ? [cloudManagementModule.manifest.id] : []),
|
|
377
403
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
@@ -386,6 +412,9 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
386
412
|
prepare: prepareGuilds,
|
|
387
413
|
selfAuthorizingGuildModuleIds: new Set([
|
|
388
414
|
supportModule.manifest.id,
|
|
415
|
+
...(premiumActivationModule
|
|
416
|
+
? [premiumActivationModule.manifest.id]
|
|
417
|
+
: []),
|
|
389
418
|
...(oauthModule ? [oauthModule.manifest.id] : []),
|
|
390
419
|
...(operationsModule ? [operationsModule.manifest.id] : []),
|
|
391
420
|
]),
|
|
@@ -424,6 +453,8 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
424
453
|
process.off("SIGTERM", onSigterm);
|
|
425
454
|
if (retentionCleanupTimer)
|
|
426
455
|
clearInterval(retentionCleanupTimer);
|
|
456
|
+
if (premiumCatalogueTimer)
|
|
457
|
+
clearInterval(premiumCatalogueTimer);
|
|
427
458
|
await scheduledTasks?.stop().catch((error) => {
|
|
428
459
|
process.exitCode = 1;
|
|
429
460
|
bootstrapLogger.error({ event: "scheduler.stop_failed", error: safeError(error) }, "Scheduled task runner shutdown failed");
|
|
@@ -587,6 +618,8 @@ async function startUnshardedBot(processEnvironment) {
|
|
|
587
618
|
catch (error) {
|
|
588
619
|
if (retentionCleanupTimer)
|
|
589
620
|
clearInterval(retentionCleanupTimer);
|
|
621
|
+
if (premiumCatalogueTimer)
|
|
622
|
+
clearInterval(premiumCatalogueTimer);
|
|
590
623
|
await closeDatabase().catch((closeError) => {
|
|
591
624
|
bootstrapLogger.error({ event: "database.close_failed", error: safeError(closeError) }, "Database pool cleanup failed");
|
|
592
625
|
});
|
package/dist/interactions.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export declare function sendToChannel(client: Client, channelId: string, respons
|
|
|
39
39
|
export declare function deleteChannelMessage(client: Client, channelId: string, messageId: string): Promise<void>;
|
|
40
40
|
export declare function editChannelMessage(client: Client, channelId: string, messageId: string, response: MessageResponse): Promise<void>;
|
|
41
41
|
export declare function addChannelMessageReaction(client: Client, channelId: string, messageId: string, emojiIdentifier: string): Promise<void>;
|
|
42
|
-
export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean, requiredModuleName?: string, installationIndependentCommand?: boolean, authorization?: CommandAuthorization): Promise<boolean>;
|
|
42
|
+
export declare function authorizeCommand(interaction: ChatInputCommandInteraction, moduleId: string, commandId: string, rateLimit: CommandContribution["rateLimit"], services: ServiceAccess, selfAuthorizingGuildCommand: boolean, resourceScopedCommand?: boolean, requiredModuleName?: string, installationIndependentCommand?: boolean, authorization?: CommandAuthorization, module?: ModuleDefinition): Promise<boolean>;
|
|
43
43
|
export declare function normalizeDiscordPermission(permission: string): string;
|
|
44
44
|
export declare function toReplyOptions(response: MessageResponse): InteractionReplyOptions;
|
|
45
45
|
export declare function toUpdateOptions(response: MessageResponse): InteractionUpdateOptions;
|
package/dist/interactions.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { lookup as lookupDns } from "node:dns/promises";
|
|
2
2
|
import { HELYX_SERVICE_NAMES, NexusEconomyError, UserFacingInteractionError, } from "@helyx/sdk";
|
|
3
|
-
import { canUseCommand } from "@helyx/core";
|
|
3
|
+
import { canUseCommand, safeError } from "@helyx/core";
|
|
4
4
|
import { commandRegistrationPolicy, contributionRegistrationPolicy, } from "./command-registration-policy.js";
|
|
5
5
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, EmbedBuilder, Events, GuildMember, ApplicationIntegrationType, ChannelType, InteractionContextType, LabelBuilder, MessageFlags, MentionableSelectMenuBuilder, ModalBuilder, RoleSelectMenuBuilder, Routes, StringSelectMenuBuilder, SlashCommandBuilder, TextInputBuilder, TextInputStyle, UserSelectMenuBuilder, TextDisplayBuilder, PermissionFlagsBits, escapeMarkdown, } from "discord.js";
|
|
6
6
|
import { collectCommandRegistrations, } from "./shared-commands.js";
|
|
7
7
|
import { resolveDirectCommandPath } from "./command-path-contract.js";
|
|
8
|
+
import { canAccessModule, PREMIUM_MODULE_UNAVAILABLE_MESSAGE, } from "./premium-access.js";
|
|
8
9
|
const buttonStyles = {
|
|
9
10
|
primary: ButtonStyle.Primary,
|
|
10
11
|
secondary: ButtonStyle.Secondary,
|
|
@@ -110,7 +111,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
110
111
|
!subcommand?.autocomplete ||
|
|
111
112
|
!ownerModuleId ||
|
|
112
113
|
!(await authorizeAutocomplete(interaction, ownerModuleId, resolveDirectCommandPath(registered.contribution, subcommand, groupName).permissionId, services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
113
|
-
false, subcommand.authorization === "resource-scoped", isInstallationIndependentModule(modulesById, ownerModuleId), subcommand.authorization))) {
|
|
114
|
+
false, subcommand.authorization === "resource-scoped", isInstallationIndependentModule(modulesById, ownerModuleId), subcommand.authorization, requiredCommandModule(modulesById, ownerModuleId)))) {
|
|
114
115
|
await response.respond([]);
|
|
115
116
|
return;
|
|
116
117
|
}
|
|
@@ -190,7 +191,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
190
191
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
191
192
|
}
|
|
192
193
|
if (await authorizeCommand(interaction, ownerModuleId, permissionId, rateLimit, services, registration.selfAuthorizingGuildModuleIds?.has(ownerModuleId) ??
|
|
193
|
-
false, subcommand?.authorization === "resource-scoped", owner?.moduleName, installationIndependentCommand, authorization)) {
|
|
194
|
+
false, subcommand?.authorization === "resource-scoped", owner?.moduleName, installationIndependentCommand, authorization, requiredCommandModule(modulesById, ownerModuleId))) {
|
|
194
195
|
const context = createCommandContext(interaction, services);
|
|
195
196
|
await execute(context);
|
|
196
197
|
}
|
|
@@ -217,12 +218,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
217
218
|
}
|
|
218
219
|
if (button &&
|
|
219
220
|
context &&
|
|
220
|
-
button.
|
|
221
|
-
await button.contribution.execute(context);
|
|
222
|
-
}
|
|
223
|
-
else if (button &&
|
|
224
|
-
context &&
|
|
225
|
-
(await authorizeComponent(interaction, button.moduleId, services, undefined, isInstallationIndependentModule(modulesById, button.moduleId)))) {
|
|
221
|
+
(await authorizeComponent(interaction, button.moduleId, services, undefined, isInstallationIndependentModule(modulesById, button.moduleId), modulesById.get(button.moduleId)))) {
|
|
226
222
|
await button.contribution.execute(context);
|
|
227
223
|
}
|
|
228
224
|
}
|
|
@@ -239,7 +235,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
239
235
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
240
236
|
}
|
|
241
237
|
if (modal &&
|
|
242
|
-
(await authorizeComponent(interaction, modal.moduleId, services, undefined, isInstallationIndependentModule(modulesById, modal.moduleId)))) {
|
|
238
|
+
(await authorizeComponent(interaction, modal.moduleId, services, undefined, isInstallationIndependentModule(modulesById, modal.moduleId), modulesById.get(modal.moduleId)))) {
|
|
243
239
|
await modal.contribution.execute(createModalContext(interaction, services));
|
|
244
240
|
}
|
|
245
241
|
}
|
|
@@ -264,7 +260,7 @@ export function installInteractions(client, modules, registration, logger, servi
|
|
|
264
260
|
}
|
|
265
261
|
if (select &&
|
|
266
262
|
context &&
|
|
267
|
-
(await authorizeComponent(interaction, select.moduleId, services, undefined, isInstallationIndependentModule(modulesById, select.moduleId)))) {
|
|
263
|
+
(await authorizeComponent(interaction, select.moduleId, services, undefined, isInstallationIndependentModule(modulesById, select.moduleId), modulesById.get(select.moduleId)))) {
|
|
268
264
|
await select.contribution.execute(context);
|
|
269
265
|
}
|
|
270
266
|
}
|
|
@@ -857,19 +853,31 @@ export async function addChannelMessageReaction(client, channelId, messageId, em
|
|
|
857
853
|
const message = await channel.messages.fetch(messageId);
|
|
858
854
|
await message.react(emojiIdentifier);
|
|
859
855
|
}
|
|
860
|
-
async function authorizeComponent(interaction, moduleId, services, requiredModuleName, installationIndependent = false) {
|
|
856
|
+
async function authorizeComponent(interaction, moduleId, services, requiredModuleName, installationIndependent = false, module) {
|
|
861
857
|
if (installationIndependent)
|
|
862
858
|
return true;
|
|
863
|
-
|
|
859
|
+
const installations = interaction.guildId
|
|
860
|
+
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
861
|
+
: undefined;
|
|
862
|
+
const [globallyAvailable, moduleEnabled, premiumAccessible] = await Promise.all([
|
|
863
|
+
moduleIsGloballyAvailable(services, moduleId),
|
|
864
|
+
installations
|
|
865
|
+
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
866
|
+
: Promise.resolve(true),
|
|
867
|
+
interaction.guildId && module
|
|
868
|
+
? canAccessModule(services, interaction.guildId, module)
|
|
869
|
+
: Promise.resolve(true),
|
|
870
|
+
]);
|
|
871
|
+
if (!globallyAvailable) {
|
|
864
872
|
await replyEphemeralText(interaction, GLOBAL_MODULE_UNAVAILABLE_MESSAGE);
|
|
865
873
|
return false;
|
|
866
874
|
}
|
|
867
|
-
if (!
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
if (await installations.isModuleEnabled(interaction.guildId, moduleId)) {
|
|
871
|
-
return true;
|
|
875
|
+
if (!premiumAccessible) {
|
|
876
|
+
await replyEphemeralText(interaction, PREMIUM_MODULE_UNAVAILABLE_MESSAGE);
|
|
877
|
+
return false;
|
|
872
878
|
}
|
|
879
|
+
if (moduleEnabled)
|
|
880
|
+
return true;
|
|
873
881
|
await replyEphemeralText(interaction, requiredModuleName
|
|
874
882
|
? `This command needs ${requiredModuleName}. Ask a server administrator to enable it in the Helyx dashboard.`
|
|
875
883
|
: "This module is disabled in this server. Ask a server administrator to enable it with `/helyx` or in the Helyx dashboard.");
|
|
@@ -882,22 +890,29 @@ async function replyEphemeralText(interaction, content) {
|
|
|
882
890
|
}
|
|
883
891
|
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
884
892
|
}
|
|
885
|
-
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false, requiredModuleName, installationIndependentCommand = false, authorization) {
|
|
893
|
+
export async function authorizeCommand(interaction, moduleId, commandId, rateLimit, services, selfAuthorizingGuildCommand, resourceScopedCommand = false, requiredModuleName, installationIndependentCommand = false, authorization, module) {
|
|
886
894
|
const bypassPlatformAuthorization = authorization === "public" || authorization === "self-authorized";
|
|
887
895
|
if (!installationIndependentCommand && !selfAuthorizingGuildCommand) {
|
|
888
896
|
const installations = interaction.guildId
|
|
889
897
|
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
890
898
|
: undefined;
|
|
891
|
-
const [globallyAvailable, moduleEnabled] = await Promise.all([
|
|
899
|
+
const [globallyAvailable, moduleEnabled, premiumAccessible] = await Promise.all([
|
|
892
900
|
moduleIsGloballyAvailable(services, moduleId),
|
|
893
901
|
installations && !selfAuthorizingGuildCommand
|
|
894
902
|
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
895
903
|
: Promise.resolve(true),
|
|
904
|
+
interaction.guildId && module
|
|
905
|
+
? canAccessModule(services, interaction.guildId, module)
|
|
906
|
+
: Promise.resolve(true),
|
|
896
907
|
]);
|
|
897
908
|
if (!globallyAvailable) {
|
|
898
909
|
await replyEphemeralText(interaction, GLOBAL_MODULE_UNAVAILABLE_MESSAGE);
|
|
899
910
|
return false;
|
|
900
911
|
}
|
|
912
|
+
if (!premiumAccessible) {
|
|
913
|
+
await replyEphemeralText(interaction, PREMIUM_MODULE_UNAVAILABLE_MESSAGE);
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
901
916
|
if (!moduleEnabled) {
|
|
902
917
|
await replyEphemeralText(interaction, requiredModuleName
|
|
903
918
|
? `This command needs ${requiredModuleName}. Ask a server administrator to enable it in the Helyx dashboard.`
|
|
@@ -939,19 +954,22 @@ export async function authorizeCommand(interaction, moduleId, commandId, rateLim
|
|
|
939
954
|
}
|
|
940
955
|
return true;
|
|
941
956
|
}
|
|
942
|
-
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand, installationIndependentCommand = false, authorization) {
|
|
957
|
+
async function authorizeAutocomplete(interaction, moduleId, commandId, services, selfAuthorizingGuildCommand, resourceScopedCommand, installationIndependentCommand = false, authorization, module) {
|
|
943
958
|
const bypassPlatformAuthorization = authorization === "public" || authorization === "self-authorized";
|
|
944
959
|
if (!installationIndependentCommand && !selfAuthorizingGuildCommand) {
|
|
945
960
|
const installations = interaction.guildId
|
|
946
961
|
? services.get(HELYX_SERVICE_NAMES.installations)
|
|
947
962
|
: undefined;
|
|
948
|
-
const [globallyAvailable, moduleEnabled] = await Promise.all([
|
|
963
|
+
const [globallyAvailable, moduleEnabled, premiumAccessible] = await Promise.all([
|
|
949
964
|
moduleIsGloballyAvailable(services, moduleId),
|
|
950
965
|
installations && !selfAuthorizingGuildCommand
|
|
951
966
|
? installations.isModuleEnabled(interaction.guildId, moduleId)
|
|
952
967
|
: Promise.resolve(true),
|
|
968
|
+
interaction.guildId && module
|
|
969
|
+
? canAccessModule(services, interaction.guildId, module)
|
|
970
|
+
: Promise.resolve(true),
|
|
953
971
|
]);
|
|
954
|
-
if (!globallyAvailable || !moduleEnabled)
|
|
972
|
+
if (!globallyAvailable || !moduleEnabled || !premiumAccessible)
|
|
955
973
|
return false;
|
|
956
974
|
}
|
|
957
975
|
if (bypassPlatformAuthorization ||
|
|
@@ -1365,17 +1383,6 @@ async function sendFailure(interaction, error) {
|
|
|
1365
1383
|
.reply({ content: message, flags: MessageFlags.Ephemeral })
|
|
1366
1384
|
.catch(() => undefined);
|
|
1367
1385
|
}
|
|
1368
|
-
function safeError(error) {
|
|
1369
|
-
const value = error instanceof Error ? error : new Error(String(error));
|
|
1370
|
-
return {
|
|
1371
|
-
name: value.name,
|
|
1372
|
-
message: value.message,
|
|
1373
|
-
...("code" in value &&
|
|
1374
|
-
(typeof value.code === "string" || typeof value.code === "number")
|
|
1375
|
-
? { code: value.code }
|
|
1376
|
-
: {}),
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
1386
|
function interactionLogContext(interaction) {
|
|
1380
1387
|
return {
|
|
1381
1388
|
interactionId: interaction.id,
|
package/dist/module-logging.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InstallationRepository, ModuleConfigurationRepository, ModuleLoggingPolicyRepository, } from "@helyx/database";
|
|
2
|
+
import { safeError } from "@helyx/core";
|
|
2
3
|
import { toReplyOptions } from "./interactions.js";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
const LOGGING_MODULE_ID = "helyx.logging";
|
|
@@ -108,8 +109,4 @@ function normalizeText(value, maximum, label) {
|
|
|
108
109
|
}
|
|
109
110
|
return normalized;
|
|
110
111
|
}
|
|
111
|
-
function safeError(error) {
|
|
112
|
-
const value = error instanceof Error ? error : new Error(String(error));
|
|
113
|
-
return { name: value.name, message: value.message };
|
|
114
|
-
}
|
|
115
112
|
//# sourceMappingURL=module-logging.js.map
|
|
@@ -13,7 +13,7 @@ export async function mutateDashboardModuleStatus(input) {
|
|
|
13
13
|
input.services.has(HELYX_SERVICE_NAMES.numericWriteBehind)
|
|
14
14
|
? input.services.get(HELYX_SERVICE_NAMES.numericWriteBehind)
|
|
15
15
|
: null;
|
|
16
|
-
if (
|
|
16
|
+
if (change.enabled)
|
|
17
17
|
await input.requireModuleAvailable();
|
|
18
18
|
let mutationResult;
|
|
19
19
|
const setEnabled = async () => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ModuleDefinition, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
export declare const PREMIUM_MODULE_UNAVAILABLE_MESSAGE = "This Premium module is not assigned to this server. A Premium subscriber can assign a slot in Billing or with `/activate`.";
|
|
3
|
+
export declare function moduleRequiresPremium(module: ModuleDefinition): boolean;
|
|
4
|
+
export declare function requirePremiumProviderEnvironment(modules: readonly ModuleDefinition[], environment: "sandbox" | "live" | undefined): "sandbox" | "live" | undefined;
|
|
5
|
+
export declare function canAccessModule(services: ServiceAccess, guildId: string, module: ModuleDefinition): Promise<boolean>;
|
|
6
|
+
export declare function assertReviewedPremiumCatalogue(modules: readonly ModuleDefinition[]): void;
|
|
7
|
+
//# sourceMappingURL=premium-access.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getModuleTier, HELYX_PREMIUM_ENTITLEMENT_CAPABILITY, isReviewedPremiumModuleId, REVIEWED_PREMIUM_MODULE_IDS, } from "@helyx/module-manifest";
|
|
2
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
3
|
+
export const PREMIUM_MODULE_UNAVAILABLE_MESSAGE = "This Premium module is not assigned to this server. A Premium subscriber can assign a slot in Billing or with `/activate`.";
|
|
4
|
+
export function moduleRequiresPremium(module) {
|
|
5
|
+
return getModuleTier(module.manifest) === "premium";
|
|
6
|
+
}
|
|
7
|
+
export function requirePremiumProviderEnvironment(modules, environment) {
|
|
8
|
+
if (!modules.some(moduleRequiresPremium))
|
|
9
|
+
return undefined;
|
|
10
|
+
if (!environment)
|
|
11
|
+
throw new Error("STRIPE_ENVIRONMENT is required when a reviewed Premium module is loaded");
|
|
12
|
+
return environment;
|
|
13
|
+
}
|
|
14
|
+
export async function canAccessModule(services, guildId, module) {
|
|
15
|
+
if (!moduleRequiresPremium(module))
|
|
16
|
+
return true;
|
|
17
|
+
if (!isReviewedPremiumModuleId(module.manifest.id))
|
|
18
|
+
return false;
|
|
19
|
+
if (typeof services.has === "function" &&
|
|
20
|
+
!services.has(HELYX_SERVICE_NAMES.entitlements))
|
|
21
|
+
return false;
|
|
22
|
+
const entitlements = services.get(HELYX_SERVICE_NAMES.entitlements);
|
|
23
|
+
return entitlements.has(guildId, HELYX_PREMIUM_ENTITLEMENT_CAPABILITY);
|
|
24
|
+
}
|
|
25
|
+
export function assertReviewedPremiumCatalogue(modules) {
|
|
26
|
+
const byId = new Map(modules.map((module) => [module.manifest.id, module]));
|
|
27
|
+
for (const module of modules) {
|
|
28
|
+
if (moduleRequiresPremium(module) &&
|
|
29
|
+
!isReviewedPremiumModuleId(module.manifest.id))
|
|
30
|
+
throw new Error(`Premium module has not passed catalogue review: ${module.manifest.id}`);
|
|
31
|
+
}
|
|
32
|
+
for (const moduleId of REVIEWED_PREMIUM_MODULE_IDS) {
|
|
33
|
+
const module = byId.get(moduleId);
|
|
34
|
+
if (module && !moduleRequiresPremium(module))
|
|
35
|
+
throw new Error(`Reviewed Premium module is missing its Premium manifest tier: ${moduleId}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=premium-access.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BillingEnvironment, PremiumAllocationRepository } from "@helyx/database";
|
|
2
|
+
import { type ModuleDefinition } from "@helyx/sdk";
|
|
3
|
+
type AllocationService = Pick<PremiumAllocationRepository, "getAccountAllocation" | "inspectTarget" | "assignLowestAvailable" | "moveSlot" | "releaseSlot" | "auditDenied">;
|
|
4
|
+
export interface PremiumGuildAuthority {
|
|
5
|
+
canManageGuild(input: {
|
|
6
|
+
guildId: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
}): Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
export interface PremiumActivationModuleOptions {
|
|
11
|
+
allocations: AllocationService;
|
|
12
|
+
authority: PremiumGuildAuthority;
|
|
13
|
+
provider: string;
|
|
14
|
+
providerEnvironment: BillingEnvironment;
|
|
15
|
+
reviewedPremiumModuleIds: readonly string[];
|
|
16
|
+
now?: () => Date;
|
|
17
|
+
}
|
|
18
|
+
export declare function createPremiumActivationModule(options: PremiumActivationModuleOptions): ModuleDefinition;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=premium-activation-module.d.ts.map
|