- Reaction roles rewritten completely. They now support multiple exclusivity groups per message and level requirements. However they can only be added one by one

- Bot now support much higher XP values for global and server levels
This commit is contained in:
Kwoth
2022-05-04 02:08:15 +02:00
parent 5cb95cf94d
commit 5b5bc278ff
35 changed files with 10820 additions and 715 deletions

View File

@@ -0,0 +1,52 @@
#nullable disable
using NadekoBot.Services.Database.Models;
using System.Collections;
namespace NadekoBot.Modules.Administration.Services;
public interface IReactionRoleService
{
/// <summary>
/// Adds a single reaction role
/// </summary>
/// <param name="guildId"></param>
/// <param name="msg"></param>
/// <param name="channel"></param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns></returns>
Task<bool> AddReactionRole(
ulong guildId,
IMessage msg,
ITextChannel channel,
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,166 @@
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)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial 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 succ = await _rero.AddReactionRole(ctx.Guild.Id,
msg,
(ITextChannel)ctx.Channel,
emoteStr,
role,
group,
levelReq);
if (succ)
{
await ctx.OkAsync();
}
else
{
await ctx.ErrorAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
{
var reros = await _rero.GetReactionRolesAsync(ctx.Guild.Id);
var embed = _eb.Create(ctx)
.WithOkColor();
var content = string.Empty;
foreach (var g in reros.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);
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial 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)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesDeleteAll()
{
await _rero.RemoveAllReactionRoles(ctx.Guild.Id);
await ctx.OkAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Ratelimit(60)]
public async partial 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,356 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Modules.Xp.Extensions;
using NadekoBot.Services.Database.Models;
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);
public ReactionRolesService(DiscordSocketClient client, DbService db, IBotCredentials creds)
{
_db = db;
_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(
SocketReaction r,
ReactionRoleV2 rero)
{
var guild = _client.GetGuild(rero.GuildId);
var role = guild?.GetRole(rero.RoleId);
if (role is null)
return default;
var user = guild.GetUser(r.UserId) as IGuildUser
?? await _client.Rest.GetGuildUserAsync(guild.Id, r.UserId);
if (user is null)
return default;
return (user, role);
}
private Task ClientOnReactionRemoved(
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, 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;
}
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, 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="guildId"></param>
/// <param name="msg"></param>
/// <param name="channel"></param>
/// <param name="emote"></param>
/// <param name="role"></param>
/// <param name="group"></param>
/// <param name="levelReq"></param>
/// <returns></returns>
public async Task<bool> AddReactionRole(
ulong guildId,
IMessage msg,
ITextChannel channel,
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();
var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
.Where(x => x.GuildId == guildId)
.CountAsync();
if (activeReactionRoles >= 50)
return false;
var changed = await ctx.GetTable<ReactionRoleV2>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
ChannelId = 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,
});
if (changed == 0)
return false;
var obj = new ReactionRoleV2()
{
GuildId = guildId,
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 true;
}
/// <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

@@ -7,172 +7,18 @@ using Color = SixLabors.ImageSharp.Color;
namespace NadekoBot.Modules.Administration;
public partial class Administration
{
public partial class RoleCommands : NadekoModule<RoleCommandsService>
public partial class RoleCommands : NadekoModule
{
public enum Exclude { Excl }
private readonly IServiceProvider _services;
public RoleCommands(IServiceProvider services)
=> _services = services;
public async Task InternalReactionRoles(bool exclusive, ulong? messageId, params string[] input)
{
var target = messageId is { } msgId
? await ctx.Channel.GetMessageAsync(msgId)
: (await ctx.Channel.GetMessagesAsync(2).FlattenAsync()).Skip(1).FirstOrDefault();
if (input.Length % 2 != 0 || target is null)
return;
var all = await input.Chunk(2)
.Select(async x =>
{
var inputRoleStr = x.First();
var roleReader = new RoleTypeReader<SocketRole>();
var roleResult = await roleReader.ReadAsync(ctx, inputRoleStr, _services);
if (!roleResult.IsSuccess)
{
Log.Warning("Role {Role} not found", inputRoleStr);
return null;
}
var role = (IRole)roleResult.BestMatch;
if (role.Position
> ((IGuildUser)ctx.User).GetRoles()
.Select(r => r.Position)
.Max()
&& ctx.User.Id != ctx.Guild.OwnerId)
return null;
var emote = x.Last().ToIEmote();
return new
{
role,
emote
};
})
.Where(x => x is not null)
.WhenAll();
if (!all.Any())
return;
foreach (var x in all)
{
try
{
await target.AddReactionAsync(x.emote,
new()
{
RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit
});
}
catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.BadRequest)
{
await ReplyErrorLocalizedAsync(strs.reaction_cant_access(Format.Code(x.emote.ToString())));
return;
}
await Task.Delay(500);
}
if (_service.Add(ctx.Guild.Id,
new()
{
Exclusive = exclusive,
MessageId = target.Id,
ChannelId = target.Channel.Id,
ReactionRoles = all.Select(x =>
{
return new ReactionRole
{
EmoteName = x.emote.ToString(),
RoleId = x.role.Id
};
})
.ToList()
}))
await ctx.OkAsync();
else
await ReplyErrorLocalizedAsync(strs.reaction_roles_full);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public partial Task ReactionRoles(ulong messageId, params string[] input)
=> InternalReactionRoles(false, messageId, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public partial Task ReactionRoles(ulong messageId, Exclude _, params string[] input)
=> InternalReactionRoles(true, messageId, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(0)]
public partial Task ReactionRoles(params string[] input)
=> InternalReactionRoles(false, null, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
[BotPerm(GuildPerm.ManageRoles)]
[Priority(1)]
public partial Task ReactionRoles(Exclude _, params string[] input)
=> InternalReactionRoles(true, null, input);
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesList()
{
var embed = _eb.Create().WithOkColor();
if (!_service.Get(ctx.Guild.Id, out var rrs) || !rrs.Any())
embed.WithDescription(GetText(strs.no_reaction_roles));
else
{
var g = (SocketGuild)ctx.Guild;
foreach (var rr in rrs)
{
var ch = g.GetTextChannel(rr.ChannelId);
IUserMessage msg = null;
if (ch is not null)
msg = await ch.GetMessageAsync(rr.MessageId) as IUserMessage;
var content = msg?.Content.TrimTo(30) ?? "DELETED!";
embed.AddField($"**{rr.Index + 1}.** {ch?.Name ?? "DELETED!"}",
GetText(strs.reaction_roles_message(rr.ReactionRoles?.Count ?? 0, content)));
}
}
await ctx.Channel.EmbedAsync(embed);
}
[Cmd]
[RequireContext(ContextType.Guild)]
[NoPublicBot]
[UserPerm(GuildPerm.ManageRoles)]
public async partial Task ReactionRolesRemove(int index)
{
if (index < 1 || !_service.Get(ctx.Guild.Id, out var rrs) || !rrs.Any() || rrs.Count < index)
return;
index--;
_service.Remove(ctx.Guild.Id, index);
await ReplyConfirmLocalizedAsync(strs.reaction_role_removed(index + 1));
_services = services;
}
[Cmd]

View File

@@ -1,253 +0,0 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.Collections;
using NadekoBot.Db;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Administration.Services;
public class RoleCommandsService : INService
{
private readonly DbService _db;
private readonly DiscordSocketClient _client;
private readonly ConcurrentDictionary<ulong, IndexedCollection<ReactionRoleMessage>> _models;
/// <summary>
/// Contains the (Message ID, User ID) of reaction roles that are currently being processed.
/// </summary>
private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new();
public RoleCommandsService(DiscordSocketClient client, DbService db, Bot bot)
{
_db = db;
_client = client;
#if !GLOBAL_NADEKO
_models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.ReactionRoleMessages).ToConcurrent();
_client.ReactionAdded += _client_ReactionAdded;
_client.ReactionRemoved += _client_ReactionRemoved;
#endif
}
private Task _client_ReactionAdded(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> chan,
SocketReaction reaction)
{
_ = Task.Run(async () =>
{
if (!reaction.User.IsSpecified
|| reaction.User.Value.IsBot
|| reaction.User.Value is not SocketGuildUser gusr
|| chan.Value is not SocketGuildChannel gch
|| !_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
// compare emote names for backwards compatibility :facepalm:
var reactionRole = conf.ReactionRoles.FirstOrDefault(x
=> x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole is not null)
{
if (!conf.Exclusive)
{
await AddReactionRoleAsync(gusr, reactionRole);
return;
}
// If same (message, user) are being processed in an exclusive rero, quit
if (!_reacting.Add((msg.Id, reaction.UserId)))
return;
try
{
var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg,
gusr,
reaction,
conf,
reactionRole,
CancellationToken.None);
var addRoleTask = AddReactionRoleAsync(gusr, reactionRole);
await Task.WhenAll(removeExclusiveTask, addRoleTask);
}
finally
{
// Free (message/user) for another exclusive rero
_reacting.TryRemove((msg.Id, reaction.UserId));
}
}
else
{
var dl = await msg.GetOrDownloadAsync();
await dl.RemoveReactionAsync(reaction.Emote,
dl.Author,
new()
{
RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502
});
Log.Warning("User {Author} is adding unrelated reactions to the reaction roles message", dl.Author);
}
});
return Task.CompletedTask;
}
private Task _client_ReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> chan,
SocketReaction reaction)
{
_ = Task.Run(async () =>
{
try
{
if (!reaction.User.IsSpecified
|| reaction.User.Value.IsBot
|| reaction.User.Value is not SocketGuildUser gusr)
return;
if (chan.Value is not SocketGuildChannel gch)
return;
if (!_models.TryGetValue(gch.Guild.Id, out var confs))
return;
var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id);
if (conf is null)
return;
var reactionRole = conf.ReactionRoles.FirstOrDefault(x
=> x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString());
if (reactionRole is not null)
{
var role = gusr.Guild.GetRole(reactionRole.RoleId);
if (role is null)
return;
await gusr.RemoveRoleAsync(role);
}
}
catch { }
});
return Task.CompletedTask;
}
public bool Get(ulong id, out IndexedCollection<ReactionRoleMessage> rrs)
=> _models.TryGetValue(id, out rrs);
public bool Add(ulong id, ReactionRoleMessage rrm)
{
using var uow = _db.GetDbContext();
var table = uow.GetTable<ReactionRoleMessage>();
table.Delete(x => x.MessageId == rrm.MessageId);
var gc = uow.GuildConfigsForId(id,
set => set.Include(x => x.ReactionRoleMessages).ThenInclude(x => x.ReactionRoles));
if (gc.ReactionRoleMessages.Count >= 10)
return false;
gc.ReactionRoleMessages.Add(rrm);
uow.SaveChanges();
_models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; });
return true;
}
public void Remove(ulong id, int index)
{
using var uow = _db.GetDbContext();
var gc = uow.GuildConfigsForId(id,
set => set.Include(x => x.ReactionRoleMessages).ThenInclude(x => x.ReactionRoles));
uow.Set<ReactionRole>().RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles);
gc.ReactionRoleMessages.RemoveAt(index);
_models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; });
uow.SaveChanges();
}
/// <summary>
/// Adds a reaction role to the specified user.
/// </summary>
/// <param name="user">A Discord guild user.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero)
{
var toAdd = user.Guild.GetRole(dbRero.RoleId);
return toAdd is not null && !user.Roles.Contains(toAdd) ? user.AddRoleAsync(toAdd) : Task.CompletedTask;
}
/// <summary>
/// Removes the exclusive reaction roles and reactions from the specified user.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="dbReroMsg">The database entry of the reaction role message.</param>
/// <param name="dbRero">The database settings of this reaction role.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private Task RemoveExclusiveReactionRoleAsync(
Cacheable<IUserMessage, ulong> reactionMessage,
SocketGuildUser user,
SocketReaction reaction,
ReactionRoleMessage dbReroMsg,
ReactionRole dbRero,
CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId)
.Where(x => x != dbRero.RoleId)
.Select(x => user.Guild.GetRole(x))
.Where(x => x is not null);
var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken);
var removeRolesTask = user.RemoveRolesAsync(roleIds);
return Task.WhenAll(removeReactionsTask, removeRolesTask);
}
/// <summary>
/// Removes old reactions from an exclusive reaction role.
/// </summary>
/// <param name="reactionMessage">The Discord message that contains the reaction roles.</param>
/// <param name="user">A Discord guild user.</param>
/// <param name="reaction">The Discord reaction of the user.</param>
/// <param name="cToken">A cancellation token to cancel the operation.</param>
/// <exception cref="OperationCanceledException">Occurs when the operation is cancelled before it began.</exception>
/// <exception cref="TaskCanceledException">Occurs when the operation is cancelled while it's still executing.</exception>
private async Task RemoveOldReactionsAsync(
Cacheable<IUserMessage, ulong> reactionMessage,
SocketGuildUser user,
SocketReaction reaction,
CancellationToken cToken = default)
{
cToken.ThrowIfCancellationRequested();
//if the role is exclusive,
// remove all other reactions user added to the message
var dl = await reactionMessage.GetOrDownloadAsync();
foreach (var r in dl.Reactions)
{
if (r.Key.Name == reaction.Emote.Name)
continue;
try { await dl.RemoveReactionAsync(r.Key, user); }
catch { }
await Task.Delay(100, cToken);
}
}
}

View File

@@ -4,7 +4,6 @@ using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
// todo .h [group] should show commands in that group
public partial class Gambling
{
[Name("Bank")]

View File

@@ -76,6 +76,7 @@ public class RemindService : INService, IReadyExecutor
await uow.SaveChangesAsync();
}
// todo move isonshard to a method
private async Task<List<Reminder>> GetRemindersBeforeAsync(DateTime now)
{
await using var uow = _db.GetDbContext();

View File

@@ -17,7 +17,6 @@ using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Xp.Services;
// todo improve xp with linqtodb
public class XpService : INService, IReadyExecutor, IExecNoCommand
{
public const int XP_REQUIRED_LVL_1 = 36;
@@ -133,7 +132,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
try
{
var toNotify =
new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, int Level,
new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, long Level,
XpNotificationLocation NotifyType, NotifOf NotifOf)>();
var roleRewards = new Dictionary<ulong, List<XpRoleReward>>();
var curRewards = new Dictionary<ulong, List<XpCurrencyReward>>();
@@ -640,7 +639,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
{
DiscordUser du;
UserXpStats stats;
int totalXp;
long totalXp;
int globalRank;
int guildRank;
await using (var uow = _db.GetDbContext())

View File

@@ -1,29 +1,15 @@
#nullable disable
using NadekoBot.Modules.Xp.Services;
using LinqToDB;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Modules.Xp.Extensions;
public static class Extensions
{
public static (int Level, int LevelXp, int LevelRequiredXp) GetLevelData(this UserXpStats stats)
{
var baseXp = XpService.XP_REQUIRED_LVL_1;
var required = baseXp;
var totalXp = 0;
var lvl = 1;
while (true)
{
required = (int)(baseXp + (baseXp / 4.0 * (lvl - 1)));
if (required + totalXp > stats.Xp)
break;
totalXp += required;
lvl++;
}
return (lvl - 1, stats.Xp - totalXp, required);
}
public static async Task<LevelStats> GetLevelDataFor(this ITable<UserXpStats> userXp, ulong guildId, ulong userId)
=> await userXp
.Where(x => x.GuildId == guildId && x.UserId == userId)
.FirstOrDefaultAsync() is UserXpStats uxs
? new(uxs.Xp + uxs.AwardedXp)
: new(0);
}

View File

@@ -5,12 +5,12 @@ namespace NadekoBot.Modules.Xp;
public class LevelStats
{
public int Level { get; }
public int LevelXp { get; }
public int RequiredXp { get; }
public int TotalXp { get; }
public long Level { get; }
public long LevelXp { get; }
public long RequiredXp { get; }
public long TotalXp { get; }
public LevelStats(int xp)
public LevelStats(long xp)
{
if (xp < 0)
xp = 0;