Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
32d27a7ddb | |||
265aac01f1 | |||
168b9c0224 | |||
2b581473c8 | |||
fb658b8189 | |||
d5d3801e1a | |||
54f7a36c5b | |||
2efa3c5347 | |||
ad472fd52e | |||
37986ed0b2 |
48 changed files with 2076 additions and 473 deletions
.gitignoreCHANGELOG.md
src/EllieBot
Db
EllieBot.csprojMigrations
PostgreSql
20250310101121_userroles.sql20250310143026_club-banners.sql20250310143051_init.Designer.cs20250310143051_init.csPostgreSqlContextModelSnapshot.cs
Sqlite
Modules
Gambling
Music
Searches/Crypto
Utility
Quote
UserRole
IDiscordRoleManager.csIUserRoleService.csUserRole.csUserRoleCommands.csUserRoleDiscordManager.csUserRoleService.cs
Utility.csXp/Club
_common
data
strings
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -372,3 +372,9 @@ site/
|
|||
.aider.*
|
||||
PROMPT.md
|
||||
.aider*
|
||||
.windsurfrules
|
||||
|
||||
## Python pip/env files
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
.venv
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -1,6 +1,54 @@
|
|||
# Changelog
|
||||
|
||||
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
|
||||
*a,c,f,r,o*
|
||||
|
||||
## [6.0.3] - [12.03.2025]
|
||||
|
||||
### Added
|
||||
|
||||
- `.xp` system reworked
|
||||
- Global XP has been removed in favor of server XP
|
||||
- You can now set `.xprate` for each channel in your server!
|
||||
- You can set voice, image, and text rates
|
||||
- Use `.xpratereset` to reset it back to default
|
||||
- This feature makes `.xpexclude` obsolete
|
||||
- Requirement to create a club removed
|
||||
- `.xp` card should generate faster
|
||||
- Fixed countless possible issues with xp where some users didn't gain xp, or froze, etc
|
||||
- user-role commands added!
|
||||
- `.ura <user> <role>` - assign a role to a user
|
||||
- `.url <user?>` - list assigned roles for all users or a specific user
|
||||
- `.urm` - show 'my' (your) assigned roles
|
||||
- `.urn <role> <new_name>` - set a name for your role
|
||||
- `.urc <role> <hex_color>` - set a color for your role
|
||||
- `.uri <role> <url/server_emoji>` - set an icon for your role (accepts either a server emoji or a link to an image)
|
||||
- `.notify` improved
|
||||
- Lets you specify source channel (for some events) as the message output
|
||||
- `.pload <id> --shuffle` lets you load a saved playlist in random order
|
||||
- `.lyrics <song_name>` added - find lyrics for a song (it's not always accurate)
|
||||
|
||||
- For Selfhosters
|
||||
- you have to update to latest v5 before updating to v6, otherwise migrations will fail
|
||||
- migration system was reworked
|
||||
- Xp card is now 500x245
|
||||
- xp_template.json backed up to old_xp_template.json
|
||||
- check pinned message in #dev channel to see full selfhoster announcement
|
||||
- Get bot version via --version
|
||||
|
||||
### Changed
|
||||
|
||||
- `.lopl` will queue subdirectories too now
|
||||
- Some music playlist commands have been renamed to fit with other commands
|
||||
- Removed gold/silver frames from xp card
|
||||
- `.inrole` is now showing users in alphabetical order
|
||||
- `.curtrs` are now paginated
|
||||
- pagination now lasts for 10+ minutes
|
||||
- selfhosters: Restart command default now assumes binary installation
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed several fonts
|
||||
- Xp Exclusion commands (superseded by `.xprate`)
|
||||
|
||||
## [5.3.9] - 31.01.2025
|
||||
|
||||
|
|
|
@ -7,14 +7,23 @@ namespace EllieBot.Db;
|
|||
|
||||
public static class CurrencyTransactionExtensions
|
||||
{
|
||||
public static Task<List<CurrencyTransaction>> GetPageFor(
|
||||
public static async Task<IReadOnlyCollection<CurrencyTransaction>> GetPageFor(
|
||||
this DbSet<CurrencyTransaction> set,
|
||||
ulong userId,
|
||||
int page)
|
||||
=> set.ToLinqToDBTable()
|
||||
{
|
||||
var items = await set.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.DateAdded)
|
||||
.Skip(15 * page)
|
||||
.Take(15)
|
||||
.ToListAsyncLinqToDB();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public static async Task<int> GetCountFor(this DbSet<CurrencyTransaction> set, ulong userId)
|
||||
=> await set.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId)
|
||||
.CountAsyncLinqToDB();
|
||||
}
|
|
@ -9,6 +9,7 @@ public class ClubInfo : DbEntity
|
|||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
public string BannerUrl { get; set; } = string.Empty;
|
||||
|
||||
public int Xp { get; set; } = 0;
|
||||
public int? OwnerId { get; set; }
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<Version>6.0.2</Version>
|
||||
<Version>6.0.3</Version>
|
||||
|
||||
<!-- Output/build -->
|
||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||
|
@ -61,7 +61,7 @@
|
|||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||
|
||||
<PackageReference Include="SixLabors.Fonts" Version="2.1.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.5" />
|
||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
||||
|
|
|
@ -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;
|
|
@ -0,0 +1,7 @@
|
|||
START TRANSACTION;
|
||||
ALTER TABLE clubs ADD bannerurl text;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||
VALUES ('20250310143026_club-banners', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||
namespace EllieBot.Migrations.PostgreSql
|
||||
{
|
||||
[DbContext(typeof(PostgreSqlContext))]
|
||||
[Migration("20250228044209_init")]
|
||||
[Migration("20250310143051_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -572,6 +572,10 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("BannerUrl")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bannerurl");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
|
@ -3444,6 +3448,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
|
||||
|
@ -1588,6 +1601,7 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
name = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||
description = table.Column<string>(type: "text", nullable: true),
|
||||
imageurl = table.Column<string>(type: "text", nullable: true),
|
||||
bannerurl = table.Column<string>(type: "text", nullable: true),
|
||||
xp = table.Column<int>(type: "integer", nullable: false),
|
||||
ownerid = table.Column<int>(type: "integer", nullable: true),
|
||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||
|
@ -2131,6 +2145,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 +2516,9 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
migrationBuilder.DropTable(
|
||||
name: "userfishstats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userrole");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "userxpstats");
|
||||
|
|
@ -569,6 +569,10 @@ namespace EllieBot.Migrations.PostgreSql
|
|||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("BannerUrl")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("bannerurl");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("timestamp without time zone")
|
||||
.HasColumnName("dateadded");
|
||||
|
@ -3441,6 +3445,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;
|
|
@ -0,0 +1,7 @@
|
|||
BEGIN TRANSACTION;
|
||||
ALTER TABLE "Clubs" ADD "BannerUrl" TEXT NULL;
|
||||
|
||||
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||
VALUES ('20250310143023_club-banners', '9.0.1');
|
||||
|
||||
COMMIT;
|
|
@ -11,7 +11,7 @@ using EllieBot.Db;
|
|||
namespace EllieBot.Migrations.Sqlite
|
||||
{
|
||||
[DbContext(typeof(SqliteContext))]
|
||||
[Migration("20250228044206_init")]
|
||||
[Migration("20250310143048_init")]
|
||||
partial class init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -428,6 +428,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BannerUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -2563,6 +2566,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
|
||||
|
@ -1590,6 +1603,7 @@ namespace EllieBot.Migrations.Sqlite
|
|||
Name = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
ImageUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
BannerUrl = table.Column<string>(type: "TEXT", nullable: true),
|
||||
Xp = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
OwnerId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||
|
@ -2133,6 +2147,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 +2518,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
migrationBuilder.DropTable(
|
||||
name: "UserFishStats");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRole");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserXpStats");
|
||||
|
|
@ -425,6 +425,9 @@ namespace EllieBot.Migrations.Sqlite
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("BannerUrl")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("DateAdded")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -2560,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")
|
||||
|
|
|
@ -373,18 +373,31 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
return;
|
||||
}
|
||||
|
||||
List<CurrencyTransaction> trs;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.transactions(
|
||||
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
||||
?? $"{userId}")))
|
||||
.WithOkColor();
|
||||
|
||||
int count;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
count = await uow.Set<CurrencyTransaction>()
|
||||
.GetCountFor(userId);
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(async (curPage) =>
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<CurrencyTransaction>()
|
||||
.GetPageFor(userId, curPage);
|
||||
})
|
||||
.PageSize(15)
|
||||
.TotalElements(count)
|
||||
.Page((trs, _) =>
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var tr in trs)
|
||||
{
|
||||
|
@ -406,8 +419,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
embed.WithDescription(sb.ToString());
|
||||
embed.WithFooter(GetText(strs.page(page + 1)));
|
||||
await Response().Embed(embed).SendAsync();
|
||||
return Task.FromResult(embed);
|
||||
}).SendAsync();
|
||||
}
|
||||
|
||||
private static string GetFormattedCurtrDate(CurrencyTransaction ct)
|
||||
|
|
|
@ -7,13 +7,24 @@ namespace EllieBot.Modules.Music;
|
|||
[NoPublicBot]
|
||||
public sealed partial class Music : EllieModule<IMusicService>
|
||||
{
|
||||
public enum All { All = -1 }
|
||||
public enum All
|
||||
{
|
||||
All = -1
|
||||
}
|
||||
|
||||
public enum InputRepeatType
|
||||
{
|
||||
N = 0, No = 0, None = 0,
|
||||
T = 1, Track = 1, S = 1, Song = 1,
|
||||
Q = 2, Queue = 2, Playlist = 2, Pl = 2
|
||||
N = 0,
|
||||
No = 0,
|
||||
None = 0,
|
||||
T = 1,
|
||||
Track = 1,
|
||||
S = 1,
|
||||
Song = 1,
|
||||
Q = 2,
|
||||
Queue = 2,
|
||||
Playlist = 2,
|
||||
Pl = 2
|
||||
}
|
||||
|
||||
public const string MUSIC_ICON_URL = "https://i.imgur.com/nhKS3PT.png";
|
||||
|
@ -22,9 +33,13 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
private static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
|
||||
private readonly ILogCommandService _logService;
|
||||
private readonly ILyricsService _lyricsService;
|
||||
|
||||
public Music(ILogCommandService logService)
|
||||
=> _logService = logService;
|
||||
public Music(ILogCommandService logService, ILyricsService lyricsService)
|
||||
{
|
||||
_logService = logService;
|
||||
_lyricsService = lyricsService;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateAsync()
|
||||
{
|
||||
|
@ -768,4 +783,71 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
await Response().Confirm(strs.wrongsong_success(removed.Title.TrimTo(30))).SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Lyrics([Leftover] string name = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
if (_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)
|
||||
&& mp.GetCurrentTrack(out _) is { } currentTrack)
|
||||
{
|
||||
name = currentTrack.Title;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = await _lyricsService.SearchTracksAsync(name);
|
||||
|
||||
if (tracks.Count == 0)
|
||||
{
|
||||
await Response().Error(strs.no_lyrics_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithFooter("type 1-5 to select");
|
||||
|
||||
for (var i = 0; i <= 5 && i < tracks.Count; i++)
|
||||
{
|
||||
var item = tracks[i];
|
||||
embed.AddField($"`{(i + 1)}`. {item.Author}", item.Title, false);
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(embed)
|
||||
.SendAsync();
|
||||
|
||||
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id, str => int.TryParse(str, out _));
|
||||
|
||||
if (input is null)
|
||||
return;
|
||||
|
||||
var index = int.Parse(input) - 1;
|
||||
if (index < 0 || index > 4)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var track = tracks[index];
|
||||
var lyrics = await _lyricsService.GetLyricsAsync(track.Id);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(lyrics))
|
||||
{
|
||||
await Response().Error(strs.no_lyrics_found).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Embed(CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(track.Author)
|
||||
.WithTitle(track.Title)
|
||||
.WithDescription(lyrics))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
7
src/EllieBot/Modules/Music/Services/ILyricsService.cs
Normal file
7
src/EllieBot/Modules/Music/Services/ILyricsService.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace EllieBot.Modules.Music;
|
||||
|
||||
public interface ILyricsService
|
||||
{
|
||||
public Task<IReadOnlyList<TracksItem>> SearchTracksAsync(string name);
|
||||
public Task<string> GetLyricsAsync(int trackId);
|
||||
}
|
25
src/EllieBot/Modules/Music/Services/LyricsService.cs
Normal file
25
src/EllieBot/Modules/Music/Services/LyricsService.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Musix;
|
||||
|
||||
namespace EllieBot.Modules.Music;
|
||||
|
||||
public sealed class LyricsService(HttpClient client) : ILyricsService, IEService
|
||||
{
|
||||
private readonly MusixMatchAPI _api = new(client);
|
||||
|
||||
private static string NormalizeName(string name)
|
||||
=> string.Join("-", name.Split()
|
||||
.Select(x => new string(x.Where(c => char.IsLetterOrDigit(c)).ToArray())))
|
||||
.Trim('-');
|
||||
|
||||
public async Task<IReadOnlyList<TracksItem>> SearchTracksAsync(string name)
|
||||
=> await _api.SearchTracksAsync(NormalizeName(name))
|
||||
.Fmap(x => x
|
||||
.Message
|
||||
.Body
|
||||
.TrackList
|
||||
.Map(x => new TracksItem(x.Track.ArtistName, x.Track.TrackName, x.Track.TrackId)));
|
||||
|
||||
public async Task<string> GetLyricsAsync(int trackId)
|
||||
=> await _api.GetTrackLyricsAsync(trackId)
|
||||
.Fmap(x => x.Message.Body.Lyrics.LyricsBody);
|
||||
}
|
12
src/EllieBot/Modules/Music/_common/Musix/Header.cs
Normal file
12
src/EllieBot/Modules/Music/_common/Musix/Header.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Header
|
||||
{
|
||||
[JsonPropertyName("status_code")]
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
[JsonPropertyName("execute_time")]
|
||||
public double ExecuteTime { get; set; }
|
||||
}
|
9
src/EllieBot/Modules/Music/_common/Musix/Lyrics.cs
Normal file
9
src/EllieBot/Modules/Music/_common/Musix/Lyrics.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Lyrics
|
||||
{
|
||||
[JsonPropertyName("lyrics_body")]
|
||||
public string LyricsBody { get; set; } = string.Empty;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class LyricsResponse
|
||||
{
|
||||
[JsonPropertyName("lyrics")]
|
||||
public Lyrics Lyrics { get; set; } = null!;
|
||||
}
|
12
src/EllieBot/Modules/Music/_common/Musix/Message.cs
Normal file
12
src/EllieBot/Modules/Music/_common/Musix/Message.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Message<T>
|
||||
{
|
||||
[JsonPropertyName("header")]
|
||||
public Header Header { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
public T Body { get; set; } = default!;
|
||||
}
|
141
src/EllieBot/Modules/Music/_common/Musix/MusixMatchAPI.cs
Normal file
141
src/EllieBot/Modules/Music/_common/Musix/MusixMatchAPI.cs
Normal file
|
@ -0,0 +1,141 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Musix.Models;
|
||||
|
||||
// All credit goes to https://github.com/Strvm/musicxmatch-api for the original implementation
|
||||
namespace Musix
|
||||
{
|
||||
public sealed class MusixMatchAPI
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly string _baseUrl = "https://www.musixmatch.com/ws/1.1/";
|
||||
|
||||
private readonly string _userAgent =
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36";
|
||||
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public MusixMatchAPI(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent);
|
||||
_httpClient.DefaultRequestHeaders.Add("Cookie", "mxm_bab=AB");
|
||||
|
||||
_jsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
_cache = new MemoryCache(new MemoryCacheOptions { });
|
||||
}
|
||||
|
||||
private async Task<string> GetLatestAppUrlAsync()
|
||||
{
|
||||
var url = "https://www.musixmatch.com/search";
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.UserAgent.ParseAdd(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||
request.Headers.Add("Cookie", "mxm_bab=AB");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var htmlContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var pattern = @"src=""([^""]*/_next/static/chunks/pages/_app-[^""]+\.js)""";
|
||||
var matches = Regex.Matches(htmlContent, pattern);
|
||||
|
||||
return matches.Count > 0
|
||||
? matches[^1].Groups[1].Value
|
||||
: throw new("_app URL not found in the HTML content.");
|
||||
}
|
||||
|
||||
private async Task<string> GetSecret()
|
||||
{
|
||||
var latestAppUrl = await GetLatestAppUrlAsync();
|
||||
var response = await _httpClient.GetAsync(latestAppUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var javascriptCode = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var pattern = @"from\(\s*""(.*?)""\s*\.split";
|
||||
var match = Regex.Match(javascriptCode, pattern);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var encodedString = match.Groups[1].Value;
|
||||
var reversedString = new string(encodedString.Reverse().ToArray());
|
||||
var decodedBytes = Convert.FromBase64String(reversedString);
|
||||
return Encoding.UTF8.GetString(decodedBytes);
|
||||
}
|
||||
|
||||
throw new Exception("Encoded string not found in the JavaScript code.");
|
||||
}
|
||||
|
||||
// It seems this is required in order to have multiword queries.
|
||||
// Spaces don't work in the original implementation either
|
||||
private string UrlEncode(string value)
|
||||
=> HttpUtility.UrlEncode(value)
|
||||
.Replace("+", "-");
|
||||
|
||||
private async Task<string> GenerateSignature(string url)
|
||||
{
|
||||
var currentDate = DateTime.Now;
|
||||
var l = currentDate.Year.ToString();
|
||||
var s = currentDate.Month.ToString("D2");
|
||||
var r = currentDate.Day.ToString("D2");
|
||||
|
||||
var message = (url + l + s + r);
|
||||
var secret = await _cache.GetOrCreateAsync("secret", async _ => await GetSecret());
|
||||
var key = Encoding.UTF8.GetBytes(secret ?? string.Empty);
|
||||
var messageBytes = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
using var hmac = new HMACSHA256(key);
|
||||
var hashBytes = hmac.ComputeHash(messageBytes);
|
||||
var signature = Convert.ToBase64String(hashBytes);
|
||||
return $"&signature={UrlEncode(signature)}&signature_protocol=sha256";
|
||||
}
|
||||
|
||||
public async Task<MusixMatchResponse<TrackSearchResponse>> SearchTracksAsync(string trackQuery, int page = 1)
|
||||
{
|
||||
var endpoint =
|
||||
$"track.search?app_id=community-app-v1.0&format=json&q={UrlEncode(trackQuery)}&f_has_lyrics=true&page_size=100&page={page}";
|
||||
var jsonResponse = await MakeRequestAsync(endpoint);
|
||||
return JsonSerializer.Deserialize<MusixMatchResponse<TrackSearchResponse>>(jsonResponse, _jsonOptions)
|
||||
?? throw new JsonException("Failed to deserialize track search response");
|
||||
}
|
||||
|
||||
public async Task<MusixMatchResponse<LyricsResponse>> GetTrackLyricsAsync(int trackId)
|
||||
{
|
||||
var endpoint = $"track.lyrics.get?app_id=community-app-v1.0&format=json&track_id={trackId}";
|
||||
var jsonResponse = await MakeRequestAsync(endpoint);
|
||||
return JsonSerializer.Deserialize<MusixMatchResponse<LyricsResponse>>(jsonResponse, _jsonOptions)
|
||||
?? throw new JsonException("Failed to deserialize lyrics response");
|
||||
}
|
||||
|
||||
private async Task<string> MakeRequestAsync(string endpoint)
|
||||
{
|
||||
var fullUrl = _baseUrl + endpoint;
|
||||
var signedUrl = fullUrl + await GenerateSignature(fullUrl);
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, signedUrl);
|
||||
request.Headers.UserAgent.ParseAdd(_userAgent);
|
||||
request.Headers.Add("Cookie", "mxm_bab=AB");
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Log.Warning("Error in musix api request. Status: {ResponseStatusCode}, Content: {Content}",
|
||||
response.StatusCode,
|
||||
content);
|
||||
response.EnsureSuccessStatusCode(); // This will throw with the appropriate status code
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models
|
||||
{
|
||||
public class MusixMatchResponse<T>
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public Message<T> Message { get; set; } = null!;
|
||||
}
|
||||
}
|
23
src/EllieBot/Modules/Music/_common/Musix/Track.cs
Normal file
23
src/EllieBot/Modules/Music/_common/Musix/Track.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class Track
|
||||
{
|
||||
[JsonPropertyName("track_id")]
|
||||
public int TrackId { get; set; }
|
||||
|
||||
[JsonPropertyName("track_name")]
|
||||
public string TrackName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("artist_name")]
|
||||
public string ArtistName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("album_name")]
|
||||
public string AlbumName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("track_share_url")]
|
||||
public string TrackShareUrl { get; set; } = string.Empty;
|
||||
|
||||
public override string ToString() => $"{TrackName} by {ArtistName} (Album: {AlbumName})";
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class TrackListItem
|
||||
{
|
||||
[JsonPropertyName("track")]
|
||||
public Track Track { get; set; } = null!;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musix.Models;
|
||||
|
||||
public class TrackSearchResponse
|
||||
{
|
||||
[JsonPropertyName("track_list")]
|
||||
public List<TrackListItem> TrackList { get; set; } = new();
|
||||
}
|
3
src/EllieBot/Modules/Music/_common/TracksItem.cs
Normal file
3
src/EllieBot/Modules/Music/_common/TracksItem.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
namespace EllieBot.Modules.Music;
|
||||
|
||||
public record struct TracksItem(string Author, string Title, int Id);
|
|
@ -34,7 +34,7 @@ public class CryptoService : IEService
|
|||
|
||||
var gElement = xml["svg"]?["g"];
|
||||
if (gElement is null)
|
||||
return Array.Empty<PointF>();
|
||||
return [];
|
||||
|
||||
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
|
||||
var cnt = 0;
|
||||
|
@ -73,7 +73,7 @@ public class CryptoService : IEService
|
|||
}
|
||||
|
||||
if (cnt == 0)
|
||||
return Array.Empty<PointF>();
|
||||
return [];
|
||||
|
||||
return points.Slice(0, cnt).ToArray();
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
262
src/EllieBot/Modules/Utility/UserRole/UserRoleCommands.cs
Normal file
262
src/EllieBot/Modules/Utility/UserRole/UserRoleCommands.cs
Normal file
|
@ -0,0 +1,262 @@
|
|||
using EllieBot.Modules.Utility.UserRole;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace EllieBot.Modules.Utility;
|
||||
|
||||
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 botUser = ((SocketGuild)ctx.Guild).CurrentUser;
|
||||
|
||||
if (botUser.GetRoles().Max(x => x.Position) <= role.Position)
|
||||
{
|
||||
await Response().Error(strs.hierarchy).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);
|
||||
}
|
||||
}
|
|
@ -161,9 +161,11 @@ public partial class Utility : EllieModule
|
|||
CacheMode.CacheOnly
|
||||
);
|
||||
|
||||
users = role is null
|
||||
users = (role is null
|
||||
? users
|
||||
: users.Where(u => u.RoleIds.Contains(role.Id)).ToList();
|
||||
: users.Where(u => u.RoleIds.Contains(role.Id)))
|
||||
.OrderBy(x => x.DisplayName)
|
||||
.ToList();
|
||||
|
||||
|
||||
var roleUsers = new List<string>(users.Count);
|
||||
|
@ -790,7 +792,9 @@ public partial class Utility : EllieModule
|
|||
if (ctx.Message.ReferencedMessage is not { } msg)
|
||||
{
|
||||
var msgs = await ctx.Channel.GetMessagesAsync(ctx.Message, Direction.Before, 3).FlattenAsync();
|
||||
msg = msgs.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Content) || (x.Attachments.FirstOrDefault()?.Width is not null)) as IUserMessage;
|
||||
msg = msgs.FirstOrDefault(x
|
||||
=> !string.IsNullOrWhiteSpace(x.Content) ||
|
||||
(x.Attachments.FirstOrDefault()?.Width is not null)) as IUserMessage;
|
||||
|
||||
if (msg is null)
|
||||
return;
|
||||
|
|
|
@ -108,6 +108,31 @@ public partial class Xp
|
|||
await Response().Error(strs.club_icon_invalid_filetype).SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubBanner([Leftover] string url = null)
|
||||
{
|
||||
if ((!Uri.IsWellFormedUriString(url, UriKind.Absolute) && url is not null))
|
||||
{
|
||||
await Response().Error(strs.club_icon_url_format).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _service.SetClubBannerAsync(ctx.User.Id, url);
|
||||
if (result == SetClubIconResult.Success)
|
||||
{
|
||||
if (url is null)
|
||||
await Response().Confirm(strs.club_banner_reset).SendAsync();
|
||||
else
|
||||
await Response().Confirm(strs.club_banner_set).SendAsync();
|
||||
}
|
||||
else if (result == SetClubIconResult.NotOwner)
|
||||
await Response().Error(strs.club_owner_only).SendAsync();
|
||||
else if (result == SetClubIconResult.TooLarge)
|
||||
await Response().Error(strs.club_icon_too_large).SendAsync();
|
||||
else if (result == SetClubIconResult.InvalidFileType)
|
||||
await Response().Error(strs.club_icon_invalid_filetype).SendAsync();
|
||||
}
|
||||
|
||||
private async Task InternalClubInfoAsync(ClubInfo club)
|
||||
{
|
||||
var lvl = new LevelStats(club.Xp);
|
||||
|
@ -154,7 +179,10 @@ public partial class Xp
|
|||
})));
|
||||
|
||||
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
|
||||
return embed.WithThumbnailUrl(club.ImageUrl);
|
||||
embed.WithThumbnailUrl(club.ImageUrl);
|
||||
|
||||
if (Uri.IsWellFormedUriString(club.BannerUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(club.BannerUrl);
|
||||
|
||||
return embed;
|
||||
})
|
||||
|
@ -415,7 +443,7 @@ public partial class Xp
|
|||
? "-"
|
||||
: desc;
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
var eb = CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.club_desc_update))
|
||||
.WithOkColor()
|
||||
|
|
|
@ -120,6 +120,26 @@ public class ClubService : IEService, IClubService
|
|||
return SetClubIconResult.Success;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets club banner url
|
||||
/// </summary>
|
||||
/// <param name="ownerUserId">User ID of the club owner</param>
|
||||
/// <param name="url">Banner URL to set</param>
|
||||
/// <returns>Result of the operation</returns>
|
||||
public async Task<SetClubIconResult> SetClubBannerAsync(ulong ownerUserId, string? url)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(ownerUserId);
|
||||
|
||||
if (club is null)
|
||||
return SetClubIconResult.NotOwner;
|
||||
|
||||
club.BannerUrl = url;
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
return SetClubIconResult.Success;
|
||||
}
|
||||
|
||||
public bool GetClubByName(string clubName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
|
|
|
@ -10,6 +10,7 @@ public interface IClubService
|
|||
Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin);
|
||||
ClubInfo? GetClubByMember(IUser user);
|
||||
Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string? url);
|
||||
Task<SetClubIconResult> SetClubBannerAsync(ulong ownerUserId, string? url);
|
||||
bool GetClubByName(string clubName, out ClubInfo club);
|
||||
ClubApplyResult ApplyToClub(IUser user, ClubInfo club);
|
||||
ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser? discordUser);
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace EllieBot.Common;
|
|||
public sealed class Creds : IBotCreds
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; } = 13;
|
||||
public int Version { get; set; } = 20;
|
||||
|
||||
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
|
||||
public string Token { get; set; }
|
||||
|
@ -148,8 +148,8 @@ public sealed class Creds : IBotCreds
|
|||
{0} -> shard id
|
||||
{1} -> total shards
|
||||
Linux default
|
||||
cmd: dotnet
|
||||
args: "EllieBot.dll -- {0}"
|
||||
cmd: EllieBot
|
||||
args: "{0}"
|
||||
Windows default
|
||||
cmd: EllieBot.exe
|
||||
args: "{0}"
|
||||
|
|
|
@ -77,7 +77,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||
|
||||
if (string.IsNullOrWhiteSpace(_creds.Token))
|
||||
{
|
||||
Log.Error("Token is missing from data/creds.yml or Environment variables.\nAdd it and restart the program");
|
||||
Log.Error(
|
||||
"Token is missing from data/creds.yml or Environment variables.\nAdd it and restart the program");
|
||||
Helpers.ReadErrorAndExit(1);
|
||||
return;
|
||||
}
|
||||
|
@ -85,17 +86,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
|||
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|
||||
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix)
|
||||
if (Environment.OSVersion.Platform == PlatformID.Unix ||
|
||||
Environment.OSVersion.Platform == PlatformID.MacOSX)
|
||||
{
|
||||
_creds.RestartCommand = new RestartConfig()
|
||||
_creds.RestartCommand = new()
|
||||
{
|
||||
Args = "dotnet",
|
||||
Cmd = "EllieBot.dll -- {0}"
|
||||
Args = "EllieBot",
|
||||
Cmd = "{0}"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_creds.RestartCommand = new RestartConfig()
|
||||
_creds.RestartCommand = new()
|
||||
{
|
||||
Args = "EllieBot.exe",
|
||||
Cmd = "{0}"
|
||||
|
|
|
@ -5062,6 +5062,21 @@
|
|||
"No Public Bot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".lyrics"
|
||||
],
|
||||
"Description": "Looks up lyrics for a song. Very hit or miss.",
|
||||
"Usage": [
|
||||
".lyrics biri biri"
|
||||
],
|
||||
"Submodule": "Music",
|
||||
"Module": "Music",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"No Public Bot"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".playlists",
|
||||
|
@ -7987,6 +8002,114 @@
|
|||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userroleassign",
|
||||
".ura",
|
||||
".uradd"
|
||||
],
|
||||
"Description": "Assigns a role to a user that can later be modified by that user.",
|
||||
"Usage": [
|
||||
".userroleassign @User @Role"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"ManageRoles Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userroleremove",
|
||||
".urr",
|
||||
".urdel",
|
||||
".urrm"
|
||||
],
|
||||
"Description": "Removes a previously assigned role from a user.",
|
||||
"Usage": [
|
||||
".userroleremove @User @Role"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"ManageRoles Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userrolelist",
|
||||
".url"
|
||||
],
|
||||
"Description": "Lists all user roles in the server, or for a specific user.",
|
||||
"Usage": [
|
||||
".userrolelist",
|
||||
".userrolelist @User"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": [
|
||||
"ManageRoles Server Permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userrolemy",
|
||||
".urm"
|
||||
],
|
||||
"Description": "Lists all of the user roles assigned to you.",
|
||||
"Usage": [
|
||||
".userrolemy"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userrolecolor",
|
||||
".urc"
|
||||
],
|
||||
"Description": "Changes the color of your assigned role.",
|
||||
"Usage": [
|
||||
".userrolecolor @Role #ff0000"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userrolename",
|
||||
".urn"
|
||||
],
|
||||
"Description": "Changes the name of your assigned role.",
|
||||
"Usage": [
|
||||
".userrolename @Role New Role Name"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".userroleicon",
|
||||
".uri"
|
||||
],
|
||||
"Description": "Changes the icon of your assigned role.",
|
||||
"Usage": [
|
||||
".userroleicon @Role :thumbsup:"
|
||||
],
|
||||
"Submodule": "UserRoleCommands",
|
||||
"Module": "Utility",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
}
|
||||
],
|
||||
"Xp": [
|
||||
|
@ -8193,6 +8316,20 @@
|
|||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".clubbanner"
|
||||
],
|
||||
"Description": "Sets an image as a club banner.\nThe banner will be displayed when club information is shown.",
|
||||
"Usage": [
|
||||
".clubbanner https://i.imgur.com/example.png",
|
||||
".clubbanner"
|
||||
],
|
||||
"Submodule": "Club",
|
||||
"Module": "Xp",
|
||||
"Options": null,
|
||||
"Requirements": []
|
||||
},
|
||||
{
|
||||
"Aliases": [
|
||||
".clubinfo"
|
||||
|
|
|
@ -111,8 +111,8 @@ twitchClientSecret:
|
|||
# {0} -> shard id
|
||||
# {1} -> total shards
|
||||
# Linux default
|
||||
# cmd: dotnet
|
||||
# args: "EllieBot.dll -- {0}"
|
||||
# cmd: EllieBot
|
||||
# args: "{0}"
|
||||
# Windows default
|
||||
# cmd: EllieBot.exe
|
||||
# args: "{0}"
|
||||
|
|
|
@ -1166,6 +1166,8 @@ clubadmin:
|
|||
- clubadmin
|
||||
clubrename:
|
||||
- clubrename
|
||||
clubbanner:
|
||||
- clubbanner
|
||||
eightball:
|
||||
- eightball
|
||||
- 8ball
|
||||
|
@ -1587,3 +1589,29 @@ xprate:
|
|||
- xprate
|
||||
xpratereset:
|
||||
- xpratereset
|
||||
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
|
|
@ -3694,6 +3694,17 @@ clubicon:
|
|||
params:
|
||||
- url:
|
||||
desc: "The URL of an image file to use as the club icon."
|
||||
clubbanner:
|
||||
desc: |-
|
||||
Sets an image as a club banner.
|
||||
The banner will be displayed when club information is shown.
|
||||
ex:
|
||||
- 'https://i.imgur.com/example.png'
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
- url:
|
||||
desc: "URL to the image to set as a club banner."
|
||||
clubapps:
|
||||
desc: Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command.
|
||||
ex:
|
||||
|
@ -4978,3 +4989,82 @@ xpratereset:
|
|||
- { }
|
||||
- channel:
|
||||
desc: "The channel to reset the rate for."
|
||||
lyrics:
|
||||
desc: |-
|
||||
Looks up lyrics for a song. Very hit or miss.
|
||||
ex:
|
||||
- 'biri biri'
|
||||
params:
|
||||
- song:
|
||||
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.'
|
||||
imageUrl:
|
||||
desc: 'The image url to be used as a new icon for the role.'
|
||||
- role:
|
||||
desc: 'The assigned role to change the icon of.'
|
||||
serverEmoji:
|
||||
desc: 'The server emoji to be used as a new icon 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.'
|
|
@ -887,6 +887,8 @@
|
|||
"club_icon_invalid_filetype": "Specified image has an invalid filetype. Make sure you're specifying a direct image url.",
|
||||
"club_icon_url_format": "You must specify an absolute image url.",
|
||||
"club_icon_set": "New club icon set.",
|
||||
"club_banner_set": "New club banner set.",
|
||||
"club_banner_reset": "Club banner has been reset.",
|
||||
"club_bans_for": "Bans for {0} club",
|
||||
"club_apps_for": "Applicants for {0} club",
|
||||
"club_leaderboard": "Club leaderboard - page {0}",
|
||||
|
@ -1178,5 +1180,29 @@
|
|||
"xp_rate_channel_set": "Channel **{0}** xp rate set to **{1}** xp per every **{2}** min.",
|
||||
"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"
|
||||
"xp_rate_no_gain": "No xp gain",
|
||||
"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, or bots highest role.",
|
||||
"userrole_role_not_exists": "That role doesn't exist."
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue