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.*
|
.aider.*
|
||||||
PROMPT.md
|
PROMPT.md
|
||||||
.aider*
|
.aider*
|
||||||
|
.windsurfrules
|
||||||
|
|
||||||
|
## Python pip/env files
|
||||||
|
Pipfile
|
||||||
|
Pipfile.lock
|
||||||
|
.venv
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -1,6 +1,54 @@
|
||||||
# Changelog
|
# 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
|
## [5.3.9] - 31.01.2025
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,23 @@ namespace EllieBot.Db;
|
||||||
|
|
||||||
public static class CurrencyTransactionExtensions
|
public static class CurrencyTransactionExtensions
|
||||||
{
|
{
|
||||||
public static Task<List<CurrencyTransaction>> GetPageFor(
|
public static async Task<IReadOnlyCollection<CurrencyTransaction>> GetPageFor(
|
||||||
this DbSet<CurrencyTransaction> set,
|
this DbSet<CurrencyTransaction> set,
|
||||||
ulong userId,
|
ulong userId,
|
||||||
int page)
|
int page)
|
||||||
=> set.ToLinqToDBTable()
|
{
|
||||||
|
var items = await set.ToLinqToDBTable()
|
||||||
.Where(x => x.UserId == userId)
|
.Where(x => x.UserId == userId)
|
||||||
.OrderByDescending(x => x.DateAdded)
|
.OrderByDescending(x => x.DateAdded)
|
||||||
.Skip(15 * page)
|
.Skip(15 * page)
|
||||||
.Take(15)
|
.Take(15)
|
||||||
.ToListAsyncLinqToDB();
|
.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 Name { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string ImageUrl { get; set; } = string.Empty;
|
public string ImageUrl { get; set; } = string.Empty;
|
||||||
|
public string BannerUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public int Xp { get; set; } = 0;
|
public int Xp { get; set; } = 0;
|
||||||
public int? OwnerId { get; set; }
|
public int? OwnerId { get; set; }
|
||||||
public DiscordUser Owner { get; set; }
|
public DiscordUser Owner { get; set; }
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>true</ImplicitUsings>
|
<ImplicitUsings>true</ImplicitUsings>
|
||||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||||
<Version>6.0.2</Version>
|
<Version>6.0.3</Version>
|
||||||
|
|
||||||
<!-- Output/build -->
|
<!-- Output/build -->
|
||||||
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
|
||||||
|
|
||||||
<PackageReference Include="SixLabors.Fonts" Version="2.1.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.ImageSharp.Drawing" Version="2.1.5" />
|
||||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
|
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
|
<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
|
namespace EllieBot.Migrations.PostgreSql
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PostgreSqlContext))]
|
[DbContext(typeof(PostgreSqlContext))]
|
||||||
[Migration("20250228044209_init")]
|
[Migration("20250310143051_init")]
|
||||||
partial class init
|
partial class init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -572,6 +572,10 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bannerurl");
|
||||||
|
|
||||||
b.Property<DateTime?>("DateAdded")
|
b.Property<DateTime?>("DateAdded")
|
||||||
.HasColumnType("timestamp without time zone")
|
.HasColumnType("timestamp without time zone")
|
||||||
.HasColumnName("dateadded");
|
.HasColumnName("dateadded");
|
||||||
|
@ -3444,6 +3448,32 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("userfishstats", (string)null);
|
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 =>
|
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
|
@ -1078,6 +1078,19 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
table.PrimaryKey("pk_userfishstats", x => x.id);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "userxpstats",
|
name: "userxpstats",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
|
@ -1588,6 +1601,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
name = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
name = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: true),
|
||||||
description = table.Column<string>(type: "text", nullable: true),
|
description = table.Column<string>(type: "text", nullable: true),
|
||||||
imageurl = 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),
|
xp = table.Column<int>(type: "integer", nullable: false),
|
||||||
ownerid = table.Column<int>(type: "integer", nullable: true),
|
ownerid = table.Column<int>(type: "integer", nullable: true),
|
||||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||||
|
@ -2131,6 +2145,16 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
column: "userid",
|
column: "userid",
|
||||||
unique: true);
|
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(
|
migrationBuilder.CreateIndex(
|
||||||
name: "ix_userxpstats_guildid",
|
name: "ix_userxpstats_guildid",
|
||||||
table: "userxpstats",
|
table: "userxpstats",
|
||||||
|
@ -2492,6 +2516,9 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "userfishstats");
|
name: "userfishstats");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "userrole");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "userxpstats");
|
name: "userxpstats");
|
||||||
|
|
|
@ -569,6 +569,10 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("bannerurl");
|
||||||
|
|
||||||
b.Property<DateTime?>("DateAdded")
|
b.Property<DateTime?>("DateAdded")
|
||||||
.HasColumnType("timestamp without time zone")
|
.HasColumnType("timestamp without time zone")
|
||||||
.HasColumnName("dateadded");
|
.HasColumnName("dateadded");
|
||||||
|
@ -3441,6 +3445,32 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
b.ToTable("userfishstats", (string)null);
|
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 =>
|
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
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
|
namespace EllieBot.Migrations.Sqlite
|
||||||
{
|
{
|
||||||
[DbContext(typeof(SqliteContext))]
|
[DbContext(typeof(SqliteContext))]
|
||||||
[Migration("20250228044206_init")]
|
[Migration("20250310143048_init")]
|
||||||
partial class init
|
partial class init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -428,6 +428,9 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<DateTime?>("DateAdded")
|
b.Property<DateTime?>("DateAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
@ -2563,6 +2566,26 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
b.ToTable("UserFishStats");
|
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 =>
|
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
|
@ -1080,6 +1080,19 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
table.PrimaryKey("PK_UserFishStats", x => x.Id);
|
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(
|
migrationBuilder.CreateTable(
|
||||||
name: "UserXpStats",
|
name: "UserXpStats",
|
||||||
columns: table => new
|
columns: table => new
|
||||||
|
@ -1590,6 +1603,7 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
Name = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
Name = table.Column<string>(type: "TEXT", maxLength: 20, nullable: true),
|
||||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
ImageUrl = 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),
|
Xp = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
OwnerId = table.Column<int>(type: "INTEGER", nullable: true),
|
OwnerId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||||
|
@ -2133,6 +2147,16 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
column: "UserId",
|
column: "UserId",
|
||||||
unique: true);
|
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(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_UserXpStats_GuildId",
|
name: "IX_UserXpStats_GuildId",
|
||||||
table: "UserXpStats",
|
table: "UserXpStats",
|
||||||
|
@ -2494,6 +2518,9 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "UserFishStats");
|
name: "UserFishStats");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserRole");
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
name: "UserXpStats");
|
name: "UserXpStats");
|
||||||
|
|
|
@ -425,6 +425,9 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("BannerUrl")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<DateTime?>("DateAdded")
|
b.Property<DateTime?>("DateAdded")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
@ -2560,6 +2563,26 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
b.ToTable("UserFishStats");
|
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 =>
|
modelBuilder.Entity("EllieBot.Modules.Xp.ChannelXpConfig", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|
|
@ -218,20 +218,20 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
var val = Config.Timely.Amount;
|
var val = Config.Timely.Amount;
|
||||||
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
|
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
|
||||||
var guildUsers = await boostGuilds
|
var guildUsers = await boostGuilds
|
||||||
.Select(async gid =>
|
.Select(async gid =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var guild = await _client.Rest.GetGuildAsync(gid, false);
|
var guild = await _client.Rest.GetGuildAsync(gid, false);
|
||||||
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
|
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
|
||||||
return (guild, user);
|
return (guild, user);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.WhenAll();
|
.WhenAll();
|
||||||
|
|
||||||
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
||||||
var booster = userInfo != default;
|
var booster = userInfo != default;
|
||||||
|
@ -296,8 +296,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
.Confirm(strs.timely_set(Format.Bold(N(amount)), Format.Bold(period.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,10 +316,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
|
|
||||||
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||||
$"**{usr.Username}**",
|
$"**{usr.Username}**",
|
||||||
footer: $"ID: {usr.Id}")
|
footer: $"ID: {usr.Id}")
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -337,10 +337,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
|
|
||||||
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
var usr = membersArray[new EllieRandom().Next(0, membersArray.Length)];
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm("🎟 " + GetText(strs.raffled_user),
|
.Confirm("🎟 " + GetText(strs.raffled_user),
|
||||||
$"**{usr.Username}**",
|
$"**{usr.Username}**",
|
||||||
footer: $"ID: {usr.Id}")
|
footer: $"ID: {usr.Id}")
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -373,41 +373,54 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
return;
|
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())
|
await using (var uow = _db.GetDbContext())
|
||||||
{
|
{
|
||||||
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
|
count = await uow.Set<CurrencyTransaction>()
|
||||||
|
.GetCountFor(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
await Response()
|
||||||
.WithTitle(GetText(strs.transactions(
|
.Paginated()
|
||||||
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
|
.PageItems(async (curPage) =>
|
||||||
?? $"{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)
|
|
||||||
{
|
{
|
||||||
sb.AppendLine(transactionString);
|
await using var uow = _db.GetDbContext();
|
||||||
}
|
return await uow.Set<CurrencyTransaction>()
|
||||||
|
.GetPageFor(userId, curPage);
|
||||||
if (!string.IsNullOrWhiteSpace(tr.Note))
|
})
|
||||||
|
.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());
|
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
|
||||||
embed.WithFooter(GetText(strs.page(page + 1)));
|
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
|
||||||
await Response().Embed(embed).SendAsync();
|
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)
|
private static string GetFormattedCurtrDate(CurrencyTransaction ct)
|
||||||
|
@ -420,9 +433,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
|
|
||||||
var tr = await uow.Set<CurrencyTransaction>()
|
var tr = await uow.Set<CurrencyTransaction>()
|
||||||
.ToLinqToDBTable()
|
.ToLinqToDBTable()
|
||||||
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (tr is null)
|
if (tr is null)
|
||||||
{
|
{
|
||||||
|
@ -483,9 +496,9 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
|
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
|
||||||
|
|
||||||
await N(balance)
|
await N(balance)
|
||||||
.Pipe(strs.bank_balance)
|
.Pipe(strs.bank_balance)
|
||||||
.Pipe(GetText)
|
.Pipe(GetText)
|
||||||
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
.Pipe(text => smc.RespondConfirmAsync(_sender, text, ephemeral: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private EllieInteractionBase CreateCashInteraction()
|
private EllieInteractionBase CreateCashInteraction()
|
||||||
|
@ -507,13 +520,13 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(
|
.Confirm(
|
||||||
user.ToString()
|
user.ToString()
|
||||||
.Pipe(Format.Bold)
|
.Pipe(Format.Bold)
|
||||||
.With(cur)
|
.With(cur)
|
||||||
.Pipe(strs.has))
|
.Pipe(strs.has))
|
||||||
.Interaction(inter)
|
.Interaction(inter)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -594,10 +607,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
|
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.mass_award(N(amount),
|
.Confirm(strs.mass_award(N(amount),
|
||||||
Format.Bold(users.Count.ToString()),
|
Format.Bold(users.Count.ToString()),
|
||||||
Format.Bold(role.Name)))
|
Format.Bold(role.Name)))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -613,10 +626,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
new("take", ctx.User.ToString()!, null, ctx.User.Id));
|
new("take", ctx.User.ToString()!, null, ctx.User.Id));
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.mass_take(N(amount),
|
.Confirm(strs.mass_take(N(amount),
|
||||||
Format.Bold(users.Count.ToString()),
|
Format.Bold(users.Count.ToString()),
|
||||||
Format.Bold(role.Name)))
|
Format.Bold(role.Name)))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -639,8 +652,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
|
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -662,8 +675,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
|
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -695,12 +708,12 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
}
|
}
|
||||||
|
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithAuthor(ctx.User)
|
.WithAuthor(ctx.User)
|
||||||
.WithDescription(Format.Bold(str))
|
.WithDescription(Format.Bold(str))
|
||||||
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
|
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture), true)
|
||||||
.AddField(GetText(strs.bet), N(amount), true)
|
.AddField(GetText(strs.bet), N(amount), true)
|
||||||
.AddField(GetText(strs.won), N((long)result.Won), true)
|
.AddField(GetText(strs.won), N((long)result.Won), true)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
await Response().Embed(eb).SendAsync();
|
await Response().Embed(eb).SendAsync();
|
||||||
}
|
}
|
||||||
|
@ -741,11 +754,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
|
|
||||||
await using var uow = _db.GetDbContext();
|
await using var uow = _db.GetDbContext();
|
||||||
var cleanRichest = await uow.GetTable<DiscordUser>()
|
var cleanRichest = await uow.GetTable<DiscordUser>()
|
||||||
.Where(x => x.UserId.In(users))
|
.Where(x => x.UserId.In(users))
|
||||||
.OrderByDescending(x => x.CurrencyAmount)
|
.OrderByDescending(x => x.CurrencyAmount)
|
||||||
.Skip(curPage * perPage)
|
.Skip(curPage * perPage)
|
||||||
.Take(perPage)
|
.Take(perPage)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return cleanRichest;
|
return cleanRichest;
|
||||||
}
|
}
|
||||||
|
@ -757,34 +770,34 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.PageItems(GetTopRichest)
|
.PageItems(GetTopRichest)
|
||||||
.PageSize(9)
|
.PageSize(9)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.Page((toSend, curPage) =>
|
.Page((toSend, curPage) =>
|
||||||
{
|
{
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
|
||||||
|
|
||||||
if (!toSend.Any())
|
if (!toSend.Any())
|
||||||
{
|
{
|
||||||
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
embed.WithDescription(GetText(strs.no_user_on_this_page));
|
||||||
return Task.FromResult(embed);
|
return Task.FromResult(embed);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < toSend.Count; i++)
|
for (var i = 0; i < toSend.Count; i++)
|
||||||
{
|
{
|
||||||
var x = toSend[i];
|
var x = toSend[i];
|
||||||
var usrStr = x.ToString().TrimTo(20, true);
|
var usrStr = x.ToString().TrimTo(20, true);
|
||||||
|
|
||||||
var j = i;
|
var j = i;
|
||||||
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
embed.AddField("#" + ((9 * curPage) + j + 1) + " " + usrStr, N(x.CurrencyAmount), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(embed);
|
return Task.FromResult(embed);
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum InputRpsPick : byte
|
public enum InputRpsPick : byte
|
||||||
|
@ -895,11 +908,11 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
}
|
}
|
||||||
|
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithDescription(sb.ToString())
|
.WithDescription(sb.ToString())
|
||||||
.AddField(GetText(strs.bet), N(amount), true)
|
.AddField(GetText(strs.bet), N(amount), true)
|
||||||
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
|
.AddField(GetText(strs.won), $"{N((long)result.Won)}", true)
|
||||||
.WithAuthor(ctx.User);
|
.WithAuthor(ctx.User);
|
||||||
|
|
||||||
|
|
||||||
await Response().Embed(eb).SendAsync();
|
await Response().Embed(eb).SendAsync();
|
||||||
|
@ -924,8 +937,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
public async Task BetTest()
|
public async Task BetTest()
|
||||||
{
|
{
|
||||||
var values = Enum.GetValues<GambleTestTarget>()
|
var values = Enum.GetValues<GambleTestTarget>()
|
||||||
.Select(x => $"`{x}`")
|
.Select(x => $"`{x}`")
|
||||||
.Join(", ");
|
.Join(", ");
|
||||||
|
|
||||||
await Response().Confirm(GetText(strs.available_tests), values).SendAsync();
|
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}`");
|
sb.AppendLine($"Longest lose streak: `{maxL}`");
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(GetText(strs.test_results_for(target)),
|
.Confirm(GetText(strs.test_results_for(target)),
|
||||||
sb.ToString(),
|
sb.ToString(),
|
||||||
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private EllieInteractionBase CreateRakebackInteraction()
|
private EllieInteractionBase CreateRakebackInteraction()
|
||||||
|
@ -1032,16 +1045,16 @@ public partial class Gambling : GamblingModule<GamblingService>
|
||||||
if (rb < 1)
|
if (rb < 1)
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Error(strs.rakeback_none)
|
.Error(strs.rakeback_none)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var inter = CreateRakebackInteraction();
|
var inter = CreateRakebackInteraction();
|
||||||
await Response()
|
await Response()
|
||||||
.Pending(strs.rakeback_available(N(rb)))
|
.Pending(strs.rakeback_available(N(rb)))
|
||||||
.Interaction(inter)
|
.Interaction(inter)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,13 +7,24 @@ namespace EllieBot.Modules.Music;
|
||||||
[NoPublicBot]
|
[NoPublicBot]
|
||||||
public sealed partial class Music : EllieModule<IMusicService>
|
public sealed partial class Music : EllieModule<IMusicService>
|
||||||
{
|
{
|
||||||
public enum All { All = -1 }
|
public enum All
|
||||||
|
{
|
||||||
|
All = -1
|
||||||
|
}
|
||||||
|
|
||||||
public enum InputRepeatType
|
public enum InputRepeatType
|
||||||
{
|
{
|
||||||
N = 0, No = 0, None = 0,
|
N = 0,
|
||||||
T = 1, Track = 1, S = 1, Song = 1,
|
No = 0,
|
||||||
Q = 2, Queue = 2, Playlist = 2, Pl = 2
|
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";
|
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 static readonly SemaphoreSlim _voiceChannelLock = new(1, 1);
|
||||||
private readonly ILogCommandService _logService;
|
private readonly ILogCommandService _logService;
|
||||||
|
private readonly ILyricsService _lyricsService;
|
||||||
|
|
||||||
public Music(ILogCommandService logService)
|
public Music(ILogCommandService logService, ILyricsService lyricsService)
|
||||||
=> _logService = logService;
|
{
|
||||||
|
_logService = logService;
|
||||||
|
_lyricsService = lyricsService;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> ValidateAsync()
|
private async Task<bool> ValidateAsync()
|
||||||
{
|
{
|
||||||
|
@ -110,10 +125,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
|
||||||
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
|
||||||
.WithFooter(trackInfo.Platform.ToString());
|
.WithFooter(trackInfo.Platform.ToString());
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail))
|
||||||
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
embed.WithThumbnailUrl(trackInfo.Thumbnail);
|
||||||
|
@ -301,39 +316,39 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
|
|
||||||
|
|
||||||
desc += tracks
|
desc += tracks
|
||||||
.Select((v, index) =>
|
.Select((v, index) =>
|
||||||
{
|
{
|
||||||
index += LQ_ITEMS_PER_PAGE * curPage;
|
index += LQ_ITEMS_PER_PAGE * curPage;
|
||||||
if (index == currentIndex)
|
if (index == currentIndex)
|
||||||
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
return $"**⇒**`{index + 1}.` {v.PrettyFullName()}";
|
||||||
|
|
||||||
return $"`{index + 1}.` {v.PrettyFullName()}";
|
return $"`{index + 1}.` {v.PrettyFullName()}";
|
||||||
})
|
})
|
||||||
.Join('\n');
|
.Join('\n');
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(add))
|
if (!string.IsNullOrWhiteSpace(add))
|
||||||
desc = add + "\n" + desc;
|
desc = add + "\n" + desc;
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithAuthor(
|
.WithAuthor(
|
||||||
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
|
||||||
MUSIC_ICON_URL)
|
MUSIC_ICON_URL)
|
||||||
.WithDescription(desc)
|
.WithDescription(desc)
|
||||||
.WithFooter(
|
.WithFooter(
|
||||||
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
$" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ")
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(tracks)
|
.Items(tracks)
|
||||||
.PageSize(LQ_ITEMS_PER_PAGE)
|
.PageSize(LQ_ITEMS_PER_PAGE)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.AddFooter(false)
|
.AddFooter(false)
|
||||||
.Page(PrintAction)
|
.Page(PrintAction)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// search
|
// search
|
||||||
|
@ -353,15 +368,15 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
|
|
||||||
|
|
||||||
var embeds = videos.Select((x, i) => CreateEmbed()
|
var embeds = videos.Select((x, i) => CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithThumbnailUrl(x.Thumbnail)
|
.WithThumbnailUrl(x.Thumbnail)
|
||||||
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var msg = await Response()
|
var msg = await Response()
|
||||||
.Text(strs.queue_search_results)
|
.Text(strs.queue_search_results)
|
||||||
.Embeds(embeds)
|
.Embeds(embeds)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -425,10 +440,10 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
|
||||||
.WithDescription(track.PrettyName())
|
.WithDescription(track.PrettyName())
|
||||||
.WithFooter(track.PrettyInfo())
|
.WithFooter(track.PrettyInfo())
|
||||||
.WithErrorColor();
|
.WithErrorColor();
|
||||||
|
|
||||||
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
await _service.SendToOutputAsync(ctx.Guild.Id, embed);
|
||||||
}
|
}
|
||||||
|
@ -593,11 +608,11 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithTitle(track.Title.TrimTo(65))
|
.WithTitle(track.Title.TrimTo(65))
|
||||||
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
|
||||||
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
|
||||||
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
.AddField(GetText(strs.to_position), $"#{to + 1}", true)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute))
|
||||||
embed.WithUrl(track.Url);
|
embed.WithUrl(track.Url);
|
||||||
|
@ -652,12 +667,12 @@ public sealed partial class Music : EllieModule<IMusicService>
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
|
||||||
.WithDescription(currentTrack.PrettyName())
|
.WithDescription(currentTrack.PrettyName())
|
||||||
.WithThumbnailUrl(currentTrack.Thumbnail)
|
.WithThumbnailUrl(currentTrack.Thumbnail)
|
||||||
.WithFooter(
|
.WithFooter(
|
||||||
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
$"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}");
|
||||||
|
|
||||||
await Response().Embed(embed).SendAsync();
|
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();
|
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"];
|
var gElement = xml["svg"]?["g"];
|
||||||
if (gElement is null)
|
if (gElement is null)
|
||||||
return Array.Empty<PointF>();
|
return [];
|
||||||
|
|
||||||
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
|
Span<PointF> points = new PointF[gElement.ChildNodes.Count];
|
||||||
var cnt = 0;
|
var cnt = 0;
|
||||||
|
@ -73,7 +73,7 @@ public class CryptoService : IEService
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cnt == 0)
|
if (cnt == 0)
|
||||||
return Array.Empty<PointF>();
|
return [];
|
||||||
|
|
||||||
return points.Slice(0, cnt).ToArray();
|
return points.Slice(0, cnt).ToArray();
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ public partial class Utility
|
||||||
# txt: Quote text
|
# txt: Quote text
|
||||||
|
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
|
private static readonly ISerializer _exportSerializer = new SerializerBuilder()
|
||||||
.WithEventEmitter(args
|
.WithEventEmitter(args
|
||||||
=> new MultilineScalarFlowStyleEmitter(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);
|
message = await repSvc.ReplaceAsync(message, repCtx);
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Text(message)
|
.Text(message)
|
||||||
.Channel(channel)
|
.Channel(channel)
|
||||||
.UserBasedMentions()
|
.UserBasedMentions()
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -123,27 +123,27 @@ public partial class Utility : EllieModule
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Sanitize()
|
.Sanitize()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(userNames)
|
.Items(userNames)
|
||||||
.PageSize(20)
|
.PageSize(20)
|
||||||
.Page((names, _) =>
|
.Page((names, _) =>
|
||||||
{
|
{
|
||||||
if (names.Count == 0)
|
if (names.Count == 0)
|
||||||
{
|
{
|
||||||
return CreateEmbed()
|
return CreateEmbed()
|
||||||
.WithErrorColor()
|
.WithErrorColor()
|
||||||
.WithDescription(GetText(strs.nobody_playing_game));
|
.WithDescription(GetText(strs.nobody_playing_game));
|
||||||
}
|
}
|
||||||
|
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
|
|
||||||
var users = names.Join('\n');
|
var users = names.Join('\n');
|
||||||
|
|
||||||
return eb.WithDescription(users);
|
return eb.WithDescription(users);
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -161,9 +161,11 @@ public partial class Utility : EllieModule
|
||||||
CacheMode.CacheOnly
|
CacheMode.CacheOnly
|
||||||
);
|
);
|
||||||
|
|
||||||
users = role is null
|
users = (role is null
|
||||||
? users
|
? users
|
||||||
: users.Where(u => u.RoleIds.Contains(role.Id)).ToList();
|
: users.Where(u => u.RoleIds.Contains(role.Id)))
|
||||||
|
.OrderBy(x => x.DisplayName)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
|
||||||
var roleUsers = new List<string>(users.Count);
|
var roleUsers = new List<string>(users.Count);
|
||||||
|
@ -173,23 +175,23 @@ public partial class Utility : EllieModule
|
||||||
}
|
}
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(roleUsers)
|
.Items(roleUsers)
|
||||||
.PageSize(20)
|
.PageSize(20)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.Page((pageUsers, _) =>
|
.Page((pageUsers, _) =>
|
||||||
{
|
{
|
||||||
if (pageUsers.Count == 0)
|
if (pageUsers.Count == 0)
|
||||||
return CreateEmbed().WithOkColor().WithDescription(GetText(strs.no_user_on_this_page));
|
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()
|
return CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
|
.WithTitle(GetText(strs.inrole_list(role?.GetIconUrl() + roleName, roleUsers.Count)))
|
||||||
.WithDescription(string.Join("\n", pageUsers));
|
.WithDescription(string.Join("\n", pageUsers));
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -211,14 +213,14 @@ public partial class Utility : EllieModule
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
foreach (var p in perms.GetType()
|
foreach (var p in perms.GetType()
|
||||||
.GetProperties()
|
.GetProperties()
|
||||||
.Where(static p =>
|
.Where(static p =>
|
||||||
{
|
{
|
||||||
var method = p.GetGetMethod();
|
var method = p.GetGetMethod();
|
||||||
if (method is null)
|
if (method is null)
|
||||||
return false;
|
return false;
|
||||||
return !method.GetParameters().Any();
|
return !method.GetParameters().Any();
|
||||||
}))
|
}))
|
||||||
builder.AppendLine($"{p.Name} : {p.GetValue(perms, null)}");
|
builder.AppendLine($"{p.Name} : {p.GetValue(perms, null)}");
|
||||||
await Response().Confirm(builder.ToString()).SendAsync();
|
await Response().Confirm(builder.ToString()).SendAsync();
|
||||||
}
|
}
|
||||||
|
@ -229,20 +231,20 @@ public partial class Utility : EllieModule
|
||||||
{
|
{
|
||||||
var usr = target ?? ctx.User;
|
var usr = target ?? ctx.User;
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.userid("🆔",
|
.Confirm(strs.userid("🆔",
|
||||||
Format.Bold(usr.ToString()),
|
Format.Bold(usr.ToString()),
|
||||||
Format.Code(usr.Id.ToString())))
|
Format.Code(usr.Id.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async Task RoleId([Leftover] IRole role)
|
public async Task RoleId([Leftover] IRole role)
|
||||||
=> await Response()
|
=> await Response()
|
||||||
.Confirm(strs.roleid("🆔",
|
.Confirm(strs.roleid("🆔",
|
||||||
Format.Bold(role.ToString()),
|
Format.Bold(role.ToString()),
|
||||||
Format.Code(role.Id.ToString())))
|
Format.Code(role.Id.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
public async Task ChannelId()
|
public async Task ChannelId()
|
||||||
|
@ -267,36 +269,36 @@ public partial class Utility : EllieModule
|
||||||
if (target is not null)
|
if (target is not null)
|
||||||
{
|
{
|
||||||
var roles = target.GetRoles()
|
var roles = target.GetRoles()
|
||||||
.Except(new[] { guild.EveryoneRole })
|
.Except(new[] { guild.EveryoneRole })
|
||||||
.OrderBy(r => -r.Position)
|
.OrderBy(r => -r.Position)
|
||||||
.Skip((page - 1) * rolesPerPage)
|
.Skip((page - 1) * rolesPerPage)
|
||||||
.Take(rolesPerPage)
|
.Take(rolesPerPage)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (!roles.Any())
|
if (!roles.Any())
|
||||||
await Response().Error(strs.no_roles_on_page).SendAsync();
|
await Response().Error(strs.no_roles_on_page).SendAsync();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(GetText(strs.roles_page(page, Format.Bold(target.ToString()))),
|
.Confirm(GetText(strs.roles_page(page, Format.Bold(target.ToString()))),
|
||||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles))
|
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var roles = guild.Roles.Except(new[] { guild.EveryoneRole })
|
var roles = guild.Roles.Except(new[] { guild.EveryoneRole })
|
||||||
.OrderBy(r => -r.Position)
|
.OrderBy(r => -r.Position)
|
||||||
.Skip((page - 1) * rolesPerPage)
|
.Skip((page - 1) * rolesPerPage)
|
||||||
.Take(rolesPerPage)
|
.Take(rolesPerPage)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (!roles.Any())
|
if (!roles.Any())
|
||||||
await Response().Error(strs.no_roles_on_page).SendAsync();
|
await Response().Error(strs.no_roles_on_page).SendAsync();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(GetText(strs.roles_all_page(page)),
|
.Confirm(GetText(strs.roles_all_page(page)),
|
||||||
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true))
|
"\n• " + string.Join("\n• ", (IEnumerable<IRole>)roles).SanitizeMentions(true))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,33 +330,33 @@ public partial class Utility : EllieModule
|
||||||
ownerIds = "-";
|
ownerIds = "-";
|
||||||
|
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithAuthor($"EllieBot v{StatsService.BotVersion}",
|
.WithAuthor($"EllieBot v{StatsService.BotVersion}",
|
||||||
"https://cdn.elliebot.net/Ellie.png",
|
"https://cdn.elliebot.net/Ellie.png",
|
||||||
"https://docs.elliebot.net")
|
"https://docs.elliebot.net")
|
||||||
.AddField(GetText(strs.author), _stats.Author, true)
|
.AddField(GetText(strs.author), _stats.Author, true)
|
||||||
.AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true)
|
.AddField(GetText(strs.botid), _client.CurrentUser.Id.ToString(), true)
|
||||||
.AddField(GetText(strs.shard),
|
.AddField(GetText(strs.shard),
|
||||||
$"#{_client.ShardId} / {_creds.TotalShards}",
|
$"#{_client.ShardId} / {_creds.TotalShards}",
|
||||||
true)
|
true)
|
||||||
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
|
.AddField(GetText(strs.commands_ran), _stats.CommandsRan.ToString(), true)
|
||||||
.AddField(GetText(strs.messages),
|
.AddField(GetText(strs.messages),
|
||||||
$"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
|
$"{_stats.MessageCounter} ({_stats.MessagesPerSecond:F2}/sec)",
|
||||||
true)
|
true)
|
||||||
.AddField(GetText(strs.memory),
|
.AddField(GetText(strs.memory),
|
||||||
FormattableString.Invariant($"{_stats.GetPrivateMemoryMegabytes():F2} MB"),
|
FormattableString.Invariant($"{_stats.GetPrivateMemoryMegabytes():F2} MB"),
|
||||||
true)
|
true)
|
||||||
.AddField(GetText(strs.owner_ids), ownerIds, true)
|
.AddField(GetText(strs.owner_ids), ownerIds, true)
|
||||||
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
|
.AddField(GetText(strs.uptime), _stats.GetUptimeString("\n"), true)
|
||||||
.AddField(GetText(strs.presence),
|
.AddField(GetText(strs.presence),
|
||||||
GetText(strs.presence_txt(_coord.GetGuildCount(),
|
GetText(strs.presence_txt(_coord.GetGuildCount(),
|
||||||
_stats.TextChannels,
|
_stats.TextChannels,
|
||||||
_stats.VoiceChannels)),
|
_stats.VoiceChannels)),
|
||||||
true);
|
true);
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Embed(eb)
|
.Embed(eb)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -503,9 +505,9 @@ public partial class Utility : EllieModule
|
||||||
}
|
}
|
||||||
|
|
||||||
format = attach.Filename
|
format = attach.Filename
|
||||||
.Split('.')
|
.Split('.')
|
||||||
.Last()
|
.Last()
|
||||||
.ToLowerInvariant();
|
.ToLowerInvariant();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(format) || (format != "png" && format != "apng"))
|
if (string.IsNullOrWhiteSpace(format) || (format != "png" && format != "apng"))
|
||||||
{
|
{
|
||||||
|
@ -572,30 +574,30 @@ public partial class Utility : EllieModule
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var allGuilds = _client.Guilds
|
var allGuilds = _client.Guilds
|
||||||
.OrderBy(g => g.Name)
|
.OrderBy(g => g.Name)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(allGuilds)
|
.Items(allGuilds)
|
||||||
.PageSize(9)
|
.PageSize(9)
|
||||||
.Page((guilds, _) =>
|
.Page((guilds, _) =>
|
||||||
{
|
{
|
||||||
if (!guilds.Any())
|
if (!guilds.Any())
|
||||||
{
|
{
|
||||||
return CreateEmbed()
|
return CreateEmbed()
|
||||||
.WithDescription(GetText(strs.listservers_none))
|
.WithDescription(GetText(strs.listservers_none))
|
||||||
.WithErrorColor();
|
.WithErrorColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
foreach (var guild in guilds)
|
foreach (var guild in guilds)
|
||||||
embed.AddField(guild.Name, GetText(strs.listservers(guild.Id, guild.MemberCount, guild.OwnerId)));
|
embed.AddField(guild.Name, GetText(strs.listservers(guild.Id, guild.MemberCount, guild.OwnerId)));
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -632,7 +634,7 @@ public partial class Utility : EllieModule
|
||||||
{
|
{
|
||||||
Content = msg.Content,
|
Content = msg.Content,
|
||||||
Embeds = msg.Embeds
|
Embeds = msg.Embeds
|
||||||
.Map(x => new SmartEmbedArrayElementText(x))
|
.Map(x => new SmartEmbedArrayElementText(x))
|
||||||
}.ToJson(_showEmbedSerializerOptions);
|
}.ToJson(_showEmbedSerializerOptions);
|
||||||
|
|
||||||
await Response().Confirm(Format.Code(json, "json").Replace("](", "]\\(")).SendAsync();
|
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 title = $"Chatlog-{ctx.Guild.Name}/#{ctx.Channel.Name}-{DateTime.Now}.txt";
|
||||||
var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}")
|
var grouping = msgs.GroupBy(x => $"{x.CreatedAt.Date:dd.MM.yyyy}")
|
||||||
.Select(g => new
|
.Select(g => new
|
||||||
{
|
{
|
||||||
date = g.Key,
|
date = g.Key,
|
||||||
messages = g.OrderBy(x => x.CreatedAt)
|
messages = g.OrderBy(x => x.CreatedAt)
|
||||||
.Select(s =>
|
.Select(s =>
|
||||||
{
|
{
|
||||||
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
|
var msg = $"【{s.Timestamp:HH:mm:ss}】{s.Author}:";
|
||||||
if (string.IsNullOrWhiteSpace(s.ToString()))
|
if (string.IsNullOrWhiteSpace(s.ToString()))
|
||||||
{
|
{
|
||||||
if (s.Attachments.Any())
|
if (s.Attachments.Any())
|
||||||
{
|
{
|
||||||
msg += "FILES_UPLOADED: "
|
msg += "FILES_UPLOADED: "
|
||||||
+ string.Join("\n", s.Attachments.Select(x => x.Url));
|
+ string.Join("\n", s.Attachments.Select(x => x.Url));
|
||||||
}
|
}
|
||||||
else if (s.Embeds.Any())
|
else if (s.Embeds.Any())
|
||||||
{
|
{
|
||||||
msg += "EMBEDS: "
|
msg += "EMBEDS: "
|
||||||
+ string.Join("\n--------\n",
|
+ string.Join("\n--------\n",
|
||||||
s.Embeds.Select(x
|
s.Embeds.Select(x
|
||||||
=> $"Description: {x.Description}"));
|
=> $"Description: {x.Description}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
msg += s.ToString();
|
msg += s.ToString();
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
await using var stream = await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream();
|
await using var stream = await JsonConvert.SerializeObject(grouping, Formatting.Indented).ToStream();
|
||||||
await ctx.User.SendFileAsync(stream, title, title);
|
await ctx.User.SendFileAsync(stream, title, title);
|
||||||
}
|
}
|
||||||
|
@ -690,8 +692,8 @@ public partial class Utility : EllieModule
|
||||||
msg.DeleteAfter(0);
|
msg.DeleteAfter(0);
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms")
|
.Confirm($"{Format.Bold(ctx.User.ToString())} 🏓 {(int)sw.Elapsed.TotalMilliseconds}ms")
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -715,8 +717,8 @@ public partial class Utility : EllieModule
|
||||||
if (succ)
|
if (succ)
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.afk_set)
|
.Confirm(strs.afk_set)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -737,22 +739,22 @@ public partial class Utility : EllieModule
|
||||||
|
|
||||||
var script = CSharpScript.Create(scriptText,
|
var script = CSharpScript.Create(scriptText,
|
||||||
ScriptOptions.Default
|
ScriptOptions.Default
|
||||||
.WithReferences(this.GetType().Assembly)
|
.WithReferences(this.GetType().Assembly)
|
||||||
.WithImports(
|
.WithImports(
|
||||||
"System",
|
"System",
|
||||||
"System.Collections.Generic",
|
"System.Collections.Generic",
|
||||||
"System.IO",
|
"System.IO",
|
||||||
"System.Linq",
|
"System.Linq",
|
||||||
"System.Net.Http",
|
"System.Net.Http",
|
||||||
"System.Threading",
|
"System.Threading",
|
||||||
"System.Threading.Tasks",
|
"System.Threading.Tasks",
|
||||||
"EllieBot",
|
"EllieBot",
|
||||||
"EllieBot.Extensions",
|
"EllieBot.Extensions",
|
||||||
"Microsoft.Extensions.DependencyInjection",
|
"Microsoft.Extensions.DependencyInjection",
|
||||||
"EllieBot.Common",
|
"EllieBot.Common",
|
||||||
"EllieBot.Modules",
|
"EllieBot.Modules",
|
||||||
"System.Text",
|
"System.Text",
|
||||||
"System.Text.Json"),
|
"System.Text.Json"),
|
||||||
globalsType: typeof(EvalGlobals));
|
globalsType: typeof(EvalGlobals));
|
||||||
|
|
||||||
try
|
try
|
||||||
|
@ -771,9 +773,9 @@ public partial class Utility : EllieModule
|
||||||
if (!string.IsNullOrWhiteSpace(output))
|
if (!string.IsNullOrWhiteSpace(output))
|
||||||
{
|
{
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.AddField("Code", scriptText)
|
.AddField("Code", scriptText)
|
||||||
.AddField("Output", output.TrimTo(512)!);
|
.AddField("Output", output.TrimTo(512)!);
|
||||||
|
|
||||||
_ = Response().Embed(eb).SendAsync();
|
_ = Response().Embed(eb).SendAsync();
|
||||||
}
|
}
|
||||||
|
@ -790,19 +792,21 @@ public partial class Utility : EllieModule
|
||||||
if (ctx.Message.ReferencedMessage is not { } msg)
|
if (ctx.Message.ReferencedMessage is not { } msg)
|
||||||
{
|
{
|
||||||
var msgs = await ctx.Channel.GetMessagesAsync(ctx.Message, Direction.Before, 3).FlattenAsync();
|
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)
|
if (msg is null)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var eb = CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithDescription(msg.Content)
|
.WithDescription(msg.Content)
|
||||||
.WithAuthor(msg.Author)
|
.WithAuthor(msg.Author)
|
||||||
.WithTimestamp(msg.Timestamp)
|
.WithTimestamp(msg.Timestamp)
|
||||||
.WithImageUrl(msg.Attachments.FirstOrDefault()?.Url)
|
.WithImageUrl(msg.Attachments.FirstOrDefault()?.Url)
|
||||||
.WithFooter(GetText(strs.sniped_by(ctx.User.ToString())), ctx.User.GetDisplayAvatarUrl());
|
.WithFooter(GetText(strs.sniped_by(ctx.User.ToString())), ctx.User.GetDisplayAvatarUrl());
|
||||||
|
|
||||||
ctx.Message.DeleteAfter(1);
|
ctx.Message.DeleteAfter(1);
|
||||||
await Response().Embed(eb).SendAsync();
|
await Response().Embed(eb).SendAsync();
|
||||||
|
|
|
@ -29,13 +29,13 @@ public partial class Xp
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(
|
.Confirm(
|
||||||
strs.club_transfered(
|
strs.club_transfered(
|
||||||
Format.Bold(club.Name),
|
Format.Bold(club.Name),
|
||||||
Format.Bold(newOwner.ToString())
|
Format.Bold(newOwner.ToString())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,57 +108,85 @@ public partial class Xp
|
||||||
await Response().Error(strs.club_icon_invalid_filetype).SendAsync();
|
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)
|
private async Task InternalClubInfoAsync(ClubInfo club)
|
||||||
{
|
{
|
||||||
var lvl = new LevelStats(club.Xp);
|
var lvl = new LevelStats(club.Xp);
|
||||||
var allUsers = club.Members.OrderByDescending(x =>
|
var allUsers = club.Members.OrderByDescending(x =>
|
||||||
{
|
{
|
||||||
var l = new LevelStats(x.TotalXp).Level;
|
var l = new LevelStats(x.TotalXp).Level;
|
||||||
if (club.OwnerId == x.Id)
|
if (club.OwnerId == x.Id)
|
||||||
return int.MaxValue;
|
return int.MaxValue;
|
||||||
if (x.IsClubAdmin)
|
if (x.IsClubAdmin)
|
||||||
return (int.MaxValue / 2) + l;
|
return (int.MaxValue / 2) + l;
|
||||||
return l;
|
return l;
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var rank = await _service.GetClubRankAsync(club.Id);
|
var rank = await _service.GetClubRankAsync(club.Id);
|
||||||
|
|
||||||
await Response()
|
await Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(allUsers)
|
.Items(allUsers)
|
||||||
.PageSize(10)
|
.PageSize(10)
|
||||||
.Page((users, _) =>
|
.Page((users, _) =>
|
||||||
{
|
{
|
||||||
var embed = CreateEmbed()
|
var embed = CreateEmbed()
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithTitle($"{club}")
|
.WithTitle($"{club}")
|
||||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||||
.AddField(GetText(strs.desc),
|
.AddField(GetText(strs.desc),
|
||||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||||
.AddField(GetText(strs.rank), $"#{rank}", true)
|
.AddField(GetText(strs.rank), $"#{rank}", true)
|
||||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||||
.AddField(GetText(strs.members),
|
.AddField(GetText(strs.members),
|
||||||
string.Join("\n",
|
string.Join("\n",
|
||||||
users
|
users
|
||||||
.Select(x =>
|
.Select(x =>
|
||||||
{
|
{
|
||||||
var l = new LevelStats(x.TotalXp);
|
var l = new LevelStats(x.TotalXp);
|
||||||
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
|
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
|
||||||
if (club.OwnerId == x.Id)
|
if (club.OwnerId == x.Id)
|
||||||
return x + "🌟" + lvlStr;
|
return x + "🌟" + lvlStr;
|
||||||
if (x.IsClubAdmin)
|
if (x.IsClubAdmin)
|
||||||
return x + "⭐" + lvlStr;
|
return x + "⭐" + lvlStr;
|
||||||
return x + lvlStr;
|
return x + lvlStr;
|
||||||
})));
|
})));
|
||||||
|
|
||||||
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
|
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
|
||||||
return embed.WithThumbnailUrl(club.ImageUrl);
|
embed.WithThumbnailUrl(club.ImageUrl);
|
||||||
|
|
||||||
return embed;
|
if (Uri.IsWellFormedUriString(club.BannerUrl, UriKind.Absolute))
|
||||||
})
|
embed.WithImageUrl(club.BannerUrl);
|
||||||
.SendAsync();
|
|
||||||
|
return embed;
|
||||||
|
})
|
||||||
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -208,20 +236,20 @@ public partial class Xp
|
||||||
var bans = club.Bans.Select(x => x.User).ToArray();
|
var bans = club.Bans.Select(x => x.User).ToArray();
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(bans)
|
.Items(bans)
|
||||||
.PageSize(10)
|
.PageSize(10)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.Page((items, _) =>
|
.Page((items, _) =>
|
||||||
{
|
{
|
||||||
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
||||||
|
|
||||||
return CreateEmbed()
|
return CreateEmbed()
|
||||||
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
|
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
|
||||||
.WithDescription(toShow)
|
.WithDescription(toShow)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -237,20 +265,20 @@ public partial class Xp
|
||||||
var apps = club.Applicants.Select(x => x.User).ToArray();
|
var apps = club.Applicants.Select(x => x.User).ToArray();
|
||||||
|
|
||||||
return Response()
|
return Response()
|
||||||
.Paginated()
|
.Paginated()
|
||||||
.Items(apps)
|
.Items(apps)
|
||||||
.PageSize(10)
|
.PageSize(10)
|
||||||
.CurrentPage(page)
|
.CurrentPage(page)
|
||||||
.Page((items, _) =>
|
.Page((items, _) =>
|
||||||
{
|
{
|
||||||
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
var toShow = string.Join("\n", items.Select(x => x.ToString()));
|
||||||
|
|
||||||
return CreateEmbed()
|
return CreateEmbed()
|
||||||
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
|
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
|
||||||
.WithDescription(toShow)
|
.WithDescription(toShow)
|
||||||
.WithOkColor();
|
.WithOkColor();
|
||||||
})
|
})
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
|
@ -338,9 +366,9 @@ public partial class Xp
|
||||||
if (result == ClubKickResult.Success)
|
if (result == ClubKickResult.Success)
|
||||||
{
|
{
|
||||||
return Response()
|
return Response()
|
||||||
.Confirm(strs.club_user_kick(Format.Bold(userName),
|
.Confirm(strs.club_user_kick(Format.Bold(userName),
|
||||||
Format.Bold(club.ToString())))
|
Format.Bold(club.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == ClubKickResult.Hierarchy)
|
if (result == ClubKickResult.Hierarchy)
|
||||||
|
@ -365,9 +393,9 @@ public partial class Xp
|
||||||
if (result == ClubBanResult.Success)
|
if (result == ClubBanResult.Success)
|
||||||
{
|
{
|
||||||
return Response()
|
return Response()
|
||||||
.Confirm(strs.club_user_banned(Format.Bold(userName),
|
.Confirm(strs.club_user_banned(Format.Bold(userName),
|
||||||
Format.Bold(club.ToString())))
|
Format.Bold(club.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == ClubBanResult.Unbannable)
|
if (result == ClubBanResult.Unbannable)
|
||||||
|
@ -393,9 +421,9 @@ public partial class Xp
|
||||||
if (result == ClubUnbanResult.Success)
|
if (result == ClubUnbanResult.Success)
|
||||||
{
|
{
|
||||||
return Response()
|
return Response()
|
||||||
.Confirm(strs.club_user_unbanned(Format.Bold(userName),
|
.Confirm(strs.club_user_unbanned(Format.Bold(userName),
|
||||||
Format.Bold(club.ToString())))
|
Format.Bold(club.ToString())))
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == ClubUnbanResult.WrongUser)
|
if (result == ClubUnbanResult.WrongUser)
|
||||||
|
@ -415,11 +443,11 @@ public partial class Xp
|
||||||
? "-"
|
? "-"
|
||||||
: desc;
|
: desc;
|
||||||
|
|
||||||
var eb = _sender.CreateEmbed()
|
var eb = CreateEmbed()
|
||||||
.WithAuthor(ctx.User)
|
.WithAuthor(ctx.User)
|
||||||
.WithTitle(GetText(strs.club_desc_update))
|
.WithTitle(GetText(strs.club_desc_update))
|
||||||
.WithOkColor()
|
.WithOkColor()
|
||||||
.WithDescription(desc);
|
.WithDescription(desc);
|
||||||
|
|
||||||
await Response().Embed(eb).SendAsync();
|
await Response().Embed(eb).SendAsync();
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,6 +120,26 @@ public class ClubService : IEService, IClubService
|
||||||
return SetClubIconResult.Success;
|
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)
|
public bool GetClubByName(string clubName, out ClubInfo club)
|
||||||
{
|
{
|
||||||
using var uow = _db.GetDbContext();
|
using var uow = _db.GetDbContext();
|
||||||
|
|
|
@ -10,6 +10,7 @@ public interface IClubService
|
||||||
Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin);
|
Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin);
|
||||||
ClubInfo? GetClubByMember(IUser user);
|
ClubInfo? GetClubByMember(IUser user);
|
||||||
Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string? url);
|
Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string? url);
|
||||||
|
Task<SetClubIconResult> SetClubBannerAsync(ulong ownerUserId, string? url);
|
||||||
bool GetClubByName(string clubName, out ClubInfo club);
|
bool GetClubByName(string clubName, out ClubInfo club);
|
||||||
ClubApplyResult ApplyToClub(IUser user, ClubInfo club);
|
ClubApplyResult ApplyToClub(IUser user, ClubInfo club);
|
||||||
ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser? discordUser);
|
ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser? discordUser);
|
||||||
|
|
|
@ -6,7 +6,7 @@ namespace EllieBot.Common;
|
||||||
public sealed class Creds : IBotCreds
|
public sealed class Creds : IBotCreds
|
||||||
{
|
{
|
||||||
[Comment("""DO NOT CHANGE""")]
|
[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/""")]
|
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
|
||||||
public string Token { get; set; }
|
public string Token { get; set; }
|
||||||
|
@ -148,8 +148,8 @@ public sealed class Creds : IBotCreds
|
||||||
{0} -> shard id
|
{0} -> shard id
|
||||||
{1} -> total shards
|
{1} -> total shards
|
||||||
Linux default
|
Linux default
|
||||||
cmd: dotnet
|
cmd: EllieBot
|
||||||
args: "EllieBot.dll -- {0}"
|
args: "{0}"
|
||||||
Windows default
|
Windows default
|
||||||
cmd: EllieBot.exe
|
cmd: EllieBot.exe
|
||||||
args: "{0}"
|
args: "{0}"
|
||||||
|
|
|
@ -77,7 +77,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_creds.Token))
|
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);
|
Helpers.ReadErrorAndExit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -85,17 +86,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||||
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|
||||||
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
|
|| 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",
|
Args = "EllieBot",
|
||||||
Cmd = "EllieBot.dll -- {0}"
|
Cmd = "{0}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_creds.RestartCommand = new RestartConfig()
|
_creds.RestartCommand = new()
|
||||||
{
|
{
|
||||||
Args = "EllieBot.exe",
|
Args = "EllieBot.exe",
|
||||||
Cmd = "{0}"
|
Cmd = "{0}"
|
||||||
|
|
|
@ -5062,6 +5062,21 @@
|
||||||
"No Public Bot"
|
"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": [
|
"Aliases": [
|
||||||
".playlists",
|
".playlists",
|
||||||
|
@ -7987,6 +8002,114 @@
|
||||||
"Module": "Utility",
|
"Module": "Utility",
|
||||||
"Options": null,
|
"Options": null,
|
||||||
"Requirements": []
|
"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": [
|
"Xp": [
|
||||||
|
@ -8193,6 +8316,20 @@
|
||||||
"Options": null,
|
"Options": null,
|
||||||
"Requirements": []
|
"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": [
|
"Aliases": [
|
||||||
".clubinfo"
|
".clubinfo"
|
||||||
|
|
|
@ -111,8 +111,8 @@ twitchClientSecret:
|
||||||
# {0} -> shard id
|
# {0} -> shard id
|
||||||
# {1} -> total shards
|
# {1} -> total shards
|
||||||
# Linux default
|
# Linux default
|
||||||
# cmd: dotnet
|
# cmd: EllieBot
|
||||||
# args: "EllieBot.dll -- {0}"
|
# args: "{0}"
|
||||||
# Windows default
|
# Windows default
|
||||||
# cmd: EllieBot.exe
|
# cmd: EllieBot.exe
|
||||||
# args: "{0}"
|
# args: "{0}"
|
||||||
|
|
|
@ -1166,6 +1166,8 @@ clubadmin:
|
||||||
- clubadmin
|
- clubadmin
|
||||||
clubrename:
|
clubrename:
|
||||||
- clubrename
|
- clubrename
|
||||||
|
clubbanner:
|
||||||
|
- clubbanner
|
||||||
eightball:
|
eightball:
|
||||||
- eightball
|
- eightball
|
||||||
- 8ball
|
- 8ball
|
||||||
|
@ -1586,4 +1588,30 @@ fishspot:
|
||||||
xprate:
|
xprate:
|
||||||
- xprate
|
- xprate
|
||||||
xpratereset:
|
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:
|
params:
|
||||||
- url:
|
- url:
|
||||||
desc: "The URL of an image file to use as the club icon."
|
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:
|
clubapps:
|
||||||
desc: Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command.
|
desc: Shows the list of users who have applied to your club. Paginated. You must be club owner to use this command.
|
||||||
ex:
|
ex:
|
||||||
|
@ -4977,4 +4988,83 @@ xpratereset:
|
||||||
params:
|
params:
|
||||||
- { }
|
- { }
|
||||||
- channel:
|
- 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_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_url_format": "You must specify an absolute image url.",
|
||||||
"club_icon_set": "New club icon set.",
|
"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_bans_for": "Bans for {0} club",
|
||||||
"club_apps_for": "Applicants for {0} club",
|
"club_apps_for": "Applicants for {0} club",
|
||||||
"club_leaderboard": "Club leaderboard - page {0}",
|
"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_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_server_reset": "Server xp rate has been reset to global defaults.",
|
||||||
"xp_rate_channel_reset": "Channel {0} xp rate has been reset.",
|
"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