feat: enhance role handling by updating reply structure and adding role assignment functionality
This commit is contained in:
+7
-3
@@ -37,7 +37,11 @@ export function attachBotHandlers(client: Client, config: AppConfig): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAnchorPosition(guild: { roles: { cache: { get(id: string): { position: number } | undefined } } }, anchorRoleId: string | null): number {
|
function resolveAnchorPosition(guild: { members: { me: { roles: { highest: { position: number } } } | null }; roles: { cache: { get(id: string): { position: number } | undefined } } }, anchorRoleId: string | null): number {
|
||||||
if (!anchorRoleId) return 1;
|
if (!anchorRoleId) return botRolePosition(guild);
|
||||||
return guild.roles.cache.get(anchorRoleId)?.position ?? 1;
|
return guild.roles.cache.get(anchorRoleId)?.position ?? botRolePosition(guild);
|
||||||
|
}
|
||||||
|
|
||||||
|
function botRolePosition(guild: { members: { me: { roles: { highest: { position: number } } } | null } }): number {
|
||||||
|
return guild.members.me?.roles.highest.position ?? 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { MessageFlags } from "discord.js";
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { handleInteraction, type BoosterRoleCommandService, type ChatInputInteractionLike } from "./interactionHandler";
|
import { handleInteraction, type BoosterRoleCommandService, type ChatInputInteractionLike } from "./interactionHandler";
|
||||||
|
|
||||||
type Reply = { content: string; ephemeral: boolean };
|
type Reply = { content: string; flags: MessageFlags.Ephemeral };
|
||||||
|
|
||||||
class FakeInteraction implements ChatInputInteractionLike {
|
class FakeInteraction implements ChatInputInteractionLike {
|
||||||
commandName = "booster-role";
|
commandName = "booster-role";
|
||||||
@@ -59,7 +60,7 @@ describe("handleInteraction", () => {
|
|||||||
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.replies[0]).toEqual({ content: "Booster role created: <@&role-1>", ephemeral: true });
|
expect(interaction.replies[0]).toEqual({ content: "Booster role created: <@&role-1>", flags: MessageFlags.Ephemeral });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("routes update subcommands and replies", async () => {
|
test("routes update subcommands and replies", async () => {
|
||||||
@@ -70,7 +71,7 @@ describe("handleInteraction", () => {
|
|||||||
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
||||||
|
|
||||||
expect(service.calls).toEqual([subcommand]);
|
expect(service.calls).toEqual([subcommand]);
|
||||||
expect(interaction.replies[0]?.ephemeral).toBe(true);
|
expect(interaction.replies[0]?.flags).toBe(MessageFlags.Ephemeral);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,6 +84,6 @@ describe("handleInteraction", () => {
|
|||||||
|
|
||||||
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
await handleInteraction(interaction, service, { isBoosting: async () => true });
|
||||||
|
|
||||||
expect(interaction.replies[0]).toEqual({ content: "Role name is already used", ephemeral: true });
|
expect(interaction.replies[0]).toEqual({ content: "Role name is already used", flags: MessageFlags.Ephemeral });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MessageFlags } from "discord.js";
|
||||||
import type { BoosterRoleRecord, RoleIcon } from "../services/boosterRoleService";
|
import type { BoosterRoleRecord, RoleIcon } from "../services/boosterRoleService";
|
||||||
|
|
||||||
export type ChatInputInteractionLike = {
|
export type ChatInputInteractionLike = {
|
||||||
@@ -5,7 +6,7 @@ export type ChatInputInteractionLike = {
|
|||||||
guildId: string | null;
|
guildId: string | null;
|
||||||
user: { id: string };
|
user: { id: string };
|
||||||
isChatInputCommand(): boolean;
|
isChatInputCommand(): boolean;
|
||||||
reply(input: { content: string; ephemeral: boolean }): Promise<unknown>;
|
reply(input: { content: string; flags: MessageFlags.Ephemeral }): Promise<unknown>;
|
||||||
options: {
|
options: {
|
||||||
getSubcommand(): string;
|
getSubcommand(): string;
|
||||||
getString(name: string): string | null;
|
getString(name: string): string | null;
|
||||||
@@ -46,37 +47,37 @@ export async function handleInteraction(
|
|||||||
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}>`, ephemeral: true });
|
await interaction.reply({ content: `Booster role created: <@&${role.roleId}>`, flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === "rename") {
|
if (subcommand === "rename") {
|
||||||
await service.renameRole({ guildId, userId, name: requireString(interaction, "name") });
|
await service.renameRole({ guildId, userId, name: requireString(interaction, "name") });
|
||||||
await interaction.reply({ content: "Booster role renamed.", ephemeral: true });
|
await interaction.reply({ content: "Booster role renamed.", flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === "recolor") {
|
if (subcommand === "recolor") {
|
||||||
await service.recolorRole({ guildId, userId, color: requireString(interaction, "color") });
|
await service.recolorRole({ guildId, userId, color: requireString(interaction, "color") });
|
||||||
await interaction.reply({ content: "Booster role color updated.", ephemeral: true });
|
await interaction.reply({ content: "Booster role color updated.", flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === "icon") {
|
if (subcommand === "icon") {
|
||||||
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.", ephemeral: true });
|
await interaction.reply({ content: "Booster role icon updated.", flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subcommand === "delete") {
|
if (subcommand === "delete") {
|
||||||
await service.deleteRole({ guildId, userId });
|
await service.deleteRole({ guildId, userId });
|
||||||
await interaction.reply({ content: "Booster role deleted.", ephemeral: true });
|
await interaction.reply({ content: "Booster role deleted.", flags: MessageFlags.Ephemeral });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Unknown booster-role subcommand");
|
throw new Error("Unknown booster-role subcommand");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await interaction.reply({ content: error instanceof Error ? error.message : "Command failed", ephemeral: true });
|
await interaction.reply({ content: error instanceof Error ? error.message : "Command failed", flags: MessageFlags.Ephemeral });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class MemoryRoleStore {
|
|||||||
class FakeRoleRepository implements RoleRepository {
|
class FakeRoleRepository implements RoleRepository {
|
||||||
roles = new Map<string, { id: string; name: string; permissions: string[]; position: number; color: string | null; icon?: string | null }>();
|
roles = new Map<string, { id: string; name: string; permissions: string[]; position: number; color: string | null; icon?: string | null }>();
|
||||||
deletedRoleIds: string[] = [];
|
deletedRoleIds: string[] = [];
|
||||||
|
assignedRoles: Array<{ userId: string; roleId: string }> = [];
|
||||||
|
|
||||||
constructor(initialRoles = [{ id: "existing-vip", name: "VIP", permissions: [], position: 1, color: null }]) {
|
constructor(initialRoles = [{ id: "existing-vip", name: "VIP", permissions: [], position: 1, color: null }]) {
|
||||||
for (const role of initialRoles) {
|
for (const role of initialRoles) {
|
||||||
@@ -43,6 +44,10 @@ class FakeRoleRepository implements RoleRepository {
|
|||||||
this.roles.set(roleId, { ...role, ...input });
|
this.roles.set(roleId, { ...role, ...input });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async assignRole(userId: string, roleId: string) {
|
||||||
|
this.assignedRoles.push({ userId, roleId });
|
||||||
|
}
|
||||||
|
|
||||||
async deleteRole(roleId: string) {
|
async deleteRole(roleId: string) {
|
||||||
this.deletedRoleIds.push(roleId);
|
this.deletedRoleIds.push(roleId);
|
||||||
this.roles.delete(roleId);
|
this.roles.delete(roleId);
|
||||||
@@ -60,6 +65,7 @@ describe("BoosterRoleService", () => {
|
|||||||
expect(claimed.roleId).toBe("created-2");
|
expect(claimed.roleId).toBe("created-2");
|
||||||
expect(roles.roles.get(claimed.roleId)?.permissions).toEqual([]);
|
expect(roles.roles.get(claimed.roleId)?.permissions).toEqual([]);
|
||||||
expect(roles.roles.get(claimed.roleId)?.position).toBe(9);
|
expect(roles.roles.get(claimed.roleId)?.position).toBe(9);
|
||||||
|
expect(roles.assignedRoles).toEqual([{ userId: "user", roleId: claimed.roleId }]);
|
||||||
expect(await store.findByUser("guild", "user")).toEqual(claimed);
|
expect(await store.findByUser("guild", "user")).toEqual(claimed);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ 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; 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; icon?: string | null }): Promise<void>;
|
||||||
|
assignRole(userId: string, roleId: string): Promise<void>;
|
||||||
deleteRole(roleId: string): Promise<void>;
|
deleteRole(roleId: string): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +84,8 @@ export class BoosterRoleService {
|
|||||||
await this.roles.updateRole(role.id, { icon: input.icon.dataUri });
|
await this.roles.updateRole(role.id, { icon: input.icon.dataUri });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.roles.assignRole(userId, role.id);
|
||||||
|
|
||||||
const timestamp = this.now();
|
const timestamp = this.now();
|
||||||
const record = {
|
const record = {
|
||||||
guildId,
|
guildId,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class DiscordRoleRepository implements RoleRepository {
|
|||||||
async createRole(input: { name: string; color: string | null; permissions: string[]; position: number }): Promise<{ id: string }> {
|
async createRole(input: { name: string; color: string | null; permissions: string[]; position: number }): Promise<{ id: string }> {
|
||||||
const role = await this.guild.roles.create({
|
const role = await this.guild.roles.create({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
color: toDiscordColor(input.color),
|
colors: toDiscordColors(input.color),
|
||||||
permissions: 0n
|
permissions: 0n
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -24,11 +24,16 @@ export class DiscordRoleRepository implements RoleRepository {
|
|||||||
const role = await this.fetchRole(roleId);
|
const role = await this.fetchRole(roleId);
|
||||||
await role.edit({
|
await role.edit({
|
||||||
name: input.name,
|
name: input.name,
|
||||||
color: input.color === undefined ? undefined : toDiscordColor(input.color),
|
colors: input.color === undefined ? undefined : toDiscordColors(input.color),
|
||||||
icon: input.icon === undefined ? undefined : input.icon
|
icon: input.icon === undefined ? undefined : input.icon
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async assignRole(userId: string, roleId: string): Promise<void> {
|
||||||
|
const member = await this.guild.members.fetch(userId);
|
||||||
|
await member.roles.add(roleId);
|
||||||
|
}
|
||||||
|
|
||||||
async deleteRole(roleId: string): Promise<void> {
|
async deleteRole(roleId: string): Promise<void> {
|
||||||
const role = await this.fetchRole(roleId);
|
const role = await this.fetchRole(roleId);
|
||||||
await role.delete();
|
await role.delete();
|
||||||
@@ -48,6 +53,6 @@ export class DiscordRoleRepository implements RoleRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDiscordColor(color: string | null): ColorResolvable | undefined {
|
function toDiscordColors(color: string | null): { primaryColor: ColorResolvable } | undefined {
|
||||||
return color === null ? undefined : (color as ColorResolvable);
|
return color === null ? undefined : { primaryColor: color as ColorResolvable };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user