@helyx/bot 0.4.1 → 0.4.4

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +13 -0
  3. package/dist/cloud-connector.d.ts +7 -0
  4. package/dist/cloud-connector.js +118 -1
  5. package/dist/cloud-control-dispatcher.js +84 -0
  6. package/dist/control-server.d.ts +187 -5
  7. package/dist/control-server.js +673 -6
  8. package/dist/developer-operations-module.js +8 -0
  9. package/dist/discord-owned-message-service.d.ts +39 -0
  10. package/dist/discord-owned-message-service.js +138 -0
  11. package/dist/events.d.ts +70 -1
  12. package/dist/events.js +557 -5
  13. package/dist/index.js +61 -13
  14. package/dist/interactions.d.ts +3 -2
  15. package/dist/interactions.js +66 -31
  16. package/dist/module-logging.d.ts +1 -0
  17. package/dist/module-logging.js +19 -1
  18. package/dist/modules.d.ts +9 -0
  19. package/dist/modules.js +45 -0
  20. package/dist/resource-authorization.d.ts +7 -0
  21. package/dist/resource-authorization.js +12 -0
  22. package/dist/scheduled-task-runner.d.ts +130 -0
  23. package/dist/scheduled-task-runner.js +417 -0
  24. package/dist/shard-cloud-proxy.d.ts +4 -0
  25. package/dist/shard-cloud-proxy.js +34 -0
  26. package/dist/shard-contracts.d.ts +574 -23
  27. package/dist/shard-contracts.js +346 -0
  28. package/dist/shard-coordinator.d.ts +5 -3
  29. package/dist/shard-coordinator.js +85 -2
  30. package/dist/shard-router.d.ts +10 -0
  31. package/dist/shard-router.js +58 -0
  32. package/dist/shard-supervisor.d.ts +2 -1
  33. package/dist/shard-supervisor.js +187 -5
  34. package/dist/shard-worker.d.ts +8 -2
  35. package/dist/shard-worker.js +97 -16
  36. package/dist/support-module.d.ts +9 -0
  37. package/dist/support-module.js +172 -0
  38. package/package.json +9 -9
@@ -44,6 +44,13 @@ export function createDeveloperOperationsModule(input) {
44
44
  : `${Math.round(shard.gatewayPingMs)}ms`;
45
45
  return `\`${shard.shardId}\` ${shard.status} · ${shard.guildCount} guilds · ${ping} · restarts ${shard.restartCount}`;
46
46
  });
47
+ const schedulerLines = health.scheduler?.status === "available"
48
+ ? [
49
+ "Scheduler: available",
50
+ `One-shot: ${health.scheduler.oneShot.ready} ready / ${health.scheduler.oneShot.expiredLeases} expired leases / ${health.scheduler.oneShot.requiresReview} review`,
51
+ `Coalesced: ${health.scheduler.coalesced.ready} ready / ${health.scheduler.coalesced.expiredLeases} expired leases / ${health.scheduler.coalesced.requiresReview} review`,
52
+ ]
53
+ : ["Scheduler: unavailable"];
47
54
  await context.reply({
48
55
  ephemeral: true,
49
56
  componentsV2: {
@@ -56,6 +63,7 @@ export function createDeveloperOperationsModule(input) {
56
63
  `Mode: ${health.mode}/${health.scope}`,
57
64
  `Generation: \`${health.generation}\``,
58
65
  `Observed: ${health.observedAt}`,
66
+ ...schedulerLines,
59
67
  "",
60
68
  ...shardLines,
61
69
  ...(health.shards.length < health.configuredShardCount
@@ -0,0 +1,39 @@
1
+ import type { DiscordOwnedMessageService, MessageResponse } from "@helyx/sdk";
2
+ import { type Client } from "discord.js";
3
+ export declare class DiscordOwnedMessageDeletionService implements DiscordOwnedMessageService {
4
+ #private;
5
+ private readonly client;
6
+ constructor(client: Client);
7
+ addOwnedMessageReaction(input: {
8
+ guildId: string;
9
+ channelId: string;
10
+ messageId: string;
11
+ emojiIdentifier: string;
12
+ }): Promise<{
13
+ added: boolean;
14
+ }>;
15
+ clearOwnedMessageReaction(input: {
16
+ guildId: string;
17
+ channelId: string;
18
+ messageId: string;
19
+ emojiIdentifier: string;
20
+ }): Promise<{
21
+ cleared: boolean;
22
+ }>;
23
+ deleteOwnedMessage(input: {
24
+ guildId: string;
25
+ channelId: string;
26
+ messageId: string;
27
+ }): Promise<{
28
+ deleted: boolean;
29
+ }>;
30
+ editOwnedMessage(input: {
31
+ guildId: string;
32
+ channelId: string;
33
+ messageId: string;
34
+ response: MessageResponse;
35
+ }): Promise<{
36
+ edited: boolean;
37
+ }>;
38
+ }
39
+ //# sourceMappingURL=discord-owned-message-service.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { RESTJSONErrorCodes } from "discord.js";
2
+ import { deleteChannelMessage, toUpdateOptions } from "./interactions.js";
3
+ const discordSnowflake = /^\d{17,20}$/u;
4
+ export class DiscordOwnedMessageDeletionService {
5
+ client;
6
+ constructor(client) {
7
+ this.client = client;
8
+ }
9
+ async addOwnedMessageReaction(input) {
10
+ const emoji = prepareEmojiIdentifier(input.emojiIdentifier);
11
+ const message = await this.#fetchOwnedMessage(input);
12
+ if (!message)
13
+ return { added: false };
14
+ await message.react(emoji.identifier);
15
+ return { added: true };
16
+ }
17
+ async clearOwnedMessageReaction(input) {
18
+ const emoji = prepareEmojiIdentifier(input.emojiIdentifier);
19
+ const message = await this.#fetchOwnedMessage(input);
20
+ if (!message)
21
+ return { cleared: false };
22
+ const reaction = message.reactions.resolve(emoji.cacheKey);
23
+ if (!reaction)
24
+ return { cleared: false };
25
+ await reaction.remove();
26
+ return { cleared: true };
27
+ }
28
+ async deleteOwnedMessage(input) {
29
+ validateSnowflake(input.guildId, "guild");
30
+ validateSnowflake(input.channelId, "channel");
31
+ validateSnowflake(input.messageId, "message");
32
+ const guild = await this.client.guilds.fetch(input.guildId);
33
+ if (guild.id !== input.guildId)
34
+ throw new Error("Discord guild validation failed");
35
+ const channel = await guild.channels.fetch(input.channelId);
36
+ if (!channel ||
37
+ channel.id !== input.channelId ||
38
+ channel.guild.id !== guild.id)
39
+ throw new Error("Discord channel validation failed");
40
+ if (!channel.isTextBased() || !channel.viewable)
41
+ throw new Error("Discord channel is not a bot-accessible text channel");
42
+ const botUserId = this.client.user?.id;
43
+ if (!botUserId)
44
+ throw new Error("Discord bot user is unavailable");
45
+ try {
46
+ const message = await channel.messages.fetch(input.messageId);
47
+ if (message.id !== input.messageId ||
48
+ message.channelId !== channel.id ||
49
+ message.guildId !== guild.id)
50
+ throw new Error("Discord message validation failed");
51
+ if (message.author.id !== botUserId)
52
+ throw new Error("Discord message is not owned by Helyx");
53
+ await deleteChannelMessage(this.client, input.channelId, input.messageId);
54
+ return { deleted: true };
55
+ }
56
+ catch (error) {
57
+ if (isUnknownMessage(error))
58
+ return { deleted: false };
59
+ throw error;
60
+ }
61
+ }
62
+ async editOwnedMessage(input) {
63
+ const message = await this.#fetchOwnedMessage(input);
64
+ if (!message)
65
+ return { edited: false };
66
+ await message.edit(toUpdateOptions(input.response));
67
+ return { edited: true };
68
+ }
69
+ async #fetchOwnedMessage(input) {
70
+ validateSnowflake(input.guildId, "guild");
71
+ validateSnowflake(input.channelId, "channel");
72
+ validateSnowflake(input.messageId, "message");
73
+ const guild = await this.client.guilds.fetch(input.guildId);
74
+ if (guild.id !== input.guildId)
75
+ throw new Error("Discord guild validation failed");
76
+ const channel = await guild.channels.fetch(input.channelId);
77
+ if (!channel ||
78
+ channel.id !== input.channelId ||
79
+ channel.guild.id !== guild.id)
80
+ throw new Error("Discord channel validation failed");
81
+ if (!channel.isTextBased() || !channel.viewable)
82
+ throw new Error("Discord channel is not a bot-accessible text channel");
83
+ const botUserId = this.client.user?.id;
84
+ if (!botUserId)
85
+ throw new Error("Discord bot user is unavailable");
86
+ try {
87
+ const message = await channel.messages.fetch(input.messageId);
88
+ if (message.id !== input.messageId ||
89
+ message.channelId !== channel.id ||
90
+ message.guildId !== guild.id)
91
+ throw new Error("Discord message validation failed");
92
+ if (message.author.id !== botUserId)
93
+ throw new Error("Discord message is not owned by Helyx");
94
+ return message;
95
+ }
96
+ catch (error) {
97
+ if (isUnknownMessage(error))
98
+ return null;
99
+ throw error;
100
+ }
101
+ }
102
+ }
103
+ function validateSnowflake(value, label) {
104
+ if (!discordSnowflake.test(value))
105
+ throw new Error(`Invalid Discord ${label} ID`);
106
+ }
107
+ function prepareEmojiIdentifier(value) {
108
+ if (value.length < 1 || value.length > 128 || /[\r\n]/u.test(value)) {
109
+ throw new Error("Invalid bounded Discord emoji identifier");
110
+ }
111
+ const custom = /^(?:<a?:([A-Za-z0-9_]{2,32}):(\d{17,20})>|([A-Za-z0-9_]{2,32}):(\d{17,20}))$/u.exec(value);
112
+ if (custom) {
113
+ const name = custom[1] ?? custom[3];
114
+ const id = custom[2] ?? custom[4];
115
+ return {
116
+ identifier: `${name}:${id}`,
117
+ cacheKey: id,
118
+ };
119
+ }
120
+ const normalized = value.normalize("NFKC").trim();
121
+ const graphemes = [
122
+ ...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(normalized),
123
+ ];
124
+ if (!/\p{Emoji}/u.test(normalized) || graphemes.length !== 1) {
125
+ throw new Error("Invalid Discord emoji identifier");
126
+ }
127
+ return {
128
+ identifier: normalized,
129
+ cacheKey: normalized,
130
+ };
131
+ }
132
+ function isUnknownMessage(error) {
133
+ return (typeof error === "object" &&
134
+ error !== null &&
135
+ "code" in error &&
136
+ error.code === RESTJSONErrorCodes.UnknownMessage);
137
+ }
138
+ //# sourceMappingURL=discord-owned-message-service.js.map
package/dist/events.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ModuleDefinition, ServiceAccess } from "@helyx/sdk";
1
+ import type { DiscordChannelInspection, DiscordEmojiInspection, DiscordResourceInspectionService, DiscordRoleInspection, MemberRoleService, ModuleDefinition, ServiceAccess } from "@helyx/sdk";
2
2
  import { type Client, type MessageReaction, type PartialMessageReaction } from "discord.js";
3
3
  import { type InteractionLogger } from "./interactions.js";
4
4
  export declare function installModuleEvents(client: Client, modules: readonly ModuleDefinition[], logger: InteractionLogger, services: ServiceAccess): ModuleEventHandle;
@@ -8,4 +8,73 @@ export interface ModuleEventHandle {
8
8
  }
9
9
  export declare function reactionEmojiKey(reaction: MessageReaction | PartialMessageReaction): string;
10
10
  export declare function reactionCacheKey(emojiIdentifier: string): string;
11
+ interface ApprovedRoleMutation {
12
+ guildId: string;
13
+ memberUserId: string;
14
+ roleId: string;
15
+ moduleId: string;
16
+ reason: string;
17
+ }
18
+ export declare class DiscordMemberRoleService implements MemberRoleService {
19
+ private readonly client;
20
+ constructor(client: Client);
21
+ getCurrentRoleIds(input: {
22
+ guildId: string;
23
+ memberUserId: string;
24
+ }): Promise<readonly string[]>;
25
+ addApprovedRole(input: ApprovedRoleMutation): Promise<{
26
+ changed: boolean;
27
+ }>;
28
+ removeApprovedRole(input: ApprovedRoleMutation): Promise<{
29
+ changed: boolean;
30
+ }>;
31
+ reconcileApprovedRoles(input: {
32
+ guildId: string;
33
+ memberUserId: string;
34
+ allowedRoleIds: readonly string[];
35
+ desiredRoleIds: readonly string[];
36
+ moduleId: string;
37
+ reason: string;
38
+ }): Promise<{
39
+ added: readonly string[];
40
+ removed: readonly string[];
41
+ }>;
42
+ private fetchGuildAndMember;
43
+ }
44
+ export declare class DiscordResourceInspection implements DiscordResourceInspectionService {
45
+ #private;
46
+ private readonly client;
47
+ constructor(client: Client);
48
+ resolveMemberSnapshots(input: {
49
+ guildId: string;
50
+ userIds: readonly string[];
51
+ }): Promise<readonly {
52
+ userId: string;
53
+ exists: boolean;
54
+ isBot: boolean;
55
+ roleIds: readonly string[];
56
+ joinedAt: Date | null;
57
+ }[]>;
58
+ resolveMembers(input: {
59
+ guildId: string;
60
+ userIds: readonly string[];
61
+ }): Promise<readonly {
62
+ userId: string;
63
+ displayName: string | null;
64
+ }[]>;
65
+ inspectRole(input: {
66
+ guildId: string;
67
+ roleId: string;
68
+ actorUserId?: string;
69
+ }): Promise<DiscordRoleInspection>;
70
+ inspectEmoji(input: {
71
+ guildId: string;
72
+ emojiIdentifier: string;
73
+ }): Promise<DiscordEmojiInspection>;
74
+ inspectChannel(input: {
75
+ guildId: string;
76
+ channelId: string;
77
+ }): Promise<DiscordChannelInspection>;
78
+ }
79
+ export {};
11
80
  //# sourceMappingURL=events.d.ts.map