Restructured the project structure back to the way it was, there's no reasonable way to split the modules

This commit is contained in:
Kwoth
2024-04-26 22:26:24 +00:00
parent 6c9c8bf63e
commit e0819f760c
768 changed files with 192 additions and 1047 deletions

View File

@@ -0,0 +1,52 @@
#nullable disable
using NadekoBot.Modules.Patronage;
using Nadeko.Bot.Db.Models;
using OneOf;
using OneOf.Types;
namespace NadekoBot.Modules.Administration.Services;
public interface IReactionRoleService
{
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guild">Guild where to add a reaction role</param>
/// <param name="msg">Message to which to add a reaction role</param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns>The result of the operation</returns>
Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
IRole role,
int group = 0,
int levelReq = 0);
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId);
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId);
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
Task<int> RemoveAllReactionRoles(ulong guildId);
Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId);
}

View File

@@ -0,0 +1,171 @@
using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class ReactionRoleCommands : NadekoModule
{
private readonly IReactionRoleService _rero;
public ReactionRoleCommands(IReactionRoleService rero)
{
_rero = rero;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRoleAdd(
ulong messageId,
string emoteStr,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
return;
if (levelReq < 0)
return;
var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null)
{
await ReplyErrorLocalizedAsync(strs.not_found);
return;
}
if (ctx.User.Id != ctx.Guild.OwnerId && ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position) <= role.Position)
{
await ReplyErrorLocalizedAsync(strs.hierarchy);
return;
}
var emote = emoteStr.ToIEmote();
await msg.AddReactionAsync(emote);
var res = await _rero.AddReactionRole(ctx.Guild,
msg,
emoteStr,
role,
group,
levelReq);
await res.Match(
_ => ctx.OkAsync(),
fl =>
{
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
return !fl.IsPatronLimit
? ReplyErrorLocalizedAsync(strs.limit_reached(fl.Quota))
: ReplyPendingLocalizedAsync(strs.feature_limit_reached_owner(fl.Quota, fl.Name));
});
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesList(int page = 1)
{
if (--page < 0)
return;
var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
await ctx.SendPaginatedConfirmAsync(page, curPage =>
{
var embed = _eb.Create(ctx)
.WithOkColor();
var content = string.Empty;
foreach (var g in reros.OrderBy(x => x.Group)
.Skip(curPage * 10)
.GroupBy(x => x.MessageId)
.OrderBy(x => x.Key))
{
var messageId = g.Key;
content +=
$"[{messageId}](https://discord.com/channels/{ctx.Guild.Id}/{g.First().ChannelId}/{g.Key})\n";
var groupGroups = g.GroupBy(x => x.Group);
foreach (var ggs in groupGroups)
{
content += $"`< {(g.Key == 0 ? ("Not Exclusive (Group 0)") : ($"Group {ggs.Key}"))} >`\n";
foreach (var rero in ggs)
{
content +=
$"\t{rero.Emote} -> {(ctx.Guild.GetRole(rero.RoleId)?.Mention ?? "<missing role>")}";
if (rero.LevelReq > 0)
content += $" (lvl {rero.LevelReq}+)";
content += '\n';
}
}
}
embed.WithDescription(string.IsNullOrWhiteSpace(content)
? "There are no reaction roles on this server"
: content);
return embed;
}, reros.Count, 10);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesRemove(ulong messageId)
{
var succ = await _rero.RemoveReactionRoles(ctx.Guild.Id, messageId);
if (succ)
await ctx.OkAsync();
else
await ctx.ErrorAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task ReactionRolesDeleteAll()
{
await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Ratelimit(60)]
public async Task ReactionRolesTransfer(ulong fromMessageId, ulong toMessageId)
{
var msg = await ctx.Channel.GetMessageAsync(toMessageId);
if (msg is null)
{
await ctx.ErrorAsync();
return;
}
var reactions = await _rero.TransferReactionRolesAsync(ctx.Guild.Id, fromMessageId, toMessageId);
if (reactions.Count == 0)
{
await ctx.ErrorAsync();
}
else
{
foreach (var r in reactions)
{
await msg.AddReactionAsync(r);
}
}
}
}
}

View File

@@ -0,0 +1,396 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db;
using NadekoBot.Modules.Patronage;
using Nadeko.Bot.Db.Models;
using OneOf.Types;
using OneOf;
namespace NadekoBot.Modules.Administration.Services;
public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionRoleService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new();
private readonly SemaphoreSlim _assignementLock = new(1, 1);
private readonly IPatronageService _ps;
private static readonly FeatureLimitKey _reroFLKey = new()
{
Key = "rero:max_count",
PrettyName = "Reaction Role"
};
public ReactionRolesService(
DiscordSocketClient client,
DbService db,
IBotCredentials creds,
IPatronageService ps)
{
_db = db;
_ps = ps;
_client = client;
_creds = creds;
_cache = new();
}
public async Task OnReadyAsync()
{
await using var uow = _db.GetDbContext();
var reros = await uow.GetTable<ReactionRoleV2>()
.Where(
x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
.ToListAsyncLinqToDB();
foreach (var group in reros.GroupBy(x => x.MessageId))
{
_cache[group.Key] = group.ToList();
}
_client.ReactionAdded += ClientOnReactionAdded;
_client.ReactionRemoved += ClientOnReactionRemoved;
}
private async Task<(IGuildUser, IRole)> GetUserAndRoleAsync(
ulong userId,
ReactionRoleV2 rero)
{
var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId);
if (role is null)
return default;
var user = guild.GetUser(userId) as IGuildUser
?? await _client.Rest.GetGuildUserAsync(guild.Id, userId);
if (user is null)
return default;
return (user, role);
}
private Task ClientOnReactionRemoved(
Cacheable<IUserMessage, ulong> cmsg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(cmsg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var emote = await GetFixedEmoteAsync(cmsg, r.Emote);
var rero = reros.FirstOrDefault(x => x.Emote == emote.Name
|| x.Emote == emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (user.RoleIds.Contains(role.Id))
{
await user.RemoveRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
// had to add this because for some reason, reactionremoved event's reaction doesn't have IsAnimated set,
// causing the .ToString() to be wrong on animated custom emotes
private async Task<IEmote> GetFixedEmoteAsync(
Cacheable<IUserMessage, ulong> cmsg,
IEmote inputEmote)
{
// this should only run for emote
if (inputEmote is not Emote e)
return inputEmote;
// try to get the message and pull
var msg = await cmsg.GetOrDownloadAsync();
var emote = msg.Reactions.Keys.FirstOrDefault(x => e.Equals(x));
return emote ?? inputEmote;
}
private Task ClientOnReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> ch,
SocketReaction r)
{
if (!_cache.TryGetValue(msg.Id, out var reros))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var rero = reros.FirstOrDefault(x => x.Emote == r.Emote.Name || x.Emote == r.Emote.ToString());
if (rero is null)
return;
var (user, role) = await GetUserAndRoleAsync(r.UserId, rero);
if (user.IsBot)
return;
await _assignementLock.WaitAsync();
try
{
if (!user.RoleIds.Contains(role.Id))
{
// first check if there is a level requirement
// and if there is, make sure user satisfies it
if (rero.LevelReq > 0)
{
await using var ctx = _db.GetDbContext();
var levelData = await ctx.GetTable<UserXpStats>()
.GetLevelDataFor(user.GuildId, user.Id);
if (levelData.Level < rero.LevelReq)
return;
}
// remove all other roles from the same group from the user
// execept in group 0, which is a special, non-exclusive group
if (rero.Group != 0)
{
var exclusive = reros
.Where(x => x.Group == rero.Group && x.RoleId != role.Id)
.Select(x => x.RoleId)
.Distinct();
try { await user.RemoveRolesAsync(exclusive); }
catch { }
// remove user's previous reaction
try
{
var m = await msg.GetOrDownloadAsync();
if (m is not null)
{
var reactToRemove = m.Reactions
.FirstOrDefault(x => x.Key.ToString() != r.Emote.ToString())
.Key;
if (reactToRemove is not null)
{
await m.RemoveReactionAsync(reactToRemove, user);
}
}
}
catch
{
}
}
await user.AddRoleAsync(role.Id);
}
}
finally
{
_assignementLock.Release();
}
});
return Task.CompletedTask;
}
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guild">Guild where to add a reaction role</param>
/// <param name="msg">Message to which to add a reaction role</param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns>The result of the operation</returns>
public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
IGuild guild,
IMessage msg,
string emote,
IRole role,
int group = 0,
int levelReq = 0)
{
if (group < 0)
throw new ArgumentOutOfRangeException(nameof(group));
if (levelReq < 0)
throw new ArgumentOutOfRangeException(nameof(group));
await using var ctx = _db.GetDbContext();
await using var tran = await ctx.Database.BeginTransactionAsync();
var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guild.Id)
.CountAsync();
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
return result;
await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guild.Id,
ChannelId = msg.Channel.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
await tran.CommitAsync();
var obj = new ReactionRoleV2()
{
GuildId = guild.Id,
MessageId = msg.Id,
Emote = emote,
RoleId = role.Id,
Group = group,
LevelReq = levelReq
};
lock (_cacheLock)
{
_cache.AddOrUpdate(msg.Id,
_ => new()
{
obj
},
(_, list) =>
{
list.RemoveAll(x => x.Emote == emote);
list.Add(obj);
return list;
});
}
return new Success();
}
/// <summary>
/// Get all reaction roles on the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<IReadOnlyCollection<ReactionRoleV2>> GetReactionRolesAsync(ulong guildId)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.ToListAsync();
}
/// <summary>
/// Remove reaction roles on the specified message
/// </summary>
/// <param name="guildId"></param>
/// <param name="messageId"></param>
/// <returns></returns>
public async Task<bool> RemoveReactionRoles(ulong guildId, ulong messageId)
{
// guildid is used for quick index lookup
await using var ctx = _db.GetDbContext();
var changed = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == messageId)
.DeleteAsync();
_cache.TryRemove(messageId, out _);
if (changed == 0)
return false;
return true;
}
/// <summary>
/// Remove all reaction roles in the specified server
/// </summary>
/// <param name="guildId"></param>
/// <returns></returns>
public async Task<int> RemoveAllReactionRoles(ulong guildId)
{
await using var ctx = _db.GetDbContext();
var output = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.DeleteWithOutputAsync(x => x.MessageId);
lock (_cacheLock)
{
foreach (var o in output)
{
_cache.TryRemove(o, out _);
}
}
return output.Length;
}
public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(
ulong guildId,
ulong fromMessageId,
ulong toMessageId)
{
await using var ctx = _db.GetDbContext();
var updated = ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId && x.MessageId == fromMessageId)
.UpdateWithOutput(old => new()
{
MessageId = toMessageId
},
(old, neu) => neu);
lock (_cacheLock)
{
if (_cache.TryRemove(fromMessageId, out var data))
{
if (_cache.TryGetValue(toMessageId, out var newData))
{
newData.AddRange(data);
}
else
{
_cache[toMessageId] = data;
}
}
}
return updated.Select(x => x.Emote.ToIEmote()).ToList();
}
}

View File

@@ -0,0 +1,207 @@
#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
}
private readonly IServiceProvider _services;
private StickyRolesService _stickyRoleSvc;
public RoleCommands(IServiceProvider services, StickyRolesService stickyRoleSvc)
{
_services = services;
_stickyRoleSvc = stickyRoleSvc;
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task SetRole(IGuildUser targetUser, [Leftover] IRole roleToAdd)
{
var runnerUser = (IGuildUser)ctx.User;
var runnerMaxRolePosition = runnerUser.GetRoles().Max(x => x.Position);
if (ctx.User.Id != ctx.Guild.OwnerId && runnerMaxRolePosition <= roleToAdd.Position)
return;
try
{
await targetUser.AddRoleAsync(roleToAdd);
await ReplyConfirmLocalizedAsync(strs.setrole(Format.Bold(roleToAdd.Name),
Format.Bold(targetUser.ToString())));
}
catch (Exception ex)
{
Log.Warning(ex, "Error in setrole command");
await ReplyErrorLocalizedAsync(strs.setrole_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RemoveRole(IGuildUser targetUser, [Leftover] IRole roleToRemove)
{
var runnerUser = (IGuildUser)ctx.User;
if (ctx.User.Id != runnerUser.Guild.OwnerId
&& runnerUser.GetRoles().Max(x => x.Position) <= roleToRemove.Position)
return;
try
{
await targetUser.RemoveRoleAsync(roleToRemove);
await ReplyConfirmLocalizedAsync(strs.remrole(Format.Bold(roleToRemove.Name),
Format.Bold(targetUser.ToString())));
}
catch
{
await ReplyErrorLocalizedAsync(strs.remrole_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RenameRole(IRole roleToEdit, [Leftover] string newname)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= roleToEdit.Position)
return;
try
{
if (roleToEdit.Position > (await ctx.Guild.GetCurrentUserAsync()).GetRoles().Max(r => r.Position))
{
await ReplyErrorLocalizedAsync(strs.renrole_perms);
return;
}
await roleToEdit.ModifyAsync(g => g.Name = newname);
await ReplyConfirmLocalizedAsync(strs.renrole);
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.renrole_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RemoveAllRoles([Leftover] IGuildUser user)
{
var guser = (IGuildUser)ctx.User;
var userRoles = user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole).ToList();
if (user.Id == ctx.Guild.OwnerId
|| (ctx.User.Id != ctx.Guild.OwnerId
&& guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position)))
return;
try
{
await user.RemoveRolesAsync(userRoles);
await ReplyConfirmLocalizedAsync(strs.rar(Format.Bold(user.ToString())));
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.rar_err);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task CreateRole([Leftover] string roleName = null)
{
if (string.IsNullOrWhiteSpace(roleName))
return;
var r = await ctx.Guild.CreateRoleAsync(roleName, isMentionable: false);
await ReplyConfirmLocalizedAsync(strs.cr(Format.Bold(r.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task DeleteRole([Leftover] IRole role)
{
var guser = (IGuildUser)ctx.User;
if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position)
return;
await role.DeleteAsync();
await ReplyConfirmLocalizedAsync(strs.dr(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async Task RoleHoist([Leftover] IRole role)
{
var newHoisted = !role.IsHoisted;
await role.ModifyAsync(r => r.Hoist = newHoisted);
if (newHoisted)
await ReplyConfirmLocalizedAsync(strs.rolehoist_enabled(Format.Bold(role.Name)));
else
await ReplyConfirmLocalizedAsync(strs.rolehoist_disabled(Format.Bold(role.Name)));
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(1)]
public async Task RoleColor([Leftover] IRole role)
=> await SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6"));
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public async Task RoleColor(Color color, [Leftover] IRole role)
{
try
{
var rgba32 = color.ToPixel<Rgba32>();
await role.ModifyAsync(r => r.Color = new Discord.Color(rgba32.R, rgba32.G, rgba32.B));
await ReplyConfirmLocalizedAsync(strs.rc(Format.Bold(role.Name)));
}
catch (Exception)
{
await ReplyErrorLocalizedAsync(strs.rc_perms);
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
[BotPerm(GuildPerm.ManageRoles)]
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,139 @@
#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 = new();
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 Task ClientOnUserLeft(SocketGuild guild, SocketUser user)
{
if (user is not SocketGuildUser gu)
return Task.CompletedTask;
if (!_stickyRoles.Contains(guild.Id))
return Task.CompletedTask;
_ = Task.Run(async () => await SaveRolesAsync(guild.Id, gu.Id, gu.Roles));
return Task.CompletedTask;
}
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.Where(x => !x.IsEveryone && !x.IsManaged).Select(x => x.Id.ToString())),
DateAdded = DateTime.UtcNow
});
}
private Task ClientOnUserJoined(SocketGuildUser user)
{
if (!_stickyRoles.Contains(user.Guild.Id))
return Task.CompletedTask;
_ = Task.Run(async () =>
{
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;
}
}