sar rework, improved

This commit is contained in:
Toastie 2024-11-27 22:38:06 +13:00
parent af71e88985
commit 3532554a13
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
30 changed files with 8417 additions and 569 deletions

View file

@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Administration.Services;
// ReSharper disable UnusedAutoPropertyAccessor.Global // ReSharper disable UnusedAutoPropertyAccessor.Global
@ -14,7 +15,6 @@ public abstract class EllieContext : DbContext
public DbSet<Quote> Quotes { get; set; } public DbSet<Quote> Quotes { get; set; }
public DbSet<Reminder> Reminders { get; set; } public DbSet<Reminder> Reminders { get; set; }
public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
public DbSet<MusicPlaylist> MusicPlaylists { get; set; } public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
public DbSet<EllieExpression> Expressions { get; set; } public DbSet<EllieExpression> Expressions { get; set; }
public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; } public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
@ -74,6 +74,34 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#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 #region Rakeback
modelBuilder.Entity<Rakeback>() modelBuilder.Entity<Rakeback>()
@ -84,16 +112,23 @@ public abstract class EllieContext : DbContext
#region UserBetStats #region UserBetStats
modelBuilder.Entity<UserBetStats>() modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new { x.UserId, x.Game }) .HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique(); .IsUnique();
#endregion #endregion
#region Flag Translate #region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>() modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new { x.GuildId, x.ChannelId }) .HasIndex(x => new
{
x.GuildId,
x.ChannelId
})
.IsUnique(); .IsUnique();
#endregion #endregion
@ -286,11 +321,6 @@ public abstract class EllieContext : DbContext
.HasForeignKey(x => x.GuildConfigId) .HasForeignKey(x => x.GuildConfigId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<GuildConfig>()
.HasMany(x => x.SelfAssignableRoleGroupNames)
.WithOne()
.OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<FeedSub>() modelBuilder.Entity<FeedSub>()
.HasAlternateKey(x => new .HasAlternateKey(x => new
{ {
@ -319,21 +349,6 @@ public abstract class EllieContext : DbContext
#endregion #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 #region MusicPlaylists
var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>(); var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>();
@ -401,10 +416,10 @@ public abstract class EllieContext : DbContext
var xps = modelBuilder.Entity<UserXpStats>(); var xps = modelBuilder.Entity<UserXpStats>();
xps.HasIndex(x => new xps.HasIndex(x => new
{ {
x.UserId, x.UserId,
x.GuildId x.GuildId
}) })
.IsUnique(); .IsUnique();
xps.HasIndex(x => x.UserId); xps.HasIndex(x => x.UserId);
@ -450,9 +465,9 @@ public abstract class EllieContext : DbContext
.OnDelete(DeleteBehavior.SetNull); .OnDelete(DeleteBehavior.SetNull);
ci.HasIndex(x => new ci.HasIndex(x => new
{ {
x.Name x.Name
}) })
.IsUnique(); .IsUnique();
#endregion #endregion
@ -516,23 +531,6 @@ public abstract class EllieContext : DbContext
#endregion #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 #region BanTemplate
modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique(); modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();
@ -571,10 +569,10 @@ public abstract class EllieContext : DbContext
.IsUnique(false); .IsUnique(false);
rr2.HasIndex(x => new rr2.HasIndex(x => new
{ {
x.MessageId, x.MessageId,
x.Emote x.Emote
}) })
.IsUnique(); .IsUnique();
}); });
@ -649,11 +647,11 @@ public abstract class EllieContext : DbContext
{ {
// user can own only one of each item // user can own only one of each item
x.HasIndex(model => new x.HasIndex(model => new
{ {
model.UserId, model.UserId,
model.ItemType, model.ItemType,
model.ItemKey model.ItemKey
}) })
.IsUnique(); .IsUnique();
}); });
@ -678,10 +676,10 @@ public abstract class EllieContext : DbContext
#region Sticky Roles #region Sticky Roles
modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new
{ {
x.GuildId, x.GuildId,
x.UserId x.UserId
}) })
.IsUnique()); .IsUnique());
#endregion #endregion
@ -726,10 +724,10 @@ public abstract class EllieContext : DbContext
modelBuilder modelBuilder
.Entity<GreetSettings>(gs => gs.HasIndex(x => new .Entity<GreetSettings>(gs => gs.HasIndex(x => new
{ {
x.GuildId, x.GuildId,
x.GreetType x.GreetType
}) })
.IsUnique()); .IsUnique());
modelBuilder.Entity<GreetSettings>(gs => modelBuilder.Entity<GreetSettings>(gs =>

View file

@ -1,5 +1,4 @@
#nullable disable using LinqToDB;
using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -8,6 +7,9 @@ namespace EllieBot.Db;
public static class UserXpExtensions 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) public static UserXpStats GetOrCreateUserXpStats(this DbContext ctx, ulong guildId, ulong userId)
{ {
var usr = ctx.Set<UserXpStats>().FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId); 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, Picarto = 3,
Youtube = 4, Youtube = 4,
Facebook = 5, Facebook = 5,
Trovo = 6 Trovo = 6,
Kick = 7,
} }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }

View file

@ -1,11 +1,18 @@
#nullable disable using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models; namespace EllieBot.Db.Models;
public class GroupName : DbEntity public sealed class SarGroup : DbEntity
{ {
public int GuildConfigId { get; set; } [Key]
public GuildConfig GuildConfig { get; set; } public int Id { get; set; }
public int Number { get; set; } public int GroupNumber { get; set; }
public string Name { 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

@ -31,10 +31,11 @@ public class GuildConfig : DbEntity
// public bool SendBoostMessage { get; set; } // public bool SendBoostMessage { get; set; }
// pulic int BoostMessageDeleteAfter { get; set; } // pulic int BoostMessageDeleteAfter { get; set; }
//self assignable roles //todo FUTURE: DELETE, UNUSED
public bool ExclusiveSelfAssignedRoles { get; set; } public bool ExclusiveSelfAssignedRoles { get; set; }
public bool AutoDeleteSelfAssignedRoleMessages { get; set; } public bool AutoDeleteSelfAssignedRoleMessages { get; set; }
//stream notifications //stream notifications
public HashSet<FollowedStream> FollowedStreams { get; set; } = new(); public HashSet<FollowedStream> FollowedStreams { get; set; } = new();
@ -91,7 +92,6 @@ public class GuildConfig : DbEntity
public List<FeedSub> FeedSubs { get; set; } = new(); public List<FeedSub> FeedSubs { get; set; } = new();
public bool NotifyStreamOffline { get; set; } public bool NotifyStreamOffline { get; set; }
public bool DeleteStreamOnlineMessage { get; set; } public bool DeleteStreamOnlineMessage { get; set; }
public List<GroupName> SelfAssignableRoleGroupNames { get; set; }
public int WarnExpireHours { get; set; } public int WarnExpireHours { get; set; }
public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear; public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear;

View file

@ -1,11 +1,24 @@
#nullable disable using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models; 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 GuildId { get; set; }
public ulong RoleId { get; set; } public ulong RoleId { get; set; }
public int Group { get; set; } public int SarGroupId { get; set; }
public int LevelRequirement { 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> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.20</Version> <Version>5.2.0</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View file

@ -5,6 +5,37 @@ namespace EllieBot.Migrations;
public static class MigrationQueries 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) public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' || Username WHERE Discriminator = '????';"); migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' || Username WHERE Discriminator = '????';");

View file

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

View file

@ -1253,41 +1253,6 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("giveawayuser", (string)null); b.ToTable("giveawayuser", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.GroupName", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<int>("GuildConfigId")
.HasColumnType("integer")
.HasColumnName("guildconfigid");
b.Property<string>("Name")
.HasColumnType("text")
.HasColumnName("name");
b.Property<int>("Number")
.HasColumnType("integer")
.HasColumnName("number");
b.HasKey("Id")
.HasName("pk_groupname");
b.HasIndex("GuildConfigId", "Number")
.IsUnique()
.HasDatabaseName("ix_groupname_guildconfigid_number");
b.ToTable("groupname", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.GuildConfig", b => modelBuilder.Entity("EllieBot.Db.Models.GuildConfig", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -2200,7 +2165,71 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("rotatingstatus", (string)null); 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") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -2213,32 +2242,34 @@ namespace EllieBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone") .HasColumnType("timestamp without time zone")
.HasColumnName("dateadded"); .HasColumnName("dateadded");
b.Property<int>("Group") b.Property<int>("GroupNumber")
.ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasDefaultValue(0) .HasColumnName("groupnumber");
.HasColumnName("group");
b.Property<decimal>("GuildId") b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("guildid"); .HasColumnName("guildid");
b.Property<int>("LevelRequirement") b.Property<bool>("IsExclusive")
.HasColumnType("integer") .HasColumnType("boolean")
.HasColumnName("levelrequirement"); .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)") .HasColumnType("numeric(20,0)")
.HasColumnName("roleid"); .HasColumnName("rolereq");
b.HasKey("Id") b.HasKey("Id")
.HasName("pk_selfassignableroles"); .HasName("pk_sargroup");
b.HasIndex("GuildId", "RoleId") b.HasAlternateKey("GuildId", "GroupNumber")
.IsUnique() .HasName("ak_sargroup_guildid_groupnumber");
.HasDatabaseName("ix_selfassignableroles_guildid_roleid");
b.ToTable("selfassignableroles", (string)null); b.ToTable("sargroup", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b => modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
@ -3527,18 +3558,6 @@ namespace EllieBot.Migrations.PostgreSql
.HasConstraintName("fk_giveawayuser_giveawaymodel_giveawayid"); .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 => modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{ {
b.HasOne("EllieBot.Db.Models.LogSetting", "LogSetting") b.HasOne("EllieBot.Db.Models.LogSetting", "LogSetting")
@ -3578,6 +3597,16 @@ namespace EllieBot.Migrations.PostgreSql
.HasConstraintName("fk_playlistsong_musicplaylists_musicplaylistid"); .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 => modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{ {
b.HasOne("EllieBot.Db.Models.GuildConfig", null) b.HasOne("EllieBot.Db.Models.GuildConfig", null)
@ -3854,8 +3883,6 @@ namespace EllieBot.Migrations.PostgreSql
b.Navigation("Permissions"); b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries"); b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles"); b.Navigation("SlowmodeIgnoredRoles");
@ -3885,6 +3912,11 @@ namespace EllieBot.Migrations.PostgreSql
b.Navigation("Songs"); b.Navigation("Songs");
}); });
modelBuilder.Entity("EllieBot.Db.Models.SarGroup", b =>
{
b.Navigation("Roles");
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b => modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{ {
b.Navigation("Items"); b.Navigation("Items");

View file

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

View file

@ -932,32 +932,6 @@ namespace EllieBot.Migrations
b.ToTable("GiveawayUser"); b.ToTable("GiveawayUser");
}); });
modelBuilder.Entity("EllieBot.Db.Models.GroupName", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<int>("GuildConfigId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("Number")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildConfigId", "Number")
.IsUnique();
b.ToTable("GroupName");
});
modelBuilder.Entity("EllieBot.Db.Models.GuildConfig", b => modelBuilder.Entity("EllieBot.Db.Models.GuildConfig", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1640,7 +1614,54 @@ namespace EllieBot.Migrations
b.ToTable("RotatingStatus"); 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") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@ -1649,26 +1670,27 @@ namespace EllieBot.Migrations
b.Property<DateTime?>("DateAdded") b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("Group") b.Property<int>("GroupNumber")
.ValueGeneratedOnAdd() .HasColumnType("INTEGER");
.HasColumnType("INTEGER")
.HasDefaultValue(0);
b.Property<ulong>("GuildId") b.Property<ulong>("GuildId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("LevelRequirement") b.Property<bool>("IsExclusive")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<ulong>("RoleId") b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<ulong?>("RoleReq")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("GuildId", "RoleId") b.HasAlternateKey("GuildId", "GroupNumber");
.IsUnique();
b.ToTable("SelfAssignableRoles"); b.ToTable("SarGroup");
}); });
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b => modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
@ -2660,17 +2682,6 @@ namespace EllieBot.Migrations
.IsRequired(); .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 => modelBuilder.Entity("EllieBot.Db.Models.IgnoredLogItem", b =>
{ {
b.HasOne("EllieBot.Db.Models.LogSetting", "LogSetting") b.HasOne("EllieBot.Db.Models.LogSetting", "LogSetting")
@ -2706,6 +2717,15 @@ namespace EllieBot.Migrations
.OnDelete(DeleteBehavior.Cascade); .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 => modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{ {
b.HasOne("EllieBot.Db.Models.GuildConfig", null) b.HasOne("EllieBot.Db.Models.GuildConfig", null)
@ -2960,8 +2980,6 @@ namespace EllieBot.Migrations
b.Navigation("Permissions"); b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries"); b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles"); b.Navigation("SlowmodeIgnoredRoles");
@ -2991,6 +3009,11 @@ namespace EllieBot.Migrations
b.Navigation("Songs"); b.Navigation("Songs");
}); });
modelBuilder.Entity("EllieBot.Db.Models.SarGroup", b =>
{
b.Navigation("Roles");
});
modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b => modelBuilder.Entity("EllieBot.Db.Models.ShopEntry", b =>
{ {
b.Navigation("Items"); b.Navigation("Items");

View file

@ -75,7 +75,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
Log.Information("Leaving {RemainingCount} guilds, 1 every second. {DontDeleteCount} will remain", Log.Information("Leaving {RemainingCount} guilds, 1 every second. {DontDeleteCount} will remain",
allGuildIds.Length - dontDelete.Count, allGuildIds.Length - dontDelete.Count,
dontDelete.Count); dontDelete.Count);
foreach (var guildId in allGuildIds) foreach (var guildId in allGuildIds)
{ {
if (dontDelete.Contains(guildId)) if (dontDelete.Contains(guildId))
@ -136,7 +136,7 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
await using var linqCtx = ctx.CreateLinqToDBContext(); await using var linqCtx = ctx.CreateLinqToDBContext();
await using var tempTable = linqCtx.CreateTempTable<CleanupId>(); 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() await tempTable.BulkCopyAsync(chunk.Select(x => new CleanupId()
{ {
@ -187,13 +187,6 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .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 // delete perm overrides
await ctx.GetTable<DiscordPermOverride>() await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null .Where(x => x.GuildId != null
@ -219,6 +212,48 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .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();
return new() return new()
{ {
GuildCount = guildIds.Keys.Count, GuildCount = guildIds.Keys.Count,

View file

@ -6,16 +6,155 @@ namespace EllieBot.Modules.Administration;
public partial class 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> public partial class SelfAssignedRolesCommands : EllieModule<SelfAssignedRolesService>
{ {
private readonly SarAssignerService _sas;
public SelfAssignedRolesCommands(SarAssignerService sas)
{
_sas = sas;
}
protected async Task<bool> CheckRoleHierarchy(IRole role)
{
var botUser = ((SocketGuild)ctx.Guild).CurrentUser;
var ownerId = ctx.Guild.OwnerId;
var modMaxRole = ((IGuildUser)ctx.User).GetRoles().Max(r => r.Position);
var botMaxRole = botUser.GetRoles().Max(r => r.Position);
// role must be lower than the bot role
// and the mod must have a higher role
if (botMaxRole <= role.Position
|| (ctx.User.Id != ownerId && role.Position >= modMaxRole))
{
await Response().Error(strs.hierarchy).SendAsync();
return false;
}
return true;
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[BotPerm(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) if (newVal)
await Response().Confirm(strs.adsarm_enable(prefix)).SendAsync(); await Response().Confirm(strs.adsarm_enable(prefix)).SendAsync();
@ -28,30 +167,24 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(1)] [Priority(1)]
public Task Asar([Leftover] IRole role) public Task SarAdd([Leftover] IRole role)
=> Asar(0, role); => SarAdd(0, role);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(0)] [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 (!await CheckRoleHierarchy(role))
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return; 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())))
await Response() .SendAsync();
.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] [Cmd]
@ -59,9 +192,9 @@ public partial class Administration
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
[Priority(0)] [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) if (set)
{ {
@ -70,19 +203,19 @@ public partial class Administration
.SendAsync(); .SendAsync();
} }
else else
{
await Response().Confirm(strs.group_name_removed(Format.Bold(group.ToString()))).SendAsync(); await Response().Confirm(strs.group_name_removed(Format.Bold(group.ToString()))).SendAsync();
}
} }
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
public async Task Rsar([Leftover] IRole role) public async Task SarRemove([Leftover] IRole role)
{ {
var guser = (IGuildUser)ctx.User; 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) if (!success)
await Response().Error(strs.self_assign_not).SendAsync(); await Response().Error(strs.self_assign_not).SendAsync();
else else
@ -91,59 +224,81 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Lsar(int page = 1) public async Task SarList(int page = 1)
{ {
if (--page < 0) if (--page < 0)
return; 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() await Response()
.Paginated() .Paginated()
.Items(roles.OrderBy(x => x.Model.Group).ToList()) .Items(groups.SelectMany(x => x.Roles).ToList())
.PageSize(20) .PageSize(20)
.CurrentPage(page) .CurrentPage(page)
.Page((items, _) => .Page(async (items, _) =>
{ {
var rolesStr = new StringBuilder();
var roleGroups = items var roleGroups = items
.GroupBy(x => x.Model.Group) .GroupBy(x => x.SarGroupId)
.OrderBy(x => x.Key); .OrderBy(x => x.Key);
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.self_assign_list(groups.Sum(x => x.Roles.Count))));
foreach (var kvp in roleGroups) foreach (var kvp in roleGroups)
{ {
string groupNameText; var group = gDict[kvp.Key];
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)}");
rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫"); var groupNameText = "";
foreach (var (model, role) in kvp.AsEnumerable())
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 else
{ {
// first character is invisible space rolesStr.AppendLine(
if (model.LevelRequirement == 0) Format.Italics(GetText(strs.requires_role(Format.Bold(rr.Name)))));
rolesStr.AppendLine(" " + role.Name);
else
rolesStr.AppendLine(" " + role.Name + $" (lvl {model.LevelRequirement}+)");
} }
} }
rolesStr.AppendLine(); 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();
}
eb.AddField(groupNameText, rolesStr, false);
} }
return _sender.CreateEmbed() return eb;
.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));
}) })
.SendAsync(); .SendAsync();
} }
@ -152,9 +307,9 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)] [BotPerm(GuildPerm.ManageRoles)]
public async Task Togglexclsar() public async Task SarExclusive(int groupNumber)
{ {
var areExclusive = _service.ToggleEsar(ctx.Guild.Id); var areExclusive = await _service.SetGroupExclusivityAsync(ctx.Guild.Id, groupNumber);
if (areExclusive) if (areExclusive)
await Response().Confirm(strs.self_assign_excl).SendAsync(); await Response().Confirm(strs.self_assign_excl).SendAsync();
else else
@ -165,12 +320,12 @@ public partial class Administration
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)] [UserPerm(GuildPerm.ManageRoles)]
[BotPerm(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) if (level < 0)
return; return;
var succ = _service.SetLevelReq(ctx.Guild.Id, role, level); var succ = await _service.SetRoleLevelReq(ctx.Guild.Id, role.Id, level);
if (!succ) if (!succ)
{ {
@ -186,54 +341,35 @@ public partial class Administration
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [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); if (!succ)
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)
{ {
msg.DeleteAfter(3); await Response().Error(strs.sar_group_not_found).SendAsync();
ctx.Message.DeleteAfter(3); return;
} }
await Response()
.Confirm(strs.self_assign_group_role_req(
Format.Bold(groupNumber.ToString()),
Format.Bold(role.Name)))
.SendAsync();
} }
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [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 succ = await _service.DeleteRoleGroup(ctx.Guild.Id, groupNumber);
if (succ)
var (result, autoDelete) = await _service.Remove(guildUser, role); await Response().Confirm(strs.sar_group_deleted(Format.Bold(groupNumber.ToString()))).SendAsync();
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();
else else
msg = await Response().Confirm(strs.self_assign_remove(Format.Bold(role.Name))).SendAsync(); await Response().Error(strs.sar_group_not_found).SendAsync();
if (autoDelete)
{
msg.DeleteAfter(3);
ctx.Message.DeleteAfter(3);
}
} }
} }
} }

View file

@ -1,233 +1,327 @@
#nullable disable using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; 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; namespace EllieBot.Modules.Administration.Services;
public class SelfAssignedRolesService : IEService public class SelfAssignedRolesService : IEService, IReadyExecutor
{ {
public enum AssignResult private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCreds _creds;
private ConcurrentHashSet<ulong> _sarAds = new();
public SelfAssignedRolesService(DbService db, DiscordSocketClient client, IBotCreds creds)
{ {
Assigned, // successfully removed _db = db;
ErrNotAssignable, // not assignable (error) _client = client;
ErrAlreadyHave, // you already have that role (error) _creds = creds;
ErrNotPerms, // bot doesn't have perms (error)
ErrLvlReq // you are not required level (error)
} }
public enum RemoveResult public async Task AddAsync(ulong guildId, ulong roleId, int groupNumber)
{ {
Removed, // successfully removed await using var ctx = _db.GetDbContext();
ErrNotAssignable, // not assignable (error)
ErrNotHave, // you don't have a role you want to remove (error) await ctx.GetTable<SarGroup>()
ErrNotPerms // bot doesn't have perms (error) .InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
GroupNumber = groupNumber,
IsExclusive = false
},
_ => new()
{
},
() => new()
{
GuildId = guildId,
GroupNumber = groupNumber
});
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)
{
// todo group not found
return false;
}
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.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 GetAutoDelete(ulong guildId)
=> _sarAds.Contains(guildId);
public async Task OnReadyAsync()
{
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 DbService _db;
public SelfAssignedRolesService(DbService db) private readonly Channel<SarAssignerDataItem> _channel =
=> _db = db; Channel.CreateBounded<SarAssignerDataItem>(100);
public bool AddNew(ulong guildId, IRole role, int group)
public SarAssignerService(XpService xp, DbService db)
{ {
using var uow = _db.GetDbContext(); _xp = xp;
var roles = uow.Set<SelfAssignedRole>().GetFromGuild(guildId); _db = db;
if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id))
return false;
uow.Set<SelfAssignedRole>().Add(new()
{
Group = group,
RoleId = role.Id,
GuildId = role.Guild.Id
});
uow.SaveChanges();
return true;
} }
public bool ToggleAdSarm(ulong guildId) public async Task OnReadyAsync()
{ {
bool newval; var reader = _channel.Reader;
using var uow = _db.GetDbContext(); while (true)
var config = uow.GuildConfigsForId(guildId, set => set);
newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages;
uow.SaveChanges();
return newval;
}
public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
{
LevelStats userLevelData;
await using (var uow = _db.GetDbContext())
{ {
var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id); var item = await reader.ReadAsync();
userLevelData = new(stats.Xp + stats.AwardedXp);
}
var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id); try
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); var sar = item.Group.Roles.First(x => x.RoleId == item.RoleId);
if (sameRole is not null)
if (item.User.RoleIds.Contains(item.RoleId))
{ {
try item.CompletionTask.TrySetResult(new SarAlreadyHasRole());
{ continue;
await guildUser.RemoveRoleAsync(sameRole);
await Task.Delay(300);
}
catch
{
// ignored
}
} }
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)
{
Log.Error(ex, "Unknown error ocurred in SAR runner: {Error}", ex.Message);
item.CompletionTask.TrySetResult(new Error());
} }
} }
try
{
await guildUser.AddRoleAsync(role);
}
catch (Exception ex)
{
return (AssignResult.ErrNotPerms, autoDelete, ex);
}
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 _channel.Writer.WriteAsync(item);
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 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) public sealed class SarAssignerDataItem
return (RemoveResult.ErrNotAssignable, autoDelete); {
if (!guildUser.RoleIds.Contains(role.Id)) public required SarGroup Group { get; init; }
return (RemoveResult.ErrNotHave, autoDelete); public required IGuildUser User { get; init; }
try public required ulong RoleId { get; init; }
{ public required TaskCompletionSource<SarAssignResult> CompletionTask { get; init; }
await guildUser.RemoveRoleAsync(role); }
}
catch (Exception)
{
return (RemoveResult.ErrNotPerms, autoDelete);
}
return (RemoveResult.Removed, autoDelete); [GenerateOneOf]
} public sealed partial class SarAssignResult
: OneOfBase<Success, Error, SarLevelRequirement, SarRoleRequirement, SarAlreadyHasRole, SarInsuffPerms>
{
}
public bool RemoveSar(ulong guildId, ulong roleId) public record class SarLevelRequirement(int Level);
{
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) public record class SarRoleRequirement(ulong RoleId);
{
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 record class SarAlreadyHasRole();
}
public bool SetLevelReq(ulong guildId, IRole role, int level) public record class SarInsuffPerms();
{
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);
}
}

View file

@ -23,27 +23,6 @@ public partial class Administration
_mute = mute; _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] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]

View file

@ -6,9 +6,9 @@ namespace EllieBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable 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 MAX_MULTIPLIER = 0.94;
public const double MULTI_PER_USER = 0.01; public const double MULTI_PER_USER = 0.005;
public enum Phase public enum Phase
{ {

View file

@ -154,16 +154,16 @@ public partial class Gambling : GamblingModule<GamblingService>
{ {
var password = _service.GeneratePassword(); var password = _service.GeneratePassword();
var img = new Image<Rgba32>(70, 35); var img = new Image<Rgba32>(60, 30);
var font = _fonts.NotoSans.CreateFont(30); var font = _fonts.NotoSans.CreateFont(25);
var outlinePen = new SolidPen(Color.Black, 1f); var outlinePen = new SolidPen(Color.Black, 1f);
var strikeoutRun = new RichTextRun var strikeoutRun = new RichTextRun
{ {
Start = 0, Start = 0,
End = password.GetGraphemeCount(), End = password.GetGraphemeCount(),
Font = font, Font = font,
StrikeoutPen = new SolidPen(Color.White, 3), StrikeoutPen = new SolidPen(Color.White, 4),
TextDecorations = TextDecorations.Strikeout TextDecorations = TextDecorations.Strikeout
}; };
// draw password on the image // draw password on the image

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 = _sender.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 = _sender.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

@ -5,6 +5,48 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
namespace EllieBot.Modules.Searches.Common.StreamNotifications.Providers; 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();
// todo implement
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 public sealed class TwitchHelixProvider : Provider
{ {
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
@ -94,9 +136,9 @@ public sealed class TwitchHelixProvider : Provider
var loginsSet = logins.Select(x => x.ToLowerInvariant()) var loginsSet = logins.Select(x => x.ToLowerInvariant())
.Distinct() .Distinct()
.ToHashSet(); .ToHashSet();
var dataDict = new Dictionary<string, StreamData>(); var dataDict = new Dictionary<string, StreamData>();
foreach (var chunk in logins.Chunk(100)) foreach (var chunk in logins.Chunk(100))
{ {
try try
@ -125,13 +167,13 @@ public sealed class TwitchHelixProvider : Provider
return []; return [];
} }
} }
// any item left over loginsSet is an invalid username // any item left over loginsSet is an invalid username
foreach (var login in loginsSet) foreach (var login in loginsSet)
{ {
_failingStreams.TryAdd(login, DateTime.UtcNow); _failingStreams.TryAdd(login, DateTime.UtcNow);
} }
// only get streams for users which exist // only get streams for users which exist
foreach (var chunk in dataDict.Keys.Chunk(100)) foreach (var chunk in dataDict.Keys.Chunk(100))
{ {
@ -178,7 +220,7 @@ public sealed class TwitchHelixProvider : Provider
StreamType = FollowedStream.FType.Twitch, StreamType = FollowedStream.FType.Twitch,
Preview = user.OfflineImageUrl Preview = user.OfflineImageUrl
}; };
private StreamData FillStreamData(StreamData partial, HelixStreamsResponse.StreamData apiData) private StreamData FillStreamData(StreamData partial, HelixStreamsResponse.StreamData apiData)
=> partial with => partial with
{ {

View file

@ -1,7 +1,9 @@
using EllieBot.Db.Models;
namespace Ellie.Common; namespace Ellie.Common;
public static class Extensions public static class Extensions
{ {
public static long ToTimestamp(this in DateTime value) public static long ToTimestamp(this in DateTime value)
=> (value.Ticks - 621355968000000000) / 10000000; => (value.Ticks 621355968000000000) / 10000000;
} }

View file

@ -100,6 +100,27 @@ public abstract class EllieModule : ModuleBase
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
protected 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;
}
} }
public abstract class EllieModule<TService> : EllieModule public abstract class EllieModule<TService> : EllieModule

View file

@ -81,17 +81,43 @@ vcrole:
- vcrole - vcrole
vcrolerm: vcrolerm:
- vcrolerm - vcrolerm
asar: saradd:
- add
- a
- asar - asar
rsar: sarremove:
- remove
- rm
- rem
- rsar - rsar
lsar: sarlist:
- list
- l
- ls
- lsar - lsar
sargn: sarautodelete:
- ad
- autodel
- adsarm
sargroupname:
- groupname
- gn
- sargn - sargn
togglexclsar: sargrouprolereq:
- togglexclsar - grouprolereq
- grr
sargroupdelete:
- groupdelete
- gd
- gdel
sarexclusive:
- exclusive
- ex
- excl
- tesar - tesar
sarrolelevelreq:
- rolelvlreq
- rlr
iam: iam:
- iam - iam
iamnot: iamnot:
@ -772,8 +798,6 @@ voicemute:
muterole: muterole:
- muterole - muterole
- setmuterole - setmuterole
adsarm:
- adsarm
setstream: setstream:
- setstream - setstream
chatunmute: chatunmute:
@ -1184,9 +1208,6 @@ timelyreset:
crypto: crypto:
- crypto - crypto
- c - c
rolelevelreq:
- rolelevelreq
- rlr
massban: massban:
- massban - massban
masskill: masskill:

View file

@ -28,7 +28,7 @@ betRoll:
multiplyBy: 10 multiplyBy: 10
- whenAbove: 90 - whenAbove: 90
multiplyBy: 4 multiplyBy: 4
- whenAbove: 65 - whenAbove: 64
multiplyBy: 2 multiplyBy: 2
# Automatic currency generation settings. # Automatic currency generation settings.
generation: generation:

View file

@ -323,7 +323,7 @@ vcrolerm:
params: params:
- vcId: - vcId:
desc: "The voice channel ID to remove the vcrole from." desc: "The voice channel ID to remove the vcrole from."
asar: saradd:
desc: Adds a role to the list of self-assignable roles. You can also specify a group. If 'Exclusive self-assignable roles' feature is enabled (`{0}tesar`), users will be able to pick one role per group. desc: Adds a role to the list of self-assignable roles. You can also specify a group. If 'Exclusive self-assignable roles' feature is enabled (`{0}tesar`), users will be able to pick one role per group.
ex: ex:
- Gamer - Gamer
@ -336,7 +336,7 @@ asar:
desc: "The ID of a group that the designated role should belong to." desc: "The ID of a group that the designated role should belong to."
role: role:
desc: "The role that can be assigned by the user." desc: "The role that can be assigned by the user."
rsar: sarremove:
desc: Removes a specified role from the list of self-assignable roles. desc: Removes a specified role from the list of self-assignable roles.
ex: ex:
- Gamer - Gamer
@ -345,7 +345,7 @@ rsar:
params: params:
- role: - role:
desc: "The role to remove from the list of self-assignable roles." desc: "The role to remove from the list of self-assignable roles."
lsar: sarlist:
desc: Lists self-assignable roles. Shows 20 roles per page. desc: Lists self-assignable roles. Shows 20 roles per page.
ex: ex:
- '' - ''
@ -353,22 +353,53 @@ lsar:
params: params:
- page: - page:
desc: "The page number to show." desc: "The page number to show."
sargn: sarautodelete:
desc: Toggles the automatic deletion of the user's message and Nadeko's confirmations for `{0}iam` and `{0}iamn` commands.
ex:
- ''
params:
- { }
sargroupname:
desc: Sets a self assignable role group name. Provide no name to remove. desc: Sets a self assignable role group name. Provide no name to remove.
ex: ex:
- 1 Faction - 1 Faction
- 2 - 2
params: params:
- group: - group:
desc: "The ID of the group to name." desc: "The group number"
name: name:
desc: "The name to assign." desc: "The name to assign."
togglexclsar: sargroupdelete:
desc: "Deletes a self-assignable role group"
ex:
- 0
params:
- group:
desc: "The number of the group to delete."
sarexclusive:
desc: Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group. desc: Toggles whether self-assigned roles are exclusive. While enabled, users can only have one self-assignable role per group.
ex: ex:
- '' - ''
params: params:
- { } - { }
sarrolelevelreq:
desc: Set a level requirement on a self-assignable role.
ex:
- 5 SomeRole
params:
- level:
desc: "The minimum level required for the role."
role:
desc: "The role that must be assigned before the user can assign this one."
sargrouprolereq:
desc: Set a role that users have to have in order to assign a self-assignable role from the specified group.
ex:
- 1 SomeRole
params:
- group:
desc: "The group number."
role:
desc: "The role that is required."
iam: iam:
desc: Adds a role to you that you choose. Role must be on the list of self-assignable roles. desc: Adds a role to you that you choose. Role must be on the list of self-assignable roles.
ex: ex:
@ -2464,12 +2495,6 @@ muterole:
params: params:
- role: - role:
desc: "The role that determines whether users are muted or not." desc: "The role that determines whether users are muted or not."
adsarm:
desc: Toggles the automatic deletion of the user's message and Ellie's confirmations for `{0}iam` and `{0}iamn` commands.
ex:
- ''
params:
- { }
setstream: setstream:
desc: Sets the bots stream. First parameter is the twitch link, second parameter is stream name. desc: Sets the bots stream. First parameter is the twitch link, second parameter is stream name.
ex: ex:
@ -3356,7 +3381,7 @@ globalcommand:
globalmodule: globalmodule:
desc: Toggles whether a module can be used on any server. desc: Toggles whether a module can be used on any server.
ex: ex:
- 'Gambling' - 'Gambling'
params: params:
- module: - module:
desc: "The type of module or configuration information being toggled." desc: "The type of module or configuration information being toggled."
@ -3919,15 +3944,6 @@ stock:
params: params:
- query: - query:
desc: "The ticker symbol or company name used to retrieve the stock's information." desc: "The ticker symbol or company name used to retrieve the stock's information."
rolelevelreq:
desc: Set a level requirement on a self-assignable role.
ex:
- 5 SomeRole
params:
- level:
desc: "The minimum level required for the role."
role:
desc: "The role that must be assigned before the user can assign this one."
massban: massban:
desc: Bans multiple users at once. Specify a space separated list of IDs of users who you wish to ban. desc: Bans multiple users at once. Specify a space separated list of IDs of users who you wish to ban.
ex: ex:
@ -4676,7 +4692,7 @@ betstats:
- '@someone lula' - '@someone lula'
- 'bd' - 'bd'
params: params:
- {} - { }
- user: - user:
desc: 'The user for who to show the betstats for.' desc: 'The user for who to show the betstats for.'
- user: - user:
@ -4691,10 +4707,10 @@ rakeback:
Rakeback is accumulated by betting (not by winning or losing). Rakeback is accumulated by betting (not by winning or losing).
Default rakeback is 0.05 * house edge Default rakeback is 0.05 * house edge
House edge is defined per game House edge is defined per game
ex: ex:
- '' - ''
params: params:
- {} - { }
snipe: snipe:
desc: |- desc: |-
Snipe the message you replied to with this command. Snipe the message you replied to with this command.

View file

@ -896,9 +896,6 @@
"rank": "Rank", "rank": "Rank",
"template_reloaded": "Xp template has been reloaded.", "template_reloaded": "Xp template has been reloaded.",
"expr_edited": "Expression Edited", "expr_edited": "Expression Edited",
"self_assign_are_exclusive": "You can only choose 1 role from each group.",
"self_assign_are_not_exclusive": "You can choose any number of roles from any group.",
"self_assign_group": "Group {0}",
"new_reaction_event": "Add {0} reaction to this message to get {1}\n{2} left to be awarded.", "new_reaction_event": "Add {0} reaction to this message to get {1}\n{2} left to be awarded.",
"new_gamestatus_event": "Type the secret code in any channel to receive {1}\n{2} left to be awarded.", "new_gamestatus_event": "Type the secret code in any channel to receive {1}\n{2} left to be awarded.",
"event_duration_footer": "This event is active for up to {0} hours.", "event_duration_footer": "This event is active for up to {0} hours.",
@ -947,6 +944,7 @@
"did_you_mean": "Did you mean {0}?", "did_you_mean": "Did you mean {0}?",
"self_assign_level_req": "Self assignable role {0} now requires at least server level {1}.", "self_assign_level_req": "Self assignable role {0} now requires at least server level {1}.",
"self_assign_not_level": "That self-assignable role requires at least server level {0}.", "self_assign_not_level": "That self-assignable role requires at least server level {0}.",
"self_assign_role_req": "You need {0} role in order to assign yourself a role from group {1}",
"invalid": "Invalid / Can't be found ({0})", "invalid": "Invalid / Can't be found ({0})",
"mass_kill_in_progress": "Mass Banning and Blacklisting of {0} users is in progress...", "mass_kill_in_progress": "Mass Banning and Blacklisting of {0} users is in progress...",
"mass_ban_in_progress": "Banning {0} users...", "mass_ban_in_progress": "Banning {0} users...",
@ -1118,5 +1116,10 @@
"rakeback_claimed": "You've claimed {0} as rakeback!", "rakeback_claimed": "You've claimed {0} as rakeback!",
"rakeback_none": "You don't have any rakeback to claim yet.", "rakeback_none": "You don't have any rakeback to claim yet.",
"rakeback_available": "You have {0} rakeback available. Click the button to claim.", "rakeback_available": "You have {0} rakeback available. Click the button to claim.",
"sniped_by": "Sniped by {0}" "sniped_by": "Sniped by {0}",
"self_assign_group_role_req": "Users can now self-assign a role from group {0} only if they have {1} role.",
"sar_group_not_found": "Group with that number doesn't exist.",
"sar_group_deleted": "Group {0} deleted.",
"choose_one": "Choose one",
"requires_role": "Requires role: {0}"
} }