520 #27

Manually merged
toastie_t0ast merged 4 commits from 520 into v5 2024-11-27 12:18:13 +00:00
136 changed files with 17512 additions and 1160 deletions

View file

@ -2,6 +2,59 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
## [5.2.0] - 28.11.2024
### Added
- Added `.todo undone` command to unmark a todo as done
- Added Button Roles!
- `.btr a` to add a button role to the specified message
- `.btr list` to list all button roles on the server
- `.btr rm` to remove a button role from the specified message
- `.btr rma` to remove all button roles on the specified message
- Use `.h` on any of the above for more info
- Added `.wrongsong` which will delete the last queued song.
- Useful in case you made a mistake, or the bot queued a wrong song
- It will reset after a shuffle or fairplay toggle, or similar events.
- Added Server color Commands!
- Every Server can now set their own colors for ok/error/pending embed (the default green/red/yellow color on the left side of the message the bot sends)
- Use `.h .sclr` to see the list of commands
- `.sclr show` will show the current server colors
- `.sclr ok <color hex>` to set ok color
- `.sclr warn <color hex>` to set warn color
- `.sclr error <color hex>` to set error color
### Changed
- Self Assigned Roles reworked! Use `.h .sar` for the list of commands
- `.sar autodel`
- Toggles the automatic deletion of the user's message and Nadeko's confirmations for .iam and .iamn commands.
- `.sar ad`
- Adds a role to the list of self-assignable roles. You can also specify a group.
- If 'Exclusive self-assignable roles' feature is enabled (.sar exclusive), users will be able to pick one role per group.
- `.sar groupname`
- Sets a self assignable role group name. Provide no name to remove.
- `.sar remove`
- Removes a specified role from the list of self-assignable roles.
- `.sar list`
- Lists self-assignable roles. Shows 20 roles per page.
- `.sar exclusive`
- Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.
- `.sar rolelvlreq`
- Set a level requirement on a self-assignable role.
- `.sar grouprolereq`
- Set a role that users have to have in order to assign a self-assignable role from the specified group.
- `.sar groupdelete`
- Deletes a self-assignable role group
- `.iam` and `.iamn` are unchanged
- Removed patron limits from Reaction Roles. Anyone can have as many reros as they like.
- `.timely` captcha made stronger and cached per user.
- `.bsreset` price reduced by 90%
### Fixed
- Fixed `.sinfo` for servers on other shard
## [5.1.20] - 13.11.2024
### Added

View file

@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using EllieBot.Db.Models;
using EllieBot.Modules.Administration.Services;
// ReSharper disable UnusedAutoPropertyAccessor.Global
@ -14,7 +15,6 @@ public abstract class EllieContext : DbContext
public DbSet<Quote> Quotes { get; set; }
public DbSet<Reminder> Reminders { get; set; }
public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
public DbSet<EllieExpression> Expressions { get; set; }
public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
@ -74,6 +74,58 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region GuildColors
modelBuilder.Entity<GuildColors>()
.HasIndex(x => x.GuildId)
.IsUnique(true);
#endregion
#region Button Roles
modelBuilder.Entity<ButtonRole>(br =>
{
br.HasIndex(x => x.GuildId)
.IsUnique(false);
br.HasAlternateKey(x => new
{
x.RoleId,
x.MessageId,
});
});
#endregion
#region New Sar
modelBuilder.Entity<SarGroup>(sg =>
{
sg.HasAlternateKey(x => new
{
x.GuildId,
x.GroupNumber
});
sg.HasMany(x => x.Roles)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Sar>()
.HasAlternateKey(x => new
{
x.GuildId,
x.RoleId
});
modelBuilder.Entity<SarAutoDelete>()
.HasIndex(x => x.GuildId)
.IsUnique();
#endregion
#region Rakeback
modelBuilder.Entity<Rakeback>()
@ -84,16 +136,23 @@ public abstract class EllieContext : DbContext
#region UserBetStats
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new { x.UserId, x.Game })
.HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique();
#endregion
#region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new { x.GuildId, x.ChannelId })
.HasIndex(x => new
{
x.GuildId,
x.ChannelId
})
.IsUnique();
#endregion
@ -286,11 +345,6 @@ public abstract class EllieContext : DbContext
.HasForeignKey(x => x.GuildConfigId)
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.SelfAssignableRoleGroupNames)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FeedSub>()
.HasAlternateKey(x => new
{
@ -319,21 +373,6 @@ public abstract class EllieContext : DbContext
#endregion
#region Self Assignable Roles
var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
selfassignableRolesEntity.HasIndex(s => new
{
s.GuildId,
s.RoleId
})
.IsUnique();
selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
#endregion
#region MusicPlaylists
var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>();
@ -516,23 +555,6 @@ public abstract class EllieContext : DbContext
#endregion
#region GroupName
modelBuilder.Entity<GroupName>()
.HasIndex(x => new
{
x.GuildConfigId,
x.Number
})
.IsUnique();
modelBuilder.Entity<GroupName>()
.HasOne(x => x.GuildConfig)
.WithMany(x => x.SelfAssignableRoleGroupNames)
.IsRequired();
#endregion
#region BanTemplate
modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();

View file

@ -1,5 +1,4 @@
#nullable disable
using LinqToDB;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models;
@ -8,6 +7,9 @@ namespace EllieBot.Db;
public static class UserXpExtensions
{
public static async Task<UserXpStats?> GetGuildUserXp(this ITable<UserXpStats> table, ulong guildId, ulong userId)
=> await table.FirstOrDefaultAsyncLinqToDB(x => x.GuildId == guildId && x.UserId == userId);
public static UserXpStats GetOrCreateUserXpStats(this DbContext ctx, ulong guildId, ulong userId)
{
var usr = ctx.Set<UserXpStats>().FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId);

View file

@ -9,7 +9,8 @@ public class FollowedStream : DbEntity
Picarto = 3,
Youtube = 4,
Facebook = 5,
Trovo = 6
Trovo = 6,
Kick = 7,
}
public ulong GuildId { get; set; }

View file

@ -1,11 +1,18 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public class GroupName : DbEntity
public sealed class SarGroup : DbEntity
{
public int GuildConfigId { get; set; }
public GuildConfig GuildConfig { get; set; }
[Key]
public int Id { get; set; }
public int Number { get; set; }
public string Name { get; set; }
public int GroupNumber { get; set; }
public ulong GuildId { get; set; }
public ulong? RoleReq { get; set; }
public ICollection<Sar> Roles { get; set; } = [];
public bool IsExclusive { get; set; }
[MaxLength(100)]
public string? Name { get; set; }
}

View file

@ -5,14 +5,15 @@ namespace EllieBot.Db.Models;
public class GuildColors
{
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; }
[Length(0, 9)]
[MaxLength(9)]
public string? OkColor { get; set; }
[Length(0, 9)]
[MaxLength(9)]
public string? ErrorColor { get; set; }
[Length(0, 9)]
[MaxLength(9)]
public string? PendingColor { get; set; }
}

View file

@ -13,28 +13,11 @@ public class GuildConfig : DbEntity
public string AutoAssignRoleIds { get; set; }
// //greet stuff
// public int AutoDeleteGreetMessagesTimer { get; set; } = 30;
// public int AutoDeleteByeMessagesTimer { get; set; } = 30;
//
// public ulong GreetMessageChannelId { get; set; }
// public ulong ByeMessageChannelId { get; set; }
//
// public bool SendDmGreetMessage { get; set; }
// public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
// public bool SendChannelGreetMessage { get; set; }
// public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
// public bool SendChannelByeMessage { get; set; }
// public string ChannelByeMessageText { get; set; } = "%user% has left!";
// public bool SendBoostMessage { get; set; }
// pulic int BoostMessageDeleteAfter { get; set; }
//self assignable roles
//todo FUTURE: DELETE, UNUSED
public bool ExclusiveSelfAssignedRoles { get; set; }
public bool AutoDeleteSelfAssignedRoleMessages { get; set; }
//stream notifications
public HashSet<FollowedStream> FollowedStreams { get; set; } = new();
@ -91,15 +74,10 @@ public class GuildConfig : DbEntity
public List<FeedSub> FeedSubs { get; set; } = new();
public bool NotifyStreamOffline { get; set; }
public bool DeleteStreamOnlineMessage { get; set; }
public List<GroupName> SelfAssignableRoleGroupNames { get; set; }
public int WarnExpireHours { get; set; }
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;
public bool DisableGlobalExpressions { get; set; } = false;
#region Boost Message
public bool StickyRoles { get; set; }
#endregion
}

View file

@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public sealed class ButtonRole
{
[Key]
public int Id { get; set; }
[MaxLength(200)]
public string ButtonId { get; set; } = string.Empty;
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
public ulong MessageId { get; set; }
public int Position { get; set; }
public ulong RoleId { get; set; }
[MaxLength(100)]
public string Emote { get; set; } = string.Empty;
[MaxLength(50)]
public string Label { get; set; } = string.Empty;
}

View file

@ -1,11 +1,24 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public class SelfAssignedRole : DbEntity
public sealed class Sar
{
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; }
public ulong RoleId { get; set; }
public int Group { get; set; }
public int LevelRequirement { get; set; }
public int SarGroupId { get; set; }
public int LevelReq { get; set; }
}
public sealed class SarAutoDelete
{
[Key]
public int Id { get; set; }
public ulong GuildId { get; set; }
public bool IsEnabled { get; set; } = false;
}

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.20</Version>
<Version>5.2.0</Version>
<!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View file

@ -5,6 +5,37 @@ namespace EllieBot.Migrations;
public static class MigrationQueries
{
public static void MigrateSar(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
INSERT INTO GroupName (Number, GuildConfigId)
SELECT DISTINCT "Group", GC.Id
FROM SelfAssignableRoles as SAR
INNER JOIN GuildConfigs as GC
ON SAR.GuildId = GC.GuildId
WHERE SAR.GuildId not in (SELECT GuildConfigs.GuildId from GroupName LEFT JOIN GuildConfigs ON GroupName.GuildConfigId = GuildConfigs.Id);
INSERT INTO SarGroup (Id, GroupNumber, Name, IsExclusive, GuildId)
SELECT GN.Id, GN.Number, GN.Name, GC.ExclusiveSelfAssignedRoles, GC.GuildId
FROM GroupName as GN
INNER JOIN GuildConfigs as GC ON GN.GuildConfigId = GC.Id;
INSERT INTO Sar (GuildId, RoleId, SarGroupId, LevelReq)
SELECT SAR.GuildId, SAR.RoleId, MIN(SG2.Id), MIN(SAR.LevelRequirement)
FROM SelfAssignableRoles as SAR
INNER JOIN (SELECT GuildId FROM GroupName as gn
INNER JOIN GuildConfigs as gc ON gn.GuildConfigId =gc.Id
) as SG
ON SG.GuildId = SAR.GuildId
INNER JOIN GroupName as SG2
ON SG2.Number = SAR."Group"
GROUP BY SAR.GuildId, SAR.RoleId;
INSERT INTO SarAutoDelete (GuildId, IsEnabled)
SELECT GuildId, AutoDeleteSelfAssignedRoleMessages FROM GuildConfigs WHERE AutoDeleteSelfAssignedRoleMessages = TRUE;
""");
}
public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' || Username WHERE Discriminator = '????';");

View file

@ -12,8 +12,6 @@ namespace EllieBot.Migrations.PostgreSql
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn(
name: "discriminator",
table: "discorduser");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class sarrework : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "groupname");
migrationBuilder.DropTable(
name: "selfassignableroles");
migrationBuilder.CreateTable(
name: "sarautodelete",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
isenabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sarautodelete", x => x.id);
});
migrationBuilder.CreateTable(
name: "sargroup",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
groupnumber = table.Column<int>(type: "integer", nullable: false),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
rolereq = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
isexclusive = table.Column<bool>(type: "boolean", nullable: false),
name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_sargroup", x => x.id);
table.UniqueConstraint("ak_sargroup_guildid_groupnumber", x => new { x.guildid, x.groupnumber });
});
migrationBuilder.CreateTable(
name: "sar",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
sargroupid = table.Column<int>(type: "integer", nullable: false),
levelreq = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_sar", x => x.id);
table.UniqueConstraint("ak_sar_guildid_roleid", x => new { x.guildid, x.roleid });
table.ForeignKey(
name: "fk_sar_sargroup_sargroupid",
column: x => x.sargroupid,
principalTable: "sargroup",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_sar_sargroupid",
table: "sar",
column: "sargroupid");
migrationBuilder.CreateIndex(
name: "ix_sarautodelete_guildid",
table: "sarautodelete",
column: "guildid",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "sar");
migrationBuilder.DropTable(
name: "sarautodelete");
migrationBuilder.DropTable(
name: "sargroup");
migrationBuilder.CreateTable(
name: "groupname",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildconfigid = table.Column<int>(type: "integer", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
name = table.Column<string>(type: "text", nullable: true),
number = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_groupname", x => x.id);
table.ForeignKey(
name: "fk_groupname_guildconfigs_guildconfigid",
column: x => x.guildconfigid,
principalTable: "guildconfigs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "selfassignableroles",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true),
group = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
levelrequirement = table.Column<int>(type: "integer", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_selfassignableroles", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_groupname_guildconfigid_number",
table: "groupname",
columns: new[] { "guildconfigid", "number" },
unique: true);
migrationBuilder.CreateIndex(
name: "ix_selfassignableroles_guildid_roleid",
table: "selfassignableroles",
columns: new[] { "guildid", "roleid" },
unique: true);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,73 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class guildcolors : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "buttonrole",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
buttonid = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
messageid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
position = table.Column<int>(type: "integer", nullable: false),
roleid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
emote = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
label = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_buttonrole", x => x.id);
table.UniqueConstraint("ak_buttonrole_roleid_messageid", x => new { x.roleid, x.messageid });
});
migrationBuilder.CreateTable(
name: "guildcolors",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
okcolor = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
errorcolor = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true),
pendingcolor = table.Column<string>(type: "character varying(9)", maxLength: 9, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_guildcolors", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_buttonrole_guildid",
table: "buttonrole",
column: "guildid");
migrationBuilder.CreateIndex(
name: "ix_guildcolors_guildid",
table: "guildcolors",
column: "guildid",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "buttonrole");
migrationBuilder.DropTable(
name: "guildcolors");
}
}
}

View file

@ -451,6 +451,65 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("blacklist", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.ButtonRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ButtonId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("buttonid");
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<string>("Emote")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("emote");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("label");
b.Property<decimal>("MessageId")
.HasColumnType("numeric(20,0)")
.HasColumnName("messageid");
b.Property<int>("Position")
.HasColumnType("integer")
.HasColumnName("position");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.HasKey("Id")
.HasName("pk_buttonrole");
b.HasAlternateKey("RoleId", "MessageId")
.HasName("ak_buttonrole_roleid_messageid");
b.HasIndex("GuildId")
.HasDatabaseName("ix_buttonrole_guildid");
b.ToTable("buttonrole", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@ -1253,7 +1312,7 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("giveawayuser", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.GroupName", b =>
modelBuilder.Entity("EllieBot.Db.Models.GuildColors", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@ -1262,30 +1321,33 @@ namespace EllieBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("ErrorColor")
.HasMaxLength(9)
.HasColumnType("character varying(9)")
.HasColumnName("errorcolor");
b.Property<int>("GuildConfigId")
.HasColumnType("integer")
.HasColumnName("guildconfigid");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Name")
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("OkColor")
.HasMaxLength(9)
.HasColumnType("character varying(9)")
.HasColumnName("okcolor");
b.Property<int>("Number")
.HasColumnType("integer")
.HasColumnName("number");
b.Property<string>("PendingColor")
.HasMaxLength(9)
.HasColumnType("character varying(9)")
.HasColumnName("pendingcolor");
b.HasKey("Id")
.HasName("pk_groupname");
.HasName("pk_guildcolors");
b.HasIndex("GuildConfigId", "Number")
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_groupname_guildconfigid_number");
.HasDatabaseName("ix_guildcolors_guildid");
b.ToTable("groupname", (string)null);
b.ToTable("guildcolors", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.GuildConfig", b =>
@ -2200,7 +2262,71 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("rotatingstatus", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.SelfAssignedRole", b =>
modelBuilder.Entity("EllieBot.Db.Models.Sar", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("LevelReq")
.HasColumnType("integer")
.HasColumnName("levelreq");
b.Property<decimal>("RoleId")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
b.Property<int>("SarGroupId")
.HasColumnType("integer")
.HasColumnName("sargroupid");
b.HasKey("Id")
.HasName("pk_sar");
b.HasAlternateKey("GuildId", "RoleId")
.HasName("ak_sar_guildid_roleid");
b.HasIndex("SarGroupId")
.HasDatabaseName("ix_sar_sargroupid");
b.ToTable("sar", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.SarAutoDelete", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<bool>("IsEnabled")
.HasColumnType("boolean")
.HasColumnName("isenabled");
b.HasKey("Id")
.HasName("pk_sarautodelete");
b.HasIndex("GuildId")
.IsUnique()
.HasDatabaseName("ix_sarautodelete_guildid");
b.ToTable("sarautodelete", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.SarGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@ -2213,32 +2339,34 @@ namespace EllieBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<int>("Group")
.ValueGeneratedOnAdd()
b.Property<int>("GroupNumber")
.HasColumnType("integer")
.HasDefaultValue(0)
.HasColumnName("group");
.HasColumnName("groupnumber");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<int>("LevelRequirement")
.HasColumnType("integer")
.HasColumnName("levelrequirement");
b.Property<bool>("IsExclusive")
.HasColumnType("boolean")
.HasColumnName("isexclusive");
b.Property<decimal>("RoleId")
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("name");
b.Property<decimal?>("RoleReq")
.HasColumnType("numeric(20,0)")
.HasColumnName("roleid");
.HasColumnName("rolereq");
b.HasKey("Id")
.HasName("pk_selfassignableroles");
.HasName("pk_sargroup");
b.HasIndex("GuildId", "RoleId")
.IsUnique()
.HasDatabaseName("ix_selfassignableroles_guildid_roleid");
b.HasAlternateKey("GuildId", "GroupNumber")
.HasName("ak_sargroup_guildid_groupnumber");
b.ToTable("selfassignableroles", (string)null);
b.ToTable("sargroup", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
@ -3527,18 +3655,6 @@ namespace EllieBot.Migrations.PostgreSql
.HasConstraintName("fk_giveawayuser_giveawaymodel_giveawayid");
});
modelBuilder.Entity("EllieBot.Db.Models.GroupName", b =>
{
b.HasOne("EllieBot.Db.Models.GuildConfig", "GuildConfig")
.WithMany("SelfAssignableRoleGroupNames")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_groupname_guildconfigs_guildconfigid");
b.Navigation("GuildConfig");
});
modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{
b.HasOne("EllieBot.Db.Models.LogSetting", "LogSetting")
@ -3578,6 +3694,16 @@ namespace EllieBot.Migrations.PostgreSql
.HasConstraintName("fk_playlistsong_musicplaylists_musicplaylistid");
});
modelBuilder.Entity("EllieBot.Db.Models.Sar", b =>
{
b.HasOne("EllieBot.Db.Models.SarGroup", null)
.WithMany("Roles")
.HasForeignKey("SarGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_sar_sargroup_sargroupid");
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{
b.HasOne("EllieBot.Db.Models.GuildConfig", null)
@ -3854,8 +3980,6 @@ namespace EllieBot.Migrations.PostgreSql
b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles");
@ -3885,6 +4009,11 @@ namespace EllieBot.Migrations.PostgreSql
b.Navigation("Songs");
});
modelBuilder.Entity("EllieBot.Db.Models.SarGroup", b =>
{
b.Navigation("Roles");
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{
b.Navigation("Items");

View file

@ -11,8 +11,6 @@ namespace EllieBot.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn(
name: "Discriminator",
table: "DiscordUser");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,152 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class sarrework : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GroupName");
migrationBuilder.DropTable(
name: "SelfAssignableRoles");
migrationBuilder.CreateTable(
name: "SarAutoDelete",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SarAutoDelete", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SarGroup",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GroupNumber = table.Column<int>(type: "INTEGER", nullable: false),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
RoleReq = table.Column<ulong>(type: "INTEGER", nullable: true),
IsExclusive = table.Column<bool>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SarGroup", x => x.Id);
table.UniqueConstraint("AK_SarGroup_GuildId_GroupNumber", x => new { x.GuildId, x.GroupNumber });
});
migrationBuilder.CreateTable(
name: "Sar",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
SarGroupId = table.Column<int>(type: "INTEGER", nullable: false),
LevelReq = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Sar", x => x.Id);
table.UniqueConstraint("AK_Sar_GuildId_RoleId", x => new { x.GuildId, x.RoleId });
table.ForeignKey(
name: "FK_Sar_SarGroup_SarGroupId",
column: x => x.SarGroupId,
principalTable: "SarGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Sar_SarGroupId",
table: "Sar",
column: "SarGroupId");
migrationBuilder.CreateIndex(
name: "IX_SarAutoDelete_GuildId",
table: "SarAutoDelete",
column: "GuildId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Sar");
migrationBuilder.DropTable(
name: "SarAutoDelete");
migrationBuilder.DropTable(
name: "SarGroup");
migrationBuilder.CreateTable(
name: "GroupName",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildConfigId = table.Column<int>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
Number = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GroupName", x => x.Id);
table.ForeignKey(
name: "FK_GroupName_GuildConfigs_GuildConfigId",
column: x => x.GuildConfigId,
principalTable: "GuildConfigs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SelfAssignableRoles",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true),
Group = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
LevelRequirement = table.Column<int>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SelfAssignableRoles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_GroupName_GuildConfigId_Number",
table: "GroupName",
columns: new[] { "GuildConfigId", "Number" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SelfAssignableRoles_GuildId_RoleId",
table: "SelfAssignableRoles",
columns: new[] { "GuildId", "RoleId" },
unique: true);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,72 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class guildcolors : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ButtonRole",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ButtonId = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
MessageId = table.Column<ulong>(type: "INTEGER", nullable: false),
Position = table.Column<int>(type: "INTEGER", nullable: false),
RoleId = table.Column<ulong>(type: "INTEGER", nullable: false),
Emote = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Label = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ButtonRole", x => x.Id);
table.UniqueConstraint("AK_ButtonRole_RoleId_MessageId", x => new { x.RoleId, x.MessageId });
});
migrationBuilder.CreateTable(
name: "GuildColors",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
OkColor = table.Column<string>(type: "TEXT", maxLength: 9, nullable: true),
ErrorColor = table.Column<string>(type: "TEXT", maxLength: 9, nullable: true),
PendingColor = table.Column<string>(type: "TEXT", maxLength: 9, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_GuildColors", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ButtonRole_GuildId",
table: "ButtonRole",
column: "GuildId");
migrationBuilder.CreateIndex(
name: "IX_GuildColors_GuildId",
table: "GuildColors",
column: "GuildId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ButtonRole");
migrationBuilder.DropTable(
name: "GuildColors");
}
}
}

View file

@ -335,6 +335,51 @@ namespace EllieBot.Migrations
b.ToTable("Blacklist");
});
modelBuilder.Entity("EllieBot.Db.Models.ButtonRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ButtonId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<string>("Emote")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Label")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<ulong>("MessageId")
.HasColumnType("INTEGER");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("RoleId", "MessageId");
b.HasIndex("GuildId");
b.ToTable("ButtonRole");
});
modelBuilder.Entity("EllieBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")
@ -932,30 +977,33 @@ namespace EllieBot.Migrations
b.ToTable("GiveawayUser");
});
modelBuilder.Entity("EllieBot.Db.Models.GroupName", b =>
modelBuilder.Entity("EllieBot.Db.Models.GuildColors", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
b.Property<string>("ErrorColor")
.HasMaxLength(9)
.HasColumnType("TEXT");
b.Property<int>("GuildConfigId")
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
b.Property<string>("OkColor")
.HasMaxLength(9)
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.Property<string>("PendingColor")
.HasMaxLength(9)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("GuildConfigId", "Number")
b.HasIndex("GuildId")
.IsUnique();
b.ToTable("GroupName");
b.ToTable("GuildColors");
});
modelBuilder.Entity("EllieBot.Db.Models.GuildConfig", b =>
@ -1640,7 +1688,54 @@ namespace EllieBot.Migrations
b.ToTable("RotatingStatus");
});
modelBuilder.Entity("EllieBot.Db.Models.SelfAssignedRole", b =>
modelBuilder.Entity("EllieBot.Db.Models.Sar", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("LevelReq")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
.HasColumnType("INTEGER");
b.Property<int>("SarGroupId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "RoleId");
b.HasIndex("SarGroupId");
b.ToTable("Sar");
});
modelBuilder.Entity("EllieBot.Db.Models.SarAutoDelete", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId")
.IsUnique();
b.ToTable("SarAutoDelete");
});
modelBuilder.Entity("EllieBot.Db.Models.SarGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
@ -1649,26 +1744,27 @@ namespace EllieBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<int>("Group")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.Property<int>("GroupNumber")
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<int>("LevelRequirement")
b.Property<bool>("IsExclusive")
.HasColumnType("INTEGER");
b.Property<ulong>("RoleId")
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<ulong?>("RoleReq")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId", "RoleId")
.IsUnique();
b.HasAlternateKey("GuildId", "GroupNumber");
b.ToTable("SelfAssignableRoles");
b.ToTable("SarGroup");
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
@ -2660,17 +2756,6 @@ namespace EllieBot.Migrations
.IsRequired();
});
modelBuilder.Entity("EllieBot.Db.Models.GroupName", b =>
{
b.HasOne("EllieBot.Db.Models.GuildConfig", "GuildConfig")
.WithMany("SelfAssignableRoleGroupNames")
.HasForeignKey("GuildConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("GuildConfig");
});
modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{
b.HasOne("EllieBot.Db.Models.LogSetting", "LogSetting")
@ -2706,6 +2791,15 @@ namespace EllieBot.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("EllieBot.Db.Models.Sar", b =>
{
b.HasOne("EllieBot.Db.Models.SarGroup", null)
.WithMany("Roles")
.HasForeignKey("SarGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{
b.HasOne("EllieBot.Db.Models.GuildConfig", null)
@ -2960,8 +3054,6 @@ namespace EllieBot.Migrations
b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles");
@ -2991,6 +3083,11 @@ namespace EllieBot.Migrations
b.Navigation("Songs");
});
modelBuilder.Entity("EllieBot.Db.Models.SarGroup", b =>
{
b.Navigation("Roles");
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{
b.Navigation("Items");

View file

@ -96,7 +96,7 @@ public partial class Administration : EllieModule<AdministrationService>
var guild = (SocketGuild)ctx.Guild;
var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.server_delmsgoncmd))
.WithDescription(enabled ? "✅" : "❌");

View file

@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models;
using EllieBot.Modules.Administration._common.results;
namespace EllieBot.Modules.Administration.Services;
namespace EllieBot.Modules.Administration;
public class AdministrationService : IEService
{

View file

@ -136,7 +136,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
await using var linqCtx = ctx.CreateLinqToDBContext();
await using var tempTable = linqCtx.CreateTempTable<CleanupId>();
foreach (var chunk in allIds.Chunk(20000))
foreach (var chunk in allIds.Chunk(10000))
{
await tempTable.BulkCopyAsync(chunk.Select(x => new CleanupId()
{
@ -187,13 +187,6 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Contains(x.GuildId))
.DeleteAsync();
// delete ignored users
await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null
&& !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync();
// delete perm overrides
await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null
@ -219,6 +212,54 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Contains(x.GuildId))
.DeleteAsync();
// delete sar
await ctx.GetTable<SarGroup>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete warnings
await ctx.GetTable<Warning>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete warn punishments
await ctx.GetTable<WarningPunishment>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete sticky roles
await ctx.GetTable<StickyRole>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete at channels
await ctx.GetTable<AutoTranslateChannel>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete ban templates
await ctx.GetTable<BanTemplate>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
// delete reminders
await ctx.GetTable<Reminder>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.ServerId))
.DeleteAsync();
// delete button roles
await ctx.GetTable<ButtonRole>()
.Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId))
.DeleteAsync();
return new()
{
GuildCount = guildIds.Keys.Count,

View file

@ -42,9 +42,9 @@ public partial class Administration
.Page((items, _) =>
{
if (!items.Any())
return _sender.CreateEmbed().WithErrorColor().WithFooter(sql).WithDescription("-");
return CreateEmbed().WithErrorColor().WithFooter(sql).WithDescription("-");
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithFooter(sql)
.WithTitle(string.Join(" ║ ", result.ColumnNames))
@ -99,7 +99,7 @@ public partial class Administration
{
try
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle(GetText(strs.sql_confirm_exec))
.WithDescription(Format.Code(sql));
@ -119,7 +119,7 @@ public partial class Administration
[OwnerOnly]
public async Task PurgeUser(ulong userId)
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithDescription(GetText(strs.purge_user_confirm(Format.Bold(userId.ToString()))));
if (!await PromptUserConfirmAsync(embed))

View file

@ -123,7 +123,7 @@ public partial class Administration
[Cmd]
public async Task LanguagesList()
=> await Response().Embed(_sender.CreateEmbed()
=> await Response().Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.lang_list))
.WithDescription(string.Join("\n",

View file

@ -122,7 +122,7 @@ public class MuteService : IEService
return;
_ = Task.Run(() => _sender.Response(user)
.Embed(_sender.CreateEmbed()
.Embed(_sender.CreateEmbed(user?.GuildId)
.WithDescription($"You've been muted in {user.Guild} server")
.AddField("Mute Type", type.ToString())
.AddField("Moderator", mod.ToString())
@ -140,7 +140,7 @@ public class MuteService : IEService
return;
_ = Task.Run(() => _sender.Response(user)
.Embed(_sender.CreateEmbed()
.Embed(_sender.CreateEmbed(user.GuildId)
.WithDescription($"You've been unmuted in {user.Guild} server")
.AddField("Unmute Type", type.ToString())
.AddField("Moderator", mod.ToString())

View file

@ -36,7 +36,7 @@ public partial class Administration
[UserPerm(GuildPerm.Administrator)]
public async Task DiscordPermOverrideReset()
{
var result = await PromptUserConfirmAsync(_sender.CreateEmbed()
var result = await PromptUserConfirmAsync(CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.perm_override_all_confirm)));
@ -65,7 +65,7 @@ public partial class Administration
.CurrentPage(page)
.Page((items, _) =>
{
var eb = _sender.CreateEmbed().WithTitle(GetText(strs.perm_overrides)).WithOkColor();
var eb = CreateEmbed().WithTitle(GetText(strs.perm_overrides)).WithOkColor();
if (items.Count == 0)
eb.WithDescription(GetText(strs.perm_override_page_none));

View file

@ -9,7 +9,9 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
{
private readonly BotConfigService _bss;
private readonly SelfService _selfService;
private readonly IReplacementService _repService;
// private readonly Replacer _rep;
private readonly DbService _db;
private readonly DiscordSocketClient _client;
@ -27,7 +29,6 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
_selfService = selfService;
_repService = repService;
_client = client;
}
public async Task OnReadyAsync()
@ -57,7 +58,7 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
? rotatingStatuses[index = 0]
: rotatingStatuses[index++];
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new (client: _client));
var statusText = await _repService.ReplaceAsync(playingStatus.Status, new(client: _client));
await _selfService.SetActivityAsync(statusText, (ActivityType)playingStatus.Type);
}
catch (Exception ex)
@ -72,7 +73,11 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
ArgumentOutOfRangeException.ThrowIfNegative(index);
await using var uow = _db.GetDbContext();
var toRemove = await uow.Set<RotatingPlayingStatus>().AsQueryable().AsNoTracking().Skip(index).FirstOrDefaultAsync();
var toRemove = await uow.Set<RotatingPlayingStatus>()
.AsQueryable()
.AsNoTracking()
.Skip(index)
.FirstOrDefaultAsync();
if (toRemove is null)
return null;
@ -94,6 +99,11 @@ public sealed class PlayingRotateService : IEService, IReadyExecutor
await uow.SaveChangesAsync();
}
public void DisableRotatePlaying()
{
_bss.ModifyConfig(bs => { bs.RotateStatuses = false; });
}
public bool ToggleRotatePlaying()
{
var enabled = false;

View file

@ -241,7 +241,7 @@ public partial class Administration
return;
}
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.prot_active));
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.prot_active));
if (spam is not null)
embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true);

View file

@ -113,7 +113,7 @@ public partial class Administration
{
await progressMsg.ModifyAsync(props =>
{
props.Embed = _sender.CreateEmbed()
props.Embed = CreateEmbed()
.WithPendingColor()
.WithDescription(GetText(strs.prune_progress(deleted, total)))
.Build();

View file

@ -0,0 +1,269 @@
using EllieBot.Db.Models;
using EllieBot.Modules.Administration.Services;
using System.Text;
using ContextType = Discord.Commands.ContextType;
namespace EllieBot.Modules.Administration;
public partial class Administration
{
public partial class ButtonRoleCommands : EllieModule<ButtonRolesService>
{
private List<ActionRowBuilder> GetActionRows(IReadOnlyList<ButtonRole> roles)
{
var rows = roles.Select((x, i) => (Index: i, ButtonRole: x))
.GroupBy(x => x.Index / 5)
.Select(x => x.Select(y => y.ButtonRole))
.Select(x =>
{
var ab = new ActionRowBuilder()
.WithComponents(x.Select(y =>
{
var curRole = ctx.Guild.GetRole(y.RoleId);
var label = string.IsNullOrWhiteSpace(y.Label)
? curRole?.ToString() ?? "?missing " + y.RoleId
: y.Label;
var btnEmote = EmoteTypeReader.TryParse(y.Emote, out var e)
? e
: null;
return new ButtonBuilder()
.WithCustomId(y.ButtonId)
.WithEmote(btnEmote)
.WithLabel(label)
.WithStyle(ButtonStyle.Secondary)
.Build() as IMessageComponent;
})
.ToList());
return ab;
})
.ToList();
return rows;
}
private async Task<MessageLink?> CreateMessageLinkAsync(ulong messageId)
{
var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null)
return null;
return new MessageLink(ctx.Guild, ctx.Channel, msg);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleAdd(ulong messageId, IEmote emote, [Leftover] IRole role)
{
var link = await CreateMessageLinkAsync(messageId);
if (link is null)
{
await Response().Error(strs.invalid_message_id).SendAsync();
return;
}
await BtnRoleAdd(link, emote, role);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleAdd(MessageLink link, IEmote emote, [Leftover] IRole role)
{
if (link.Message is not IUserMessage msg || !msg.IsAuthor(ctx.Client))
{
await Response().Error(strs.invalid_message_link).SendAsync();
return;
}
if (!await CheckRoleHierarchy(role))
{
await Response().Error(strs.hierarchy).SendAsync();
return;
}
var success = await _service.AddButtonRole(ctx.Guild.Id, link.Channel.Id, role.Id, link.Message.Id, emote);
if (!success)
{
await Response().Error(strs.btnrole_message_max).SendAsync();
return;
}
var roles = await _service.GetButtonRoles(ctx.Guild.Id, link.Message.Id);
var rows = GetActionRows(roles);
await msg.ModifyAsync(x => x.Components = new(new ComponentBuilder().WithRows(rows).Build()));
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemove(ulong messageId, IRole role)
=> BtnRoleRemove(messageId, role.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemove(MessageLink link, IRole role)
=> BtnRoleRemove(link.Message.Id, role.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemove(MessageLink link, ulong roleId)
=> BtnRoleRemove(link.Message.Id, roleId);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleRemove(ulong messageId, ulong roleId)
{
var removed = await _service.RemoveButtonRole(ctx.Guild.Id, messageId, roleId);
if (removed is null)
{
await Response().Error(strs.btnrole_not_found).SendAsync();
return;
}
var roles = await _service.GetButtonRoles(ctx.Guild.Id, messageId);
var ch = await ctx.Guild.GetTextChannelAsync(removed.ChannelId);
if (ch is null)
{
await Response().Error(strs.btnrole_removeall_not_found).SendAsync();
return;
}
var msg = await ch.GetMessageAsync(removed.MessageId) as IUserMessage;
if (msg is null)
{
await Response().Error(strs.btnrole_removeall_not_found).SendAsync();
return;
}
var rows = GetActionRows(roles);
await msg.ModifyAsync(x => x.Components = new(new ComponentBuilder().WithRows(rows).Build()));
await Response().Confirm(strs.btnrole_removed).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public Task BtnRoleRemoveAll(MessageLink link)
=> BtnRoleRemoveAll(link.Message.Id);
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleRemoveAll(ulong messageId)
{
var succ = await _service.RemoveButtonRoles(ctx.Guild.Id, messageId);
if (succ.Count == 0)
{
await Response().Error(strs.btnrole_not_found).SendAsync();
return;
}
var info = succ[0];
var ch = await ctx.Guild.GetTextChannelAsync(info.ChannelId);
if (ch is null)
{
await Response().Pending(strs.btnrole_removeall_not_found).SendAsync();
return;
}
var msg = await ch.GetMessageAsync(info.MessageId) as IUserMessage;
if (msg is null)
{
await Response().Pending(strs.btnrole_removeall_not_found).SendAsync();
return;
}
await msg.ModifyAsync(x => x.Components = new(new ComponentBuilder().Build()));
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageRoles)]
[RequireUserPermission(GuildPerm.ManageRoles)]
public async Task BtnRoleList()
{
var btnRoles = await _service.GetButtonRoles(ctx.Guild.Id, null);
var groups = btnRoles
.GroupBy(x => (x.ChannelId, x.MessageId))
.ToList();
await Response()
.Paginated()
.Items(groups)
.PageSize(1)
.AddFooter(false)
.Page(async (items, page) =>
{
var eb = CreateEmbed()
.WithOkColor();
var item = items.FirstOrDefault();
if (item == default)
{
eb.WithPendingColor()
.WithDescription(GetText(strs.btnrole_none));
return eb;
}
var (cid, msgId) = item.Key;
var str = new StringBuilder();
var ch = await ctx.Client.GetChannelAsync(cid) as IMessageChannel;
str.AppendLine($"Channel: {ch?.ToString() ?? cid.ToString()}");
str.AppendLine($"Message: {msgId}");
if (ch is not null)
{
var msg = await ch.GetMessageAsync(msgId);
if (msg is not null)
{
str.AppendLine(new MessageLink(ctx.Guild, ch, msg).ToString());
}
}
str.AppendLine("---");
foreach (var x in item.AsEnumerable())
{
var role = ctx.Guild.GetRole(x.RoleId);
str.AppendLine($"{x.Emote} {(role?.ToString() ?? x.RoleId.ToString())}");
}
eb.WithDescription(str.ToString());
return eb;
})
.SendAsync();
}
}
}

View file

@ -0,0 +1,154 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.SqlQuery;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using NCalc;
namespace EllieBot.Modules.Administration.Services;
public sealed class ButtonRolesService : IEService, IReadyExecutor
{
private const string BTN_PREFIX = "n:btnrole:";
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
public ButtonRolesService(IBotCreds creds, DiscordSocketClient client, DbService db)
{
_creds = creds;
_client = client;
_db = db;
}
public Task OnReadyAsync()
{
_client.InteractionCreated += OnInteraction;
return Task.CompletedTask;
}
private async Task OnInteraction(SocketInteraction inter)
{
if (inter is not SocketMessageComponent smc)
return;
if (!smc.Data.CustomId.StartsWith(BTN_PREFIX))
return;
await inter.DeferAsync();
_ = Task.Run(async () =>
{
await using var uow = _db.GetDbContext();
var buttonRole = await uow.GetTable<ButtonRole>()
.Where(x => x.ButtonId == smc.Data.CustomId && x.MessageId == smc.Message.Id)
.FirstOrDefaultAsyncLinqToDB();
if (buttonRole is null)
return;
var guild = _client.GetGuild(buttonRole.GuildId);
if (guild is null)
return;
var role = guild.GetRole(buttonRole.RoleId);
if (role is null)
return;
if (smc.User is not IGuildUser user)
return;
if (user.GetRoles().Any(x => x.Id == role.Id))
{
await user.RemoveRoleAsync(role.Id);
return;
}
await user.AddRoleAsync(role.Id);
});
}
public async Task<bool> AddButtonRole(
ulong guildId,
ulong channelId,
ulong roleId,
ulong messageId,
IEmote emote
)
{
await using var uow = _db.GetDbContext();
// up to 25 per message
if (await uow.GetTable<ButtonRole>()
.Where(x => x.MessageId == messageId)
.CountAsyncLinqToDB()
>= 25)
return false;
var emoteStr = emote.ToString()!;
var guid = Guid.NewGuid();
await uow.GetTable<ButtonRole>()
.InsertOrUpdateAsync(() => new ButtonRole()
{
GuildId = guildId,
ChannelId = channelId,
RoleId = roleId,
MessageId = messageId,
Position =
uow
.GetTable<ButtonRole>()
.Any(x => x.MessageId == messageId)
? uow.GetTable<ButtonRole>()
.Where(x => x.MessageId == messageId)
.Max(x => x.Position)
: 1,
Emote = emoteStr,
Label = string.Empty,
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}"
},
_ => new()
{
Emote = emoteStr,
Label = string.Empty,
ButtonId = $"{BTN_PREFIX}:{guildId}:{guid}"
},
() => new()
{
RoleId = roleId,
MessageId = messageId,
});
return true;
}
public async Task<IReadOnlyList<ButtonRole>> RemoveButtonRoles(ulong guildId, ulong messageId)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.DeleteWithOutputAsync();
}
public async Task<ButtonRole?> RemoveButtonRole(ulong guildId, ulong messageId, ulong roleId)
{
await using var uow = _db.GetDbContext();
var deleted = await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId && x.RoleId == roleId)
.DeleteWithOutputAsync();
return deleted.FirstOrDefault();
}
public async Task<IReadOnlyList<ButtonRole>> GetButtonRoles(ulong guildId, ulong? messageId)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<ButtonRole>()
.Where(x => x.GuildId == guildId && (messageId == null || x.MessageId == messageId))
.OrderBy(x => x.Id)
.ToListAsyncLinqToDB();
}
}

View file

@ -80,7 +80,7 @@ public partial class Administration
.CurrentPage(page)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor();
var content = string.Empty;

View file

@ -1,8 +1,6 @@
#nullable disable
using LinqToDB;
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Modules.Patronage;
using EllieBot.Db.Models;
using OneOf.Types;
using OneOf;
@ -18,18 +16,15 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps;
public ReactionRolesService(
DiscordSocketClient client,
IPatronageService ps,
DbService db,
IBotCreds creds)
{
_db = db;
_client = client;
_creds = creds;
_ps = ps;
_cache = new();
}
@ -57,7 +52,7 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId);
if (role is null)
if (guild is null || role is null)
return default;
var user = guild.GetUser(userId) as IGuildUser
@ -96,7 +91,8 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
{
if (user.RoleIds.Contains(role.Id))
{
await user.RemoveRoleAsync(role.Id, new RequestOptions()
await user.RemoveRoleAsync(role.Id,
new RequestOptions()
{
AuditLogReason = $"Reaction role"
});
@ -210,7 +206,8 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
}
}
await user.AddRoleAsync(role.Id, new()
await user.AddRoleAsync(role.Id,
new()
{
AuditLogReason = "Reaction role"
});
@ -244,23 +241,10 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
int levelReq = 0)
{
ArgumentOutOfRangeException.ThrowIfNegative(group);
ArgumentOutOfRangeException.ThrowIfNegative(levelReq);
await using var ctx = _db.GetDbContext();
await using var tran = await ctx.Database.BeginTransactionAsync();
var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guild.Id)
.CountAsync();
var limit = await _ps.GetUserLimit(LimitedFeatureName.ReactionRole, guild.OwnerId);
if (!_creds.IsOwner(guild.OwnerId) && (activeReactionRoles >= limit.Quota && limit.Quota >= 0))
{
return new Error();
}
await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
@ -286,8 +270,6 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
Emote = emote,
});
await tran.CommitAsync();
var obj = new ReactionRoleV2()
{
GuildId = guild.Id,

View file

@ -174,12 +174,11 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task RoleColor(Color color, [Leftover] IRole role)
public async Task RoleColor(Rgba32 color, [Leftover] IRole role)
{
try
{
var rgba32 = color.ToPixel<Rgba32>();
await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B));
await role.ModifyAsync(r => r.Color = new Discord.Color(color.R, color.G, color.B));
await Response().Confirm(strs.rc(Format.Bold(role.Name))).SendAsync();
}
catch (Exception)

View file

@ -62,7 +62,7 @@ public partial class Administration
var (added, updated) = await _service.RefreshUsersAsync(users);
await message.ModifyAsync(x =>
x.Embed = _sender.CreateEmbed()
x.Embed = CreateEmbed()
.WithDescription(GetText(strs.cache_users_done(added, updated)))
.WithOkColor()
.Build()
@ -115,7 +115,7 @@ public partial class Administration
_service.AddNewAutoCommand(cmd);
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.scadd))
.AddField(GetText(strs.server),
@ -343,7 +343,7 @@ public partial class Administration
if (string.IsNullOrWhiteSpace(str))
str = GetText(strs.no_shards_on_page);
return _sender.CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}");
return CreateEmbed().WithOkColor().WithDescription($"{status}\n\n{str}");
})
.SendAsync();
}

View file

@ -6,16 +6,136 @@ namespace EllieBot.Modules.Administration;
public partial class Administration
{
[Group]
public partial class SelfAssignedRolesHelpers : EllieModule<SelfAssignedRolesService>
{
private readonly SarAssignerService _sas;
public SelfAssignedRolesHelpers(SarAssignerService sas)
{
_sas = sas;
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iam([Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
var group = await _service.GetRoleGroup(ctx.User.Id, role.Id);
IUserMessage msg = null;
try
{
if (group is null)
{
msg = await Response().Error(strs.self_assign_not).SendAsync();
return;
}
var tcs = new TaskCompletionSource<SarAssignResult>(TaskCreationOptions.RunContinuationsAsynchronously);
await _sas.Add(new()
{
Group = group,
RoleId = role.Id,
User = guildUser,
CompletionTask = tcs
});
var res = await tcs.Task;
if (res.TryPickT0(out _, out var error))
{
msg = await Response()
.Confirm(strs.self_assign_success(Format.Bold(role.Name)))
.SendAsync();
}
else
{
var resStr = error.Match(
_ => strs.error_occured,
lvlReq => strs.self_assign_not_level(Format.Bold(lvlReq.Level.ToString())),
roleRq => strs.self_assign_role_req(Format.Bold(ctx.Guild.GetRole(roleRq.RoleId).ToString()
?? "missing role " + roleRq.RoleId),
group.Name),
_ => strs.self_assign_already(Format.Bold(role.Name)),
_ => strs.self_assign_perms);
msg = await Response().Error(resStr).SendAsync();
}
}
finally
{
var ad = _service.GetAutoDelete(ctx.Guild.Id);
if (ad)
{
msg?.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iamnot([Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
IUserMessage msg = null;
try
{
if (guildUser.RoleIds.Contains(role.Id))
{
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
return;
}
var group = await _service.GetRoleGroup(role.Guild.Id, role.Id);
if (group is null || group.Roles.All(x => x.RoleId != role.Id))
{
msg = await Response().Error(strs.self_assign_not).SendAsync();
return;
}
if (role.Position >= ((SocketGuild)ctx.Guild).CurrentUser.Roles.Max(x => x.Position))
{
msg = await Response().Error(strs.self_assign_perms).SendAsync();
return;
}
await guildUser.RemoveRoleAsync(role);
msg = await Response().Confirm(strs.self_assign_remove(Format.Bold(role.Name))).SendAsync();
}
finally
{
var ad = _service.GetAutoDelete(ctx.Guild.Id);
if (ad)
{
msg?.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
}
}
}
[Group("sar")]
public partial class SelfAssignedRolesCommands : EllieModule<SelfAssignedRolesService>
{
private readonly SarAssignerService _sas;
public SelfAssignedRolesCommands(SarAssignerService sas)
{
_sas = sas;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)]
[BotPerm(GuildPerm.ManageMessages)]
public async Task AdSarm()
public async Task SarAutoDelete()
{
var newVal = _service.ToggleAdSarm(ctx.Guild.Id);
var newVal = await _service.ToggleAutoDelete(ctx.Guild.Id);
if (newVal)
await Response().Confirm(strs.adsarm_enable(prefix)).SendAsync();
@ -28,40 +148,34 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public Task Asar([Leftover] IRole role)
=> Asar(0, role);
public Task SarAdd([Leftover] IRole role)
=> SarAdd(0, role);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task Asar(int group, [Leftover] IRole role)
public async Task SarAdd(int group, [Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
if (!await CheckRoleHierarchy(role))
return;
var succ = _service.AddNew(ctx.Guild.Id, role, group);
await _service.AddAsync(ctx.Guild.Id, role.Id, group);
if (succ)
{
await Response()
.Confirm(strs.role_added(Format.Bold(role.Name), Format.Bold(group.ToString())))
.SendAsync();
}
else
await Response().Error(strs.role_in_list(Format.Bold(role.Name))).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task Sargn(int group, [Leftover] string name = null)
public async Task SarGroupName(int group, [Leftover] string name = null)
{
var set = await _service.SetNameAsync(ctx.Guild.Id, group, name);
var set = await _service.SetGroupNameAsync(ctx.Guild.Id, group, name);
if (set)
{
@ -70,19 +184,19 @@ public partial class Administration
.SendAsync();
}
else
{
await Response().Confirm(strs.group_name_removed(Format.Bold(group.ToString()))).SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
public async Task Rsar([Leftover] IRole role)
public async Task SarRemove([Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return;
var success = _service.RemoveSar(role.Guild.Id, role.Id);
var success = await _service.RemoveAsync(role.Guild.Id, role.Id);
if (!success)
await Response().Error(strs.self_assign_not).SendAsync();
else
@ -91,59 +205,81 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Lsar(int page = 1)
public async Task SarList(int page = 1)
{
if (--page < 0)
return;
var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild);
var groups = await _service.GetSarsAsync(ctx.Guild.Id);
var gDict = groups.ToDictionary(x => x.Id, x => x);
await Response()
.Paginated()
.Items(roles.OrderBy(x => x.Model.Group).ToList())
.Items(groups.SelectMany(x => x.Roles).ToList())
.PageSize(20)
.CurrentPage(page)
.Page((items, _) =>
.Page(async (items, _) =>
{
var rolesStr = new StringBuilder();
var roleGroups = items
.GroupBy(x => x.Model.Group)
.GroupBy(x => x.SarGroupId)
.OrderBy(x => x.Key);
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.self_assign_list(groups.Sum(x => x.Roles.Count))));
foreach (var kvp in roleGroups)
{
string groupNameText;
if (!groups.TryGetValue(kvp.Key, out var name))
groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key)));
else
groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}");
var group = gDict[kvp.Key];
rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫");
foreach (var (model, role) in kvp.AsEnumerable())
var groupNameText = "";
if (!string.IsNullOrWhiteSpace(group.Name))
groupNameText += $" **{group.Name}**";
groupNameText = $"`{group.GroupNumber}` {groupNameText}";
var rolesStr = new StringBuilder();
if (group.IsExclusive)
{
if (role is null)
rolesStr.AppendLine(Format.Italics(GetText(strs.choose_one)));
}
if (group.RoleReq is ulong rrId)
{
var rr = ctx.Guild.GetRole(rrId);
if (rr is null)
{
await _service.SetGroupRoleReq(group.GuildId, group.GroupNumber, null);
}
else
{
// first character is invisible space
if (model.LevelRequirement == 0)
rolesStr.AppendLine(" " + role.Name);
else
rolesStr.AppendLine(" " + role.Name + $" (lvl {model.LevelRequirement}+)");
rolesStr.AppendLine(
Format.Italics(GetText(strs.requires_role(Format.Bold(rr.Name)))));
}
}
foreach (var sar in kvp)
{
var roleName = (ctx.Guild.GetRole(sar.RoleId)?.Name ?? (sar.RoleId + " (deleted)"));
rolesStr.Append("- " + Format.Code(roleName));
if (sar.LevelReq > 0)
{
rolesStr.Append($" *[lvl {sar.LevelReq}+]*");
}
rolesStr.AppendLine();
}
return _sender.CreateEmbed()
.WithOkColor()
.WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count()))))
.WithDescription(rolesStr.ToString())
.WithFooter(exclusive
? GetText(strs.self_assign_are_exclusive)
: GetText(strs.self_assign_are_not_exclusive));
eb.AddField(groupNameText, rolesStr, false);
}
return eb;
})
.SendAsync();
}
@ -152,25 +288,36 @@ public partial class Administration
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task Togglexclsar()
public async Task SarExclusive(int groupNumber)
{
var areExclusive = await _service.SetGroupExclusivityAsync(ctx.Guild.Id, groupNumber);
if (areExclusive is null)
{
await Response().Error(strs.sar_group_not_found).SendAsync();
return;
}
if (areExclusive is true)
{
var areExclusive = _service.ToggleEsar(ctx.Guild.Id);
if (areExclusive)
await Response().Confirm(strs.self_assign_excl).SendAsync();
}
else
{
await Response().Confirm(strs.self_assign_no_excl).SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RoleLevelReq(int level, [Leftover] IRole role)
public async Task SarRoleLevelReq(int level, [Leftover] IRole role)
{
if (level < 0)
return;
var succ = _service.SetLevelReq(ctx.Guild.Id, role, level);
var succ = await _service.SetRoleLevelReq(ctx.Guild.Id, role.Id, level);
if (!succ)
{
@ -186,54 +333,35 @@ public partial class Administration
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iam([Leftover] IRole role)
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task SarGroupRoleReq(int groupNumber, [Leftover] IRole role)
{
var guildUser = (IGuildUser)ctx.User;
var succ = await _service.SetGroupRoleReq(ctx.Guild.Id, groupNumber, role.Id);
var (result, autoDelete, extra) = await _service.Assign(guildUser, role);
IUserMessage msg;
if (result == SelfAssignedRolesService.AssignResult.ErrNotAssignable)
msg = await Response().Error(strs.self_assign_not).SendAsync();
else if (result == SelfAssignedRolesService.AssignResult.ErrLvlReq)
msg = await Response().Error(strs.self_assign_not_level(Format.Bold(extra.ToString()))).SendAsync();
else if (result == SelfAssignedRolesService.AssignResult.ErrAlreadyHave)
msg = await Response().Error(strs.self_assign_already(Format.Bold(role.Name))).SendAsync();
else if (result == SelfAssignedRolesService.AssignResult.ErrNotPerms)
msg = await Response().Error(strs.self_assign_perms).SendAsync();
else
msg = await Response().Confirm(strs.self_assign_success(Format.Bold(role.Name))).SendAsync();
if (autoDelete)
if (!succ)
{
msg.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
await Response().Error(strs.sar_group_not_found).SendAsync();
return;
}
await Response()
.Confirm(strs.self_assign_group_role_req(
Format.Bold(groupNumber.ToString()),
Format.Bold(role.Name)))
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Iamnot([Leftover] IRole role)
[UserPerm(GuildPerm.ManageRoles)]
public async Task SarGroupDelete(int groupNumber)
{
var guildUser = (IGuildUser)ctx.User;
var (result, autoDelete) = await _service.Remove(guildUser, role);
IUserMessage msg;
if (result == SelfAssignedRolesService.RemoveResult.ErrNotAssignable)
msg = await Response().Error(strs.self_assign_not).SendAsync();
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotHave)
msg = await Response().Error(strs.self_assign_not_have(Format.Bold(role.Name))).SendAsync();
else if (result == SelfAssignedRolesService.RemoveResult.ErrNotPerms)
msg = await Response().Error(strs.self_assign_perms).SendAsync();
var succ = await _service.DeleteRoleGroup(ctx.Guild.Id, groupNumber);
if (succ)
await Response().Confirm(strs.sar_group_deleted(Format.Bold(groupNumber.ToString()))).SendAsync();
else
msg = await Response().Confirm(strs.self_assign_remove(Format.Bold(role.Name))).SendAsync();
if (autoDelete)
{
msg.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
await Response().Error(strs.sar_group_not_found).SendAsync();
}
}
}

View file

@ -1,233 +1,326 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Xp.Services;
using OneOf;
using OneOf.Types;
using System.ComponentModel.DataAnnotations;
using System.Threading.Channels;
namespace EllieBot.Modules.Administration.Services;
public class SelfAssignedRolesService : IEService
public class SelfAssignedRolesService : IEService, IReadyExecutor
{
public enum AssignResult
{
Assigned, // successfully removed
ErrNotAssignable, // not assignable (error)
ErrAlreadyHave, // you already have that role (error)
ErrNotPerms, // bot doesn't have perms (error)
ErrLvlReq // you are not required level (error)
}
public enum RemoveResult
{
Removed, // successfully removed
ErrNotAssignable, // not assignable (error)
ErrNotHave, // you don't have a role you want to remove (error)
ErrNotPerms // bot doesn't have perms (error)
}
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
public SelfAssignedRolesService(DbService db)
=> _db = db;
private ConcurrentHashSet<ulong> _sarAds = new();
public bool AddNew(ulong guildId, IRole role, int group)
public SelfAssignedRolesService(DbService db, DiscordSocketClient client, IBotCreds creds)
{
using var uow = _db.GetDbContext();
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
return false;
_db = db;
_client = client;
_creds = creds;
}
uow.Set<SelfAssignedRole>().Add(new()
public async Task AddAsync(ulong guildId, ulong roleId, int groupNumber)
{
Group = group,
RoleId = role.Id,
GuildId = role.Guild.Id
await using var ctx = _db.GetDbContext();
await ctx.GetTable<SarGroup>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
GroupNumber = groupNumber,
IsExclusive = false
},
_ => new()
{
},
() => new()
{
GuildId = guildId,
GroupNumber = groupNumber
});
uow.SaveChanges();
await ctx.GetTable<Sar>()
.InsertOrUpdateAsync(() => new()
{
RoleId = roleId,
LevelReq = 0,
GuildId = guildId,
SarGroupId = ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.Select(x => x.Id)
.First()
},
_ => new()
{
},
() => new()
{
RoleId = roleId,
});
}
public async Task<bool> RemoveAsync(ulong guildId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<Sar>()
.Where(x => x.RoleId == roleId && x.GuildId == guildId)
.DeleteAsync();
return deleted > 0;
}
public async Task<bool> SetGroupNameAsync(ulong guildId, int groupNumber, string? name)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.UpdateAsync(x => new()
{
Name = name
});
return changes > 0;
}
public async Task<IReadOnlyCollection<SarGroup>> GetSarsAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var sgs = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId)
.LoadWith(x => x.Roles)
.ToListAsyncLinqToDB();
return sgs;
}
public async Task<bool> SetRoleLevelReq(ulong guildId, ulong roleId, int levelReq)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<Sar>()
.Where(x => x.GuildId == guildId && x.RoleId == roleId)
.UpdateAsync(_ => new()
{
LevelReq = levelReq,
});
return changes > 0;
}
public async Task<bool> SetGroupRoleReq(ulong guildId, int groupNumber, ulong? roleId)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.UpdateAsync(_ => new()
{
RoleReq = roleId
});
return changes > 0;
}
public async Task<bool?> SetGroupExclusivityAsync(ulong guildId, int groupNumber)
{
await using var ctx = _db.GetDbContext();
var changes = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.UpdateWithOutputAsync(old => new()
{
IsExclusive = !old.IsExclusive
},
(o, n) => n.IsExclusive);
if (changes.Length == 0)
{
return null;
}
return changes[0];
}
public async Task<SarGroup?> GetRoleGroup(ulong guildId, ulong roleId)
{
await using var ctx = _db.GetDbContext();
var group = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.Roles.Any(x => x.RoleId == roleId))
.LoadWith(x => x.Roles)
.FirstOrDefaultAsyncLinqToDB();
return group;
}
public async Task<bool> DeleteRoleGroup(ulong guildId, int groupNumber)
{
await using var ctx = _db.GetDbContext();
var deleted = await ctx.GetTable<SarGroup>()
.Where(x => x.GuildId == guildId && x.GroupNumber == groupNumber)
.DeleteAsync();
return deleted > 0;
}
public async Task<bool> ToggleAutoDelete(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var delted = await ctx.GetTable<SarAutoDelete>()
.DeleteAsync(x => x.GuildId == guildId);
if (delted > 0)
{
_sarAds.TryRemove(guildId);
return false;
}
await ctx.GetTable<SarAutoDelete>()
.InsertOrUpdateAsync(() => new()
{
IsEnabled = true,
GuildId = guildId,
},
(_) => new()
{
IsEnabled = true
},
() => new()
{
GuildId = guildId
});
_sarAds.Add(guildId);
return true;
}
public bool ToggleAdSarm(ulong guildId)
public bool GetAutoDelete(ulong guildId)
=> _sarAds.Contains(guildId);
public async Task OnReadyAsync()
{
bool newval;
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages;
uow.SaveChanges();
return newval;
await using var uow = _db.GetDbContext();
var guilds = await uow.GetTable<SarAutoDelete>()
.Where(x => x.IsEnabled && Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.Select(x => x.GuildId)
.ToListAsyncLinqToDB();
_sarAds = new(guilds);
}
}
public sealed class SarAssignerService : IEService, IReadyExecutor
{
private readonly XpService _xp;
private readonly DbService _db;
private readonly Channel<SarAssignerDataItem> _channel =
Channel.CreateBounded<SarAssignerDataItem>(100);
public SarAssignerService(XpService xp, DbService db)
{
_xp = xp;
_db = db;
}
public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
public async Task OnReadyAsync()
{
LevelStats userLevelData;
await using (var uow = _db.GetDbContext())
var reader = _channel.Reader;
while (true)
{
var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id);
userLevelData = new(stats.Xp + stats.AwardedXp);
}
var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id);
var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id);
if (theRoleYouWant is null)
return (AssignResult.ErrNotAssignable, autoDelete, null);
if (theRoleYouWant.LevelRequirement > userLevelData.Level)
return (AssignResult.ErrLvlReq, autoDelete, theRoleYouWant.LevelRequirement);
if (guildUser.RoleIds.Contains(role.Id))
return (AssignResult.ErrAlreadyHave, autoDelete, null);
var roleIds = roles.Where(x => x.Group == theRoleYouWant.Group).Select(x => x.RoleId).ToArray();
if (exclusive)
{
var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r));
foreach (var roleId in sameRoles)
{
var sameRole = guildUser.Guild.GetRole(roleId);
if (sameRole is not null)
{
try
{
await guildUser.RemoveRoleAsync(sameRole);
await Task.Delay(300);
}
catch
{
// ignored
}
}
}
}
var item = await reader.ReadAsync();
try
{
await guildUser.AddRoleAsync(role);
var sar = item.Group.Roles.First(x => x.RoleId == item.RoleId);
if (item.User.RoleIds.Contains(item.RoleId))
{
item.CompletionTask.TrySetResult(new SarAlreadyHasRole());
continue;
}
if (item.Group.RoleReq is { } rid)
{
if (!item.User.RoleIds.Contains(rid))
{
item.CompletionTask.TrySetResult(new SarRoleRequirement(rid));
continue;
}
// passed
}
// check level requirement
if (sar.LevelReq > 0)
{
await using var ctx = _db.GetDbContext();
var xpStats = await ctx.GetTable<UserXpStats>().GetGuildUserXp(sar.GuildId, item.User.Id);
var lvlData = new LevelStats(xpStats?.Xp ?? 0);
if (lvlData.Level < sar.LevelReq)
{
item.CompletionTask.TrySetResult(new SarLevelRequirement(sar.LevelReq));
continue;
}
// passed
}
if (item.Group.IsExclusive)
{
var rolesToRemove = item.Group.Roles.Select(x => x.RoleId);
await item.User.RemoveRolesAsync(rolesToRemove);
}
await item.User.AddRoleAsync(item.RoleId);
item.CompletionTask.TrySetResult(new Success());
}
catch (Exception ex)
{
return (AssignResult.ErrNotPerms, autoDelete, ex);
Log.Error(ex, "Unknown error ocurred in SAR runner: {Error}", ex.Message);
item.CompletionTask.TrySetResult(new Error());
}
}
}
return (AssignResult.Assigned, autoDelete, null);
}
public async Task<bool> SetNameAsync(ulong guildId, int group, string name)
public async Task Add(SarAssignerDataItem item)
{
var set = false;
await using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames));
var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group);
if (string.IsNullOrWhiteSpace(name))
{
if (toUpdate is not null)
gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
}
else if (toUpdate is null)
{
gc.SelfAssignableRoleGroupNames.Add(new()
{
Name = name,
Number = group
});
set = true;
}
else
{
toUpdate.Name = name;
set = true;
await _channel.Writer.WriteAsync(item);
}
await uow.SaveChangesAsync();
return set;
}
public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role)
{
var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id);
if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null)
return (RemoveResult.ErrNotAssignable, autoDelete);
if (!guildUser.RoleIds.Contains(role.Id))
return (RemoveResult.ErrNotHave, autoDelete);
try
{
await guildUser.RemoveRoleAsync(role);
}
catch (Exception)
{
return (RemoveResult.ErrNotPerms, autoDelete);
}
return (RemoveResult.Removed, autoDelete);
}
public bool RemoveSar(ulong guildId, ulong roleId)
{
bool success;
using var uow = _db.GetDbContext();
success = uow.Set<SelfAssignedRole>().DeleteByGuildAndRoleId(guildId, roleId);
uow.SaveChanges();
return success;
}
public (bool AutoDelete, bool Exclusive, IReadOnlyCollection<SelfAssignedRole>) GetAdAndRoles(ulong guildId)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(guildId, set => set);
var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages;
var exclusive = gc.ExclusiveSelfAssignedRoles;
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
return (autoDelete, exclusive, roles);
}
public bool SetLevelReq(ulong guildId, IRole role, int level)
{
using var uow = _db.GetDbContext();
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId);
var sar = roles.FirstOrDefault(x => x.RoleId == role.Id);
if (sar is not null)
{
sar.LevelRequirement = level;
uow.SaveChanges();
}
else
return false;
return true;
}
public bool ToggleEsar(ulong guildId)
{
bool areExclusive;
using var uow = _db.GetDbContext();
var config = uow.GuildConfigsForId(guildId, set => set);
areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles;
uow.SaveChanges();
return areExclusive;
}
public (bool Exclusive, IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary<int, string>
GroupNames
) GetRoles(IGuild guild)
{
var exclusive = false;
IReadOnlyCollection<(SelfAssignedRole Model, IRole Role)> roles;
IDictionary<int, string> groupNames;
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames));
exclusive = gc.ExclusiveSelfAssignedRoles;
groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name);
var roleModels = uow.Set<SelfAssignedRole>().GetFromGuild(guild.Id);
roles = roleModels.Select(x => (Model: x, Role: guild.GetRole(x.RoleId)))
.ToList();
uow.Set<SelfAssignedRole>().RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray());
uow.SaveChanges();
}
return (exclusive, roles.Where(x => x.Role is not null).ToList(), groupNames);
}
}
public sealed class SarAssignerDataItem
{
public required SarGroup Group { get; init; }
public required IGuildUser User { get; init; }
public required ulong RoleId { get; init; }
public required TaskCompletionSource<SarAssignResult> CompletionTask { get; init; }
}
[GenerateOneOf]
public sealed partial class SarAssignResult
: OneOfBase<Success, Error, SarLevelRequirement, SarRoleRequirement, SarAlreadyHasRole, SarInsuffPerms>
{
}
public record class SarLevelRequirement(int Level);
public record class SarRoleRequirement(ulong RoleId);
public record class SarAlreadyHasRole();
public record class SarInsuffPerms();

View file

@ -35,7 +35,7 @@ public partial class Administration
var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList()
?? new List<IgnoredLogItem>();
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.AddField(GetText(strs.log_ignored_channels),
chs.Count == 0

View file

@ -42,7 +42,7 @@ public partial class Administration
.Items(timezoneStrings)
.PageSize(timezonesPerPage)
.CurrentPage(page)
.Page((items, _) => _sender.CreateEmbed()
.Page((items, _) => CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.timezones_available))
.WithDescription(string.Join("\n", items)))

View file

@ -23,27 +23,6 @@ public partial class Administration
_mute = mute;
}
private async Task<bool> CheckRoleHierarchy(IGuildUser target)
{
var curUser = ((SocketGuild)ctx.Guild).CurrentUser;
var ownerId = ctx.Guild.OwnerId;
var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position);
var targetMaxRole = target.GetRoles().Max(r => r.Position);
var botMaxRole = curUser.GetRoles().Max(r => r.Position);
// bot can't punish a user who is higher in the hierarchy. Discord will return 403
// moderator can be owner, in which case role hierarchy doesn't matter
// otherwise, moderator has to have a higher role
if (botMaxRole <= targetMaxRole
|| (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)
|| target.Id == ownerId)
{
await Response().Error(strs.hierarchy).SendAsync();
return false;
}
return true;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)]
@ -65,7 +44,7 @@ public partial class Administration
try
{
await _sender.Response(user)
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithErrorColor()
.WithDescription(GetText(strs.warned_on(ctx.Guild.ToString())))
.AddField(GetText(strs.moderator), ctx.User.ToString())
@ -85,7 +64,7 @@ public partial class Administration
catch (Exception ex)
{
Log.Warning(ex, "Exception occured while warning a user");
var errorEmbed = _sender.CreateEmbed()
var errorEmbed = CreateEmbed()
.WithErrorColor()
.WithDescription(GetText(strs.cant_apply_punishment));
@ -96,7 +75,7 @@ public partial class Administration
return;
}
var embed = _sender.CreateEmbed().WithOkColor();
var embed = CreateEmbed().WithOkColor();
if (punishment is null)
embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString()))));
else
@ -205,7 +184,7 @@ public partial class Administration
.Page((warnings, page) =>
{
var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString();
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.warnlog_for(user)));
if (!warnings.Any())
embed.WithDescription(GetText(strs.warnings_none));
@ -266,7 +245,7 @@ public partial class Administration
+ $" | {total} ({all} - {forgiven})";
});
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.warnings_list))
.WithDescription(string.Join("\n", ws));
@ -478,7 +457,7 @@ public partial class Administration
var user = await ctx.Client.GetUserAsync(userId);
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await _mute.TimedBan(ctx.Guild, userId, time.Time, (ctx.User + " | " + msg).TrimTo(512), banPrune);
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user?.ToString() ?? userId.ToString(), true)
@ -507,7 +486,7 @@ public partial class Administration
await ctx.Guild.AddBanAsync(userId, banPrune, (ctx.User + " | " + msg).TrimTo(512));
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField("ID", userId.ToString(), true))
@ -544,7 +523,7 @@ public partial class Administration
var banPrune = await _service.GetBanPruneAsync(ctx.Guild.Id) ?? 7;
await ctx.Guild.AddBanAsync(user, banPrune, (ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("⛔️ " + GetText(strs.banned_user))
.AddField(GetText(strs.username), user.ToString(), true)
@ -740,7 +719,7 @@ public partial class Administration
{ await ctx.Guild.RemoveBanAsync(user); }
catch { await ctx.Guild.RemoveBanAsync(user); }
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("☣ " + GetText(strs.sb_user))
.AddField(GetText(strs.username), user.ToString(), true)
@ -795,7 +774,7 @@ public partial class Administration
await user.KickAsync((ctx.User + " | " + msg).TrimTo(512));
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.kicked_user))
.AddField(GetText(strs.username), user.ToString(), true)
@ -828,7 +807,7 @@ public partial class Administration
{
var dmMessage = GetText(strs.timeoutdm(Format.Bold(ctx.Guild.Name), msg));
await _sender.Response(user)
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithPendingColor()
.WithDescription(dmMessage))
.SendAsync();
@ -840,7 +819,7 @@ public partial class Administration
await user.SetTimeOutAsync(time.Time);
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithTitle("⏳ " + GetText(strs.timedout_user))
.AddField(GetText(strs.username), user.ToString(), true)
@ -901,7 +880,7 @@ public partial class Administration
if (string.IsNullOrWhiteSpace(missStr))
missStr = "-";
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithDescription(GetText(strs.mass_ban_in_progress(banning.Count)))
.AddField(GetText(strs.invalid(missing.Count)), missStr)
.WithPendingColor();
@ -921,7 +900,7 @@ public partial class Administration
}
}
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
.WithDescription(
GetText(strs.mass_ban_completed(
banning.Count())))
@ -949,7 +928,7 @@ public partial class Administration
//send a message but don't wait for it
var banningMessageTask = Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithDescription(
GetText(strs.mass_kill_in_progress(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)
@ -970,7 +949,7 @@ public partial class Administration
//wait for the message and edit it
var banningMessage = await banningMessageTask;
await banningMessage.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
await banningMessage.ModifyAsync(x => x.Embed = CreateEmbed()
.WithDescription(
GetText(strs.mass_kill_completed(bans.Count())))
.AddField(GetText(strs.invalid(missing)), missStr)

View file

@ -68,7 +68,7 @@ public partial class Administration
else
text = GetText(strs.no_vcroles);
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.vc_role_list))
.WithDescription(text)).SendAsync();

View file

@ -34,7 +34,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
var ex = await _service.AddAsync(ctx.Guild?.Id, key, message);
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.expr_new))
.WithDescription($"#{new kwum(ex.Id)}")
@ -104,7 +104,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
if (ex is not null)
{
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.expr_edited))
.WithDescription($"#{id}")
@ -159,7 +159,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
: " // " + string.Join(" ", ex.GetReactions())))
.Join('\n');
return _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc);
return CreateEmbed().WithOkColor().WithTitle(GetText(strs.expressions)).WithDescription(desc);
})
.SendAsync();
}
@ -179,7 +179,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
await Response()
.Interaction(IsValidExprEditor() ? inter : null)
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithDescription($"#{id}")
.AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024))
@ -224,7 +224,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
if (ex is not null)
{
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.expr_deleted))
.WithDescription($"#{id}")
@ -375,7 +375,7 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
[UserPerm(GuildPerm.Administrator)]
public async Task ExprClear()
{
if (await PromptUserConfirmAsync(_sender.CreateEmbed()
if (await PromptUserConfirmAsync(CreateEmbed()
.WithTitle("Expression clear")
.WithDescription("This will delete all expressions on this server.")))
{

View file

@ -6,9 +6,9 @@ namespace EllieBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable
{
public const double BASE_MULTIPLIER = 0.82;
public const double BASE_MULTIPLIER = 0.87;
public const double MAX_MULTIPLIER = 0.94;
public const double MULTI_PER_USER = 0.01;
public const double MULTI_PER_USER = 0.005;
public enum Phase
{

View file

@ -74,7 +74,7 @@ public partial class Gambling
if (race.FinishedUsers[0].Bet > 0)
{
return Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.animal_race))
.WithDescription(GetText(strs.animal_race_won_money(
@ -132,7 +132,7 @@ public partial class Gambling
raceMessage = await Response().Confirm(text).SendAsync();
else
{
await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
await msg.ModifyAsync(x => x.Embed = CreateEmbed()
.WithTitle(GetText(strs.animal_race))
.WithDescription(text)
.WithOkColor()

View file

@ -59,7 +59,7 @@ public partial class Gambling
{
var bal = await _bank.GetBalanceAsync(ctx.User.Id);
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.bank_balance(N(bal))));
@ -80,7 +80,7 @@ public partial class Gambling
{
var bal = await _bank.GetBalanceAsync(user.Id);
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal))));

View file

@ -24,7 +24,7 @@ public partial class Gambling
{
var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game);
var result = await PromptUserConfirmAsync(_sender.CreateEmbed()
var result = await PromptUserConfirmAsync(CreateEmbed()
.WithDescription(
$"""
Are you sure you want to reset your bet stats for **{GetGameName(game)}**?
@ -87,7 +87,7 @@ public partial class Gambling
}
};
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
@ -120,7 +120,7 @@ public partial class Gambling
{
var stats = await _gamblingTxTracker.GetAllAsync();
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
@ -156,7 +156,7 @@ public partial class Gambling
[OwnerOnly]
public async Task GambleStatsReset()
{
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription(
"""
Are you sure?

View file

@ -95,7 +95,7 @@ public partial class Gambling
var cStr = string.Concat(c.Select(x => x[..^1] + " "));
cStr += "\n" + string.Concat(c.Select(x => x.Last() + " "));
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle("BlackJack")
.AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr);

View file

@ -132,7 +132,7 @@ public partial class Gambling
else
title = GetText(strs.connect4_draw);
return msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
return msg.ModifyAsync(x => x.Embed = CreateEmbed()
.WithTitle(title)
.WithDescription(GetGameStateText(game))
.WithOkColor()
@ -142,7 +142,7 @@ public partial class Gambling
private async Task Game_OnGameStateUpdated(Connect4Game game)
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}")
.WithDescription(GetGameStateText(game))
.WithOkColor();

View file

@ -38,7 +38,7 @@ public partial class Gambling
var fileName = $"dice.{format.FileExtensions.First()}";
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.roll2), gen)
@ -115,7 +115,7 @@ public partial class Gambling
d.Dispose();
var imageName = $"dice.{format.FileExtensions.First()}";
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.AddField(GetText(strs.rolls), values.Select(x => Format.Code(x.ToString())).Join(' '), true)
@ -141,7 +141,7 @@ public partial class Gambling
for (var i = 0; i < n1; i++)
rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(Format.Bold(n1.ToString()))))
@ -170,7 +170,7 @@ public partial class Gambling
arr[i] = rng.Next(1, n2 + 1);
var sum = arr.Sum();
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`")))

View file

@ -56,7 +56,7 @@ public partial class Gambling
foreach (var i in images)
i.Dispose();
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor();
var toSend = string.Empty;
@ -171,7 +171,7 @@ public partial class Gambling
return;
}
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(result.Card.GetEmoji())

View file

@ -30,12 +30,12 @@ public partial class Gambling
private EmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
=> type switch
{
CurrencyEvent.Type.Reaction => _sender.CreateEmbed()
CurrencyEvent.Type.Reaction => CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetReactionDescription(opts.Amount, currentPot))
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
CurrencyEvent.Type.GameStatus => _sender.CreateEmbed()
CurrencyEvent.Type.GameStatus => CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.event_title(type.ToString())))
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot))

View file

@ -84,7 +84,7 @@ public partial class Gambling
? Format.Bold(GetText(strs.heads))
: Format.Bold(GetText(strs.tails))));
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithDescription(msg)
@ -130,7 +130,7 @@ public partial class Gambling
str = Format.Bold(GetText(strs.better_luck));
}
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithAuthor(ctx.User)
.WithDescription(str)
.WithOkColor()

View file

@ -39,6 +39,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IPatronageService _ps;
private readonly RakebackService _rb;
private readonly IBotCache _cache;
public Gambling(
IGamblingService gs,
@ -52,7 +53,8 @@ public partial class Gambling : GamblingModule<GamblingService>
IRemindService remind,
IPatronageService patronage,
GamblingTxTracker gamblingTxTracker,
RakebackService rb)
RakebackService rb,
IBotCache cache)
: base(configService)
{
_gs = gs;
@ -63,6 +65,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_remind = remind;
_gamblingTxTracker = gamblingTxTracker;
_rb = rb;
_cache = cache;
_ps = patronage;
_rng = new EllieRandom();
@ -151,19 +154,62 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
else if (Config.Timely.ProtType == TimelyProt.Captcha)
{
var password = await GetUserTimelyPassword(ctx.User.Id);
var img = GetPasswordImage(password);
using var stream = await img.ToStreamAsync();
var captcha = await Response()
.File(stream, "timely.png")
.SendAsync();
try
{
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
{
return;
}
await ClearUserTimelyPassword(ctx.User.Id);
}
finally
{
_ = captcha.DeleteAsync();
}
}
await ClaimTimely();
}
private static TypedKey<string> TimelyPasswordKey(ulong userId)
=> new($"timely_password:{userId}");
private async Task<string> GetUserTimelyPassword(ulong userId)
{
var pw = await _cache.GetOrAddAsync(TimelyPasswordKey(userId),
() =>
{
var password = _service.GeneratePassword();
return Task.FromResult(password);
});
var img = new Image<Rgba32>(70, 35);
return pw;
}
var font = _fonts.NotoSans.CreateFont(30);
var outlinePen = new SolidPen(Color.Black, 1f);
private ValueTask<bool> ClearUserTimelyPassword(ulong userId)
=> _cache.RemoveAsync(TimelyPasswordKey(userId));
private Image<Rgba32> GetPasswordImage(string password)
{
var img = new Image<Rgba32>(50, 24);
var font = _fonts.NotoSans.CreateFont(22);
var outlinePen = new SolidPen(Color.Black, 0.5f);
var strikeoutRun = new RichTextRun
{
Start = 0,
End = password.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 3),
StrikeoutPen = new SolidPen(Color.White, 4),
TextDecorations = TextDecorations.Strikeout
};
// draw password on the image
@ -174,35 +220,15 @@ public partial class Gambling : GamblingModule<GamblingService>
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(35, 17),
Origin = new(25, 12),
TextRuns = [strikeoutRun]
},
password,
Brushes.Solid(Color.White),
outlinePen);
});
using var stream = await img.ToStreamAsync();
var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
.File(stream, "timely.png")
.SendAsync();
try
{
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
{
return;
}
}
finally
{
_ = captcha.DeleteAsync();
}
}
await ClaimTimely();
return img;
}
private async Task ClaimTimely()
@ -384,7 +410,7 @@ public partial class Gambling : GamblingModule<GamblingService>
trs = await uow.Set<CurrencyTransaction>().GetPageFor(userId, page);
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle(GetText(strs.transactions(
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
@ -435,7 +461,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var eb = _sender.CreateEmbed().WithOkColor();
var eb = CreateEmbed().WithOkColor();
eb.WithAuthor(ctx.User);
eb.WithTitle(GetText(strs.transaction));
@ -699,7 +725,7 @@ public partial class Gambling : GamblingModule<GamblingService>
str = GetText(strs.better_luck);
}
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithAuthor(ctx.User)
.WithDescription(Format.Bold(str))
.AddField(GetText(strs.roll2), result.Roll.ToString(CultureInfo.InvariantCulture))
@ -766,7 +792,7 @@ public partial class Gambling : GamblingModule<GamblingService>
.CurrentPage(page)
.Page((toSend, curPage) =>
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(CurrencySign + " " + GetText(strs.leaderboard));
@ -829,7 +855,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var embed = _sender.CreateEmbed();
var embed = CreateEmbed();
string msg;
if (result.Result == RpsResultType.Draw)
@ -893,7 +919,7 @@ public partial class Gambling : GamblingModule<GamblingService>
sb.AppendLine();
}
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithDescription(sb.ToString())
.AddField(GetText(strs.multiplier), $"{result.Multiplier:0.##}x", true)

View file

@ -0,0 +1,92 @@
// namespace EllieBot.Modules.Gambling;
// public sealed class Loan
// {
// public int Id { get; set; }
// public ulong LenderId { get; set; }
// public string LenderName { get; set; }
// public ulong BorrowerId { get; set; }
// public string BorrowerName { get; set; }
// public long Amount { get; set; }
// public decimal Interest { get; set; }
// public DateTime DueDate { get; set; }
// public bool Repaid { get; set; }
// }
//
// public sealed class LoanService : INService
// {
// public async Task<IReadOnlyList<Loan>> GetLoans(ulong userId)
// {
// }
//
// public async Task<object> RepayAsync(object loandId)
// {
// }
// }
//
// public partial class Gambling
// {
// public partial class LoanCommands : EllieModule<LoanService>
// {
// [Cmd]
// public async Task Loan(
// IUser lender,
// long amount,
// decimal interest = 0,
// TimeSpan dueIn = default)
// {
// var eb = CreateEmbed()
// .WithOkColor()
// .WithDescription("User 0 Requests a loan from User {1}")
// .AddField("Amount", amount, true)
// .AddField("Interest", (interest * 0.01m).ToString("P2"), true);
// }
//
// public Task Loans()
// => Loans(ctx.User);
//
// public async Task Loans([Leftover] IUser user)
// {
// var loans = await _service.GetLoans(user.Id);
//
// Response()
// .Paginated()
// .PageItems(loans)
// .Page((items, page) =>
// {
// var eb = CreateEmbed()
// .WithOkColor()
// .WithDescription("Current Loans");
//
// foreach (var item in items)
// {
// eb.AddField(new kwum(item.id).ToString(),
// $"""
// To: {item.LenderName}
// Amount: {}
// """,
// true);
// }
//
// return eb;
// });
// }
//
// [Cmd]
// public async Task Repay(kwum loanId)
// {
// var res = await _service.RepayAsync(loandId);
//
// if (res.TryPickT0(out var _, out var err))
// {
// }
// else
// {
// var errStr = err.Match(
// _ => "Not enough funds",
// _ => "Loan not found");
//
// await Response().Error(errStr).SendAsync();
// }
// }
// }
// }

View file

@ -103,9 +103,9 @@ public partial class Gambling
.Page((items, _) =>
{
if (!items.Any())
return _sender.CreateEmbed().WithErrorColor().WithDescription("-");
return CreateEmbed().WithErrorColor().WithDescription("-");
return items.Aggregate(_sender.CreateEmbed().WithOkColor(),
return items.Aggregate(CreateEmbed().WithOkColor(),
(eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId));
})
.SendAsync();

View file

@ -56,8 +56,8 @@ public partial class Gambling
.Page((items, curPage) =>
{
if (!items.Any())
return _sender.CreateEmbed().WithErrorColor().WithDescription(GetText(strs.shop_none));
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.shop));
return CreateEmbed().WithErrorColor().WithDescription(GetText(strs.shop_none));
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.shop));
for (var i = 0; i < items.Count; i++)
{
@ -188,7 +188,7 @@ public partial class Gambling
{
await Response()
.User(ctx.User)
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.shop_purchase(ctx.Guild.Name)))
.AddField(GetText(strs.item), item.Text)
@ -254,7 +254,7 @@ public partial class Gambling
.Replace("%you.name%", buyer.GlobalName ?? buyer.Username)
.Replace("%you.nick%", buyer.DisplayName);
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithPendingColor()
.WithTitle("Executing shop command")
.WithDescription(cmd);
@ -541,7 +541,7 @@ public partial class Gambling
public EmbedBuilder EntryToEmbed(ShopEntry entry)
{
var embed = _sender.CreateEmbed().WithOkColor();
var embed = CreateEmbed().WithOkColor();
if (entry.Type == ShopEntryType.Role)
{

View file

@ -65,7 +65,7 @@ public partial class Gambling
await using var imgStream = await image.ToStreamAsync();
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithAuthor(ctx.User)
.WithDescription(Format.Bold(text))
.WithImageUrl($"attachment://result.png")

View file

@ -21,7 +21,7 @@ public partial class Gambling
public async Task WaifuReset()
{
var price = _service.GetResetPrice(ctx.User);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle(GetText(strs.waifu_reset_confirm))
.WithDescription(GetText(strs.waifu_reset_price(Format.Bold(N(price)))));
@ -46,7 +46,7 @@ public partial class Gambling
.PageItems(async (page) => await _service.GetClaimsAsync(ctx.User.Id, page))
.Page((items, page) =>
{
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithTitle("Waifus");
@ -266,7 +266,7 @@ public partial class Gambling
return;
}
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.waifus_top_waifus)).WithOkColor();
var embed = CreateEmbed().WithTitle(GetText(strs.waifus_top_waifus)).WithOkColor();
var i = 0;
foreach (var w in waifus)
@ -350,7 +350,7 @@ public partial class Gambling
if (string.IsNullOrWhiteSpace(fansStr))
fansStr = "-";
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.waifu)
+ " "
@ -393,7 +393,7 @@ public partial class Gambling
.CurrentPage(page)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor();
var embed = CreateEmbed().WithTitle(GetText(strs.waifu_gift_shop)).WithOkColor();
items
.ToList()

View file

@ -67,7 +67,7 @@ public partial class Games
private Task Game_OnStarted(AcrophobiaGame game)
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(
@ -92,7 +92,7 @@ public partial class Games
if (submissions.Length == 1)
{
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithDescription(GetText(
strs.acro_winner_only(
@ -103,7 +103,7 @@ public partial class Games
var i = 0;
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed))
.WithDescription(GetText(strs.acro_nym_was(
@ -127,7 +127,7 @@ public partial class Games
var table = votes.OrderByDescending(v => v.Value);
var winner = table.First();
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.acrophobia))
.WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName),

View file

@ -38,7 +38,7 @@ public partial class Games : EllieModule<GamesService>
return;
var res = _service.GetEightballResponse(ctx.User.Id, question);
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithDescription(ctx.User.ToString())
.AddField("❓ " + GetText(strs.question), question)

View file

@ -53,7 +53,7 @@ public partial class Games
var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight()));
await Response()
.File(stream, "ncanvas.png")
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
#if GLOBAL_ELLIE
.WithDescription("This is not available yet.")
@ -164,7 +164,7 @@ public partial class Games
Culture,
_gcs.Data.Currency.Sign))));
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
if (!await PromptUserConfirmAsync(CreateEmbed()
.WithPendingColor()
.WithDescription(prompt)))
{
@ -193,7 +193,7 @@ public partial class Games
await using var stream = await img.ToStreamAsync();
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
.WithImageUrl($"attachment://zoom_{position}.png"))
@ -231,7 +231,7 @@ public partial class Games
var pos = new kwum(pixel.Position);
await Response()
.File(stream, $"{pixel.Position}.png")
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
.WithTitle(GetText(strs.nc_pixel(pos)))
@ -263,7 +263,7 @@ public partial class Games
return;
}
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription(
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
+ "Are you sure you want to continue?")))
@ -293,7 +293,7 @@ public partial class Games
{
await _service.ResetAsync();
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
if (!await PromptUserConfirmAsync(CreateEmbed()
.WithDescription(
"This will delete all pixels and reset the canvas.\n\n"
+ "Are you sure you want to continue?")))

View file

@ -94,7 +94,7 @@ public partial class Games
if (removed is null)
return;
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle($"Removed typing article #{index + 1}")
.WithDescription(removed.Text.TrimTo(50))
.WithOkColor();

View file

@ -160,7 +160,7 @@ public partial class Games
{
try
{
questionEmbed = _sender.CreateEmbed()
questionEmbed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.AddField(GetText(strs.category), question.Category)
@ -189,7 +189,7 @@ public partial class Games
{
try
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithErrorColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_times_up(Format.Bold(question.Answer))));
@ -221,7 +221,7 @@ public partial class Games
{
try
{
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithAuthor(GetText(strs.trivia_ended))
.WithTitle(GetText(strs.leaderboard))
@ -247,7 +247,7 @@ public partial class Games
{
try
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.trivia_game))
.WithDescription(GetText(strs.trivia_win(user.Name,

View file

@ -114,7 +114,7 @@ public sealed partial class Help : EllieModule<HelpService>
.AddFooter(false)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed().WithOkColor().WithTitle(GetText(strs.list_of_modules));
var embed = CreateEmbed().WithOkColor().WithTitle(GetText(strs.list_of_modules));
if (!items.Any())
{
@ -315,7 +315,7 @@ public sealed partial class Help : EllieModule<HelpService>
.WithPlaceholder("Select a submodule to see detailed commands");
var groups = cmdsWithGroup.ToArray();
var embed = _sender.CreateEmbed().WithOkColor();
var embed = CreateEmbed().WithOkColor();
foreach (var g in groups)
{
sb.AddOption(g.Key, g.Key);
@ -383,7 +383,7 @@ public sealed partial class Help : EllieModule<HelpService>
.Interaction(inter)
.Page((items, _) =>
{
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithTitle(GetText(strs.cmd_group_commands(group.Name)))
.WithOkColor();
@ -520,7 +520,7 @@ public sealed partial class Help : EllieModule<HelpService>
[OnlyPublicBot]
public async Task Donate()
{
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithTitle("Thank you for considering to donate to the EllieBot project!");

View file

@ -40,7 +40,7 @@ public partial class Marmalade : EllieModule<IMarmaladeLoaderService>
.PageSize(10)
.Page((items, _) =>
{
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.list_of_unloaded))
.WithDescription(items.Join('\n'));
@ -81,7 +81,7 @@ public partial class Marmalade : EllieModule<IMarmaladeLoaderService>
}
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.loaded_marmalades))
.WithDescription(loaded.Select(x => x.Name)
@ -136,7 +136,7 @@ public partial class Marmalade : EllieModule<IMarmaladeLoaderService>
.Paginated()
.Items(output)
.PageSize(10)
.Page((items, _) => _sender.CreateEmbed()
.Page((items, _) => CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.list_of_marmalades))
.WithDescription(items.Join('\n')))
@ -168,7 +168,7 @@ public partial class Marmalade : EllieModule<IMarmaladeLoaderService>
: $"{x.Prefix} {x.Name}"))
.Join("\n");
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(GetText(strs.marmalade_info))
.WithTitle(found.Name)
@ -201,7 +201,7 @@ public partial class Marmalade : EllieModule<IMarmaladeLoaderService>
.CurrentPage(0)
.Page((items, _) =>
{
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor();
foreach (var marmalade in items)
@ -224,7 +224,7 @@ public partial class Marmalade : EllieModule<IMarmaladeLoaderService>
[OwnerOnly]
public async Task MarmaladeSearch()
{
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithTitle(GetText(strs.list_of_marmalades))
.WithOkColor();

View file

@ -109,7 +109,7 @@ public sealed partial class Music : EllieModule<IMusicService>
try
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithAuthor(GetText(strs.queued_track) + " #" + (index + 1), MUSIC_ICON_URL)
.WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ")
@ -314,7 +314,7 @@ public sealed partial class Music : EllieModule<IMusicService>
if (!string.IsNullOrWhiteSpace(add))
desc = add + "\n" + desc;
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithAuthor(
GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)),
MUSIC_ICON_URL)
@ -352,7 +352,7 @@ public sealed partial class Music : EllieModule<IMusicService>
}
var embeds = videos.Select((x, i) => _sender.CreateEmbed()
var embeds = videos.Select((x, i) => CreateEmbed()
.WithOkColor()
.WithThumbnailUrl(x.Thumbnail)
.WithDescription($"`{i + 1}.` {Format.Bold(x.Title)}\n\t{x.Url}"))
@ -424,7 +424,7 @@ public sealed partial class Music : EllieModule<IMusicService>
return;
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithAuthor(GetText(strs.removed_track) + " #" + index, MUSIC_ICON_URL)
.WithDescription(track.PrettyName())
.WithFooter(track.PrettyInfo())
@ -592,7 +592,7 @@ public sealed partial class Music : EllieModule<IMusicService>
return;
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle(track.Title.TrimTo(65))
.WithAuthor(GetText(strs.track_moved), MUSIC_ICON_URL)
.AddField(GetText(strs.from_position), $"#{from + 1}", true)
@ -651,7 +651,7 @@ public sealed partial class Music : EllieModule<IMusicService>
if (currentTrack is null)
return;
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithAuthor(GetText(strs.now_playing), MUSIC_ICON_URL)
.WithDescription(currentTrack.PrettyName())
@ -752,4 +752,20 @@ public sealed partial class Music : EllieModule<IMusicService>
else
await Response().Error(strs.no_player).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task WrongSong()
{
var removed = await _service.RemoveLastQueuedTrackAsync(ctx.Guild.Id);
if (removed is null)
{
await Response().Error(strs.no_last_queued_found).SendAsync();
}
else
{
await Response().Confirm(strs.wrongsong_success(removed.Title.TrimTo(30))).SendAsync();
}
}
}

View file

@ -49,7 +49,7 @@ public sealed partial class Music
playlists = uow.Set<MusicPlaylist>().GetPlaylistsOnPage(num);
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithAuthor(GetText(strs.playlists_page(num)), MUSIC_ICON_URL)
.WithDescription(string.Join("\n",
playlists.Select(r => GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count)))))
@ -113,7 +113,7 @@ public sealed partial class Music
var str = string.Join("\n",
items
.Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`"));
return _sender.CreateEmbed().WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
return CreateEmbed().WithTitle($"\"{mpl.Name}\" by {mpl.Author}")
.WithOkColor()
.WithDescription(str);
})
@ -155,7 +155,7 @@ public sealed partial class Music
}
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.playlist_saved))
.AddField(GetText(strs.name), name)

View file

@ -33,4 +33,5 @@ public interface IMusicService
Task SetMusicQualityAsync(ulong guildId, QualityPreset preset);
Task<bool> ToggleQueueAutoPlayAsync(ulong guildId);
Task<bool> FairplayAsync(ulong guildId);
Task<IQueuedTrackInfo?> RemoveLastQueuedTrackAsync(ulong guildId);
}

View file

@ -179,7 +179,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
return async (mp, trackInfo) =>
{
_ = lastFinishedMessage?.DeleteAsync();
var embed = _sender.CreateEmbed()
var embed = _sender.CreateEmbed(guildId)
.WithOkColor()
.WithAuthor(GetText(guildId, strs.finished_track), Music.MUSIC_ICON_URL)
.WithDescription(trackInfo.PrettyName())
@ -195,7 +195,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
return async (mp, trackInfo, index) =>
{
_ = lastPlayingMessage?.DeleteAsync();
var embed = _sender.CreateEmbed()
var embed = _sender.CreateEmbed(guildId)
.WithOkColor()
.WithAuthor(GetText(guildId, strs.playing_track(index + 1)), Music.MUSIC_ICON_URL)
.WithDescription(trackInfo.PrettyName())
@ -290,7 +290,8 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
return "-";
return randomPlayingTrack.Title;
});
}
);
// number of servers currently listening to music
yield return ("%music.servers%", () =>
@ -298,14 +299,16 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
var count = _players.Select(x => x.Value.GetCurrentTrack(out _)).Count(x => x is not null);
return count.ToString();
});
}
);
yield return ("%music.queued%", () =>
{
var count = _players.Sum(x => x.Value.GetQueuedTracks().Count);
return count.ToString();
});
}
);
}
#region Settings
@ -434,5 +437,17 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
return Task.FromResult(false);
}
public async Task<IQueuedTrackInfo?> RemoveLastQueuedTrackAsync(ulong guildId)
{
if (TryGetMusicPlayer(guildId, out var mp))
{
var last = await mp.RemoveLastQueuedTrack();
return last;
}
return null;
}
#endregion
}

View file

@ -38,4 +38,5 @@ public interface IMusicPlayer : IDisposable
void SetRepeat(PlayerRepeatType type);
void ShuffleQueue();
void SetFairplay();
Task<IQueuedTrackInfo?> RemoveLastQueuedTrack();
}

View file

@ -20,4 +20,5 @@ public interface IMusicQueue
void Shuffle(Random rng);
bool IsLast();
void ReorderFairly();
int? GetLastQueuedIndex();
}

View file

@ -260,7 +260,6 @@ public sealed class MusicPlayer : IMusicPlayer
IsStopped = true;
Log.Error("Please install ffmpeg and make sure it's added to your "
+ "PATH environment variable before trying again");
}
catch (OperationCanceledException)
{
@ -542,4 +541,15 @@ public sealed class MusicPlayer : IMusicPlayer
{
_queue.ReorderFairly();
}
public Task<IQueuedTrackInfo?> RemoveLastQueuedTrack()
{
var last = _queue.GetLastQueuedIndex();
if (last is null)
return Task.FromResult<IQueuedTrackInfo?>(null);
return TryRemoveTrackAt(last.Value, out var trackInfo)
? Task.FromResult(trackInfo)
: Task.FromResult<IQueuedTrackInfo?>(null);
}
}

View file

@ -60,6 +60,7 @@ public sealed partial class MusicQueue : IMusicQueue
private LinkedList<QueuedTrackInfo> tracks;
private int index;
private int? _lastQueued = null;
private readonly object _locker = new();
@ -74,7 +75,7 @@ public sealed partial class MusicQueue : IMusicQueue
lock (_locker)
{
var added = new QueuedTrackInfo(trackInfo, queuer);
enqueuedAt = tracks.Count;
_lastQueued = enqueuedAt = tracks.Count;
tracks.AddLast(added);
return added;
@ -99,6 +100,8 @@ public sealed partial class MusicQueue : IMusicQueue
tracks.AddAfter(currentNode, added);
_lastQueued = i;
return added;
}
}
@ -112,6 +115,8 @@ public sealed partial class MusicQueue : IMusicQueue
var added = new QueuedTrackInfo(track, queuer);
tracks.AddLast(added);
}
_lastQueued = tracks.Count;
}
}
@ -146,6 +151,7 @@ public sealed partial class MusicQueue : IMusicQueue
lock (_locker)
{
tracks.Clear();
_lastQueued = null;
}
}
@ -177,6 +183,18 @@ public sealed partial class MusicQueue : IMusicQueue
if (index < 0)
index = Count;
if (i < _lastQueued)
{
if (_lastQueued is not null)
{
_lastQueued -= 1;
}
}
else if (i == _lastQueued)
{
_lastQueued = null;
}
// if it was the last song in the queue
// // wrap back to start
// if (_index == Count)
@ -207,6 +225,11 @@ public sealed partial class MusicQueue : IMusicQueue
if (from >= Count || to >= Count)
return null;
if (from == _lastQueued)
_lastQueued = to;
else if (to == _lastQueued)
_lastQueued += 1;
// update current track index
if (from == index)
{
@ -267,6 +290,7 @@ public sealed partial class MusicQueue : IMusicQueue
var list = tracks.ToArray();
rng.Shuffle(list);
tracks = new(list);
_lastQueued = null;
}
}
@ -318,6 +342,8 @@ public sealed partial class MusicQueue : IMusicQueue
if (queuers.Count == 0)
break;
}
_lastQueued = null;
}
}
@ -339,4 +365,6 @@ public sealed partial class MusicQueue : IMusicQueue
return true;
}
}
public int? GetLastQueuedIndex() => _lastQueued;
}

View file

@ -32,7 +32,6 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
_sender = sender;
_config = config;
_client = client;
}
public Task OnReadyAsync()
@ -103,7 +102,7 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
// if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars,
// up to 100%
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update"));
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron", "update"));
_ = SendMessageToUser(newPatron.UserId,
$"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!");

View file

@ -53,7 +53,7 @@ public partial class Help
//
// var patron = _service.GiftPatronAsync(user, amount);
//
// var eb = _sender.CreateEmbed();
// var eb = CreateEmbed();
//
// await Response().Embed(eb.WithDescription($"Added **{days}** days of Patron benefits to {user.Mention}!")
// .AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
@ -75,7 +75,7 @@ public partial class Help
var quotaStats = await _service.LimitStats(user.Id);
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithAuthor(user)
.WithTitle(GetText(strs.patron_info))
.WithOkColor();

View file

@ -60,12 +60,12 @@ public partial class Permissions
.Page((pageItems, _) =>
{
if (pageItems.Count == 0)
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithTitle(title)
.WithDescription(GetText(strs.empty_page));
return _sender.CreateEmbed()
return CreateEmbed()
.WithTitle(title)
.WithDescription(pageItems.Join('\n'))
.WithOkColor();

View file

@ -95,7 +95,7 @@ public partial class Permissions
var output = items.Select(x =>
$"{Format.Code(x.CommandName)}: {x.Seconds}s");
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithDescription(output.Join("\n"));
})

View file

@ -28,7 +28,7 @@ public partial class Permissions
[RequireContext(ContextType.Guild)]
public async Task FilterList()
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle("Server filter settings");
@ -316,7 +316,7 @@ public partial class Permissions
.Items(fws)
.PageSize(10)
.CurrentPage(page)
.Page((items, _) => _sender.CreateEmbed()
.Page((items, _) => CreateEmbed()
.WithTitle(GetText(strs.filter_word_list))
.WithDescription(string.Join("\n", items))
.WithOkColor())

View file

@ -30,7 +30,7 @@ public partial class Permissions
return;
}
var embed = _sender.CreateEmbed().WithOkColor();
var embed = CreateEmbed().WithOkColor();
if (blockedModule.Any())
embed.AddField(GetText(strs.blocked_modules), string.Join("\n", _service.BlockedModules));

View file

@ -24,7 +24,7 @@ public partial class Searches
return;
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithDescription(animeData.Synopsis.Replace("<br>",
Environment.NewLine,
@ -56,7 +56,7 @@ public partial class Searches
return;
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithDescription(mangaData.Synopsis.Replace("<br>",
Environment.NewLine,

View file

@ -35,7 +35,7 @@ public partial class Searches
}
var symbol = symbols.First();
var promptEmbed = _sender.CreateEmbed()
var promptEmbed = CreateEmbed()
.WithDescription(symbol.Description)
.WithTitle(GetText(strs.did_you_mean(symbol.Symbol)));
@ -67,7 +67,7 @@ public partial class Searches
var price = stock.Price.ToString("C2", localCulture);
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithAuthor(stock.Symbol)
.WithUrl($"https://www.tradingview.com/chart/?symbol={stock.Symbol}")
@ -112,7 +112,7 @@ public partial class Searches
if (nearest is not null)
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithTitle(GetText(strs.crypto_not_found))
.WithDescription(
GetText(strs.did_you_mean(Format.Bold($"{nearest.Name} ({nearest.Symbol})"))));
@ -145,7 +145,7 @@ public partial class Searches
await using var sparkline = await _service.GetSparklineAsync(crypto.Id, usd.PercentChange7d >= 0);
var fileName = $"{crypto.Slug}_7d.png";
var toSend = _sender.CreateEmbed()
var toSend = CreateEmbed()
.WithOkColor()
.WithAuthor($"#{crypto.CmcRank}")
.WithTitle($"{crypto.Name} ({crypto.Symbol})")
@ -198,7 +198,7 @@ public partial class Searches
.PageSize(10)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor();
if (items.Count > 0)

View file

@ -123,7 +123,7 @@ public partial class Searches
if (!feeds.Any())
{
await Response()
.Embed(_sender.CreateEmbed().WithOkColor().WithDescription(GetText(strs.feed_no_feed)))
.Embed(CreateEmbed().WithOkColor().WithDescription(GetText(strs.feed_no_feed)))
.SendAsync();
return;
}
@ -135,7 +135,7 @@ public partial class Searches
.CurrentPage(page)
.Page((items, cur) =>
{
var embed = _sender.CreateEmbed().WithOkColor();
var embed = CreateEmbed().WithOkColor();
var i = 0;
var fs = string.Join("\n",
items.Select(x => $"`{(cur * 10) + ++i}.` <#{x.ChannelId}> {x.Url}"));

View file

@ -44,7 +44,7 @@ public partial class Searches
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle($"osu! {smode} profile for {user}")
.WithThumbnailUrl($"https://a.ppy.sh/{userId}")
@ -78,7 +78,7 @@ public partial class Searches
return;
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle($"osu!Gatari {modeStr} profile for {user}")
.WithThumbnailUrl($"https://a.gatari.pw/{userStats.Id}")
@ -113,7 +113,7 @@ public partial class Searches
var plays = await _service.GetOsuPlay(user, mode);
var eb = _sender.CreateEmbed().WithOkColor().WithTitle($"Top 5 plays for {user}");
var eb = CreateEmbed().WithOkColor().WithTitle($"Top 5 plays for {user}");
foreach(var (title, desc) in plays)
eb.AddField(title, desc);

View file

@ -25,7 +25,7 @@ public partial class Searches
if (kvp.Key.ToUpperInvariant() == pokemon.ToUpperInvariant())
{
var p = kvp.Value;
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithTitle(kvp.Key.ToTitleCase())
.WithDescription(p.BaseStats.ToString())
@ -55,7 +55,7 @@ public partial class Searches
{
if (kvp.Key.ToUpperInvariant() == ability)
{
await Response().Embed(_sender.CreateEmbed()
await Response().Embed(CreateEmbed()
.WithOkColor()
.WithTitle(kvp.Value.Name)
.WithDescription(string.IsNullOrWhiteSpace(kvp.Value.Desc)

View file

@ -22,7 +22,7 @@ public partial class Searches
}
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle($"{verse.BookName} {verse.Chapter}:{verse.Verse}")
.WithDescription(verse.Text))
@ -48,7 +48,7 @@ public partial class Searches
await using var audio = await http.GetStreamAsync(arabic.Audio);
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.AddField("Arabic", arabic.Text)
.AddField("English", english.Text)

View file

@ -59,7 +59,7 @@ public partial class Searches
descStr = descStr.TrimTo(4096);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithTitle(query.TrimTo(64)!)
@ -93,9 +93,9 @@ public partial class Searches
return;
}
EmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
EmbedBuilder CreateImageEmbed(IImageSearchResultEntry entry)
{
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithAuthor(ctx.User)
.WithTitle(query)
@ -112,10 +112,11 @@ public partial class Searches
var item = items.FirstOrDefault();
if (item is null)
return _sender.CreateEmbed()
return CreateEmbed()
.WithPendingColor()
.WithDescription(GetText(strs.no_search_results));
var embed = CreateEmbed(item);
var embed = CreateImageEmbed(item);
return embed;
})
@ -184,7 +185,7 @@ public partial class Searches
//
// var descStr = string.Join("\n\n", desc);
//
// var embed = _sender.CreateEmbed()
// var embed = CreateEmbed()
// .WithAuthor(ctx.User.ToString(),
// "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
// .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)

View file

@ -39,7 +39,7 @@ public partial class Searches : EllieModule<SearchesService>
if (!await ValidateQuery(query))
return;
var embed = _sender.CreateEmbed();
var embed = CreateEmbed();
var data = await _service.GetWeatherDataAsync(query);
if (data is null)
@ -102,7 +102,7 @@ public partial class Searches : EllieModule<SearchesService>
return;
}
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.time_new))
.WithDescription(Format.Code(data.Time.ToString(Culture)))
@ -128,7 +128,7 @@ public partial class Searches : EllieModule<SearchesService>
}
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.WithTitle(movie.Title)
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
@ -161,7 +161,7 @@ public partial class Searches : EllieModule<SearchesService>
private Task InternalRandomImage(SearchesService.ImageTag tag)
{
var url = _service.GetRandomImageUrl(tag);
return Response().Embed(_sender.CreateEmbed().WithOkColor().WithImageUrl(url)).SendAsync();
return Response().Embed(CreateEmbed().WithOkColor().WithImageUrl(url)).SendAsync();
}
[Cmd]
@ -190,7 +190,7 @@ public partial class Searches : EllieModule<SearchesService>
}
await Response()
.Embed(_sender.CreateEmbed()
.Embed(CreateEmbed()
.WithOkColor()
.AddField(GetText(strs.original_url), $"<{query}>")
.AddField(GetText(strs.short_url), $"<{shortLink}>"))
@ -213,7 +213,7 @@ public partial class Searches : EllieModule<SearchesService>
return;
}
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithTitle(card.Name)
.WithDescription(card.Description)
@ -246,7 +246,7 @@ public partial class Searches : EllieModule<SearchesService>
return;
}
var embed = _sender.CreateEmbed().WithOkColor().WithImageUrl(card.Img);
var embed = CreateEmbed().WithOkColor().WithImageUrl(card.Img);
if (!string.IsNullOrWhiteSpace(card.Flavor))
embed.WithDescription(card.Flavor);
@ -280,7 +280,7 @@ public partial class Searches : EllieModule<SearchesService>
.Page((items, _) =>
{
var item = items[0];
return _sender.CreateEmbed()
return CreateEmbed()
.WithOkColor()
.WithUrl(item.Permalink)
.WithTitle(item.Word)
@ -311,7 +311,7 @@ public partial class Searches : EllieModule<SearchesService>
.Page((items, _) =>
{
var model = items.First();
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithDescription(ctx.User.Mention)
.AddField(GetText(strs.word), model.Word, true)
.AddField(GetText(strs._class), model.WordType, true)
@ -374,7 +374,7 @@ public partial class Searches : EllieModule<SearchesService>
}
[Cmd]
public async Task Color(params Color[] colors)
public async Task Color(params Rgba32[] colors)
{
if (!colors.Any())
return;
@ -403,7 +403,7 @@ public partial class Searches : EllieModule<SearchesService>
await Response()
.Embed(
_sender.CreateEmbed()
CreateEmbed()
.WithOkColor()
.AddField("Username", usr.ToString())
.AddField("Avatar Url", avatarUrl)

View file

@ -79,9 +79,9 @@ public partial class Searches
.Page((elements, cur) =>
{
if (elements.Count == 0)
return _sender.CreateEmbed().WithDescription(GetText(strs.streams_none)).WithErrorColor();
return CreateEmbed().WithDescription(GetText(strs.streams_none)).WithErrorColor();
var eb = _sender.CreateEmbed().WithTitle(GetText(strs.streams_follow_title)).WithOkColor();
var eb = CreateEmbed().WithTitle(GetText(strs.streams_follow_title)).WithOkColor();
for (var index = 0; index < elements.Count; index++)
{
var elem = elements[index];

View file

@ -491,7 +491,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
public EmbedBuilder GetEmbed(ulong guildId, StreamData status, bool showViewers = true)
{
var embed = _sender.CreateEmbed()
var embed = _sender.CreateEmbed(guildId)
.WithTitle(status.Name)
.WithUrl(status.StreamUrl)
.WithDescription(status.StreamUrl)

View file

@ -135,7 +135,7 @@ public sealed partial class FlagTranslateService : IReadyExecutor, IEService
var response = await _ts.Translate("", lang, msg.Content).ConfigureAwait(false);
await msg.ReplyAsync(embed: _sender.CreateEmbed()
await msg.ReplyAsync(embed: _sender.CreateEmbed(tc.Guild?.Id)
.WithOkColor()
.WithFooter(user.ToString() ?? reaction.UserId.ToString(),
user.RealAvatarUrl().ToString())

View file

@ -67,7 +67,7 @@ public sealed class TranslateService : ITranslateService, IExecNoCommand, IReady
|| msg.Content.Equals(output, StringComparison.InvariantCultureIgnoreCase))
return;
var embed = _sender.CreateEmbed().WithOkColor();
var embed = _sender.CreateEmbed(guild?.Id).WithOkColor();
if (autoDelete)
{

View file

@ -28,7 +28,7 @@ public partial class Searches
await ctx.Channel.TriggerTypingAsync();
var translation = await _service.Translate(fromLang, toLang, text);
var embed = _sender.CreateEmbed().WithOkColor().AddField(fromLang, text).AddField(toLang, translation);
var embed = CreateEmbed().WithOkColor().AddField(fromLang, text).AddField(toLang, translation);
await Response().Embed(embed).SendAsync();
}
@ -88,7 +88,7 @@ public partial class Searches
{
var langs = _service.GetLanguages().ToList();
var eb = _sender.CreateEmbed()
var eb = CreateEmbed()
.WithTitle(GetText(strs.supported_languages))
.WithOkColor();

View file

@ -25,7 +25,7 @@ public partial class Searches
using var http = _httpFactory.CreateClient();
var res = await http.GetStringAsync($"{XKCD_URL}/info.0.json");
var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithImageUrl(comic.ImageLink)
.WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{XKCD_URL}/{comic.Num}")
@ -60,7 +60,7 @@ public partial class Searches
var res = await http.GetStringAsync($"{XKCD_URL}/{num}/info.0.json");
var comic = JsonConvert.DeserializeObject<XkcdComic>(res);
var embed = _sender.CreateEmbed()
var embed = CreateEmbed()
.WithOkColor()
.WithImageUrl(comic.ImageLink)
.WithAuthor(comic.Title, "https://xkcd.com/s/919f27.ico", $"{XKCD_URL}/{num}")

View file

@ -5,6 +5,46 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
namespace EllieBot.Modules.Searches.Common.StreamNotifications.Providers;
// public sealed class YoutubeProvide : Provider
// {
// private readonly IGoogleApiService _api;
// private readonly IHttpClientFactory _httpFactory;
//
// public override FollowedStream.FType Platform
// => FollowedStream.FType.Youtube;
//
// public YoutubeProvide(IGoogleApiService api, IHttpClientFactory httpFactory)
// {
// _api = api;
// _httpFactory = httpFactory;
// }
//
// public override async Task<bool> IsValidUrl(string url)
// {
// await Task.Yield();
// return url.Contains("youtube.com");
// }
//
// public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
// {
// return default;
// }
//
// public override Task<StreamData?> GetStreamDataAsync(string login)
// {
// var client = _httpFactory.CreateClient();
//
// client.GetAsync()
//
// return default;
// }
//
// public override Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames)
// {
// return default;
// }
// }
//
public sealed class TwitchHelixProvider : Provider
{
private readonly IHttpClientFactory _httpClientFactory;

View file

@ -164,8 +164,8 @@ public sealed class AiAssistantService
funcs.Add(new()
{
Name = cmd,
Desc = commandStrings?.Desc?.Replace("currency", "flowers") ?? string.Empty,
Params = commandStrings?.Params.FirstOrDefault()
Desc = commandStrings.Desc?.Replace("currency", "flowers") ?? string.Empty,
Params = commandStrings.Params.FirstOrDefault()
?.Select(x => new AiCommandParamModel()
{
Desc = x.Value.Desc,
@ -219,6 +219,9 @@ public sealed class AiAssistantService
ITextChannel channel,
string query)
{
if (guild is not SocketGuild sg)
return false;
// check permissions
var pcResult = await _permChecker.CheckPermsAsync(
guild,
@ -239,9 +242,6 @@ public sealed class AiAssistantService
{
if (model.Name == ".ai_chat")
{
if (guild is not SocketGuild sg)
return false;
var sess = _cbs.GetOrCreateSession(guild.Id);
if (sess is null)
return false;
@ -253,7 +253,7 @@ public sealed class AiAssistantService
var commandString = GetCommandString(model);
var msgTask = _sender.Response(channel)
.Embed(_sender.CreateEmbed()
.Embed(_sender.CreateEmbed(guild?.Id)
.WithOkColor()
.WithAuthor(msg.Author.GlobalName,
msg.Author.RealAvatarUrl().ToString())
@ -261,8 +261,7 @@ public sealed class AiAssistantService
.SendAsync();
await _cmdHandler.TryRunCommand(
(SocketGuild)guild,
await _cmdHandler.TryRunCommand(sg,
(ISocketMessageChannel)channel,
new DoAsUserMessage((SocketUserMessage)msg, msg.Author, commandString));

View file

@ -8,7 +8,7 @@ public sealed class AiCommandModel
public required string Name { get; set; }
[JsonPropertyName("desc")]
public required string Desc { get; set; }
public required string? Desc { get; set; }
[JsonPropertyName("params")]
public required IReadOnlyList<AiCommandParamModel> Params { get; set; }

Some files were not shown because too many files have changed in this diff Show more