forked from EllieBotDevs/elliebot
user role commands added
This commit is contained in:
parent
54f7a36c5b
commit
d5d3801e1a
19 changed files with 930 additions and 7 deletions
.gitignore
src/EllieBot
Migrations
PostgreSql
20250310101121_userroles.sql20250310101144_init.Designer.cs20250310101144_init.csPostgreSqlContextModelSnapshot.cs
Sqlite
Modules/Utility
Quote
UserRole
strings
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -371,4 +371,10 @@ site/
|
|||
|
||||
.aider.*
|
||||
PROMPT.md
|
||||
.aider*
|
||||
.aider*
|
||||
.windsurfrules
|
||||
|
||||
## Python pip/env files
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
.venv
|
|
@ -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;
|
|
@ -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")
|
|
@ -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");
|
||||
|
|
@ -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")
|
||||
|
|
16
src/EllieBot/Migrations/Sqlite/20250310101118_userroles.sql
Normal file
16
src/EllieBot/Migrations/Sqlite/20250310101118_userroles.sql
Normal 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;
|
|
@ -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")
|
|
@ -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");
|
||||
|
|
@ -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")
|
||||
|
|
|
@ -20,7 +20,6 @@ public partial class Utility
|
|||
# txt: Quote text
|
||||
|
||||
""";
|
||||
|
||||
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
|
||||
.WithEventEmitter(args
|
||||
=> new MultilineScalarFlowStyleEmitter(args))
|
||||
|
|
21
src/EllieBot/Modules/Utility/UserRole/IDiscordRoleManager.cs
Normal file
21
src/EllieBot/Modules/Utility/UserRole/IDiscordRoleManager.cs
Normal 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
|
||||
);
|
||||
}
|
76
src/EllieBot/Modules/Utility/UserRole/IUserRoleService.cs
Normal file
76
src/EllieBot/Modules/Utility/UserRole/IUserRoleService.cs
Normal 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);
|
||||
}
|
38
src/EllieBot/Modules/Utility/UserRole/UserRole.cs
Normal file
38
src/EllieBot/Modules/Utility/UserRole/UserRole.cs
Normal 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 });
|
||||
}
|
||||
}
|
253
src/EllieBot/Modules/Utility/UserRole/UserRoleCommands.cs
Normal file
253
src/EllieBot/Modules/Utility/UserRole/UserRoleCommands.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
184
src/EllieBot/Modules/Utility/UserRole/UserRoleService.cs
Normal file
184
src/EllieBot/Modules/Utility/UserRole/UserRoleService.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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.'
|
|
@ -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."
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue