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
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
|
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,7 +9,8 @@ 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; }
|
||||
public DiscordUser Owner { 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")
|
||||
|
|
|
@ -218,20 +218,20 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
var val = Config.Timely.Amount;
|
||||
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
|
||||
var guildUsers = await boostGuilds
|
||||
.Select(async gid =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var guild = await _client.Rest.GetGuildAsync(gid, false);
|
||||
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
|
||||
return (guild, user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.WhenAll();
|
||||
.Select(async gid =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var guild = await _client.Rest.GetGuildAsync(gid, false);
|
||||
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
|
||||
return (guild, user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
||||
var booster = userInfo != default;
|
||||
|
@ -296,8 +296,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,10 +316,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
||||
await Response()
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -337,10 +337,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
||||
await Response()
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||
$"**{usr.Username}**",
|
||||
footer: $"ID: {usr.Id}")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -373,41 +373,54 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
return;
|
||||
}
|
||||
|
||||
List<CurrencyTransaction> trs;
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.transactions(
|
||||
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
||||
?? $"{userId}")))
|
||||
.WithOkColor();
|
||||
|
||||
int count;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
|
||||
count = await uow.Set<CurrencyTransaction>()
|
||||
.GetCountFor(userId);
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(GetText(strs.transactions(
|
||||
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
||||
?? $"{userId}")))
|
||||
.WithOkColor();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var tr in trs)
|
||||
{
|
||||
var change = tr.Amount >= 0 ? "🔵" : "🔴";
|
||||
var kwumId = new kwum(tr.Id).ToString();
|
||||
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
|
||||
|
||||
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
|
||||
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
|
||||
if (transactionString is not null)
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(async (curPage) =>
|
||||
{
|
||||
sb.AppendLine(transactionString);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tr.Note))
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<CurrencyTransaction>()
|
||||
.GetPageFor(userId, curPage);
|
||||
})
|
||||
.PageSize(15)
|
||||
.TotalElements(count)
|
||||
.Page((trs, _) =>
|
||||
{
|
||||
sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}");
|
||||
}
|
||||
}
|
||||
var sb = new StringBuilder();
|
||||
foreach (var tr in trs)
|
||||
{
|
||||
var change = tr.Amount >= 0 ? "🔵" : "🔴";
|
||||
var kwumId = new kwum(tr.Id).ToString();
|
||||
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
|
||||
|
||||
embed.WithDescription(sb.ToString());
|
||||
embed.WithFooter(GetText(strs.page(page + 1)));
|
||||
await Response().Embed(embed).SendAsync();
|
||||
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
|
||||
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
|
||||
if (transactionString is not null)
|
||||
{
|
||||
sb.AppendLine(transactionString);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tr.Note))
|
||||
{
|
||||
sb.AppendLine($"\t`Note:` {tr.Note.TrimTo(50)}");
|
||||
}
|
||||
}
|
||||
|
||||
embed.WithDescription(sb.ToString());
|
||||
return Task.FromResult(embed);
|
||||
}).SendAsync();
|
||||
}
|
||||
|
||||
private static string GetFormattedCurtrDate(CurrencyTransaction ct)
|
||||
|
@ -420,9 +433,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await using var uow = _db.GetDbContext();
|
||||
|
||||
var tr = await uow.Set<CurrencyTransaction>()
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (tr is null)
|
||||
{
|
||||
|
@ -483,9 +496,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
|
||||
|
||||
await N(balance)
|
||||
.Pipe(strs.bank_balance)
|
||||
.Pipe(GetText)
|
||||
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
||||
.Pipe(strs.bank_balance)
|
||||
.Pipe(GetText)
|
||||
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
||||
}
|
||||
|
||||
private EllieInteractionBase CreateCashInteraction()
|
||||
|
@ -507,13 +520,13 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
: null;
|
||||
|
||||
await Response()
|
||||
.Confirm(
|
||||
user.ToString()
|
||||
.Pipe(Format.Bold)
|
||||
.With(cur)
|
||||
.Pipe(strs.has))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
.Confirm(
|
||||
user.ToString()
|
||||
.Pipe(Format.Bold)
|
||||
.With(cur)
|
||||
.Pipe(strs.has))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -594,10 +607,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.mass_award(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.mass_award(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -613,10 +626,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
new("take", ctx.User.ToString()!, null, ctx.User.Id));
|
||||
|
||||
await Response()
|
||||
.Confirm(strs.mass_take(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
.Confirm(strs.mass_take(N(amount),
|
||||
Format.Bold(users.Count.ToString()),
|
||||
Format.Bold(role.Name)))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -639,8 +652,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -662,8 +675,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -695,12 +708,12 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(Format.Bold(str))
|
||||
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), true)
|
||||
.WithOkColor();
|
||||
.WithAuthor(ctx.User)
|
||||
.WithDescription(Format.Bold(str))
|
||||
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), N((long)result.Won), true)
|
||||
.WithOkColor();
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
|
@ -741,11 +754,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var cleanRichest = await uow.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId.In(users))
|
||||
.OrderByDescending(x => x.CurrencyAmount)
|
||||
.Skip(curPage * perPage)
|
||||
.Take(perPage)
|
||||
.ToListAsync();
|
||||
.Where(x => x.UserId.In(users))
|
||||
.OrderByDescending(x => x.CurrencyAmount)
|
||||
.Skip(curPage * perPage)
|
||||
.Take(perPage)
|
||||
.ToListAsync();
|
||||
|
||||
return cleanRichest;
|
||||
}
|
||||
|
@ -757,34 +770,34 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.PageItems(GetTopRichest)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((toSend, curPage) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
||||
.Paginated()
|
||||
.PageItems(GetTopRichest)
|
||||
.PageSize(9)
|
||||
.CurrentPage(page)
|
||||
.Page((toSend, curPage) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
||||
|
||||
if (!toSend.Any())
|
||||
{
|
||||
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
||||
return Task.FromResult(embed);
|
||||
}
|
||||
if (!toSend.Any())
|
||||
{
|
||||
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
||||
return Task.FromResult(embed);
|
||||
}
|
||||
|
||||
for (var i = 0; i < toSend.Count; i++)
|
||||
{
|
||||
var x = toSend[i];
|
||||
var usrStr = x.ToString().TrimTo(20, true);
|
||||
for (var i = 0; i < toSend.Count; i++)
|
||||
{
|
||||
var x = toSend[i];
|
||||
var usrStr = x.ToString().TrimTo(20, true);
|
||||
|
||||
var j = i;
|
||||
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
||||
}
|
||||
var j = i;
|
||||
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
||||
}
|
||||
|
||||
return Task.FromResult(embed);
|
||||
})
|
||||
.SendAsync();
|
||||
return Task.FromResult(embed);
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
public enum InputRpsPick : byte
|
||||
|
@ -895,11 +908,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(sb.ToString())
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
|
||||
.WithAuthor(ctx.User);
|
||||
.WithOkColor()
|
||||
.WithDescription(sb.ToString())
|
||||
.AddField(GetText(strs.bet), N(amount), true)
|
||||
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
|
||||
.WithAuthor(ctx.User);
|
||||
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
|
@ -924,8 +937,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
public async Task BetTest()
|
||||
{
|
||||
var values = Enum.GetValues<GambleTestTarget>()
|
||||
.Select(x => $"`{x}`")
|
||||
.Join(", ");
|
||||
.Select(x => $"`{x}`")
|
||||
.Join(", ");
|
||||
|
||||
await Response().Confirm(GetText(strs.available_tests), values).SendAsync();
|
||||
}
|
||||
|
@ -998,10 +1011,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
sb.AppendLine($"Longest lose streak: `{maxL}`");
|
||||
|
||||
await Response()
|
||||
.Confirm(GetText(strs.test_results_for(target)),
|
||||
sb.ToString(),
|
||||
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.test_results_for(target)),
|
||||
sb.ToString(),
|
||||
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
private EllieInteractionBase CreateRakebackInteraction()
|
||||
|
@ -1032,16 +1045,16 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
if (rb < 1)
|
||||
{
|
||||
await Response()
|
||||
.Error(strs.rakeback_none)
|
||||
.SendAsync();
|
||||
.Error(strs.rakeback_none)
|
||||
.SendAsync();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var inter = CreateRakebackInteraction();
|
||||
await Response()
|
||||
.Pending(strs.rakeback_available(N(rb)))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
.Pending(strs.rakeback_available(N(rb)))
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
|
@ -110,10 +125,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
try
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||
.WithFooter(trackInfo.Platform.ToString());
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||
.WithFooter(trackInfo.Platform.ToString());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
||||
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
||||
|
@ -301,39 +316,39 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
|
||||
desc += tracks
|
||||
.Select((v, index) =>
|
||||
{
|
||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||
if (index == currentIndex)
|
||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||
.Select((v, index) =>
|
||||
{
|
||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||
if (index == currentIndex)
|
||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||
|
||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||
})
|
||||
.Join('\n');
|
||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||
})
|
||||
.Join('\n');
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(add))
|
||||
desc = add + "\n" + desc;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(
|
||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||
MUSIC_ICON_URL)
|
||||
.WithDescription(desc)
|
||||
.WithFooter(
|
||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||
.WithOkColor();
|
||||
.WithAuthor(
|
||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||
MUSIC_ICON_URL)
|
||||
.WithDescription(desc)
|
||||
.WithFooter(
|
||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(tracks)
|
||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.Page(PrintAction)
|
||||
.SendAsync();
|
||||
.Paginated()
|
||||
.Items(tracks)
|
||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||
.CurrentPage(page)
|
||||
.AddFooter(false)
|
||||
.Page(PrintAction)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
// search
|
||||
|
@ -353,15 +368,15 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
|
||||
|
||||
var embeds = videos.Select((x, i) => CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithThumbnailUrl(x.Thumbnail)
|
||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||
.ToList();
|
||||
.WithOkColor()
|
||||
.WithThumbnailUrl(x.Thumbnail)
|
||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||
.ToList();
|
||||
|
||||
var msg = await Response()
|
||||
.Text(strs.queue_search_results)
|
||||
.Embeds(embeds)
|
||||
.SendAsync();
|
||||
.Text(strs.queue_search_results)
|
||||
.Embeds(embeds)
|
||||
.SendAsync();
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -425,10 +440,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||
.WithDescription(track.PrettyName())
|
||||
.WithFooter(track.PrettyInfo())
|
||||
.WithErrorColor();
|
||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||
.WithDescription(track.PrettyName())
|
||||
.WithFooter(track.PrettyInfo())
|
||||
.WithErrorColor();
|
||||
|
||||
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
||||
}
|
||||
|
@ -593,11 +608,11 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithTitle(track.Title.TrimTo(65))
|
||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||
.WithOkColor();
|
||||
.WithTitle(track.Title.TrimTo(65))
|
||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||
.WithOkColor();
|
||||
|
||||
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
||||
embed.WithUrl(track.Url);
|
||||
|
@ -652,12 +667,12 @@ public sealed partial class Music : EllieModule<IMusicService>
|
|||
return;
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||
.WithDescription(currentTrack.PrettyName())
|
||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||
.WithFooter(
|
||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||
.WithOkColor()
|
||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||
.WithDescription(currentTrack.PrettyName())
|
||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||
.WithFooter(
|
||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||
|
||||
await Response().Embed(embed).SendAsync();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -85,10 +85,10 @@ public partial class Utility : EllieModule
|
|||
message = await repSvc.ReplaceAsync(message, repCtx);
|
||||
|
||||
await Response()
|
||||
.Text(message)
|
||||
.Channel(channel)
|
||||
.UserBasedMentions()
|
||||
.SendAsync();
|
||||
.Text(message)
|
||||
.Channel(channel)
|
||||
.UserBasedMentions()
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -123,27 +123,27 @@ public partial class Utility : EllieModule
|
|||
}
|
||||
|
||||
await Response()
|
||||
.Sanitize()
|
||||
.Paginated()
|
||||
.Items(userNames)
|
||||
.PageSize(20)
|
||||
.Page((names, _) =>
|
||||
{
|
||||
if (names.Count == 0)
|
||||
{
|
||||
return CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText(strs.nobody_playing_game));
|
||||
}
|
||||
.Sanitize()
|
||||
.Paginated()
|
||||
.Items(userNames)
|
||||
.PageSize(20)
|
||||
.Page((names, _) =>
|
||||
{
|
||||
if (names.Count == 0)
|
||||
{
|
||||
return CreateEmbed()
|
||||
.WithErrorColor()
|
||||
.WithDescription(GetText(strs.nobody_playing_game));
|
||||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor();
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor();
|
||||
|
||||
var users = names.Join('\n');
|
||||
var users = names.Join('\n');
|
||||
|
||||
return eb.WithDescription(users);
|
||||
})
|
||||
.SendAsync();
|
||||
return eb.WithDescription(users);
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -161,9 +161,11 @@ public partial class Utility : EllieModule
|
|||
CacheMode.CacheOnly
|
||||
);
|
||||
|
||||
users = role is null
|
||||
? users
|
||||
: users.Where(u => u.RoleIds.Contains(role.Id)).ToList();
|
||||
users = (role is null
|
||||
? users
|
||||
: users.Where(u => u.RoleIds.Contains(role.Id)))
|
||||
.OrderBy(x => x.DisplayName)
|
||||
.ToList();
|
||||
|
||||
|
||||
var roleUsers = new List<string>(users.Count);
|
||||
|
@ -173,23 +175,23 @@ public partial class Utility : EllieModule
|
|||
}
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(roleUsers)
|
||||
.PageSize(20)
|
||||
.CurrentPage(page)
|
||||
.Page((pageUsers, _) =>
|
||||
{
|
||||
if (pageUsers.Count == 0)
|
||||
return CreateEmbed().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page));
|
||||
.Paginated()
|
||||
.Items(roleUsers)
|
||||
.PageSize(20)
|
||||
.CurrentPage(page)
|
||||
.Page((pageUsers, _) =>
|
||||
{
|
||||
if (pageUsers.Count == 0)
|
||||
return CreateEmbed().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page));
|
||||
|
||||
var roleName = Format.Bold(role?.Name ?? "No Role");
|
||||
var roleName = Format.Bold(role?.Name ?? "No Role");
|
||||
|
||||
return CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
|
||||
.WithDescription(string.Join("\n", pageUsers));
|
||||
})
|
||||
.SendAsync();
|
||||
return CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
|
||||
.WithDescription(string.Join("\n", pageUsers));
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -211,14 +213,14 @@ public partial class Utility : EllieModule
|
|||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var p in perms.GetType()
|
||||
.GetProperties()
|
||||
.Where(static p =>
|
||||
{
|
||||
var method = p.GetGetMethod();
|
||||
if (method is null)
|
||||
return false;
|
||||
return !method.GetParameters().Any();
|
||||
}))
|
||||
.GetProperties()
|
||||
.Where(static p =>
|
||||
{
|
||||
var method = p.GetGetMethod();
|
||||
if (method is null)
|
||||
return false;
|
||||
return !method.GetParameters().Any();
|
||||
}))
|
||||
builder.AppendLine($"{p.Name} : {p.GetValue(perms, null)}");
|
||||
await Response().Confirm(builder.ToString()).SendAsync();
|
||||
}
|
||||
|
@ -229,20 +231,20 @@ public partial class Utility : EllieModule
|
|||
{
|
||||
var usr = target ?? ctx.User;
|
||||
await Response()
|
||||
.Confirm(strs.userid("🆔",
|
||||
Format.Bold(usr.ToString()),
|
||||
Format.Code(usr.Id.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.userid("🆔",
|
||||
Format.Bold(usr.ToString()),
|
||||
Format.Code(usr.Id.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task RoleId([Leftover] IRole role)
|
||||
=> await Response()
|
||||
.Confirm(strs.roleid("🆔",
|
||||
Format.Bold(role.ToString()),
|
||||
Format.Code(role.Id.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.roleid("🆔",
|
||||
Format.Bold(role.ToString()),
|
||||
Format.Code(role.Id.ToString())))
|
||||
.SendAsync();
|
||||
|
||||
[Cmd]
|
||||
public async Task ChannelId()
|
||||
|
@ -267,36 +269,36 @@ public partial class Utility : EllieModule
|
|||
if (target is not null)
|
||||
{
|
||||
var roles = target.GetRoles()
|
||||
.Except(new[] { guild.EveryoneRole })
|
||||
.OrderBy(r => -r.Position)
|
||||
.Skip((page - 1) * rolesPerPage)
|
||||
.Take(rolesPerPage)
|
||||
.ToArray();
|
||||
.Except(new[] { guild.EveryoneRole })
|
||||
.OrderBy(r => -r.Position)
|
||||
.Skip((page - 1) * rolesPerPage)
|
||||
.Take(rolesPerPage)
|
||||
.ToArray();
|
||||
if (!roles.Any())
|
||||
await Response().Error(strs.no_roles_on_page).SendAsync();
|
||||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(GetText(strs.roles_page(page, Format.Bold(target.ToString()))),
|
||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles))
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.roles_page(page, Format.Bold(target.ToString()))),
|
||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var roles = guild.Roles.Except(new[] { guild.EveryoneRole })
|
||||
.OrderBy(r => -r.Position)
|
||||
.Skip((page - 1) * rolesPerPage)
|
||||
.Take(rolesPerPage)
|
||||
.ToArray();
|
||||
.OrderBy(r => -r.Position)
|
||||
.Skip((page - 1) * rolesPerPage)
|
||||
.Take(rolesPerPage)
|
||||
.ToArray();
|
||||
if (!roles.Any())
|
||||
await Response().Error(strs.no_roles_on_page).SendAsync();
|
||||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(GetText(strs.roles_all_page(page)),
|
||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true))
|
||||
.SendAsync();
|
||||
.Confirm(GetText(strs.roles_all_page(page)),
|
||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true))
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -328,33 +330,33 @@ public partial class Utility : EllieModule
|
|||
ownerIds = "-";
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithAuthor($"EllieBot v{StatsService.BotVersion}",
|
||||
"https://cdn.elliebot.net/Ellie.png",
|
||||
"https://docs.elliebot.net")
|
||||
.AddField(GetText(strs.author), _stats.Author, true)
|
||||
.AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true)
|
||||
.AddField(GetText(strs.shard),
|
||||
$"#{_client.ShardId} / {_creds.TotalShards}",
|
||||
true)
|
||||
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
|
||||
.AddField(GetText(strs.messages),
|
||||
$"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
|
||||
true)
|
||||
.AddField(GetText(strs.memory),
|
||||
FormattableString.Invariant($"{_stats.GetPrivateMemoryMegabytes():F2} MB"),
|
||||
true)
|
||||
.AddField(GetText(strs.owner_ids), ownerIds, true)
|
||||
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
|
||||
.AddField(GetText(strs.presence),
|
||||
GetText(strs.presence_txt(_coord.GetGuildCount(),
|
||||
_stats.TextChannels,
|
||||
_stats.VoiceChannels)),
|
||||
true);
|
||||
.WithOkColor()
|
||||
.WithAuthor($"EllieBot v{StatsService.BotVersion}",
|
||||
"https://cdn.elliebot.net/Ellie.png",
|
||||
"https://docs.elliebot.net")
|
||||
.AddField(GetText(strs.author), _stats.Author, true)
|
||||
.AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true)
|
||||
.AddField(GetText(strs.shard),
|
||||
$"#{_client.ShardId} / {_creds.TotalShards}",
|
||||
true)
|
||||
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
|
||||
.AddField(GetText(strs.messages),
|
||||
$"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
|
||||
true)
|
||||
.AddField(GetText(strs.memory),
|
||||
FormattableString.Invariant($"{_stats.GetPrivateMemoryMegabytes():F2} MB"),
|
||||
true)
|
||||
.AddField(GetText(strs.owner_ids), ownerIds, true)
|
||||
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
|
||||
.AddField(GetText(strs.presence),
|
||||
GetText(strs.presence_txt(_coord.GetGuildCount(),
|
||||
_stats.TextChannels,
|
||||
_stats.VoiceChannels)),
|
||||
true);
|
||||
|
||||
await Response()
|
||||
.Embed(eb)
|
||||
.SendAsync();
|
||||
.Embed(eb)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -503,9 +505,9 @@ public partial class Utility : EllieModule
|
|||
}
|
||||
|
||||
format = attach.Filename
|
||||
.Split('.')
|
||||
.Last()
|
||||
.ToLowerInvariant();
|
||||
.Split('.')
|
||||
.Last()
|
||||
.ToLowerInvariant();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(format) || (format != "png" && format != "apng"))
|
||||
{
|
||||
|
@ -572,30 +574,30 @@ public partial class Utility : EllieModule
|
|||
return;
|
||||
|
||||
var allGuilds = _client.Guilds
|
||||
.OrderBy(g => g.Name)
|
||||
.ToList();
|
||||
.OrderBy(g => g.Name)
|
||||
.ToList();
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allGuilds)
|
||||
.PageSize(9)
|
||||
.Page((guilds, _) =>
|
||||
{
|
||||
if (!guilds.Any())
|
||||
{
|
||||
return CreateEmbed()
|
||||
.WithDescription(GetText(strs.listservers_none))
|
||||
.WithErrorColor();
|
||||
}
|
||||
.Paginated()
|
||||
.Items(allGuilds)
|
||||
.PageSize(9)
|
||||
.Page((guilds, _) =>
|
||||
{
|
||||
if (!guilds.Any())
|
||||
{
|
||||
return CreateEmbed()
|
||||
.WithDescription(GetText(strs.listservers_none))
|
||||
.WithErrorColor();
|
||||
}
|
||||
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor();
|
||||
foreach (var guild in guilds)
|
||||
embed.AddField(guild.Name, GetText(strs.listservers(guild.Id, guild.MemberCount, guild.OwnerId)));
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor();
|
||||
foreach (var guild in guilds)
|
||||
embed.AddField(guild.Name, GetText(strs.listservers(guild.Id, guild.MemberCount, guild.OwnerId)));
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -632,7 +634,7 @@ public partial class Utility : EllieModule
|
|||
{
|
||||
Content = msg.Content,
|
||||
Embeds = msg.Embeds
|
||||
.Map(x => new SmartEmbedArrayElementText(x))
|
||||
.Map(x => new SmartEmbedArrayElementText(x))
|
||||
}.ToJson(_showEmbedSerializerOptions);
|
||||
|
||||
await Response().Confirm(Format.Code(json, "json").Replace("](", "]\\(")).SendAsync();
|
||||
|
@ -648,34 +650,34 @@ public partial class Utility : EllieModule
|
|||
|
||||
var title = $"Chatlog-{ctx.Guild.Name}/#{ctx.Channel.Name}-{DateTime.Now}.txt";
|
||||
var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}")
|
||||
.Select(g => new
|
||||
{
|
||||
date = g.Key,
|
||||
messages = g.OrderBy(x => x.CreatedAt)
|
||||
.Select(s =>
|
||||
{
|
||||
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
|
||||
if (string.IsNullOrWhiteSpace(s.ToString()))
|
||||
{
|
||||
if (s.Attachments.Any())
|
||||
{
|
||||
msg += "FILES_UPLOADED: "
|
||||
+ string.Join("\n", s.Attachments.Select(x => x.Url));
|
||||
}
|
||||
else if (s.Embeds.Any())
|
||||
{
|
||||
msg += "EMBEDS: "
|
||||
+ string.Join("\n--------\n",
|
||||
s.Embeds.Select(x
|
||||
=> $"Description: {x.Description}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
msg += s.ToString();
|
||||
.Select(g => new
|
||||
{
|
||||
date = g.Key,
|
||||
messages = g.OrderBy(x => x.CreatedAt)
|
||||
.Select(s =>
|
||||
{
|
||||
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
|
||||
if (string.IsNullOrWhiteSpace(s.ToString()))
|
||||
{
|
||||
if (s.Attachments.Any())
|
||||
{
|
||||
msg += "FILES_UPLOADED: "
|
||||
+ string.Join("\n", s.Attachments.Select(x => x.Url));
|
||||
}
|
||||
else if (s.Embeds.Any())
|
||||
{
|
||||
msg += "EMBEDS: "
|
||||
+ string.Join("\n--------\n",
|
||||
s.Embeds.Select(x
|
||||
=> $"Description: {x.Description}"));
|
||||
}
|
||||
}
|
||||
else
|
||||
msg += s.ToString();
|
||||
|
||||
return msg;
|
||||
})
|
||||
});
|
||||
return msg;
|
||||
})
|
||||
});
|
||||
await using var stream = await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream();
|
||||
await ctx.User.SendFileAsync(stream, title, title);
|
||||
}
|
||||
|
@ -690,8 +692,8 @@ public partial class Utility : EllieModule
|
|||
msg.DeleteAfter(0);
|
||||
|
||||
await Response()
|
||||
.Confirm($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms")
|
||||
.SendAsync();
|
||||
.Confirm($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms")
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -715,8 +717,8 @@ public partial class Utility : EllieModule
|
|||
if (succ)
|
||||
{
|
||||
await Response()
|
||||
.Confirm(strs.afk_set)
|
||||
.SendAsync();
|
||||
.Confirm(strs.afk_set)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -737,22 +739,22 @@ public partial class Utility : EllieModule
|
|||
|
||||
var script = CSharpScript.Create(scriptText,
|
||||
ScriptOptions.Default
|
||||
.WithReferences(this.GetType().Assembly)
|
||||
.WithImports(
|
||||
"System",
|
||||
"System.Collections.Generic",
|
||||
"System.IO",
|
||||
"System.Linq",
|
||||
"System.Net.Http",
|
||||
"System.Threading",
|
||||
"System.Threading.Tasks",
|
||||
"EllieBot",
|
||||
"EllieBot.Extensions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"EllieBot.Common",
|
||||
"EllieBot.Modules",
|
||||
"System.Text",
|
||||
"System.Text.Json"),
|
||||
.WithReferences(this.GetType().Assembly)
|
||||
.WithImports(
|
||||
"System",
|
||||
"System.Collections.Generic",
|
||||
"System.IO",
|
||||
"System.Linq",
|
||||
"System.Net.Http",
|
||||
"System.Threading",
|
||||
"System.Threading.Tasks",
|
||||
"EllieBot",
|
||||
"EllieBot.Extensions",
|
||||
"Microsoft.Extensions.DependencyInjection",
|
||||
"EllieBot.Common",
|
||||
"EllieBot.Modules",
|
||||
"System.Text",
|
||||
"System.Text.Json"),
|
||||
globalsType: typeof(EvalGlobals));
|
||||
|
||||
try
|
||||
|
@ -771,9 +773,9 @@ public partial class Utility : EllieModule
|
|||
if (!string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.AddField("Code", scriptText)
|
||||
.AddField("Output", output.TrimTo(512)!);
|
||||
.WithOkColor()
|
||||
.AddField("Code", scriptText)
|
||||
.AddField("Output", output.TrimTo(512)!);
|
||||
|
||||
_ = Response().Embed(eb).SendAsync();
|
||||
}
|
||||
|
@ -790,19 +792,21 @@ 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;
|
||||
}
|
||||
|
||||
var eb = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithDescription(msg.Content)
|
||||
.WithAuthor(msg.Author)
|
||||
.WithTimestamp(msg.Timestamp)
|
||||
.WithImageUrl(msg.Attachments.FirstOrDefault()?.Url)
|
||||
.WithFooter(GetText(strs.sniped_by(ctx.User.ToString())), ctx.User.GetDisplayAvatarUrl());
|
||||
.WithOkColor()
|
||||
.WithDescription(msg.Content)
|
||||
.WithAuthor(msg.Author)
|
||||
.WithTimestamp(msg.Timestamp)
|
||||
.WithImageUrl(msg.Attachments.FirstOrDefault()?.Url)
|
||||
.WithFooter(GetText(strs.sniped_by(ctx.User.ToString())), ctx.User.GetDisplayAvatarUrl());
|
||||
|
||||
ctx.Message.DeleteAfter(1);
|
||||
await Response().Embed(eb).SendAsync();
|
||||
|
|
|
@ -29,13 +29,13 @@ public partial class Xp
|
|||
else
|
||||
{
|
||||
await Response()
|
||||
.Confirm(
|
||||
strs.club_transfered(
|
||||
Format.Bold(club.Name),
|
||||
Format.Bold(newOwner.ToString())
|
||||
)
|
||||
)
|
||||
.SendAsync();
|
||||
.Confirm(
|
||||
strs.club_transfered(
|
||||
Format.Bold(club.Name),
|
||||
Format.Bold(newOwner.ToString())
|
||||
)
|
||||
)
|
||||
.SendAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,57 +108,85 @@ 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);
|
||||
var allUsers = club.Members.OrderByDescending(x =>
|
||||
{
|
||||
var l = new LevelStats(x.TotalXp).Level;
|
||||
if (club.OwnerId == x.Id)
|
||||
return int.MaxValue;
|
||||
if (x.IsClubAdmin)
|
||||
return (int.MaxValue / 2) + l;
|
||||
return l;
|
||||
})
|
||||
.ToList();
|
||||
{
|
||||
var l = new LevelStats(x.TotalXp).Level;
|
||||
if (club.OwnerId == x.Id)
|
||||
return int.MaxValue;
|
||||
if (x.IsClubAdmin)
|
||||
return (int.MaxValue / 2) + l;
|
||||
return l;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var rank = await _service.GetClubRankAsync(club.Id);
|
||||
|
||||
await Response()
|
||||
.Paginated()
|
||||
.Items(allUsers)
|
||||
.PageSize(10)
|
||||
.Page((users, _) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{club}")
|
||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||
.AddField(GetText(strs.desc),
|
||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||
.AddField(GetText(strs.rank), $"#{rank}", true)
|
||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||
.AddField(GetText(strs.members),
|
||||
string.Join("\n",
|
||||
users
|
||||
.Select(x =>
|
||||
{
|
||||
var l = new LevelStats(x.TotalXp);
|
||||
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
|
||||
if (club.OwnerId == x.Id)
|
||||
return x + "🌟" + lvlStr;
|
||||
if (x.IsClubAdmin)
|
||||
return x + "⭐" + lvlStr;
|
||||
return x + lvlStr;
|
||||
})));
|
||||
.Paginated()
|
||||
.Items(allUsers)
|
||||
.PageSize(10)
|
||||
.Page((users, _) =>
|
||||
{
|
||||
var embed = CreateEmbed()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{club}")
|
||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||
.AddField(GetText(strs.desc),
|
||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||
.AddField(GetText(strs.rank), $"#{rank}", true)
|
||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||
.AddField(GetText(strs.members),
|
||||
string.Join("\n",
|
||||
users
|
||||
.Select(x =>
|
||||
{
|
||||
var l = new LevelStats(x.TotalXp);
|
||||
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
|
||||
if (club.OwnerId == x.Id)
|
||||
return x + "🌟" + lvlStr;
|
||||
if (x.IsClubAdmin)
|
||||
return x + "⭐" + lvlStr;
|
||||
return x + lvlStr;
|
||||
})));
|
||||
|
||||
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
|
||||
return embed.WithThumbnailUrl(club.ImageUrl);
|
||||
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
|
||||
embed.WithThumbnailUrl(club.ImageUrl);
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
if (Uri.IsWellFormedUriString(club.BannerUrl, UriKind.Absolute))
|
||||
embed.WithImageUrl(club.BannerUrl);
|
||||
|
||||
return embed;
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -208,20 +236,20 @@ public partial class Xp
|
|||
var bans = club.Bans.Select(x => x.User).ToArray();
|
||||
|
||||
return Response()
|
||||
.Paginated()
|
||||
.Items(bans)
|
||||
.PageSize(10)
|
||||
.CurrentPage(page)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
||||
.Paginated()
|
||||
.Items(bans)
|
||||
.PageSize(10)
|
||||
.CurrentPage(page)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
||||
|
||||
return CreateEmbed()
|
||||
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
|
||||
.WithDescription(toShow)
|
||||
.WithOkColor();
|
||||
})
|
||||
.SendAsync();
|
||||
return CreateEmbed()
|
||||
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
|
||||
.WithDescription(toShow)
|
||||
.WithOkColor();
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -237,20 +265,20 @@ public partial class Xp
|
|||
var apps = club.Applicants.Select(x => x.User).ToArray();
|
||||
|
||||
return Response()
|
||||
.Paginated()
|
||||
.Items(apps)
|
||||
.PageSize(10)
|
||||
.CurrentPage(page)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
||||
.Paginated()
|
||||
.Items(apps)
|
||||
.PageSize(10)
|
||||
.CurrentPage(page)
|
||||
.Page((items, _) =>
|
||||
{
|
||||
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
||||
|
||||
return CreateEmbed()
|
||||
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
|
||||
.WithDescription(toShow)
|
||||
.WithOkColor();
|
||||
})
|
||||
.SendAsync();
|
||||
return CreateEmbed()
|
||||
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
|
||||
.WithDescription(toShow)
|
||||
.WithOkColor();
|
||||
})
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
|
@ -338,9 +366,9 @@ public partial class Xp
|
|||
if (result == ClubKickResult.Success)
|
||||
{
|
||||
return Response()
|
||||
.Confirm(strs.club_user_kick(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.club_user_kick(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
if (result == ClubKickResult.Hierarchy)
|
||||
|
@ -365,9 +393,9 @@ public partial class Xp
|
|||
if (result == ClubBanResult.Success)
|
||||
{
|
||||
return Response()
|
||||
.Confirm(strs.club_user_banned(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.club_user_banned(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
if (result == ClubBanResult.Unbannable)
|
||||
|
@ -393,9 +421,9 @@ public partial class Xp
|
|||
if (result == ClubUnbanResult.Success)
|
||||
{
|
||||
return Response()
|
||||
.Confirm(strs.club_user_unbanned(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())))
|
||||
.SendAsync();
|
||||
.Confirm(strs.club_user_unbanned(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())))
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
if (result == ClubUnbanResult.WrongUser)
|
||||
|
@ -415,11 +443,11 @@ public partial class Xp
|
|||
? "-"
|
||||
: desc;
|
||||
|
||||
var eb = _sender.CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.club_desc_update))
|
||||
.WithOkColor()
|
||||
.WithDescription(desc);
|
||||
var eb = CreateEmbed()
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.club_desc_update))
|
||||
.WithOkColor()
|
||||
.WithDescription(desc);
|
||||
|
||||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -1586,4 +1588,30 @@ fishspot:
|
|||
xprate:
|
||||
- xprate
|
||||
xpratereset:
|
||||
- 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:
|
||||
|
@ -4977,4 +4988,83 @@ xpratereset:
|
|||
params:
|
||||
- { }
|
||||
- channel:
|
||||
desc: "The channel to reset the rate for."
|
||||
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