mirror of
https://github.com/arthur-pbty/flint.git
synced 2026-08-01 20:29:03 +02:00
fix bug welcome
This commit is contained in:
@@ -16,6 +16,8 @@ FROM node:20-alpine AS runner
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN apk add --no-cache fontconfig ttf-dejavu
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE bot_member_message_configs
|
||||||
|
ADD COLUMN IF NOT EXISTS auto_role_ids TEXT NOT NULL DEFAULT '[]';
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
createDefaultMemberMessageConfig,
|
createDefaultMemberMessageConfig,
|
||||||
isMemberMessageRenderTypeValue,
|
isMemberMessageRenderTypeValue,
|
||||||
|
sanitizeMemberMessageRoleIds,
|
||||||
} from "../../validators/memberMessages.js";
|
} from "../../validators/memberMessages.js";
|
||||||
import type { MemberMessageRepository } from "../../modules/memberMessages/index.js";
|
import type { MemberMessageRepository } from "../../modules/memberMessages/index.js";
|
||||||
|
|
||||||
@@ -19,11 +20,30 @@ SELECT
|
|||||||
enabled,
|
enabled,
|
||||||
channel_id,
|
channel_id,
|
||||||
message_type,
|
message_type,
|
||||||
|
auto_role_ids,
|
||||||
updated_at
|
updated_at
|
||||||
FROM bot_member_message_configs
|
FROM bot_member_message_configs
|
||||||
LIMIT 0;
|
LIMIT 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const parseRoleIds = (serialized: string | null): string[] => {
|
||||||
|
if (!serialized || serialized.trim().length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(serialized) as unknown;
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleIds = parsed.filter((value): value is string => typeof value === "string");
|
||||||
|
return sanitizeMemberMessageRoleIds(roleIds);
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toConfig = (row: MemberMessageRow): MemberMessageConfig => {
|
const toConfig = (row: MemberMessageRow): MemberMessageConfig => {
|
||||||
const fallback = createDefaultMemberMessageConfig();
|
const fallback = createDefaultMemberMessageConfig();
|
||||||
|
|
||||||
@@ -31,6 +51,7 @@ const toConfig = (row: MemberMessageRow): MemberMessageConfig => {
|
|||||||
enabled: row.enabled,
|
enabled: row.enabled,
|
||||||
channelId: row.channel_id,
|
channelId: row.channel_id,
|
||||||
messageType: isMemberMessageRenderTypeValue(row.message_type) ? row.message_type : fallback.messageType,
|
messageType: isMemberMessageRenderTypeValue(row.message_type) ? row.message_type : fallback.messageType,
|
||||||
|
autoRoleIds: parseRoleIds(row.auto_role_ids),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,7 +72,7 @@ export class PostgresMemberMessageStore implements MemberMessageRepository {
|
|||||||
public async getByBotGuildKind(botId: string, guildId: string, kind: MemberMessageKind): Promise<MemberMessageConfig> {
|
public async getByBotGuildKind(botId: string, guildId: string, kind: MemberMessageKind): Promise<MemberMessageConfig> {
|
||||||
const result = await this.pool.query<MemberMessageRow>(
|
const result = await this.pool.query<MemberMessageRow>(
|
||||||
`
|
`
|
||||||
SELECT enabled, channel_id, message_type
|
SELECT enabled, channel_id, message_type, auto_role_ids
|
||||||
FROM bot_member_message_configs
|
FROM bot_member_message_configs
|
||||||
WHERE bot_id = $1 AND guild_id = $2 AND kind = $3
|
WHERE bot_id = $1 AND guild_id = $2 AND kind = $3
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -73,6 +94,8 @@ export class PostgresMemberMessageStore implements MemberMessageRepository {
|
|||||||
kind: MemberMessageKind,
|
kind: MemberMessageKind,
|
||||||
config: MemberMessageConfig,
|
config: MemberMessageConfig,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const autoRoleIds = sanitizeMemberMessageRoleIds(config.autoRoleIds);
|
||||||
|
|
||||||
await this.pool.query(
|
await this.pool.query(
|
||||||
`
|
`
|
||||||
INSERT INTO bot_member_message_configs (
|
INSERT INTO bot_member_message_configs (
|
||||||
@@ -82,17 +105,19 @@ export class PostgresMemberMessageStore implements MemberMessageRepository {
|
|||||||
enabled,
|
enabled,
|
||||||
channel_id,
|
channel_id,
|
||||||
message_type,
|
message_type,
|
||||||
|
auto_role_ids,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
||||||
ON CONFLICT (bot_id, guild_id, kind)
|
ON CONFLICT (bot_id, guild_id, kind)
|
||||||
DO UPDATE SET
|
DO UPDATE SET
|
||||||
enabled = EXCLUDED.enabled,
|
enabled = EXCLUDED.enabled,
|
||||||
channel_id = EXCLUDED.channel_id,
|
channel_id = EXCLUDED.channel_id,
|
||||||
message_type = EXCLUDED.message_type,
|
message_type = EXCLUDED.message_type,
|
||||||
|
auto_role_ids = EXCLUDED.auto_role_ids,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
`,
|
`,
|
||||||
[botId, guildId, kind, config.enabled, config.channelId, config.messageType],
|
[botId, guildId, kind, config.enabled, config.channelId, config.messageType, JSON.stringify(autoRoleIds)],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,35 @@ export const registerGuildMemberAdd = (
|
|||||||
memberMessageService: MemberMessageService,
|
memberMessageService: MemberMessageService,
|
||||||
): void => {
|
): void => {
|
||||||
client.on(Events.GuildMemberAdd, (member) => {
|
client.on(Events.GuildMemberAdd, (member) => {
|
||||||
|
void memberMessageService.assignWelcomeAutoRoles({ client, member }).then((result) => {
|
||||||
|
if (result.assigned) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.reason === "no_roles_configured" || result.reason === "no_assignable_roles") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
{
|
||||||
|
guildId: member.guild.id,
|
||||||
|
userId: member.user.id,
|
||||||
|
reason: result.reason,
|
||||||
|
configuredRoleIds: result.configuredRoleIds,
|
||||||
|
},
|
||||||
|
"failed to assign welcome auto roles",
|
||||||
|
);
|
||||||
|
}).catch((error) => {
|
||||||
|
log.error(
|
||||||
|
{
|
||||||
|
guildId: member.guild.id,
|
||||||
|
userId: member.user.id,
|
||||||
|
err: error,
|
||||||
|
},
|
||||||
|
"welcome auto-role dispatch crashed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
void memberMessageService.dispatch({
|
void memberMessageService.dispatch({
|
||||||
client,
|
client,
|
||||||
i18n,
|
i18n,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ChannelType,
|
ChannelType,
|
||||||
ContainerBuilder,
|
ContainerBuilder,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
|
RoleSelectMenuBuilder,
|
||||||
StringSelectMenuBuilder,
|
StringSelectMenuBuilder,
|
||||||
TextDisplayBuilder,
|
TextDisplayBuilder,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
@@ -26,6 +27,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
MEMBER_MESSAGE_RENDER_TYPES,
|
MEMBER_MESSAGE_RENDER_TYPES,
|
||||||
isMemberMessageRenderTypeValue,
|
isMemberMessageRenderTypeValue,
|
||||||
|
sanitizeMemberMessageRoleIds,
|
||||||
} from "../../validators/memberMessages.js";
|
} from "../../validators/memberMessages.js";
|
||||||
import type { MemberMessageService } from "./service.js";
|
import type { MemberMessageService } from "./service.js";
|
||||||
|
|
||||||
@@ -44,8 +46,12 @@ const createCustomIds = (kind: MemberMessageKind): MemberMessageCustomIds => {
|
|||||||
toggleButton: `${kind}:toggle:${nonce}`,
|
toggleButton: `${kind}:toggle:${nonce}`,
|
||||||
channelButton: `${kind}:channel:${nonce}`,
|
channelButton: `${kind}:channel:${nonce}`,
|
||||||
channelCancelButton: `${kind}:channel-cancel:${nonce}`,
|
channelCancelButton: `${kind}:channel-cancel:${nonce}`,
|
||||||
|
roleButton: `${kind}:roles:${nonce}`,
|
||||||
|
roleCancelButton: `${kind}:roles-cancel:${nonce}`,
|
||||||
|
roleClearButton: `${kind}:roles-clear:${nonce}`,
|
||||||
typeSelect: `${kind}:type:${nonce}`,
|
typeSelect: `${kind}:type:${nonce}`,
|
||||||
channelSelect: `${kind}:channel-select:${nonce}`,
|
channelSelect: `${kind}:channel-select:${nonce}`,
|
||||||
|
roleSelect: `${kind}:role-select:${nonce}`,
|
||||||
testButton: `${kind}:test:${nonce}`,
|
testButton: `${kind}:test:${nonce}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -58,8 +64,18 @@ const messageTypeLabel = (ctx: CommandExecutionContext, messageType: MemberMessa
|
|||||||
return ctx.ct(`ui.type.options.${messageType}.label`);
|
return ctx.ct(`ui.type.options.${messageType}.label`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const autoRolesLabel = (ctx: CommandExecutionContext, config: MemberMessageConfig): string => {
|
||||||
|
const roleIds = sanitizeMemberMessageRoleIds(config.autoRoleIds);
|
||||||
|
if (roleIds.length === 0) {
|
||||||
|
return ctx.ct("ui.autoRolesNotConfigured");
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleIds.map((roleId) => `<@&${roleId}>`).join(", ");
|
||||||
|
};
|
||||||
|
|
||||||
const panelContent = (
|
const panelContent = (
|
||||||
ctx: CommandExecutionContext,
|
ctx: CommandExecutionContext,
|
||||||
|
kind: MemberMessageKind,
|
||||||
config: MemberMessageConfig,
|
config: MemberMessageConfig,
|
||||||
uiState: MemberMessagePanelUiState,
|
uiState: MemberMessagePanelUiState,
|
||||||
): string => {
|
): string => {
|
||||||
@@ -73,15 +89,24 @@ const panelContent = (
|
|||||||
`${ctx.ct("ui.embed.fields.type")}: ${messageTypeLabel(ctx, config.messageType)}`,
|
`${ctx.ct("ui.embed.fields.type")}: ${messageTypeLabel(ctx, config.messageType)}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (kind === "welcome") {
|
||||||
|
lines.push(`${ctx.ct("ui.embed.fields.autoRoles")}: ${autoRolesLabel(ctx, config)}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (uiState.channelPickerOpen) {
|
if (uiState.channelPickerOpen) {
|
||||||
lines.push("", `${ctx.ct("ui.embed.fields.channelPicker")}: ${ctx.ct("ui.channelPickerHint")}`);
|
lines.push("", `${ctx.ct("ui.embed.fields.channelPicker")}: ${ctx.ct("ui.channelPickerHint")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kind === "welcome" && uiState.rolePickerOpen) {
|
||||||
|
lines.push("", `${ctx.ct("ui.embed.fields.autoRoles")}: ${ctx.ct("ui.rolePickerHint")}`);
|
||||||
|
}
|
||||||
|
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildContainer = (
|
const buildContainer = (
|
||||||
ctx: CommandExecutionContext,
|
ctx: CommandExecutionContext,
|
||||||
|
kind: MemberMessageKind,
|
||||||
customIds: MemberMessageCustomIds,
|
customIds: MemberMessageCustomIds,
|
||||||
config: MemberMessageConfig,
|
config: MemberMessageConfig,
|
||||||
uiState: MemberMessagePanelUiState,
|
uiState: MemberMessagePanelUiState,
|
||||||
@@ -99,6 +124,18 @@ const buildContainer = (
|
|||||||
.setStyle(ButtonStyle.Secondary)
|
.setStyle(ButtonStyle.Secondary)
|
||||||
.setDisabled(disabled);
|
.setDisabled(disabled);
|
||||||
|
|
||||||
|
const roleButton = new ButtonBuilder()
|
||||||
|
.setCustomId(uiState.rolePickerOpen ? customIds.roleCancelButton : customIds.roleButton)
|
||||||
|
.setLabel(uiState.rolePickerOpen ? ctx.ct("ui.buttons.rolesCancel") : ctx.ct("ui.buttons.roles"))
|
||||||
|
.setStyle(ButtonStyle.Secondary)
|
||||||
|
.setDisabled(disabled);
|
||||||
|
|
||||||
|
const clearRolesButton = new ButtonBuilder()
|
||||||
|
.setCustomId(customIds.roleClearButton)
|
||||||
|
.setLabel(ctx.ct("ui.buttons.rolesClear"))
|
||||||
|
.setStyle(ButtonStyle.Danger)
|
||||||
|
.setDisabled(disabled || config.autoRoleIds.length === 0);
|
||||||
|
|
||||||
const testButton = new ButtonBuilder()
|
const testButton = new ButtonBuilder()
|
||||||
.setCustomId(customIds.testButton)
|
.setCustomId(customIds.testButton)
|
||||||
.setLabel(ctx.ct("ui.buttons.test"))
|
.setLabel(ctx.ct("ui.buttons.test"))
|
||||||
@@ -122,11 +159,25 @@ const buildContainer = (
|
|||||||
|
|
||||||
const container = new ContainerBuilder();
|
const container = new ContainerBuilder();
|
||||||
container.addTextDisplayComponents(
|
container.addTextDisplayComponents(
|
||||||
new TextDisplayBuilder().setContent(panelContent(ctx, config, uiState)),
|
new TextDisplayBuilder().setContent(panelContent(ctx, kind, config, uiState)),
|
||||||
);
|
|
||||||
container.addActionRowComponents(
|
|
||||||
new ActionRowBuilder<ButtonBuilder>().addComponents(toggleButton, channelButton, testButton),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (kind === "welcome") {
|
||||||
|
container.addActionRowComponents(
|
||||||
|
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
||||||
|
toggleButton,
|
||||||
|
channelButton,
|
||||||
|
roleButton,
|
||||||
|
clearRolesButton,
|
||||||
|
testButton,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
container.addActionRowComponents(
|
||||||
|
new ActionRowBuilder<ButtonBuilder>().addComponents(toggleButton, channelButton, testButton),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
container.addActionRowComponents(
|
container.addActionRowComponents(
|
||||||
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(typeSelect),
|
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(typeSelect),
|
||||||
);
|
);
|
||||||
@@ -145,6 +196,19 @@ const buildContainer = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kind === "welcome" && uiState.rolePickerOpen) {
|
||||||
|
const roleSelect = new RoleSelectMenuBuilder()
|
||||||
|
.setCustomId(customIds.roleSelect)
|
||||||
|
.setPlaceholder(ctx.ct("ui.rolePickerPlaceholder"))
|
||||||
|
.setMinValues(1)
|
||||||
|
.setMaxValues(25)
|
||||||
|
.setDisabled(disabled);
|
||||||
|
|
||||||
|
container.addActionRowComponents(
|
||||||
|
new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,11 +250,12 @@ export const createMemberMessagePanelExecute = (
|
|||||||
const customIds = createCustomIds(kind);
|
const customIds = createCustomIds(kind);
|
||||||
const uiState: MemberMessagePanelUiState = {
|
const uiState: MemberMessagePanelUiState = {
|
||||||
channelPickerOpen: false,
|
channelPickerOpen: false,
|
||||||
|
rolePickerOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const replyResult = await ctx.reply({
|
const replyResult = await ctx.reply({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
withResponse: true,
|
withResponse: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,7 +275,7 @@ export const createMemberMessagePanelExecute = (
|
|||||||
await replyMessage
|
await replyMessage
|
||||||
.edit({
|
.edit({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState, true)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState, true)],
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
};
|
};
|
||||||
@@ -237,16 +302,17 @@ export const createMemberMessagePanelExecute = (
|
|||||||
await saveConfig();
|
await saveConfig();
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interaction.customId === customIds.channelButton) {
|
if (interaction.customId === customIds.channelButton) {
|
||||||
uiState.channelPickerOpen = true;
|
uiState.channelPickerOpen = true;
|
||||||
|
uiState.rolePickerOpen = false;
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -255,7 +321,36 @@ export const createMemberMessagePanelExecute = (
|
|||||||
uiState.channelPickerOpen = false;
|
uiState.channelPickerOpen = false;
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "welcome" && interaction.customId === customIds.roleButton) {
|
||||||
|
uiState.rolePickerOpen = true;
|
||||||
|
uiState.channelPickerOpen = false;
|
||||||
|
await interaction.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "welcome" && interaction.customId === customIds.roleCancelButton) {
|
||||||
|
uiState.rolePickerOpen = false;
|
||||||
|
await interaction.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "welcome" && interaction.customId === customIds.roleClearButton) {
|
||||||
|
config.autoRoleIds = [];
|
||||||
|
await saveConfig();
|
||||||
|
await interaction.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -307,7 +402,18 @@ export const createMemberMessagePanelExecute = (
|
|||||||
await saveConfig();
|
await saveConfig();
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "welcome" && interaction.isRoleSelectMenu() && interaction.customId === customIds.roleSelect) {
|
||||||
|
config.autoRoleIds = sanitizeMemberMessageRoleIds([...config.autoRoleIds, ...interaction.values]);
|
||||||
|
uiState.rolePickerOpen = false;
|
||||||
|
await saveConfig();
|
||||||
|
await interaction.update({
|
||||||
|
flags: MessageFlags.IsComponentsV2,
|
||||||
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -327,7 +433,7 @@ export const createMemberMessagePanelExecute = (
|
|||||||
await saveConfig();
|
await saveConfig();
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
flags: MessageFlags.IsComponentsV2,
|
flags: MessageFlags.IsComponentsV2,
|
||||||
components: [buildContainer(ctx, customIds, config, uiState)],
|
components: [buildContainer(ctx, kind, customIds, config, uiState)],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const AVATAR_X = 42;
|
|||||||
const AVATAR_Y = (CARD_HEIGHT - AVATAR_SIZE) / 2;
|
const AVATAR_Y = (CARD_HEIGHT - AVATAR_SIZE) / 2;
|
||||||
const TEXT_X = AVATAR_X + AVATAR_SIZE + 56;
|
const TEXT_X = AVATAR_X + AVATAR_SIZE + 56;
|
||||||
const TEXT_MAX_WIDTH = CARD_WIDTH - TEXT_X - 36;
|
const TEXT_MAX_WIDTH = CARD_WIDTH - TEXT_X - 36;
|
||||||
|
const FONT_FAMILY = '"DejaVu Sans", sans-serif';
|
||||||
|
|
||||||
const fitFontSize = (
|
const fitFontSize = (
|
||||||
context: SKRSContext2D,
|
context: SKRSContext2D,
|
||||||
@@ -21,7 +22,7 @@ const fitFontSize = (
|
|||||||
let size = startSize;
|
let size = startSize;
|
||||||
|
|
||||||
while (size > minSize) {
|
while (size > minSize) {
|
||||||
context.font = `${fontWeight} ${size}px sans-serif`;
|
context.font = `${fontWeight} ${size}px ${FONT_FAMILY}`;
|
||||||
if (context.measureText(text).width <= maxWidth) {
|
if (context.measureText(text).width <= maxWidth) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ const drawAvatar = async (
|
|||||||
} catch {
|
} catch {
|
||||||
const initial = username.trim().charAt(0).toUpperCase() || "?";
|
const initial = username.trim().charAt(0).toUpperCase() || "?";
|
||||||
context.fillStyle = "#f8fafc";
|
context.fillStyle = "#f8fafc";
|
||||||
context.font = "700 92px sans-serif";
|
context.font = `700 92px ${FONT_FAMILY}`;
|
||||||
context.textAlign = "center";
|
context.textAlign = "center";
|
||||||
context.textBaseline = "middle";
|
context.textBaseline = "middle";
|
||||||
context.fillText(initial, AVATAR_X + AVATAR_SIZE / 2, AVATAR_Y + AVATAR_SIZE / 2);
|
context.fillText(initial, AVATAR_X + AVATAR_SIZE / 2, AVATAR_Y + AVATAR_SIZE / 2);
|
||||||
@@ -153,12 +154,12 @@ export const renderMemberMessageImage = async (input: MemberMessageImageInput):
|
|||||||
await drawAvatar(context, input.username, input.avatarUrl);
|
await drawAvatar(context, input.username, input.avatarUrl);
|
||||||
|
|
||||||
const headlineSize = fitFontSize(context, input.title, 84, 52, TEXT_MAX_WIDTH, 700);
|
const headlineSize = fitFontSize(context, input.title, 84, 52, TEXT_MAX_WIDTH, 700);
|
||||||
context.font = `700 ${headlineSize}px sans-serif`;
|
context.font = `700 ${headlineSize}px ${FONT_FAMILY}`;
|
||||||
context.fillStyle = "#f8fafc";
|
context.fillStyle = "#f8fafc";
|
||||||
context.fillText(input.title, TEXT_X, 116);
|
context.fillText(input.title, TEXT_X, 116);
|
||||||
|
|
||||||
const subtitleSize = fitFontSize(context, input.subtitle, 58, 30, TEXT_MAX_WIDTH, 500);
|
const subtitleSize = fitFontSize(context, input.subtitle, 58, 30, TEXT_MAX_WIDTH, 500);
|
||||||
context.font = `500 ${subtitleSize}px sans-serif`;
|
context.font = `500 ${subtitleSize}px ${FONT_FAMILY}`;
|
||||||
context.fillStyle = "#e5e7eb";
|
context.fillStyle = "#e5e7eb";
|
||||||
|
|
||||||
const subtitleLines = wrapText(context, input.subtitle, TEXT_MAX_WIDTH, 2);
|
const subtitleLines = wrapText(context, input.subtitle, TEXT_MAX_WIDTH, 2);
|
||||||
@@ -169,7 +170,7 @@ export const renderMemberMessageImage = async (input: MemberMessageImageInput):
|
|||||||
});
|
});
|
||||||
|
|
||||||
const usernameSize = fitFontSize(context, input.username, 54, 28, TEXT_MAX_WIDTH, 700);
|
const usernameSize = fitFontSize(context, input.username, 54, 28, TEXT_MAX_WIDTH, 700);
|
||||||
context.font = `700 ${usernameSize}px sans-serif`;
|
context.font = `700 ${usernameSize}px ${FONT_FAMILY}`;
|
||||||
context.fillStyle = input.kind === "welcome" ? "#93c5fd" : "#fda4af";
|
context.fillStyle = input.kind === "welcome" ? "#93c5fd" : "#fda4af";
|
||||||
context.fillText(input.username, TEXT_X, CARD_HEIGHT - 34);
|
context.fillText(input.username, TEXT_X, CARD_HEIGHT - 34);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
PermissionFlagsBits,
|
PermissionFlagsBits,
|
||||||
TextDisplayBuilder,
|
TextDisplayBuilder,
|
||||||
type Guild,
|
type Guild,
|
||||||
|
type GuildMember,
|
||||||
type MessageCreateOptions,
|
type MessageCreateOptions,
|
||||||
type User,
|
type User,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
@@ -13,6 +14,8 @@ import {
|
|||||||
import type { I18nService } from "../../i18n/index.js";
|
import type { I18nService } from "../../i18n/index.js";
|
||||||
import type { SupportedLang } from "../../types/command.js";
|
import type { SupportedLang } from "../../types/command.js";
|
||||||
import type {
|
import type {
|
||||||
|
AssignWelcomeAutoRolesInput,
|
||||||
|
AssignWelcomeAutoRolesResult,
|
||||||
DispatchMemberMessageInput,
|
DispatchMemberMessageInput,
|
||||||
DispatchMemberMessageResult,
|
DispatchMemberMessageResult,
|
||||||
MemberMessageConfig,
|
MemberMessageConfig,
|
||||||
@@ -21,6 +24,7 @@ import type {
|
|||||||
SendableChannel,
|
SendableChannel,
|
||||||
TemplateSuffix,
|
TemplateSuffix,
|
||||||
} from "../../types/memberMessages.js";
|
} from "../../types/memberMessages.js";
|
||||||
|
import { sanitizeMemberMessageRoleIds } from "../../validators/memberMessages.js";
|
||||||
import { renderMemberMessageImage } from "./imageRenderer.js";
|
import { renderMemberMessageImage } from "./imageRenderer.js";
|
||||||
import type { MemberMessageRepository } from "./repository.js";
|
import type { MemberMessageRepository } from "./repository.js";
|
||||||
|
|
||||||
@@ -117,6 +121,35 @@ const resolveTemplate = (
|
|||||||
return translated === key ? defaultTemplate(kind, suffix, vars) : translated;
|
return translated === key ? defaultTemplate(kind, suffix, vars) : translated;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resolveNonEmptyTemplate = (
|
||||||
|
i18n: I18nService | undefined,
|
||||||
|
lang: SupportedLang,
|
||||||
|
kind: MemberMessageKind,
|
||||||
|
suffix: TemplateSuffix,
|
||||||
|
vars: Record<string, string>,
|
||||||
|
): string => {
|
||||||
|
const resolved = resolveTemplate(i18n, lang, kind, suffix, vars).trim();
|
||||||
|
if (resolved.length > 0) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback = defaultTemplate(kind, suffix, vars).trim();
|
||||||
|
return fallback.length > 0 ? fallback : suffix;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveAssignableRoleId = async (member: GuildMember, roleId: string): Promise<string | null> => {
|
||||||
|
if (roleId === member.guild.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = member.guild.roles.cache.get(roleId) ?? await member.guild.roles.fetch(roleId).catch(() => null);
|
||||||
|
if (!role || !role.editable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return role.id;
|
||||||
|
};
|
||||||
|
|
||||||
const buildMemberMessagePayload = async (
|
const buildMemberMessagePayload = async (
|
||||||
i18n: I18nService | undefined,
|
i18n: I18nService | undefined,
|
||||||
lang: SupportedLang,
|
lang: SupportedLang,
|
||||||
@@ -165,10 +198,13 @@ const buildMemberMessagePayload = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageTitle = resolveNonEmptyTemplate(i18n, lang, kind, "imageTitle", vars);
|
||||||
|
const imageSubtitle = resolveNonEmptyTemplate(i18n, lang, kind, "imageDescription", vars);
|
||||||
|
|
||||||
const imageBuffer = await renderMemberMessageImage({
|
const imageBuffer = await renderMemberMessageImage({
|
||||||
kind,
|
kind,
|
||||||
title: resolveTemplate(i18n, lang, kind, "imageTitle", vars),
|
title: imageTitle,
|
||||||
subtitle: resolveTemplate(i18n, lang, kind, "imageDescription", vars),
|
subtitle: imageSubtitle,
|
||||||
username: user.globalName ?? user.username,
|
username: user.globalName ?? user.username,
|
||||||
avatarUrl: user.displayAvatarURL({ extension: "png", size: 512 }),
|
avatarUrl: user.displayAvatarURL({ extension: "png", size: 512 }),
|
||||||
});
|
});
|
||||||
@@ -206,6 +242,99 @@ export class MemberMessageService {
|
|||||||
await this.repository.deleteByBotGuild(botId, guildId);
|
await this.repository.deleteByBotGuild(botId, guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async assignWelcomeAutoRoles(input: AssignWelcomeAutoRolesInput): Promise<AssignWelcomeAutoRolesResult> {
|
||||||
|
const botId = this.resolveBotId(input.client);
|
||||||
|
if (!botId) {
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "bot_not_ready",
|
||||||
|
configuredRoleIds: [],
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await this.repository.getByBotGuildKind(botId, input.member.guild.id, "welcome");
|
||||||
|
const configuredRoleIds = sanitizeMemberMessageRoleIds(config.autoRoleIds);
|
||||||
|
|
||||||
|
if (configuredRoleIds.length === 0) {
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "no_roles_configured",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const me = input.member.guild.members.me;
|
||||||
|
if (!me || !me.permissions.has(PermissionFlagsBits.ManageRoles)) {
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "missing_permissions",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds: [...configuredRoleIds],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!input.member.manageable) {
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "member_not_manageable",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds: [...configuredRoleIds],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedRoleIds = await Promise.all(
|
||||||
|
configuredRoleIds.map((roleId) => resolveAssignableRoleId(input.member, roleId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
const appliedRoleIds = resolvedRoleIds.filter((roleId): roleId is string => roleId !== null);
|
||||||
|
const skippedRoleIds = configuredRoleIds.filter((roleId) => !appliedRoleIds.includes(roleId));
|
||||||
|
|
||||||
|
if (appliedRoleIds.length === 0) {
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "no_assignable_roles",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await input.member.roles.add(appliedRoleIds, "welcome auto roles");
|
||||||
|
return {
|
||||||
|
assigned: true,
|
||||||
|
reason: "assigned",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds,
|
||||||
|
skippedRoleIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (hasCode(error) && error.code === 50013) {
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "missing_permissions",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds: [...configuredRoleIds],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
assigned: false,
|
||||||
|
reason: "assign_failed",
|
||||||
|
configuredRoleIds,
|
||||||
|
appliedRoleIds: [],
|
||||||
|
skippedRoleIds: [...configuredRoleIds],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async dispatch(input: DispatchMemberMessageInput): Promise<DispatchMemberMessageResult> {
|
public async dispatch(input: DispatchMemberMessageInput): Promise<DispatchMemberMessageResult> {
|
||||||
const botId = this.resolveBotId(input.client);
|
const botId = this.resolveBotId(input.client);
|
||||||
if (!botId) {
|
if (!botId) {
|
||||||
|
|||||||
+8
-1
@@ -209,7 +209,8 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"channel": "Channel",
|
"channel": "Channel",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"channelPicker": "Channel picker"
|
"channelPicker": "Channel picker",
|
||||||
|
"autoRoles": "Auto roles"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@@ -217,12 +218,18 @@
|
|||||||
"disabled": "🔴 Disabled"
|
"disabled": "🔴 Disabled"
|
||||||
},
|
},
|
||||||
"channelNotConfigured": "#not-configured",
|
"channelNotConfigured": "#not-configured",
|
||||||
|
"autoRolesNotConfigured": "No auto role configured",
|
||||||
"channelPickerPlaceholder": "Select a welcome channel",
|
"channelPickerPlaceholder": "Select a welcome channel",
|
||||||
"channelPickerHint": "Use the select menu below to choose the destination channel.",
|
"channelPickerHint": "Use the select menu below to choose the destination channel.",
|
||||||
|
"rolePickerPlaceholder": "Select one or more auto roles",
|
||||||
|
"rolePickerHint": "Use the role select menu below to append roles to the auto-role list.",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"toggle": "Toggle status",
|
"toggle": "Toggle status",
|
||||||
"channel": "Set channel",
|
"channel": "Set channel",
|
||||||
"channelCancel": "Close channel picker",
|
"channelCancel": "Close channel picker",
|
||||||
|
"roles": "Add auto roles",
|
||||||
|
"rolesCancel": "Close role picker",
|
||||||
|
"rolesClear": "Clear auto roles",
|
||||||
"test": "Test"
|
"test": "Test"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
+8
-1
@@ -209,7 +209,8 @@
|
|||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"channel": "Canal",
|
"channel": "Canal",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"channelPicker": "Selector de canal"
|
"channelPicker": "Selector de canal",
|
||||||
|
"autoRoles": "Roles automaticos"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@@ -217,12 +218,18 @@
|
|||||||
"disabled": "🔴 Desactivado"
|
"disabled": "🔴 Desactivado"
|
||||||
},
|
},
|
||||||
"channelNotConfigured": "#sin-configurar",
|
"channelNotConfigured": "#sin-configurar",
|
||||||
|
"autoRolesNotConfigured": "Ningun rol automatico configurado",
|
||||||
"channelPickerPlaceholder": "Selecciona un canal de bienvenida",
|
"channelPickerPlaceholder": "Selecciona un canal de bienvenida",
|
||||||
"channelPickerHint": "Usa el menu de seleccion de abajo para elegir el canal destino.",
|
"channelPickerHint": "Usa el menu de seleccion de abajo para elegir el canal destino.",
|
||||||
|
"rolePickerPlaceholder": "Selecciona uno o varios roles automaticos",
|
||||||
|
"rolePickerHint": "Usa el menu de roles de abajo para agregarlos a la lista automatica.",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"toggle": "Alternar estado",
|
"toggle": "Alternar estado",
|
||||||
"channel": "Configurar canal",
|
"channel": "Configurar canal",
|
||||||
"channelCancel": "Cerrar selector",
|
"channelCancel": "Cerrar selector",
|
||||||
|
"roles": "Agregar roles auto",
|
||||||
|
"rolesCancel": "Cerrar selector de roles",
|
||||||
|
"rolesClear": "Limpiar roles auto",
|
||||||
"test": "Probar"
|
"test": "Probar"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
+8
-1
@@ -209,7 +209,8 @@
|
|||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"channel": "Salon",
|
"channel": "Salon",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"channelPicker": "Selection du salon"
|
"channelPicker": "Selection du salon",
|
||||||
|
"autoRoles": "Roles auto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
@@ -217,12 +218,18 @@
|
|||||||
"disabled": "🔴 Desactive"
|
"disabled": "🔴 Desactive"
|
||||||
},
|
},
|
||||||
"channelNotConfigured": "#non-configure",
|
"channelNotConfigured": "#non-configure",
|
||||||
|
"autoRolesNotConfigured": "Aucun role auto configure",
|
||||||
"channelPickerPlaceholder": "Selectionne un salon de bienvenue",
|
"channelPickerPlaceholder": "Selectionne un salon de bienvenue",
|
||||||
"channelPickerHint": "Choisis le salon dans le select menu ci-dessous.",
|
"channelPickerHint": "Choisis le salon dans le select menu ci-dessous.",
|
||||||
|
"rolePickerPlaceholder": "Selectionne un ou plusieurs roles auto",
|
||||||
|
"rolePickerHint": "Choisis les roles dans le select menu ci-dessous pour les ajouter a la liste auto.",
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"toggle": "Toggle statut",
|
"toggle": "Toggle statut",
|
||||||
"channel": "Configurer salon",
|
"channel": "Configurer salon",
|
||||||
"channelCancel": "Fermer select salon",
|
"channelCancel": "Fermer select salon",
|
||||||
|
"roles": "Ajouter roles auto",
|
||||||
|
"rolesCancel": "Fermer select roles",
|
||||||
|
"rolesClear": "Vider roles auto",
|
||||||
"test": "Tester"
|
"test": "Tester"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
Client,
|
Client,
|
||||||
Guild,
|
Guild,
|
||||||
|
GuildMember,
|
||||||
Message,
|
Message,
|
||||||
MessageCreateOptions,
|
MessageCreateOptions,
|
||||||
User,
|
User,
|
||||||
@@ -15,6 +16,7 @@ export interface MemberMessageConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
channelId: string | null;
|
channelId: string | null;
|
||||||
messageType: MemberMessageRenderType;
|
messageType: MemberMessageRenderType;
|
||||||
|
autoRoleIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DispatchMemberMessageFailureReason =
|
export type DispatchMemberMessageFailureReason =
|
||||||
@@ -41,6 +43,27 @@ export interface DispatchMemberMessageInput {
|
|||||||
ignoreEnabled?: boolean;
|
ignoreEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AssignWelcomeAutoRolesFailureReason =
|
||||||
|
| "bot_not_ready"
|
||||||
|
| "missing_permissions"
|
||||||
|
| "member_not_manageable"
|
||||||
|
| "no_roles_configured"
|
||||||
|
| "no_assignable_roles"
|
||||||
|
| "assign_failed";
|
||||||
|
|
||||||
|
export interface AssignWelcomeAutoRolesInput {
|
||||||
|
client: Client;
|
||||||
|
member: GuildMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignWelcomeAutoRolesResult {
|
||||||
|
assigned: boolean;
|
||||||
|
reason: AssignWelcomeAutoRolesFailureReason | "assigned";
|
||||||
|
configuredRoleIds: string[];
|
||||||
|
appliedRoleIds: string[];
|
||||||
|
skippedRoleIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SendableChannel {
|
export interface SendableChannel {
|
||||||
send: (payload: string | MessageCreateOptions) => Promise<unknown>;
|
send: (payload: string | MessageCreateOptions) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
@@ -66,19 +89,25 @@ export interface MemberMessageRow {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
channel_id: string | null;
|
channel_id: string | null;
|
||||||
message_type: string;
|
message_type: string;
|
||||||
|
auto_role_ids: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemberMessageCustomIds {
|
export interface MemberMessageCustomIds {
|
||||||
toggleButton: string;
|
toggleButton: string;
|
||||||
channelButton: string;
|
channelButton: string;
|
||||||
channelCancelButton: string;
|
channelCancelButton: string;
|
||||||
|
roleButton: string;
|
||||||
|
roleCancelButton: string;
|
||||||
|
roleClearButton: string;
|
||||||
typeSelect: string;
|
typeSelect: string;
|
||||||
channelSelect: string;
|
channelSelect: string;
|
||||||
|
roleSelect: string;
|
||||||
testButton: string;
|
testButton: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemberMessagePanelUiState {
|
export interface MemberMessagePanelUiState {
|
||||||
channelPickerOpen: boolean;
|
channelPickerOpen: boolean;
|
||||||
|
rolePickerOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemberMessagePanelSession {
|
export interface MemberMessagePanelSession {
|
||||||
|
|||||||
@@ -4,16 +4,34 @@ import type {
|
|||||||
MemberMessageRenderType,
|
MemberMessageRenderType,
|
||||||
} from "../types/memberMessages.js";
|
} from "../types/memberMessages.js";
|
||||||
|
|
||||||
|
const DISCORD_SNOWFLAKE_REGEX = /^\d{17,20}$/;
|
||||||
|
|
||||||
export const MEMBER_MESSAGE_KINDS: readonly MemberMessageKind[] = ["welcome", "goodbye"];
|
export const MEMBER_MESSAGE_KINDS: readonly MemberMessageKind[] = ["welcome", "goodbye"];
|
||||||
|
|
||||||
export const MEMBER_MESSAGE_RENDER_TYPES: readonly MemberMessageRenderType[] = ["simple", "embed", "container", "image"];
|
export const MEMBER_MESSAGE_RENDER_TYPES: readonly MemberMessageRenderType[] = ["simple", "embed", "container", "image"];
|
||||||
|
|
||||||
export const DEFAULT_MEMBER_MESSAGE_RENDER_TYPE: MemberMessageRenderType = "simple";
|
export const DEFAULT_MEMBER_MESSAGE_RENDER_TYPE: MemberMessageRenderType = "simple";
|
||||||
|
|
||||||
|
export const sanitizeMemberMessageRoleIds = (roleIds: readonly string[]): string[] => {
|
||||||
|
const uniqueRoleIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const rawRoleId of roleIds) {
|
||||||
|
const roleId = rawRoleId.trim();
|
||||||
|
if (!DISCORD_SNOWFLAKE_REGEX.test(roleId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueRoleIds.add(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...uniqueRoleIds];
|
||||||
|
};
|
||||||
|
|
||||||
export const createDefaultMemberMessageConfig = (): MemberMessageConfig => ({
|
export const createDefaultMemberMessageConfig = (): MemberMessageConfig => ({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
channelId: null,
|
channelId: null,
|
||||||
messageType: DEFAULT_MEMBER_MESSAGE_RENDER_TYPE,
|
messageType: DEFAULT_MEMBER_MESSAGE_RENDER_TYPE,
|
||||||
|
autoRoleIds: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isMemberMessageKindValue = (value: string): value is MemberMessageKind => {
|
export const isMemberMessageKindValue = (value: string): value is MemberMessageKind => {
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createDefaultMemberMessageConfig,
|
||||||
|
sanitizeMemberMessageRoleIds,
|
||||||
|
} from "../src/validators/memberMessages.js";
|
||||||
|
|
||||||
|
test("createDefaultMemberMessageConfig initialise les roles auto vides", () => {
|
||||||
|
const config = createDefaultMemberMessageConfig();
|
||||||
|
|
||||||
|
assert.equal(config.enabled, false);
|
||||||
|
assert.equal(config.channelId, null);
|
||||||
|
assert.equal(config.messageType, "simple");
|
||||||
|
assert.deepEqual(config.autoRoleIds, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sanitizeMemberMessageRoleIds filtre les valeurs invalides et dedupe", () => {
|
||||||
|
const roleIds = sanitizeMemberMessageRoleIds([
|
||||||
|
"123456789012345678",
|
||||||
|
"123456789012345678",
|
||||||
|
" 223456789012345678 ",
|
||||||
|
"not-a-role",
|
||||||
|
"",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(roleIds, ["123456789012345678", "223456789012345678"]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user