user role commands added

This commit is contained in:
Toastie 2025-03-12 10:20:55 +13:00
parent 54f7a36c5b
commit d5d3801e1a
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
19 changed files with 930 additions and 7 deletions

8
.gitignore vendored
View file

@ -371,4 +371,10 @@ site/
.aider.*
PROMPT.md
.aider*
.aider*
.windsurfrules
## Python pip/env files
Pipfile
Pipfile.lock
.venv

View file

@ -0,0 +1,16 @@
START TRANSACTION;
CREATE TABLE userrole (
guildid numeric(20,0) NOT NULL,
userid numeric(20,0) NOT NULL,
roleid numeric(20,0) NOT NULL,
CONSTRAINT pk_userrole PRIMARY KEY (guildid, userid, roleid)
);
CREATE INDEX ix_userrole_guildid ON userrole (guildid);
CREATE INDEX ix_userrole_guildid_userid ON userrole (guildid, userid);
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
VALUES ('20250310101121_userroles', '9.0.1');
COMMIT;

View file

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace EllieBot.Migrations.PostgreSql
{
[DbContext(typeof(PostgreSqlContext))]
[Migration("20250228044209_init")]
[Migration("20250310101144_init")]
partial class init
{
/// <inheritdoc />
@ -3444,6 +3444,32 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("userfishstats", (string)null);
});
modelBuilder.Entity("EllieBot.Modules.Utility.UserRole.UserRole", b =>
{
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("GuildId", "UserId", "RoleId")
.HasName("pk_userrole");
b.HasIndex("GuildId")
.HasDatabaseName("ix_userrole_guildid");
b.HasIndex("GuildId", "UserId")
.HasDatabaseName("ix_userrole_guildid_userid");
b.ToTable("userrole", (string)null);
});
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
{
b.Property<int>("Id")

View file

@ -1078,6 +1078,19 @@ namespace EllieBot.Migrations.PostgreSql
table.PrimaryKey("pk_userfishstats", x => x.id);
});
migrationBuilder.CreateTable(
name: "userrole",
columns: table => new
{
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_userrole", x => new { x.guildid, x.userid, x.roleid });
});
migrationBuilder.CreateTable(
name: "userxpstats",
columns: table => new
@ -2131,6 +2144,16 @@ namespace EllieBot.Migrations.PostgreSql
column: "userid",
unique: true);
migrationBuilder.CreateIndex(
name: "ix_userrole_guildid",
table: "userrole",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_userrole_guildid_userid",
table: "userrole",
columns: new[] { "guildid", "userid" });
migrationBuilder.CreateIndex(
name: "ix_userxpstats_guildid",
table: "userxpstats",
@ -2492,6 +2515,9 @@ namespace EllieBot.Migrations.PostgreSql
migrationBuilder.DropTable(
name: "userfishstats");
migrationBuilder.DropTable(
name: "userrole");
migrationBuilder.DropTable(
name: "userxpstats");

View file

@ -3441,6 +3441,32 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("userfishstats", (string)null);
});
modelBuilder.Entity("EllieBot.Modules.Utility.UserRole.UserRole", b =>
{
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("GuildId", "UserId", "RoleId")
.HasName("pk_userrole");
b.HasIndex("GuildId")
.HasDatabaseName("ix_userrole_guildid");
b.HasIndex("GuildId", "UserId")
.HasDatabaseName("ix_userrole_guildid_userid");
b.ToTable("userrole", (string)null);
});
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
{
b.Property<int>("Id")

View file

@ -0,0 +1,16 @@
BEGIN TRANSACTION;
CREATE TABLE "UserRole" (
"GuildId" INTEGER NOT NULL,
"UserId" INTEGER NOT NULL,
"RoleId" INTEGER NOT NULL,
CONSTRAINT "PK_UserRole" PRIMARY KEY ("GuildId", "UserId", "RoleId")
);
CREATE INDEX "IX_UserRole_GuildId" ON "UserRole" ("GuildId");
CREATE INDEX "IX_UserRole_GuildId_UserId" ON "UserRole" ("GuildId", "UserId");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20250310101118_userroles', '9.0.1');
COMMIT;

View file

@ -11,7 +11,7 @@ using EllieBot.Db;
namespace EllieBot.Migrations.Sqlite
{
[DbContext(typeof(SqliteContext))]
[Migration("20250228044206_init")]
[Migration("20250310101142_init")]
partial class init
{
/// <inheritdoc />
@ -2563,6 +2563,26 @@ namespace EllieBot.Migrations.Sqlite
b.ToTable("UserFishStats");
});
modelBuilder.Entity("EllieBot.Modules.Utility.UserRole.UserRole", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "UserId", "RoleId");
b.HasIndex("GuildId");
b.HasIndex("GuildId", "UserId");
b.ToTable("UserRole");
});
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
{
b.Property<int>("Id")

View file

@ -1080,6 +1080,19 @@ namespace EllieBot.Migrations.Sqlite
table.PrimaryKey("PK_UserFishStats", x => x.Id);
});
migrationBuilder.CreateTable(
name: "UserRole",
columns: table => new
{
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserRole", x => new { x.GuildId, x.UserId, x.RoleId });
});
migrationBuilder.CreateTable(
name: "UserXpStats",
columns: table => new
@ -2133,6 +2146,16 @@ namespace EllieBot.Migrations.Sqlite
column: "UserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_UserRole_GuildId",
table: "UserRole",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_UserRole_GuildId_UserId",
table: "UserRole",
columns: new[] { "GuildId", "UserId" });
migrationBuilder.CreateIndex(
name: "IX_UserXpStats_GuildId",
table: "UserXpStats",
@ -2494,6 +2517,9 @@ namespace EllieBot.Migrations.Sqlite
migrationBuilder.DropTable(
name: "UserFishStats");
migrationBuilder.DropTable(
name: "UserRole");
migrationBuilder.DropTable(
name: "UserXpStats");

View file

@ -2560,6 +2560,26 @@ namespace EllieBot.Migrations.Sqlite
b.ToTable("UserFishStats");
});
modelBuilder.Entity("EllieBot.Modules.Utility.UserRole.UserRole", b =>
{
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("GuildId", "UserId", "RoleId");
b.HasIndex("GuildId");
b.HasIndex("GuildId", "UserId");
b.ToTable("UserRole");
});
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
{
b.Property<int>("Id")

View file

@ -20,7 +20,6 @@ public partial class Utility
# txt: Quote text
""";
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
.WithEventEmitter(args
=> new MultilineScalarFlowStyleEmitter(args))

View file

@ -0,0 +1,21 @@
namespace EllieBot.Modules.Utility.UserRole;
public interface IDiscordRoleManager
{
/// <summary>
/// Modifies a role's properties in Discord
/// </summary>
/// <param name="guildId">The ID of the guild containing the role</param>
/// <param name="roleId">ID of the role to modify</param>
/// <param name="name">New name for the role (optional)</param>
/// <param name="color">New color for the role (optional)</param>
/// <param name="image">Image for the role (optional)</param>
/// <returns>True if successful, false otherwise</returns>
Task<bool> ModifyRoleAsync(
ulong guildId,
ulong roleId,
string? name = null,
Color? color = null,
Image? image = null
);
}

View file

@ -0,0 +1,76 @@
using SixLabors.ImageSharp.PixelFormats;
namespace EllieBot.Modules.Utility.UserRole;
public interface IUserRoleService
{
/// <summary>
/// Assigns a role to a user and updates both database and Discord
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
/// <param name="roleId">ID of the role</param>
/// <returns>True if successful, false otherwise</returns>
Task<bool> AddRoleAsync(ulong guildId, ulong userId, ulong roleId);
/// <summary>
/// Removes a role from a user and updates both database and Discord
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
/// <param name="roleId">ID of the role</param>
/// <returns>True if successful, false otherwise</returns>
Task<bool> RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId);
/// <summary>
/// Gets all user roles for a guild
/// </summary>
/// <param name="guildId">ID of the guild</param>
Task<IReadOnlyCollection<UserRole>> ListRolesAsync(ulong guildId);
/// <summary>
/// Gets all roles for a specific user in a guild
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
Task<IReadOnlyCollection<UserRole>> ListUserRolesAsync(ulong guildId, ulong userId);
/// <summary>
/// Sets the custom color for a user's role and updates both database and Discord
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
/// <param name="roleId">ID of the role</param>
/// <param name="color">Hex color code</param>
/// <returns>True if successful, false otherwise</returns>
Task<bool> SetRoleColorAsync(ulong guildId, ulong userId, ulong roleId, Rgba32 color);
/// <summary>
/// Sets the custom name for a user's role and updates both database and Discord
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
/// <param name="roleId">ID of the role</param>
/// <param name="name">New role name</param>
/// <returns>True if successful, false otherwise</returns>
Task<bool> SetRoleNameAsync(ulong guildId, ulong userId, ulong roleId, string name);
/// <summary>
/// Sets the custom icon for a user's role and updates both database and Discord
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
/// <param name="roleId">ID of the role</param>
/// <param name="icon">Icon URL or emoji</param>
/// <returns>True if successful, false otherwise</returns>
Task<bool> SetRoleIconAsync(ulong guildId, ulong userId, ulong roleId, string icon);
/// <summary>
/// Checks if a user has a specific role assigned
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="userId">ID of the user</param>
/// <param name="roleId">ID of the role</param>
/// <returns>True if the user has the role, false otherwise</returns>
Task<bool> UserOwnsRoleAsync(ulong guildId, ulong userId, ulong roleId);
}

View file

@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace EllieBot.Modules.Utility.UserRole;
/// <summary>
/// Represents a user's assigned role in a guild
/// </summary>
public class UserRole
{
/// <summary>
/// ID of the guild
/// </summary>
public ulong GuildId { get; set; }
/// <summary>
/// ID of the user
/// </summary>
public ulong UserId { get; set; }
/// <summary>
/// ID of the Discord role
/// </summary>
public ulong RoleId { get; set; }
}
public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
{
public void Configure(EntityTypeBuilder<UserRole> builder)
{
// Set composite primary key
builder.HasKey(x => new { x.GuildId, x.UserId, x.RoleId });
// Create indexes for frequently queried columns
builder.HasIndex(x => x.GuildId);
builder.HasIndex(x => new { x.GuildId, x.UserId });
}
}

View file

@ -0,0 +1,253 @@
using SixLabors.ImageSharp.PixelFormats;
namespace EllieBot.Modules.Utility.UserRole;
public partial class Utility
{
[Group]
public class UserRoleCommands : EllieModule
{
private readonly IUserRoleService _urs;
public UserRoleCommands(IUserRoleService userRoleService)
{
_urs = userRoleService;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task UserRoleAssign(IGuildUser user, IRole role)
{
var modUser = (IGuildUser)ctx.User;
if (modUser.GetRoles().Max(x => x.Position) <= role.Position)
{
await Response().Error(strs.userrole_hierarchy_error).SendAsync();
return;
}
var success = await _urs.AddRoleAsync(ctx.Guild.Id, user.Id, role.Id);
if (!success)
return;
await Response()
.Confirm(strs.userrole_assigned(Format.Bold(user.ToString()), Format.Bold(role.Name)))
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task UserRoleRemove(IUser user, IRole role)
{
var modUser = (IGuildUser)ctx.User;
if (modUser.GetRoles().Max(x => x.Position) <= role.Position)
{
await Response().Error(strs.userrole_hierarchy_error).SendAsync();
return;
}
var success = await _urs.RemoveRoleAsync(ctx.Guild.Id, user.Id, role.Id);
if (!success)
{
await Response().Error(strs.userrole_not_found).SendAsync();
return;
}
await Response()
.Confirm(strs.userrole_removed(Format.Bold(user.ToString()), Format.Bold(role.Name)))
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task UserRoleList()
{
var roles = await _urs.ListRolesAsync(ctx.Guild.Id);
if (roles.Count == 0)
{
await Response().Error(strs.userrole_none).SendAsync();
return;
}
var guild = ctx.Guild as SocketGuild;
// Group roles by user
var userGroups = roles.GroupBy(r => r.UserId)
.Select(g => (UserId: g.Key,
UserName: guild?.GetUser(g.Key)?.ToString() ?? g.Key.ToString(),
Roles: g.ToList()))
.ToList();
await Response()
.Paginated()
.Items(userGroups)
.PageSize(5)
.Page((pageUsers, _) =>
{
var eb = CreateEmbed()
.WithTitle(GetText(strs.userrole_list_title))
.WithOkColor();
foreach (var user in pageUsers)
{
var roleNames = user.Roles
.Select(r => $"- {guild?.GetRole(r.RoleId)} `{r.RoleId}`")
.Join("\n");
eb.AddField(user.UserName, roleNames);
}
return eb;
})
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task UserRoleList(IUser user)
{
var roles = await _urs.ListUserRolesAsync(ctx.Guild.Id, user.Id);
if (roles.Count == 0)
{
await Response()
.Error(strs.userrole_none_user(Format.Bold(user.ToString())))
.SendAsync();
return;
}
var guild = ctx.Guild as SocketGuild;
await Response()
.Paginated()
.Items(roles)
.PageSize(10)
.Page((pageRoles, _) =>
{
var roleList = pageRoles
.Select(r => $"- {guild?.GetRole(r.RoleId)} `{r.RoleId}`")
.Join("\n");
return CreateEmbed()
.WithTitle(GetText(strs.userrole_list_for_user(Format.Bold(user.ToString()))))
.WithDescription(roleList)
.WithOkColor();
})
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task UserRoleMy()
=> await UserRoleList(ctx.User);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task UserRoleColor(IRole role, Rgba32 color)
{
if (!await _urs.UserOwnsRoleAsync(ctx.Guild.Id, ctx.User.Id, role.Id))
{
await Response().Error(strs.userrole_no_permission).SendAsync();
return;
}
var success = await _urs.SetRoleColorAsync(
ctx.Guild.Id,
ctx.User.Id,
role.Id,
color
);
if (success)
{
await Response().Confirm(strs.userrole_color_success(Format.Bold(role.Name), color)).SendAsync();
}
else
{
await Response().Error(strs.userrole_color_fail).SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task UserRoleName(IRole role, [Leftover] string name)
{
if (string.IsNullOrWhiteSpace(name) || name.Length > 100)
{
await Response().Error(strs.userrole_name_invalid).SendAsync();
return;
}
if (!await _urs.UserOwnsRoleAsync(ctx.Guild.Id, ctx.User.Id, role.Id))
{
await Response().Error(strs.userrole_no_permission).SendAsync();
return;
}
var success = await _urs.SetRoleNameAsync(
ctx.Guild.Id,
ctx.User.Id,
role.Id,
name
);
if (success)
{
await Response().Confirm(strs.userrole_name_success(Format.Bold(name))).SendAsync();
}
else
{
await Response().Error(strs.userrole_name_fail).SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
public Task UserRoleIcon(IRole role, Emote emote)
=> UserRoleIcon(role, emote.Url);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task UserRoleIcon(IRole role, [Leftover] string icon)
{
if (string.IsNullOrWhiteSpace(icon))
{
await Response().Error(strs.userrole_icon_invalid).SendAsync();
return;
}
if (!await _urs.UserOwnsRoleAsync(ctx.Guild.Id, ctx.User.Id, role.Id))
{
await Response().Error(strs.userrole_no_permission).SendAsync();
return;
}
var success = await _urs.SetRoleIconAsync(
ctx.Guild.Id,
ctx.User.Id,
role.Id,
icon
);
if (success)
{
await Response().Confirm(strs.userrole_icon_success(Format.Bold(role.Name))).SendAsync();
}
else
{
await Response().Error(strs.userrole_icon_fail).SendAsync();
}
}
}
}

View file

@ -0,0 +1,52 @@
namespace EllieBot.Modules.Utility.UserRole;
public class UserRoleDiscordManager(DiscordSocketClient client) : IDiscordRoleManager, IEService
{
/// <summary>
/// Modifies a role's properties in Discord
/// </summary>
/// <param name="guildId">ID of the guild</param>
/// <param name="roleId">ID of the role to modify</param>
/// <param name="name">New name for the role (optional)</param>
/// <param name="color">New color for the role (optional)</param>
/// <param name="image">New emoji for the role (optional)</param>
/// <returns>True if successful, false otherwise</returns>
public async Task<bool> ModifyRoleAsync(
ulong guildId,
ulong roleId,
string? name = null,
Color? color = null,
Image? image = null
)
{
try
{
var guild = client.GetGuild(guildId);
if (guild is null)
return false;
var role = guild.GetRole(roleId);
if (role is null)
return false;
await role.ModifyAsync(properties =>
{
if (name is not null)
properties.Name = name;
if (color is not null)
properties.Color = color.Value;
if (image is not null)
properties.Icon = image;
});
return true;
}
catch (Exception ex)
{
Log.Warning(ex, "Unable to modify role {RoleId}", roleId);
return false;
}
}
}

View file

@ -0,0 +1,184 @@
#nullable disable warnings
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp.PixelFormats;
namespace EllieBot.Modules.Utility.UserRole;
public sealed class UserRoleService : IUserRoleService, IEService
{
private readonly DbService _db;
private readonly IDiscordRoleManager _discordRoleManager;
private readonly IHttpClientFactory _httpClientFactory;
public UserRoleService(
DbService db,
IDiscordRoleManager discordRoleManager,
IHttpClientFactory httpClientFactory)
{
_db = db;
_discordRoleManager = discordRoleManager;
_httpClientFactory = httpClientFactory;
}
/// <summary>
/// Assigns a role to a user and updates both database and Discord
/// </summary>
public async Task<bool> AddRoleAsync(ulong guildId, ulong userId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserRole>()
.InsertOrUpdateAsync(() => new UserRole
{
GuildId = guildId,
UserId = userId,
RoleId = roleId,
},
_ => new() { },
() => new()
{
GuildId = guildId,
UserId = userId,
RoleId = roleId
});
return true;
}
/// <summary>
/// Removes a role from a user and updates both database and Discord
/// </summary>
public async Task<bool> RemoveRoleAsync(ulong guildId, ulong userId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<UserRole>()
.Where(r => r.GuildId == guildId && r.UserId == userId && r.RoleId == roleId)
.DeleteAsync();
return deleted > 0;
}
/// <summary>
/// Gets all user roles for a guild
/// </summary>
public async Task<IReadOnlyCollection<UserRole>> ListRolesAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var roles = await ctx.GetTable<UserRole>()
.AsNoTracking()
.Where(r => r.GuildId == guildId)
.ToListAsyncLinqToDB();
return roles;
}
/// <summary>
/// Gets all roles for a specific user in a guild
/// </summary>
public async Task<IReadOnlyCollection<UserRole>> ListUserRolesAsync(ulong guildId, ulong userId)
{
await using var ctx = _db.GetDbContext();
var roles = await ctx.GetTable<UserRole>()
.AsNoTracking()
.Where(r => r.GuildId == guildId && r.UserId == userId)
.ToListAsyncLinqToDB();
return roles;
}
/// <summary>
/// Sets the custom color for a user's role and updates both database and Discord
/// </summary>
public async Task<bool> SetRoleColorAsync(ulong guildId, ulong userId, ulong roleId, Rgba32 color)
{
var discordSuccess = await _discordRoleManager.ModifyRoleAsync(
guildId,
roleId,
color: color.ToDiscordColor());
return discordSuccess;
}
/// <summary>
/// Sets the custom name for a user's role and updates both database and Discord
/// </summary>
public async Task<bool> SetRoleNameAsync(ulong guildId, ulong userId, ulong roleId, string name)
{
var discordSuccess = await _discordRoleManager.ModifyRoleAsync(
guildId,
roleId,
name: name);
return discordSuccess;
}
/// <summary>
/// Sets the custom icon for a user's role and updates both database and Discord
/// </summary>
public async Task<bool> SetRoleIconAsync(ulong guildId, ulong userId, ulong roleId, string iconUrl)
{
// Validate the URL format
if (!Uri.TryCreate(iconUrl, UriKind.Absolute, out var uri))
return false;
try
{
// Download the image
using var httpClient = _httpClientFactory.CreateClient();
using var response = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
// Check if the response is successful
if (!response.IsSuccessStatusCode)
return false;
// Check content type - must be image/png or image/jpeg
var contentType = response.Content.Headers.ContentType?.MediaType?.ToLower();
if (contentType != "image/png"
&& contentType != "image/jpeg"
&& contentType != "image/webp")
return false;
// Check file size - Discord limit is 256KB
var contentLength = response.Content.Headers.ContentLength;
if (contentLength is > 256 * 1024)
return false;
// Save the image to a memory stream
await using var stream = await response.Content.ReadAsStreamAsync();
await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
memoryStream.Position = 0;
// Create Discord image from stream
var discordImage = new Image(memoryStream);
// Upload the image to Discord
var discordSuccess = await _discordRoleManager.ModifyRoleAsync(
guildId,
roleId,
image: discordImage);
return discordSuccess;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to process role icon from URL {IconUrl}", iconUrl);
return false;
}
}
/// <summary>
/// Checks if a user has a specific role assigned
/// </summary>
public async Task<bool> UserOwnsRoleAsync(ulong guildId, ulong userId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserRole>()
.AnyAsyncLinqToDB(r => r.GuildId == guildId && r.UserId == userId && r.RoleId == roleId);
}
}

View file

@ -1588,4 +1588,28 @@ xprate:
xpratereset:
- xpratereset
lyrics:
- lyrics
- lyrics
userroleassign:
- userroleassign
- ura
- uradd
userroleremove:
- userroleremove
- urr
- urdel
- urrm
userrolelist:
- userrolelist
- url
userrolemy:
- userrolemy
- urm
userrolecolor:
- userrolecolor
- urc
userrolename:
- userrolename
- urn
userroleicon:
- userroleicon
- uri

View file

@ -4985,4 +4985,75 @@ lyrics:
- 'biri biri'
params:
- song:
desc: "The song to look up lyrics for."
desc: "The song to look up lyrics for."
userroleassign:
desc: |-
Assigns a role to a user that can later be modified by that user.
ex:
- '@User @Role'
params:
- user:
desc: 'The user to assign the role to.'
role:
desc: 'The role to assign.'
userroleremove:
desc: |-
Removes a previously assigned role from a user.
ex:
- '@User @Role'
params:
- user:
desc: 'The user to remove the role from.'
role:
desc: 'The role to remove.'
userrolelist:
desc: |-
Lists all user roles in the server, or for a specific user.
ex:
- ''
- '@User'
params:
- { }
- user:
desc: 'The user whose roles to list.'
userrolemy:
desc: |-
Lists all of the user roles assigned to you.
ex:
- ''
params:
- { }
userrolecolor:
desc: |-
Changes the color of your assigned role.
ex:
- '@Role #ff0000'
params:
- role:
desc: 'The assigned role to change the color of.'
color:
desc: 'The new color for the role in hex format.'
userroleicon:
desc: |-
Changes the icon of your assigned role.
ex:
- '@Role :thumbsup:'
params:
- role:
desc: 'The assigned role to change the icon of.'
icon:
desc: 'The new icon for the role.'
- role:
desc: 'The assigned role to change the icon of.'
emote:
desc: 'The server emoji for the role.'
userrolename:
desc: |-
Changes the name of your assigned role.
ex:
- '@Role New Role Name'
params:
- role:
desc: 'The assigned role to rename.'
name:
desc: 'The new name for the role.'

View file

@ -1179,5 +1179,28 @@
"xp_rate_server_reset": "Server xp rate has been reset to global defaults.",
"xp_rate_channel_reset": "Channel {0} xp rate has been reset.",
"xp_rate_no_gain": "No xp gain",
"no_lyrics_found": "No lyrics found."
"no_lyrics_found": "No lyrics found.",
"userrole_not_found": "Role not found or not assigned to you.",
"userrole_assigned": "{0} has been assigned the role {1}.",
"userrole_removed": "{0} has been removed from the role {1}.",
"userrole_none": "No user roles have been assigned in this server.",
"userrole_none_user": "{0} has no assigned user roles.",
"userrole_list_title": "User Roles in this server",
"userrole_list_for_user": "{0}'s User Roles",
"userrole_no_permission": "You don't have this role assigned as a user role.",
"userrole_no_user_roles": "You have no assigned user roles.",
"userrole_your_roles_title": "Your User Roles",
"userrole_your_roles_footer": "You may customize these roles",
"userrole_color_success": "The color of {0} has been changed to {1}.",
"userrole_color_fail": "Failed to set the role color. Make sure you're using a valid hex color code (e.g., #FF0000).",
"userrole_color_discord_fail": "Failed to update the role color in Discord. The change was saved in the database.",
"userrole_name_success": "The name of your role has been changed to {0}.",
"userrole_name_fail": "Failed to set the role name.",
"userrole_name_invalid": "The role name must not be empty and must be less than 100 characters.",
"userrole_name_discord_fail": "Failed to update the role name in Discord. The change was saved in the database.",
"userrole_icon_success": "The icon for {0} has been saved.",
"userrole_icon_fail": "Failed to set the role icon.",
"userrole_icon_invalid": "The role icon cannot be empty.",
"userrole_hierarchy_error": "You can't assign or modify roles that are higher than or equal to your highest role.",
"userrole_role_not_exists": "That role doesn't exist."
}