"""Create, edit, delete Discord roles and manage role assignments."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from tool_context import ToolContext
logger = logging.getLogger(__name__)
TOOL_NAME = "discord_manage_roles"
TOOL_DESCRIPTION = (
"Manage Discord roles. Actions: 'create', 'edit', 'delete', "
"'add_to_member', 'remove_from_member'. Requires admin perms."
)
TOOL_PARAMETERS = {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": [
"create", "edit", "delete",
"add_to_member", "remove_from_member",
],
"description": "The role management action.",
},
"server_id": {
"type": "string",
"description": "Server (guild) ID.",
},
"role_id": {
"type": "string",
"description": "Role ID (for edit/delete/assign).",
},
"target_user_id": {
"type": "string",
"description": (
"User ID to add/remove role from."
),
},
"name": {
"type": "string",
"description": "Role name (for create or edit).",
},
"color": {
"type": "string",
"description": "Color hex '#FF0000' or integer.",
},
"hoist": {
"type": "boolean",
"description": "Display role separately.",
},
"mentionable": {
"type": "boolean",
"description": "Allow mentioning the role.",
},
"permissions": {
"type": "integer",
"description": "Permissions bitfield.",
},
"position": {
"type": "integer",
"description": "Role hierarchy position.",
},
"reason": {
"type": "string",
"description": "Audit log reason.",
},
},
"required": ["action", "server_id"],
}
[docs]
async def run(
action: str,
server_id: str,
role_id: str | None = None,
target_user_id: str | None = None,
name: str | None = None,
color: str | None = None,
hoist: bool | None = None,
mentionable: bool | None = None,
permissions: int | None = None,
position: int | None = None,
reason: str | None = None,
ctx: ToolContext | None = None,
) -> str:
"""Execute this tool and return the result.
Args:
action (str): The action value.
server_id (str): The server id value.
role_id (str | None): The role id value.
target_user_id (str | None): The target user id value.
name (str | None): Human-readable name.
color (str | None): The color value.
hoist (bool | None): The hoist value.
mentionable (bool | None): The mentionable value.
permissions (int | None): The permissions value.
position (int | None): The position value.
reason (str | None): The reason value.
ctx (ToolContext | None): Tool execution context providing access to bot internals.
Returns:
str: Result string.
"""
import discord
from tools._discord_helpers import (
require_discord_client,
check_admin_permission,
)
client = require_discord_client(ctx)
if isinstance(client, str):
return client
user_id = ctx.user_id if ctx else ""
ok, err = await check_admin_permission(
client, user_id, server_id,
)
if not ok:
return err
try:
server = client.get_guild(int(server_id))
except ValueError:
return f"Error: Invalid server ID: '{server_id}'."
if not server:
return f"Error: Server '{server_id}' not found."
def _parse_colour():
"""Internal helper: parse colour.
"""
if color is None:
return None
try:
s = color.strip()
if s.startswith("#"):
return discord.Colour(int(s[1:], 16))
return discord.Colour(int(s))
except (ValueError, AttributeError):
return None
# --- CREATE ---
if action == "create":
if not name:
return "Error: 'name' is required for create."
kwargs: dict = {
"name": name,
"hoist": hoist or False,
"mentionable": mentionable or False,
}
colour = _parse_colour()
if colour is not None:
kwargs["colour"] = colour
if permissions is not None:
kwargs["permissions"] = discord.Permissions(permissions)
try:
role = await server.create_role(**kwargs)
if position is not None:
await role.edit(position=position)
return (
f"Created role '{role.name}' (ID: {role.id}) "
f"in '{server.name}'."
)
except discord.errors.Forbidden:
return "Error: No permission to create roles."
except Exception as exc:
return f"Error creating role: {exc}"
# --- EDIT ---
if action == "edit":
if not role_id:
return "Error: 'role_id' is required for edit."
role = server.get_role(int(role_id))
if not role:
return f"Error: Role '{role_id}' not found."
if role.is_default():
return "Error: Cannot edit the @everyone role."
kwargs = {}
if name is not None:
kwargs["name"] = name
colour = _parse_colour()
if colour is not None:
kwargs["colour"] = colour
if hoist is not None:
kwargs["hoist"] = hoist
if mentionable is not None:
kwargs["mentionable"] = mentionable
if permissions is not None:
kwargs["permissions"] = discord.Permissions(permissions)
if not kwargs and position is None:
return "Error: No parameters provided to edit."
try:
if kwargs:
await role.edit(**kwargs)
if position is not None:
await role.edit(position=position)
return f"Successfully edited role '{role.name}'."
except discord.errors.Forbidden:
return "Error: No permission to edit roles."
except Exception as exc:
return f"Error editing role: {exc}"
# --- DELETE ---
if action == "delete":
if not role_id:
return "Error: 'role_id' is required for delete."
role = server.get_role(int(role_id))
if not role:
return f"Error: Role '{role_id}' not found."
if role.is_default():
return "Error: Cannot delete the @everyone role."
rname = role.name
try:
await role.delete()
return f"Deleted role '{rname}' from '{server.name}'."
except discord.errors.Forbidden:
return "Error: No permission to delete roles."
except Exception as exc:
return f"Error deleting role: {exc}"
# --- ADD_TO_MEMBER ---
if action == "add_to_member":
if not role_id or not target_user_id:
return (
"Error: 'role_id' and 'target_user_id' are "
"required for add_to_member."
)
role = server.get_role(int(role_id))
if not role:
return f"Error: Role '{role_id}' not found."
member = server.get_member(int(target_user_id))
if not member:
return (
f"Error: User '{target_user_id}' not found "
f"in '{server.name}'."
)
if role in member.roles:
return (
f"User '{member.name}' already has role "
f"'{role.name}'."
)
try:
await member.add_roles(
role, reason=reason or "Assigned via bot",
)
return (
f"Added role '{role.name}' to '{member.name}'."
)
except discord.errors.Forbidden:
return "Error: No permission to manage roles."
except Exception as exc:
return f"Error adding role: {exc}"
# --- REMOVE_FROM_MEMBER ---
if action == "remove_from_member":
if not role_id or not target_user_id:
return (
"Error: 'role_id' and 'target_user_id' are "
"required for remove_from_member."
)
role = server.get_role(int(role_id))
if not role:
return f"Error: Role '{role_id}' not found."
member = server.get_member(int(target_user_id))
if not member:
return (
f"Error: User '{target_user_id}' not found "
f"in '{server.name}'."
)
if role not in member.roles:
return (
f"User '{member.name}' does not have role "
f"'{role.name}'."
)
try:
await member.remove_roles(
role, reason=reason or "Removed via bot",
)
return (
f"Removed role '{role.name}' from "
f"'{member.name}'."
)
except discord.errors.Forbidden:
return "Error: No permission to manage roles."
except Exception as exc:
return f"Error removing role: {exc}"
return f"Error: Unknown action '{action}'."