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.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,34 @@ public abstract class EllieContext : DbContext
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
modelBuilder.Entity<Rakeback>()
@ -84,16 +112,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 +321,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 +349,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 +531,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

@ -31,10 +31,11 @@ public class GuildConfig : DbEntity
// 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,7 +92,6 @@ 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;

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);
}
}
}

View file

@ -1253,41 +1253,6 @@ namespace EllieBot.Migrations.PostgreSql
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 =>
{
b.Property<int>("Id")
@ -2200,7 +2165,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 +2242,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 +3558,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 +3597,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 +3883,6 @@ namespace EllieBot.Migrations.PostgreSql
b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles");
@ -3885,6 +3912,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);
}
}
}

View file

@ -932,32 +932,6 @@ namespace EllieBot.Migrations
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 =>
{
b.Property<int>("Id")
@ -1640,7 +1614,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 +1670,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 +2682,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 +2717,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 +2980,6 @@ namespace EllieBot.Migrations
b.Navigation("Permissions");
b.Navigation("SelfAssignableRoleGroupNames");
b.Navigation("ShopEntries");
b.Navigation("SlowmodeIgnoredRoles");
@ -2991,6 +3009,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

@ -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,48 @@ 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();
return new()
{
GuildCount = guildIds.Keys.Count,

View file

@ -6,16 +6,155 @@ 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;
}
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]
[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 +167,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 +203,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 +224,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 = _sender.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,9 +307,9 @@ 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 = _service.ToggleEsar(ctx.Guild.Id);
var areExclusive = await _service.SetGroupExclusivityAsync(ctx.Guild.Id, groupNumber);
if (areExclusive)
await Response().Confirm(strs.self_assign_excl).SendAsync();
else
@ -165,12 +320,12 @@ public partial class Administration
[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 +341,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,327 @@
#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)
{
// 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 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 async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role)
public sealed class SarAssignerService : IEService, IReadyExecutor
{
LevelStats userLevelData;
await using (var uow = _db.GetDbContext())
private readonly XpService _xp;
private readonly DbService _db;
private readonly Channel<SarAssignerDataItem> _channel =
Channel.CreateBounded<SarAssignerDataItem>(100);
public SarAssignerService(XpService xp, DbService db)
{
var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id);
userLevelData = new(stats.Xp + stats.AwardedXp);
_xp = xp;
_db = db;
}
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)
public async Task OnReadyAsync()
{
var sameRoles = guildUser.RoleIds.Where(r => roleIds.Contains(r));
foreach (var roleId in sameRoles)
var reader = _channel.Reader;
while (true)
{
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);
await _channel.Writer.WriteAsync(item);
}
if (string.IsNullOrWhiteSpace(name))
}
public sealed class SarAssignerDataItem
{
if (toUpdate is not null)
gc.SelfAssignableRoleGroupNames.Remove(toUpdate);
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; }
}
else if (toUpdate is null)
[GenerateOneOf]
public sealed partial class SarAssignResult
: OneOfBase<Success, Error, SarLevelRequirement, SarRoleRequirement, SarAlreadyHasRole, SarInsuffPerms>
{
gc.SelfAssignableRoleGroupNames.Add(new()
{
Name = name,
Number = group
});
set = true;
}
else
{
toUpdate.Name = name;
set = true;
}
await uow.SaveChangesAsync();
public record class SarLevelRequirement(int Level);
return set;
}
public record class SarRoleRequirement(ulong RoleId);
public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role)
{
var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id);
public record class SarAlreadyHasRole();
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 record class SarInsuffPerms();

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)]

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

@ -154,16 +154,16 @@ public partial class Gambling : GamblingModule<GamblingService>
{
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 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

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;
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
{
private readonly IHttpClientFactory _httpClientFactory;

View file

@ -1,7 +1,9 @@
using EllieBot.Db.Models;
namespace Ellie.Common;
public static class Extensions
{
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;
}
}
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

View file

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

View file

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

View file

@ -323,7 +323,7 @@ vcrolerm:
params:
- vcId:
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.
ex:
- Gamer
@ -336,7 +336,7 @@ asar:
desc: "The ID of a group that the designated role should belong to."
role:
desc: "The role that can be assigned by the user."
rsar:
sarremove:
desc: Removes a specified role from the list of self-assignable roles.
ex:
- Gamer
@ -345,7 +345,7 @@ rsar:
params:
- role:
desc: "The role to remove from the list of self-assignable roles."
lsar:
sarlist:
desc: Lists self-assignable roles. Shows 20 roles per page.
ex:
- ''
@ -353,22 +353,53 @@ lsar:
params:
- page:
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.
ex:
- 1 Faction
- 2
params:
- group:
desc: "The ID of the group to name."
desc: "The group number"
name:
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.
ex:
- ''
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:
desc: Adds a role to you that you choose. Role must be on the list of self-assignable roles.
ex:
@ -2464,12 +2495,6 @@ muterole:
params:
- role:
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:
desc: Sets the bots stream. First parameter is the twitch link, second parameter is stream name.
ex:
@ -3919,15 +3944,6 @@ stock:
params:
- query:
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:
desc: Bans multiple users at once. Specify a space separated list of IDs of users who you wish to ban.
ex:

View file

@ -896,9 +896,6 @@
"rank": "Rank",
"template_reloaded": "Xp template has been reloaded.",
"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_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.",
@ -947,6 +944,7 @@
"did_you_mean": "Did you mean {0}?",
"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_role_req": "You need {0} role in order to assign yourself a role from group {1}",
"invalid": "Invalid / Can't be found ({0})",
"mass_kill_in_progress": "Mass Banning and Blacklisting of {0} users is in progress...",
"mass_ban_in_progress": "Banning {0} users...",
@ -1118,5 +1116,10 @@
"rakeback_claimed": "You've claimed {0} as rakeback!",
"rakeback_none": "You don't have any rakeback to claim yet.",
"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}"
}