feat: add color2 gradient color support and defer interactions for slow commands
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "booster_roles" ADD COLUMN "color2" text;
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"id": "619da77d-9ef1-42fe-9563-5858b7039de2",
|
||||||
|
"prevId": "66a1346c-4510-4ec6-a055-a3d0bcf803c3",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.booster_roles": {
|
||||||
|
"name": "booster_roles",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"guild_id": {
|
||||||
|
"name": "guild_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role_id": {
|
||||||
|
"name": "role_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"name": "color",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"color2": {
|
||||||
|
"name": "color2",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"name": "icon",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "bigint",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"booster_roles_guild_user_idx": {
|
||||||
|
"name": "booster_roles_guild_user_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "guild_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"booster_roles_guild_role_idx": {
|
||||||
|
"name": "booster_roles_guild_role_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "guild_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expression": "role_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": true,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1779352840357,
|
"when": 1779352840357,
|
||||||
"tag": "0001_misty_wraith",
|
"tag": "0001_misty_wraith",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780681661765,
|
||||||
|
"tag": "0002_regular_green_goblin",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,8 @@ export const boosterRoleCommand = new SlashCommandBuilder()
|
|||||||
.setName("claim")
|
.setName("claim")
|
||||||
.setDescription("Claim a new custom cosmetic booster role")
|
.setDescription("Claim a new custom cosmetic booster role")
|
||||||
.addStringOption((option) => option.setName("name").setDescription("Role name").setRequired(true))
|
.addStringOption((option) => option.setName("name").setDescription("Role name").setRequired(true))
|
||||||
.addStringOption((option) => option.setName("color").setDescription("Hex color like #AABBCC"))
|
.addStringOption((option) => option.setName("color").setDescription("Primary hex color like #AABBCC"))
|
||||||
|
.addStringOption((option) => option.setName("color2").setDescription("Secondary gradient hex color like #CCDDEE"))
|
||||||
.addAttachmentOption((option) => option.setName("icon").setDescription("Optional role icon image"))
|
.addAttachmentOption((option) => option.setName("icon").setDescription("Optional role icon image"))
|
||||||
)
|
)
|
||||||
.addSubcommand((command) =>
|
.addSubcommand((command) =>
|
||||||
@@ -21,7 +22,8 @@ export const boosterRoleCommand = new SlashCommandBuilder()
|
|||||||
command
|
command
|
||||||
.setName("recolor")
|
.setName("recolor")
|
||||||
.setDescription("Recolor your bot-managed booster role")
|
.setDescription("Recolor your bot-managed booster role")
|
||||||
.addStringOption((option) => option.setName("color").setDescription("Hex color like #AABBCC").setRequired(true))
|
.addStringOption((option) => option.setName("color").setDescription("Primary hex color like #AABBCC").setRequired(true))
|
||||||
|
.addStringOption((option) => option.setName("color2").setDescription("Secondary gradient hex color like #CCDDEE"))
|
||||||
)
|
)
|
||||||
.addSubcommand((command) =>
|
.addSubcommand((command) =>
|
||||||
command
|
command
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class FakeInteraction implements ChatInputInteractionLike {
|
|||||||
guildId: string | null = "guild";
|
guildId: string | null = "guild";
|
||||||
memberPermissions: { has(permission: bigint): boolean } | null = null;
|
memberPermissions: { has(permission: bigint): boolean } | null = null;
|
||||||
replies: Reply[] = [];
|
replies: Reply[] = [];
|
||||||
|
deferred = false;
|
||||||
|
replied = false;
|
||||||
|
|
||||||
constructor(private readonly subcommand: string, private readonly values: Record<string, unknown> = {}) {}
|
constructor(private readonly subcommand: string, private readonly values: Record<string, unknown> = {}) {}
|
||||||
|
|
||||||
@@ -19,6 +21,15 @@ class FakeInteraction implements ChatInputInteractionLike {
|
|||||||
|
|
||||||
async reply(reply: Reply): Promise<void> {
|
async reply(reply: Reply): Promise<void> {
|
||||||
this.replies.push(reply);
|
this.replies.push(reply);
|
||||||
|
this.replied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deferReply(_input: { flags: MessageFlags.Ephemeral }): Promise<void> {
|
||||||
|
this.deferred = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async editReply(input: { content: string }): Promise<void> {
|
||||||
|
this.replies.push({ content: input.content, flags: MessageFlags.Ephemeral });
|
||||||
}
|
}
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
@@ -55,13 +66,14 @@ class FakeService implements BoosterRoleCommandService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("handleInteraction", () => {
|
describe("handleInteraction", () => {
|
||||||
test("replies to claim command", async () => {
|
test("replies to claim command (deferred)", async () => {
|
||||||
const interaction = new FakeInteraction("claim", { name: "Test", color: "#AABBCC" });
|
const interaction = new FakeInteraction("claim", { name: "Test", color: "#AABBCC" });
|
||||||
const service = new FakeService();
|
const service = new FakeService();
|
||||||
|
|
||||||
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
||||||
|
|
||||||
expect(service.calls).toEqual(["claim"]);
|
expect(service.calls).toEqual(["claim"]);
|
||||||
|
expect(interaction.deferred).toBe(true);
|
||||||
expect(interaction.replies[0]).toEqual({ content: "Booster role created: <@&role-1>", flags: MessageFlags.Ephemeral });
|
expect(interaction.replies[0]).toEqual({ content: "Booster role created: <@&role-1>", flags: MessageFlags.Ephemeral });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,7 +121,7 @@ describe("handleInteraction", () => {
|
|||||||
expect(interaction.replies[0]).toEqual({ content: "Administrator permission is required", flags: MessageFlags.Ephemeral });
|
expect(interaction.replies[0]).toEqual({ content: "Administrator permission is required", flags: MessageFlags.Ephemeral });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("turns service errors into private replies", async () => {
|
test("turns service errors into private replies (uses editReply when deferred)", async () => {
|
||||||
const interaction = new FakeInteraction("claim", { name: "VIP" });
|
const interaction = new FakeInteraction("claim", { name: "VIP" });
|
||||||
const service = new FakeService();
|
const service = new FakeService();
|
||||||
service.claimRole = async () => {
|
service.claimRole = async () => {
|
||||||
@@ -118,10 +130,12 @@ describe("handleInteraction", () => {
|
|||||||
|
|
||||||
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
||||||
|
|
||||||
|
// claim is deferred, so errors use editReply
|
||||||
|
expect(interaction.deferred).toBe(true);
|
||||||
expect(interaction.replies[0]).toEqual({ content: "Role name is already used", flags: MessageFlags.Ephemeral });
|
expect(interaction.replies[0]).toEqual({ content: "Role name is already used", flags: MessageFlags.Ephemeral });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hides failed query details from user replies", async () => {
|
test("hides failed query details from user replies (deferred)", async () => {
|
||||||
const interaction = new FakeInteraction("claim", { name: "VIP" });
|
const interaction = new FakeInteraction("claim", { name: "VIP" });
|
||||||
const service = new FakeService();
|
const service = new FakeService();
|
||||||
service.claimRole = async () => {
|
service.claimRole = async () => {
|
||||||
@@ -130,6 +144,20 @@ describe("handleInteraction", () => {
|
|||||||
|
|
||||||
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
||||||
|
|
||||||
|
expect(interaction.deferred).toBe(true);
|
||||||
expect(interaction.replies[0]).toEqual({ content: "Failed to save booster role. Any created role was cleaned up. Try again.", flags: MessageFlags.Ephemeral });
|
expect(interaction.replies[0]).toEqual({ content: "Failed to save booster role. Any created role was cleaned up. Try again.", flags: MessageFlags.Ephemeral });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("non-deferred commands still use reply for errors", async () => {
|
||||||
|
const interaction = new FakeInteraction("rename", { name: "New Name" });
|
||||||
|
const service = new FakeService();
|
||||||
|
service.renameRole = async () => {
|
||||||
|
throw new Error("Some error");
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
||||||
|
|
||||||
|
expect(interaction.deferred).toBe(false);
|
||||||
|
expect(interaction.replies[0]).toEqual({ content: "Some error", flags: MessageFlags.Ephemeral });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ export type ChatInputInteractionLike = {
|
|||||||
user: { id: string };
|
user: { id: string };
|
||||||
memberPermissions: { has(permission: bigint): boolean } | null;
|
memberPermissions: { has(permission: bigint): boolean } | null;
|
||||||
isChatInputCommand(): boolean;
|
isChatInputCommand(): boolean;
|
||||||
|
deferred: boolean;
|
||||||
|
replied: boolean;
|
||||||
reply(input: { content: string; flags: MessageFlags.Ephemeral }): Promise<unknown>;
|
reply(input: { content: string; flags: MessageFlags.Ephemeral }): Promise<unknown>;
|
||||||
|
deferReply(input: { flags: MessageFlags.Ephemeral }): Promise<unknown>;
|
||||||
|
editReply(input: { content: string }): Promise<unknown>;
|
||||||
options: {
|
options: {
|
||||||
getSubcommand(): string;
|
getSubcommand(): string;
|
||||||
getString(name: string): string | null;
|
getString(name: string): string | null;
|
||||||
@@ -18,9 +22,17 @@ export type ChatInputInteractionLike = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type BoosterRoleCommandService = {
|
export type BoosterRoleCommandService = {
|
||||||
claimRole(input: { guildId: string; userId: string; name: string; color: string | null; icon?: RoleIcon | null; isBoosting: boolean }): Promise<BoosterRoleRecord | { roleId: string }>;
|
claimRole(input: {
|
||||||
|
guildId: string;
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
color: string | null;
|
||||||
|
color2?: string | null;
|
||||||
|
icon?: RoleIcon | null;
|
||||||
|
isBoosting: boolean;
|
||||||
|
}): Promise<BoosterRoleRecord | { roleId: string }>;
|
||||||
renameRole(input: { guildId: string; userId: string; name: string }): Promise<void>;
|
renameRole(input: { guildId: string; userId: string; name: string }): Promise<void>;
|
||||||
recolorRole(input: { guildId: string; userId: string; color: string }): Promise<void>;
|
recolorRole(input: { guildId: string; userId: string; color: string; color2?: string | null }): Promise<void>;
|
||||||
setRoleIcon(input: { guildId: string; userId: string; icon: RoleIcon }): Promise<void>;
|
setRoleIcon(input: { guildId: string; userId: string; icon: RoleIcon }): Promise<void>;
|
||||||
deleteRole(input: { guildId: string; userId: string }): Promise<void>;
|
deleteRole(input: { guildId: string; userId: string }): Promise<void>;
|
||||||
};
|
};
|
||||||
@@ -41,6 +53,12 @@ export async function handleInteraction(
|
|||||||
const userId = interaction.user.id;
|
const userId = interaction.user.id;
|
||||||
const subcommand = interaction.options.getSubcommand();
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
// Defer reply for potentially slow commands (claim, icon) to avoid 10062 "Unknown interaction"
|
||||||
|
const shouldDefer = subcommand === "claim" || subcommand === "icon";
|
||||||
|
if (shouldDefer) {
|
||||||
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
|
|
||||||
if (subcommand === "claim") {
|
if (subcommand === "claim") {
|
||||||
logger.info("Handling booster-role command", { guildId, userId, subcommand });
|
logger.info("Handling booster-role command", { guildId, userId, subcommand });
|
||||||
const role = await service.claimRole({
|
const role = await service.claimRole({
|
||||||
@@ -48,10 +66,11 @@ export async function handleInteraction(
|
|||||||
userId,
|
userId,
|
||||||
name: requireString(interaction, "name"),
|
name: requireString(interaction, "name"),
|
||||||
color: interaction.options.getString("color"),
|
color: interaction.options.getString("color"),
|
||||||
|
color2: interaction.options.getString("color2"),
|
||||||
icon: optionalIcon(interaction, "icon"),
|
icon: optionalIcon(interaction, "icon"),
|
||||||
isBoosting: await deps.isBoosting(guildId, userId)
|
isBoosting: await deps.isBoosting(guildId, userId)
|
||||||
});
|
});
|
||||||
await interaction.reply({ content: `Booster role created: <@&${role.roleId}>`, flags: MessageFlags.Ephemeral });
|
await interaction.editReply({ content: `Booster role created: <@&${role.roleId}>` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +83,7 @@ export async function handleInteraction(
|
|||||||
|
|
||||||
if (subcommand === "recolor") {
|
if (subcommand === "recolor") {
|
||||||
logger.info("Handling booster-role command", { guildId, userId, subcommand });
|
logger.info("Handling booster-role command", { guildId, userId, subcommand });
|
||||||
await service.recolorRole({ guildId, userId, color: requireString(interaction, "color") });
|
await service.recolorRole({ guildId, userId, color: requireString(interaction, "color"), color2: interaction.options.getString("color2") });
|
||||||
await interaction.reply({ content: "Booster role color updated.", flags: MessageFlags.Ephemeral });
|
await interaction.reply({ content: "Booster role color updated.", flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -72,7 +91,7 @@ export async function handleInteraction(
|
|||||||
if (subcommand === "icon") {
|
if (subcommand === "icon") {
|
||||||
logger.info("Handling booster-role command", { guildId, userId, subcommand });
|
logger.info("Handling booster-role command", { guildId, userId, subcommand });
|
||||||
await service.setRoleIcon({ guildId, userId, icon: requireIcon(interaction, "image") });
|
await service.setRoleIcon({ guildId, userId, icon: requireIcon(interaction, "image") });
|
||||||
await interaction.reply({ content: "Booster role icon updated.", flags: MessageFlags.Ephemeral });
|
await interaction.editReply({ content: "Booster role icon updated." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +114,12 @@ export async function handleInteraction(
|
|||||||
throw new Error("Unknown booster-role subcommand");
|
throw new Error("Unknown booster-role subcommand");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Booster-role command failed", { error });
|
logger.warn("Booster-role command failed", { error });
|
||||||
await interaction.reply({ content: toUserErrorMessage(error), flags: MessageFlags.Ephemeral });
|
// If already deferred, edit the reply instead of replying anew
|
||||||
|
if (interaction.deferred) {
|
||||||
|
await interaction.editReply({ content: toUserErrorMessage(error) });
|
||||||
|
} else {
|
||||||
|
await interaction.reply({ content: toUserErrorMessage(error), flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ class FakeRoleRepository implements RoleRepository {
|
|||||||
return [...this.roles.values()];
|
return [...this.roles.values()];
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRole(input: { name: string; color: string | null; permissions: string[]; position: number }) {
|
async createRole(input: { name: string; color: string | null; colors?: { primaryColor: string; secondaryColor?: string; tertiaryColor?: string } | null; permissions: string[]; position: number }) {
|
||||||
const id = `created-${this.roles.size + 1}`;
|
const id = `created-${this.roles.size + 1}`;
|
||||||
this.roles.set(id, { id, ...input });
|
this.roles.set(id, { id, ...input });
|
||||||
return { id };
|
return { id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRole(roleId: string, input: { name?: string; color?: string | null; icon?: string | null }) {
|
async updateRole(roleId: string, input: { name?: string; color?: string | null; colors?: { primaryColor: string; secondaryColor?: string; tertiaryColor?: string } | null; icon?: string | null }) {
|
||||||
const role = this.roles.get(roleId);
|
const role = this.roles.get(roleId);
|
||||||
if (!role) throw new Error("Role does not exist");
|
if (!role) throw new Error("Role does not exist");
|
||||||
this.roles.set(roleId, { ...role, ...input });
|
this.roles.set(roleId, { ...role, ...input });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
assertRoleNameIsAvailable,
|
assertRoleNameIsAvailable,
|
||||||
assertRolePositionIsSafe,
|
assertRolePositionIsSafe,
|
||||||
normalizeHexColor,
|
normalizeHexColor,
|
||||||
|
normalizeOptionalHexColor,
|
||||||
validateRoleName,
|
validateRoleName,
|
||||||
type ExistingRole
|
type ExistingRole
|
||||||
} from "../domain/roleGuards";
|
} from "../domain/roleGuards";
|
||||||
@@ -13,6 +14,7 @@ export type BoosterRoleRecord = {
|
|||||||
roleId: string;
|
roleId: string;
|
||||||
name: string;
|
name: string;
|
||||||
color: string | null;
|
color: string | null;
|
||||||
|
color2: string | null;
|
||||||
icon: string | null;
|
icon: string | null;
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
@@ -26,8 +28,8 @@ export type BoosterRoleStore = {
|
|||||||
|
|
||||||
export type RoleRepository = {
|
export type RoleRepository = {
|
||||||
listRoles(): Promise<ExistingRole[]>;
|
listRoles(): Promise<ExistingRole[]>;
|
||||||
createRole(input: { name: string; color: string | null; permissions: string[]; position: number }): Promise<{ id: string }>;
|
createRole(input: { name: string; color: string | null; colors?: { primaryColor: string; secondaryColor?: string; tertiaryColor?: string } | null; permissions: string[]; position: number }): Promise<{ id: string }>;
|
||||||
updateRole(roleId: string, input: { name?: string; color?: string | null; icon?: string | null }): Promise<void>;
|
updateRole(roleId: string, input: { name?: string; color?: string | null; colors?: { primaryColor: string; secondaryColor?: string; tertiaryColor?: string } | null; icon?: string | null }): Promise<void>;
|
||||||
assignRole(userId: string, roleId: string): Promise<void>;
|
assignRole(userId: string, roleId: string): Promise<void>;
|
||||||
removeRole(userId: string, roleId: string): Promise<void>;
|
removeRole(userId: string, roleId: string): Promise<void>;
|
||||||
deleteRole(roleId: string): Promise<void>;
|
deleteRole(roleId: string): Promise<void>;
|
||||||
@@ -61,6 +63,7 @@ export class BoosterRoleService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
color: string | null;
|
color: string | null;
|
||||||
|
color2?: string | null;
|
||||||
icon?: RoleIcon | null;
|
icon?: RoleIcon | null;
|
||||||
isBoosting: boolean;
|
isBoosting: boolean;
|
||||||
}): Promise<BoosterRoleRecord> {
|
}): Promise<BoosterRoleRecord> {
|
||||||
@@ -73,13 +76,15 @@ export class BoosterRoleService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = validateRoleName(input.name);
|
const name = validateRoleName(input.name);
|
||||||
const color = input.color ? normalizeHexColor(input.color) : null;
|
const color = normalizeOptionalHexColor(input.color);
|
||||||
|
const color2 = normalizeOptionalHexColor(input.color2 ?? null);
|
||||||
|
const colors = resolveGradientColors(color, color2);
|
||||||
assertRoleNameIsAvailable(name, await this.roles.listRoles());
|
assertRoleNameIsAvailable(name, await this.roles.listRoles());
|
||||||
|
|
||||||
const position = this.options.anchorPosition - 1;
|
const position = this.options.anchorPosition - 1;
|
||||||
assertRolePositionIsSafe(position, this.options.anchorPosition);
|
assertRolePositionIsSafe(position, this.options.anchorPosition);
|
||||||
|
|
||||||
const role = await this.roles.createRole({ name, color, permissions: [], position });
|
const role = await this.roles.createRole({ name, color, colors, permissions: [], position });
|
||||||
let assigned = false;
|
let assigned = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,6 +103,7 @@ export class BoosterRoleService {
|
|||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
name,
|
name,
|
||||||
color,
|
color,
|
||||||
|
color2,
|
||||||
icon: input.icon?.dataUri ?? null,
|
icon: input.icon?.dataUri ?? null,
|
||||||
createdAt: timestamp,
|
createdAt: timestamp,
|
||||||
updatedAt: timestamp
|
updatedAt: timestamp
|
||||||
@@ -119,10 +125,13 @@ export class BoosterRoleService {
|
|||||||
await this.roles.updateRole(record.roleId, { name });
|
await this.roles.updateRole(record.roleId, { name });
|
||||||
}
|
}
|
||||||
|
|
||||||
async recolorRole(input: { guildId: string; userId: string; color: string }): Promise<void> {
|
async recolorRole(input: { guildId: string; userId: string; color: string; color2?: string | null }): Promise<void> {
|
||||||
const { guildId, userId, color } = input;
|
const { guildId, userId, color, color2 } = input;
|
||||||
const record = await this.getUserRecord(guildId, userId);
|
const record = await this.getUserRecord(guildId, userId);
|
||||||
await this.roles.updateRole(record.roleId, { color: normalizeHexColor(color) });
|
const primaryColor = normalizeHexColor(color);
|
||||||
|
const secondaryColor = normalizeOptionalHexColor(color2 ?? null);
|
||||||
|
const colors = resolveGradientColors(primaryColor, secondaryColor);
|
||||||
|
await this.roles.updateRole(record.roleId, { colors });
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRoleIcon(input: { guildId: string; userId: string; icon: RoleIcon }): Promise<void> {
|
async setRoleIcon(input: { guildId: string; userId: string; icon: RoleIcon }): Promise<void> {
|
||||||
@@ -182,3 +191,11 @@ async function ignoreRollbackError(action: () => Promise<void>): Promise<void> {
|
|||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveGradientColors(color: string | null, color2: string | null): { primaryColor: string; secondaryColor?: string } | null {
|
||||||
|
if (!color) return null;
|
||||||
|
if (color2) {
|
||||||
|
return { primaryColor: color, secondaryColor: color2 };
|
||||||
|
}
|
||||||
|
return { primaryColor: color };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ColorResolvable, Guild, Role } from "discord.js";
|
import type { ColorResolvable, Guild, Role, RoleColorsResolvable } from "discord.js";
|
||||||
import type { RoleRepository } from "./boosterRoleService";
|
import type { RoleRepository } from "./boosterRoleService";
|
||||||
|
|
||||||
export class DiscordRoleRepository implements RoleRepository {
|
export class DiscordRoleRepository implements RoleRepository {
|
||||||
@@ -11,10 +11,11 @@ export class DiscordRoleRepository implements RoleRepository {
|
|||||||
return this.guild.roles.cache.map((role) => ({ id: role.id, name: role.name }));
|
return this.guild.roles.cache.map((role) => ({ id: role.id, name: role.name }));
|
||||||
}
|
}
|
||||||
|
|
||||||
async createRole(input: { name: string; color: string | null; permissions: string[]; position: number }): Promise<{ id: string }> {
|
async createRole(input: { name: string; color: string | null; colors?: { primaryColor: string; secondaryColor?: string; tertiaryColor?: string } | null; permissions: string[]; position: number }): Promise<{ id: string }> {
|
||||||
|
const colors = input.colors ?? (input.color ? { primaryColor: input.color as ColorResolvable } : undefined);
|
||||||
const role = await this.guild.roles.create({
|
const role = await this.guild.roles.create({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
colors: toDiscordColors(input.color),
|
colors: colors as RoleColorsResolvable | undefined,
|
||||||
permissions: 0n
|
permissions: 0n
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,11 +23,11 @@ export class DiscordRoleRepository implements RoleRepository {
|
|||||||
return { id: role.id };
|
return { id: role.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateRole(roleId: string, input: { name?: string; color?: string | null; icon?: string | null }): Promise<void> {
|
async updateRole(roleId: string, input: { name?: string; color?: string | null; colors?: { primaryColor: string; secondaryColor?: string; tertiaryColor?: string } | null; icon?: string | null }): Promise<void> {
|
||||||
const role = await this.fetchRole(roleId);
|
const role = await this.fetchRole(roleId);
|
||||||
await role.edit({
|
await role.edit({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
colors: input.color === undefined ? undefined : toDiscordColors(input.color),
|
colors: input.colors === undefined ? undefined : input.colors as RoleColorsResolvable,
|
||||||
icon: input.icon === undefined ? undefined : input.icon
|
icon: input.icon === undefined ? undefined : input.icon
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -56,7 +57,3 @@ export class DiscordRoleRepository implements RoleRepository {
|
|||||||
return role;
|
return role;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDiscordColors(color: string | null): { primaryColor: ColorResolvable } | undefined {
|
|
||||||
return color === null ? undefined : { primaryColor: color as ColorResolvable };
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user