* feature: Added .stickyroles command, untested

* ci: possible fix
This commit is contained in:
Kwoth
2024-04-23 15:45:09 +00:00
parent 74690c73d6
commit 08d8da25cd
20 changed files with 11093 additions and 12 deletions

View File

@@ -1,13 +1,13 @@
image: mcr.microsoft.com/dotnet/sdk:7.0
image: mcr.microsoft.com/dotnet/sdk:8.0
stages:
- build
- test
- upload-builds
- release
- publish-windows
- upload-windows-updater-release
- publish-medusa-package
# - upload-builds
# - release
# - publish-windows
# - upload-windows-updater-release
# - publish-medusa-package
variables:
project: "NadekoBot"

View File

@@ -103,6 +103,7 @@ public class GuildConfig : DbEntity
public string BoostMessage { get; set; } = "%user% just boosted this server!";
public ulong BoostMessageChannelId { get; set; }
public int BoostMessageDeleteAfter { get; set; }
public bool StickyRoles { get; set; }
#endregion
}

View File

@@ -0,0 +1,13 @@
namespace Nadeko.Bot.Db.Models;
public class StickyRole : DbEntity
{
public ulong GuildId { get; set; }
public string RoleIds { get; set; }
public ulong UserId { get; set; }
public ulong[] GetRoleIds()
=> string.IsNullOrWhiteSpace(RoleIds)
? []
: RoleIds.Split(',').Select(ulong.Parse).ToArray();
}

View File

@@ -1,21 +1,26 @@
#nullable disable
using System.Xml.Schema;
using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class RoleCommands : NadekoModule
{
public enum Exclude { Excl }
public enum Exclude
{
Excl
}
private readonly IServiceProvider _services;
private StickyRolesService _stickyRoleSvc;
public RoleCommands(IServiceProvider services)
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc)
{
_services = services;
_stickyRoleSvc = stickyRoleSvc;
}
[Cmd]
@@ -180,5 +185,23 @@ public partial class Administration
await ReplyErrorLocalizedAsync(strs.rc_perms);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.Administrator)]
public async Task StickyRoles()
{
var newState = await _stickyRoleSvc.ToggleStickyRoles(ctx.Guild.Id);
if (newState)
{
await ReplyConfirmLocalizedAsync(strs.sticky_roles_enabled);
}
else
{
await ReplyConfirmLocalizedAsync(strs.sticky_roles_disabled);
}
}
}
}

View File

@@ -0,0 +1,137 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Nadeko.Bot.Db.Models;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
namespace NadekoBot.Modules.Administration;
public sealed class StickyRolesService : INService, IReadyExecutor
{
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly DbService _db;
private HashSet<ulong> _stickyRoles;
public StickyRolesService(DiscordSocketClient client,
IBotCredentials creds,
DbService db)
{
_client = client;
_creds = creds;
_db = db;
}
public async Task OnReadyAsync()
{
await using (var ctx = _db.GetDbContext())
{
_stickyRoles = (await ctx
.Set<GuildConfig>()
.ToLinqToDBTable()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.Where(x => x.StickyRoles)
.Select(x => x.GuildId)
.ToListAsync())
.ToHashSet();
}
_client.UserJoined += ClientOnUserJoined;
_client.UserLeft += ClientOnUserLeft;
// cleanup old ones every hour
// 30 days retention
if (_client.ShardId == 0)
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
await using var ctx = _db.GetDbContext();
await ctx.GetTable<StickyRole>()
.Where(x => x.DateAdded < DateTime.UtcNow - TimeSpan.FromDays(30))
.DeleteAsync();
}
}
}
private async Task ClientOnUserLeft(SocketGuild guild, SocketUser user)
{
if (user is not SocketGuildUser gu)
{
return;
}
if (!_stickyRoles.Contains(guild.Id))
{
return;
}
_ = Task.Run(async () => await SaveRolesAsync(guild.Id, gu.Id, gu.Roles));
}
private async Task SaveRolesAsync(ulong guildId, ulong userId, IReadOnlyCollection<SocketRole> guRoles)
{
await using var ctx = _db.GetDbContext();
await ctx.GetTable<StickyRole>()
.InsertAsync(() => new()
{
GuildId = guildId,
UserId = userId,
RoleIds = string.Join(',', guRoles.Select(x => x.Id.ToString())),
DateAdded = DateTime.UtcNow
});
}
private Task ClientOnUserJoined(SocketGuildUser user)
{
_ = Task.Run(async () =>
{
if (!_stickyRoles.Contains(user.Guild.Id))
return;
var roles = await GetRolesAsync(user.Guild.Id, user.Id);
await user.AddRolesAsync(roles);
});
return Task.CompletedTask;
}
private async Task<ulong[]> GetRolesAsync(ulong guildId, ulong userId)
{
await using var ctx = _db.GetDbContext();
var stickyRolesEntry = await ctx
.GetTable<StickyRole>()
.Where(x => x.GuildId == guildId && x.UserId == userId)
.DeleteWithOutputAsync();
if (stickyRolesEntry is { Length: > 0 })
{
return stickyRolesEntry[0].GetRoleIds();
}
return [];
}
public async Task<bool> ToggleStickyRoles(ulong guildId, bool? newState = null)
{
await using var ctx = _db.GetDbContext();
var config = ctx.GuildConfigsForId(guildId, set => set);
config.StickyRoles = newState ?? !config.StickyRoles;
await ctx.SaveChangesAsync();
if (config.StickyRoles)
{
_stickyRoles.Add(guildId);
}
else
{
_stickyRoles.Remove(guildId);
}
return config.StickyRoles;
}
}

View File

@@ -60,6 +60,8 @@ public abstract class NadekoContext : DbContext
public DbSet<PatronQuota> PatronQuotas { get; set; }
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
public DbSet<StickyRole> StickyRoles { get; set; }
#region Mandatory Provider-Specific Values
@@ -481,6 +483,16 @@ public abstract class NadekoContext : DbContext
.IsUnique());
#endregion
#region Sticky Roles
modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new
{
x.GuildId,
x.UserId
}).IsUnique());
#endregion
}
#if DEBUG

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Db.Migrations.Mysql
{
/// <inheritdoc />
public partial class stickyroles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "stickyroles",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "stickyroles",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
roleids = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_stickyroles", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_stickyroles_guildid_userid",
table: "stickyroles",
columns: new[] { "guildid", "userid" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "stickyroles");
migrationBuilder.DropColumn(
name: "stickyroles",
table: "guildconfigs");
}
}
}

View File

@@ -983,6 +983,10 @@ namespace NadekoBot.Db.Migrations.Mysql
.HasColumnType("tinyint(1)")
.HasColumnName("senddmgreetmessage");
b.Property<bool>("StickyRoles")
.HasColumnType("tinyint(1)")
.HasColumnName("stickyroles");
b.Property<string>("TimeZoneId")
.HasColumnType("longtext")
.HasColumnName("timezoneid");
@@ -2124,6 +2128,42 @@ namespace NadekoBot.Db.Migrations.Mysql
b.ToTable("slowmodeignoreduser", (string)null);
});
modelBuilder.Entity("Nadeko.Bot.Db.Models.StickyRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<ulong>("GuildId")
.HasColumnType("bigint unsigned")
.HasColumnName("guildid");
b.Property<string>("RoleIds")
.IsRequired()
.HasColumnType("longtext")
.HasColumnName("roleids");
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_stickyroles");
b.HasIndex("GuildId", "UserId")
.IsUnique()
.HasDatabaseName("ix_stickyroles_guildid_userid");
b.ToTable("stickyroles", (string)null);
});
modelBuilder.Entity("Nadeko.Bot.Db.Models.StreamRoleBlacklistedUser", b =>
{
b.Property<int>("Id")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Db.Migrations
{
/// <inheritdoc />
public partial class stickyroles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "stickyroles",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "stickyroles",
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),
roleids = table.Column<string>(type: "text", nullable: false),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_stickyroles", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_stickyroles_guildid_userid",
table: "stickyroles",
columns: new[] { "guildid", "userid" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "stickyroles");
migrationBuilder.DropColumn(
name: "stickyroles",
table: "guildconfigs");
}
}
}

View File

@@ -983,6 +983,10 @@ namespace NadekoBot.Db.Migrations
.HasColumnType("boolean")
.HasColumnName("senddmgreetmessage");
b.Property<bool>("StickyRoles")
.HasColumnType("boolean")
.HasColumnName("stickyroles");
b.Property<string>("TimeZoneId")
.HasColumnType("text")
.HasColumnName("timezoneid");
@@ -2124,6 +2128,42 @@ namespace NadekoBot.Db.Migrations
b.ToTable("slowmodeignoreduser", (string)null);
});
modelBuilder.Entity("Nadeko.Bot.Db.Models.StickyRole", 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<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("RoleIds")
.IsRequired()
.HasColumnType("text")
.HasColumnName("roleids");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_stickyroles");
b.HasIndex("GuildId", "UserId")
.IsUnique()
.HasDatabaseName("ix_stickyroles_guildid_userid");
b.ToTable("stickyroles", (string)null);
});
modelBuilder.Entity("Nadeko.Bot.Db.Models.StreamRoleBlacklistedUser", b =>
{
b.Property<int>("Id")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Db.Migrations.Sqlite
{
/// <inheritdoc />
public partial class stickyroles : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "StickyRoles",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "StickyRoles",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
RoleIds = table.Column<string>(type: "TEXT", nullable: false),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_StickyRoles", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_StickyRoles_GuildId_UserId",
table: "StickyRoles",
columns: new[] { "GuildId", "UserId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StickyRoles");
migrationBuilder.DropColumn(
name: "StickyRoles",
table: "GuildConfigs");
}
}
}

View File

@@ -729,6 +729,9 @@ namespace NadekoBot.Db.Migrations.Sqlite
b.Property<bool>("SendDmGreetMessage")
.HasColumnType("INTEGER");
b.Property<bool>("StickyRoles")
.HasColumnType("INTEGER");
b.Property<string>("TimeZoneId")
.HasColumnType("TEXT");
@@ -1577,6 +1580,33 @@ namespace NadekoBot.Db.Migrations.Sqlite
b.ToTable("SlowmodeIgnoredUser");
});
modelBuilder.Entity("Nadeko.Bot.Db.Models.StickyRole", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("RoleIds")
.IsRequired()
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId", "UserId")
.IsUnique();
b.ToTable("StickyRoles");
});
modelBuilder.Entity("Nadeko.Bot.Db.Models.StreamRoleBlacklistedUser", b =>
{
b.Property<int>("Id")

View File

@@ -2382,7 +2382,11 @@ clubrename:
args:
- "New cool club name"
cacheusers:
desc: Caches users of a Discord server and saves them to the database.
desc: "Caches users of a Discord server and saves them to the database."
args:
- ""
- "serverId"
- "serverId"
stickyroles:
desc: "Toggles whether the bot will save the leaving users' roles, and reapply them once they re-join. The roles will be stored for up to 30 days."
args:
- ""

View File

@@ -1064,5 +1064,7 @@
"thread_created": "Thread Created",
"supported_languages": "Supported Languages",
"cache_users_pending": "Updating users, please wait...",
"cache_users_done": "{0} users were added and {1} users were updated."
"cache_users_done": "{0} users were added and {1} users were updated.",
"sticky_roles_enabled": "Sticky roles enabled. Leaving users' roles will be restored when they rejoin the server.",
"sticky_roles_disabled": "Sticky roles disabled."
}

10
src/NadekoBot/migrate.ps1 Normal file
View File

@@ -0,0 +1,10 @@
if ($args.Length -eq 0) {
Write-Host "Please provide a migration name." -ForegroundColor Red
}
else {
$migrationName = $args[0]
dotnet ef migrations add $migrationName -c SqliteContext
dotnet ef migrations add $migrationName -c PostgreSqlContext
dotnet ef migrations add $migrationName -c MysqlContext
}