forked from EllieBotDevs/elliebot
1587 lines
No EOL
55 KiB
C#
1587 lines
No EOL
55 KiB
C#
#nullable disable warnings
|
|
using LinqToDB;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
using EllieBot.Db.Models;
|
|
using Newtonsoft.Json;
|
|
using SixLabors.Fonts;
|
|
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.Drawing.Processing;
|
|
using SixLabors.ImageSharp.Formats;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Processing;
|
|
using System.Threading.Channels;
|
|
using LinqToDB.EntityFrameworkCore;
|
|
using LinqToDB.Tools;
|
|
using EllieBot.Modules.Administration;
|
|
using EllieBot.Modules.Patronage;
|
|
using Color = SixLabors.ImageSharp.Color;
|
|
using Exception = System.Exception;
|
|
using Image = SixLabors.ImageSharp.Image;
|
|
|
|
namespace EllieBot.Modules.Xp.Services;
|
|
|
|
public class XpService : IEService, IReadyExecutor, IExecNoCommand
|
|
{
|
|
private readonly DbService _db;
|
|
private readonly IImageCache _images;
|
|
private readonly IBotStrings _strings;
|
|
private readonly FontProvider _fonts;
|
|
private readonly IBotCreds _creds;
|
|
private readonly ICurrencyService _cs;
|
|
private readonly IHttpClientFactory _httpFactory;
|
|
private readonly XpConfigService _xpConfig;
|
|
private readonly IPubSub _pubSub;
|
|
|
|
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedRoles;
|
|
private readonly ConcurrentDictionary<ulong, ConcurrentHashSet<ulong>> _excludedChannels;
|
|
private readonly ConcurrentHashSet<ulong> _excludedServers;
|
|
|
|
private XpTemplate template;
|
|
private readonly DiscordSocketClient _client;
|
|
|
|
private readonly TypedKey<bool> _xpTemplateReloadKey;
|
|
private readonly IPatronageService _ps;
|
|
private readonly IBotCache _c;
|
|
|
|
|
|
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
|
|
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
|
|
private readonly IMessageSenderService _sender;
|
|
private readonly INotifySubscriber _notifySub;
|
|
|
|
public XpService(
|
|
DiscordSocketClient client,
|
|
IBot bot,
|
|
DbService db,
|
|
IBotStrings strings,
|
|
IImageCache images,
|
|
IBotCache c,
|
|
FontProvider fonts,
|
|
IBotCreds creds,
|
|
ICurrencyService cs,
|
|
IHttpClientFactory http,
|
|
XpConfigService xpConfig,
|
|
IPubSub pubSub,
|
|
IPatronageService ps,
|
|
IMessageSenderService sender,
|
|
INotifySubscriber notifySub)
|
|
{
|
|
_db = db;
|
|
_images = images;
|
|
_strings = strings;
|
|
_fonts = fonts;
|
|
_creds = creds;
|
|
_cs = cs;
|
|
_httpFactory = http;
|
|
_xpConfig = xpConfig;
|
|
_pubSub = pubSub;
|
|
_sender = sender;
|
|
_notifySub = notifySub;
|
|
_excludedServers = new();
|
|
_excludedChannels = new();
|
|
_client = client;
|
|
_xpTemplateReloadKey = new("xp.template.reload");
|
|
_ps = ps;
|
|
_c = c;
|
|
|
|
InternalReloadXpTemplate();
|
|
|
|
if (client.ShardId == 0)
|
|
{
|
|
_pubSub.Sub(_xpTemplateReloadKey,
|
|
_ =>
|
|
{
|
|
InternalReloadXpTemplate();
|
|
return default;
|
|
});
|
|
}
|
|
|
|
//load settings
|
|
var allGuildConfigs = bot.AllGuildConfigs.Where(x => x.XpSettings is not null).ToList();
|
|
|
|
_excludedChannels = allGuildConfigs.ToDictionary(x => x.GuildId,
|
|
x => new ConcurrentHashSet<ulong>(x.XpSettings.ExclusionList
|
|
.Where(ex => ex.ItemType == ExcludedItemType.Channel)
|
|
.Select(ex => ex.ItemId)
|
|
.Distinct()))
|
|
.ToConcurrent();
|
|
|
|
_excludedRoles = allGuildConfigs.ToDictionary(x => x.GuildId,
|
|
x => new ConcurrentHashSet<ulong>(x.XpSettings.ExclusionList
|
|
.Where(ex => ex.ItemType
|
|
== ExcludedItemType.Role)
|
|
.Select(ex => ex.ItemId)
|
|
.Distinct()))
|
|
.ToConcurrent();
|
|
|
|
_excludedServers = new(allGuildConfigs.Where(x => x.XpSettings.ServerExcluded).Select(x => x.GuildId));
|
|
|
|
#if !GLOBAL_ELLIE
|
|
_client.UserVoiceStateUpdated += Client_OnUserVoiceStateUpdated;
|
|
|
|
// Scan guilds on startup.
|
|
_client.GuildAvailable += Client_OnGuildAvailable;
|
|
foreach (var guild in _client.Guilds)
|
|
Client_OnGuildAvailable(guild);
|
|
#endif
|
|
}
|
|
|
|
public async Task OnReadyAsync()
|
|
{
|
|
_ = Task.Run(() => _levelUpQueue.RunAsync());
|
|
|
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
|
|
while (await timer.WaitForNextTickAsync())
|
|
{
|
|
await UpdateXp();
|
|
}
|
|
}
|
|
|
|
private async Task UpdateXp()
|
|
{
|
|
try
|
|
{
|
|
var reader = _xpGainQueue.Reader;
|
|
|
|
// sum up all gains into a single UserCacheItem
|
|
var globalToAdd = new Dictionary<ulong, UserXpGainData>();
|
|
var guildToAdd = new Dictionary<ulong, Dictionary<ulong, UserXpGainData>>();
|
|
while (reader.TryRead(out var item))
|
|
{
|
|
// add global xp to these users
|
|
if (!globalToAdd.TryGetValue(item.User.Id, out var ci))
|
|
globalToAdd[item.User.Id] = item.Clone();
|
|
else
|
|
ci.XpAmount += item.XpAmount;
|
|
|
|
|
|
// ad guild xp in these guilds to these users
|
|
if (!guildToAdd.TryGetValue(item.Guild.Id, out var users))
|
|
users = guildToAdd[item.Guild.Id] = new();
|
|
|
|
if (!users.TryGetValue(item.User.Id, out ci))
|
|
users[item.User.Id] = item.Clone();
|
|
else
|
|
ci.XpAmount += item.XpAmount;
|
|
}
|
|
|
|
var dus = new List<DiscordUser>(globalToAdd.Count);
|
|
var gxps = new List<UserXpStats>(globalToAdd.Count);
|
|
var conf = _xpConfig.Data;
|
|
await using (var ctx = _db.GetDbContext())
|
|
{
|
|
if (conf.CurrencyPerXp > 0)
|
|
{
|
|
foreach (var user in globalToAdd)
|
|
{
|
|
var amount = (long)(user.Value.XpAmount * conf.CurrencyPerXp);
|
|
if (amount > 0)
|
|
await _cs.AddAsync(user.Key, amount, null);
|
|
}
|
|
}
|
|
|
|
// update global user xp in batches
|
|
// group by xp amount and update the same amounts at the same time
|
|
foreach (var group in globalToAdd.GroupBy(x => x.Value.XpAmount, x => x.Key))
|
|
{
|
|
var items = await ctx.Set<DiscordUser>()
|
|
.Where(x => group.Contains(x.UserId))
|
|
.UpdateWithOutputAsync(old => new()
|
|
{
|
|
TotalXp = old.TotalXp + group.Key
|
|
},
|
|
(_, n) => n);
|
|
|
|
await ctx.Set<ClubInfo>()
|
|
.Where(x => x.Members.Any(m => group.Contains(m.UserId)))
|
|
.UpdateAsync(old => new()
|
|
{
|
|
Xp = old.Xp + (group.Key * old.Members.Count(m => group.Contains(m.UserId)))
|
|
});
|
|
|
|
dus.AddRange(items);
|
|
}
|
|
|
|
// update guild user xp in batches
|
|
foreach (var (guildId, toAdd) in guildToAdd)
|
|
{
|
|
foreach (var group in toAdd.GroupBy(x => x.Value.XpAmount, x => x.Key))
|
|
{
|
|
var items = await ctx
|
|
.Set<UserXpStats>()
|
|
.Where(x => x.GuildId == guildId)
|
|
.Where(x => group.Contains(x.UserId))
|
|
.UpdateWithOutputAsync(old => new()
|
|
{
|
|
Xp = old.Xp + group.Key
|
|
},
|
|
(_, n) => n);
|
|
|
|
gxps.AddRange(items);
|
|
|
|
var missingUserIds = group.Where(userId => !items.Any(x => x.UserId == userId)).ToArray();
|
|
foreach (var userId in missingUserIds)
|
|
{
|
|
await ctx
|
|
.Set<UserXpStats>()
|
|
.ToLinqToDBTable()
|
|
.InsertOrUpdateAsync(() => new UserXpStats()
|
|
{
|
|
UserId = userId,
|
|
GuildId = guildId,
|
|
Xp = group.Key,
|
|
DateAdded = DateTime.UtcNow,
|
|
},
|
|
_ => new()
|
|
{
|
|
},
|
|
() => new()
|
|
{
|
|
UserId = userId
|
|
});
|
|
}
|
|
|
|
if (missingUserIds.Length > 0)
|
|
{
|
|
var missingItems = await ctx.Set<UserXpStats>()
|
|
.ToLinqToDBTable()
|
|
.Where(x => missingUserIds.Contains(x.UserId))
|
|
.ToArrayAsyncLinqToDB();
|
|
|
|
gxps.AddRange(missingItems);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var du in dus)
|
|
{
|
|
var oldLevel = new LevelStats(du.TotalXp - globalToAdd[du.UserId].XpAmount);
|
|
var newLevel = new LevelStats(du.TotalXp);
|
|
|
|
if (oldLevel.Level != newLevel.Level)
|
|
{
|
|
var item = globalToAdd[du.UserId];
|
|
await _levelUpQueue.EnqueueAsync(
|
|
NotifyUser(item.Guild.Id,
|
|
item.Channel.Id,
|
|
du.UserId,
|
|
false,
|
|
oldLevel.Level,
|
|
newLevel.Level));
|
|
}
|
|
}
|
|
|
|
foreach (var du in gxps)
|
|
{
|
|
if (guildToAdd.TryGetValue(du.GuildId, out var users)
|
|
&& users.TryGetValue(du.UserId, out var xpGainData))
|
|
{
|
|
var oldLevel = new LevelStats(du.Xp - xpGainData.XpAmount);
|
|
var newLevel = new LevelStats(du.Xp);
|
|
|
|
if (oldLevel.Level < newLevel.Level)
|
|
{
|
|
await _levelUpQueue.EnqueueAsync(
|
|
NotifyUser(xpGainData.Guild.Id,
|
|
xpGainData.Channel.Id,
|
|
du.UserId,
|
|
true,
|
|
oldLevel.Level,
|
|
newLevel.Level));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Error In the XP update loop");
|
|
}
|
|
}
|
|
|
|
private Func<Task> NotifyUser(
|
|
ulong guildId,
|
|
ulong channelId,
|
|
ulong userId,
|
|
bool isServer,
|
|
long oldLevel,
|
|
long newLevel)
|
|
=> async () =>
|
|
{
|
|
if (isServer)
|
|
{
|
|
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
|
|
}
|
|
|
|
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel);
|
|
};
|
|
|
|
private async Task HandleRewardsInternalAsync(
|
|
ulong guildId,
|
|
ulong userId,
|
|
long oldLevel,
|
|
long newLevel)
|
|
{
|
|
List<XpRoleReward> rrews;
|
|
List<XpCurrencyReward> crews;
|
|
await using (var ctx = _db.GetDbContext())
|
|
{
|
|
rrews = ctx.XpSettingsFor(guildId).RoleRewards.ToList();
|
|
crews = ctx.XpSettingsFor(guildId).CurrencyRewards.ToList();
|
|
}
|
|
|
|
//loop through levels since last level up, so if a high amount of xp is gained, reward are still applied.
|
|
for (var i = oldLevel + 1; i <= newLevel; i++)
|
|
{
|
|
var rrew = rrews.FirstOrDefault(x => x.Level == i);
|
|
if (rrew is not null)
|
|
{
|
|
var guild = _client.GetGuild(guildId);
|
|
var role = guild?.GetRole(rrew.RoleId);
|
|
var user = guild?.GetUser(userId);
|
|
|
|
if (role is not null && user is not null)
|
|
{
|
|
if (rrew.Remove)
|
|
_ = user.RemoveRoleAsync(role);
|
|
else
|
|
_ = user.AddRoleAsync(role);
|
|
}
|
|
}
|
|
|
|
//get currency reward for this level
|
|
var crew = crews.FirstOrDefault(x => x.Level == i);
|
|
if (crew is not null)
|
|
{
|
|
//give the user the reward if it exists
|
|
await _cs.AddAsync(userId, crew.Amount, new("xp", "level-up"));
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HandleNotifyInternalAsync(
|
|
ulong guildId,
|
|
ulong channelId,
|
|
ulong userId,
|
|
bool isServer,
|
|
long newLevel)
|
|
{
|
|
var guild = _client.GetGuild(guildId);
|
|
var user = guild?.GetUser(userId);
|
|
|
|
if (guild is null || user is null)
|
|
return;
|
|
|
|
if (isServer)
|
|
{
|
|
var model = new LevelUpNotifyModel()
|
|
{
|
|
GuildId = guildId,
|
|
UserId = userId,
|
|
ChannelId = channelId,
|
|
Level = newLevel
|
|
};
|
|
await _notifySub.NotifyAsync(model, true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
private const string XP_TEMPLATE_PATH = "./data/xp_template.json";
|
|
|
|
private void InternalReloadXpTemplate()
|
|
{
|
|
try
|
|
{
|
|
var settings = new JsonSerializerSettings
|
|
{
|
|
ContractResolver = new RequireObjectPropertiesContractResolver()
|
|
};
|
|
|
|
if (!File.Exists(XP_TEMPLATE_PATH))
|
|
{
|
|
var newTemp = new XpTemplate();
|
|
newTemp.Version = 1;
|
|
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(newTemp, Formatting.Indented));
|
|
}
|
|
|
|
template = JsonConvert.DeserializeObject<XpTemplate>(
|
|
File.ReadAllText(XP_TEMPLATE_PATH),
|
|
settings)!;
|
|
|
|
if (template.Version < 1)
|
|
{
|
|
Log.Warning("Loaded default xp_template.json values as the old one was version 0. "
|
|
+ "Old one was renamed to xp_template.json.old");
|
|
File.WriteAllText("./data/xp_template.json.old",
|
|
JsonConvert.SerializeObject(template, Formatting.Indented));
|
|
template = new();
|
|
template.Version = 1;
|
|
File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(template, Formatting.Indented));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "xp_template.json is invalid. Loaded default values");
|
|
template = new();
|
|
template.Version = 1;
|
|
}
|
|
}
|
|
|
|
public void ReloadXpTemplate()
|
|
=> _pubSub.Pub(_xpTemplateReloadKey, true);
|
|
|
|
public void SetCurrencyReward(ulong guildId, int level, int amount)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
var settings = uow.XpSettingsFor(guildId);
|
|
|
|
if (amount <= 0)
|
|
{
|
|
var toRemove = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level);
|
|
if (toRemove is not null)
|
|
{
|
|
uow.Remove(toRemove);
|
|
settings.CurrencyRewards.Remove(toRemove);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var rew = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level);
|
|
|
|
if (rew is not null)
|
|
rew.Amount = amount;
|
|
else
|
|
{
|
|
settings.CurrencyRewards.Add(new()
|
|
{
|
|
Level = level,
|
|
Amount = amount
|
|
});
|
|
}
|
|
}
|
|
|
|
uow.SaveChanges();
|
|
}
|
|
|
|
public IEnumerable<XpCurrencyReward> GetCurrencyRewards(ulong id)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
return uow.XpSettingsFor(id).CurrencyRewards.ToArray();
|
|
}
|
|
|
|
public IEnumerable<XpRoleReward> GetRoleRewards(ulong id)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
return uow.XpSettingsFor(id).RoleRewards.ToArray();
|
|
}
|
|
|
|
public void ResetRoleReward(ulong guildId, int level)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
var settings = uow.XpSettingsFor(guildId);
|
|
|
|
var toRemove = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
|
|
if (toRemove is not null)
|
|
{
|
|
uow.Remove(toRemove);
|
|
settings.RoleRewards.Remove(toRemove);
|
|
}
|
|
|
|
uow.SaveChanges();
|
|
}
|
|
|
|
public void SetRoleReward(
|
|
ulong guildId,
|
|
int level,
|
|
ulong roleId,
|
|
bool remove)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
var settings = uow.XpSettingsFor(guildId);
|
|
|
|
|
|
var rew = settings.RoleRewards.FirstOrDefault(x => x.Level == level);
|
|
|
|
if (rew is not null)
|
|
{
|
|
rew.RoleId = roleId;
|
|
rew.Remove = remove;
|
|
}
|
|
else
|
|
{
|
|
settings.RoleRewards.Add(new()
|
|
{
|
|
Level = level,
|
|
RoleId = roleId,
|
|
Remove = remove,
|
|
});
|
|
}
|
|
|
|
uow.SaveChanges();
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, int page)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
return await uow
|
|
.UserXpStats
|
|
.Where(x => x.GuildId == guildId)
|
|
.OrderByDescending(x => x.Xp)
|
|
.Skip(page * 10)
|
|
.Take(10)
|
|
.ToArrayAsyncLinqToDB();
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<UserXpStats>> GetGuildUserXps(ulong guildId, List<ulong> users, int page)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
return await uow.Set<UserXpStats>()
|
|
.Where(x => x.GuildId == guildId && x.UserId.In(users))
|
|
.OrderByDescending(x => x.Xp)
|
|
.Skip(page * 10)
|
|
.Take(10)
|
|
.ToArrayAsyncLinqToDB();
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
|
|
return await uow.GetTable<DiscordUser>()
|
|
.OrderByDescending(x => x.TotalXp)
|
|
.Skip(page * 10)
|
|
.Take(10)
|
|
.ToArrayAsyncLinqToDB();
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<DiscordUser>> GetGlobalUserXps(int page, List<ulong> users)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
|
|
return await uow.GetTable<DiscordUser>()
|
|
.Where(x => x.UserId.In(users))
|
|
.OrderByDescending(x => x.TotalXp)
|
|
.Skip(page * 10)
|
|
.Take(10)
|
|
.ToArrayAsyncLinqToDB();
|
|
}
|
|
|
|
private Task Client_OnGuildAvailable(SocketGuild guild)
|
|
{
|
|
Task.Run(async () =>
|
|
{
|
|
foreach (var channel in guild.VoiceChannels)
|
|
await ScanChannelForVoiceXp(channel);
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private Task Client_OnUserVoiceStateUpdated(SocketUser socketUser, SocketVoiceState before, SocketVoiceState after)
|
|
{
|
|
if (socketUser is not SocketGuildUser user || user.IsBot)
|
|
return Task.CompletedTask;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
if (before.VoiceChannel is not null)
|
|
await ScanChannelForVoiceXp(before.VoiceChannel);
|
|
|
|
if (after.VoiceChannel is not null && after.VoiceChannel != before.VoiceChannel)
|
|
{
|
|
await ScanChannelForVoiceXp(after.VoiceChannel);
|
|
}
|
|
else if (after.VoiceChannel is null && before.VoiceChannel is not null)
|
|
{
|
|
// In this case, the user left the channel and the previous for loops didn't catch
|
|
// it because it wasn't in any new channel. So we need to get rid of it.
|
|
await UserLeftVoiceChannel(user, before.VoiceChannel);
|
|
}
|
|
});
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private async Task ScanChannelForVoiceXp(SocketVoiceChannel channel)
|
|
{
|
|
if (ShouldTrackVoiceChannel(channel))
|
|
{
|
|
foreach (var user in channel.ConnectedUsers)
|
|
await ScanUserForVoiceXp(user, channel);
|
|
}
|
|
else
|
|
{
|
|
foreach (var user in channel.ConnectedUsers)
|
|
await UserLeftVoiceChannel(user, channel);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Assumes that the channel itself is valid and adding xp.
|
|
/// </summary>
|
|
/// <param name="user"></param>
|
|
/// <param name="channel"></param>
|
|
private async Task ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel)
|
|
{
|
|
if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel))
|
|
await UserJoinedVoiceChannel(user);
|
|
else
|
|
await UserLeftVoiceChannel(user, channel);
|
|
}
|
|
|
|
private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel)
|
|
=> channel.ConnectedUsers.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2;
|
|
|
|
private bool UserParticipatingInVoiceChannel(SocketGuildUser user)
|
|
=> !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted;
|
|
|
|
private TypedKey<long> GetVoiceXpKey(ulong userId)
|
|
=> new($"xp:{_client.CurrentUser.Id}:vc_join:{userId}");
|
|
|
|
private async Task UserJoinedVoiceChannel(SocketGuildUser user)
|
|
{
|
|
var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
|
|
await _c.AddAsync(GetVoiceXpKey(user.Id),
|
|
value,
|
|
TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
|
|
overwrite: false);
|
|
}
|
|
|
|
// private void UserJoinedVoiceChannel(SocketGuildUser user)
|
|
// {
|
|
// var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
|
|
// var value = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
//
|
|
// _cache.Redis.GetDatabase()
|
|
// .StringSet(key,
|
|
// value,
|
|
// TimeSpan.FromMinutes(_xpConfig.Data.VoiceMaxMinutes),
|
|
// when: When.NotExists);
|
|
// }
|
|
|
|
private async Task UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
|
{
|
|
var key = GetVoiceXpKey(user.Id);
|
|
var result = await _c.GetAsync(key);
|
|
if (!await _c.RemoveAsync(key))
|
|
return;
|
|
|
|
// Allow for if this function gets called multiple times when a user leaves a channel.
|
|
if (!result.TryGetValue(out var unixTime))
|
|
return;
|
|
|
|
var dateStart = DateTimeOffset.FromUnixTimeSeconds(unixTime);
|
|
var dateEnd = DateTimeOffset.UtcNow;
|
|
var minutes = (dateEnd - dateStart).TotalMinutes;
|
|
var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
|
|
var actualXp = (int)Math.Floor(xp);
|
|
|
|
if (actualXp > 0)
|
|
{
|
|
Log.Information("Adding {Amount} voice xp to {User}", actualXp, user.ToString());
|
|
await _xpGainQueue.Writer.WriteAsync(new()
|
|
{
|
|
Guild = channel.Guild,
|
|
User = user,
|
|
XpAmount = actualXp,
|
|
Channel = channel
|
|
});
|
|
}
|
|
}
|
|
|
|
/*
|
|
* private void UserLeftVoiceChannel(SocketGuildUser user, SocketVoiceChannel channel)
|
|
{
|
|
var key = $"{_creds.RedisKey()}_user_xp_vc_join_{user.Id}";
|
|
var value = _cache.Redis.GetDatabase().StringGet(key);
|
|
_cache.Redis.GetDatabase().KeyDelete(key);
|
|
|
|
// Allow for if this function gets called multiple times when a user leaves a channel.
|
|
if (value.IsNull)
|
|
return;
|
|
|
|
if (!value.TryParse(out long startUnixTime))
|
|
return;
|
|
|
|
var dateStart = DateTimeOffset.FromUnixTimeSeconds(startUnixTime);
|
|
var dateEnd = DateTimeOffset.UtcNow;
|
|
var minutes = (dateEnd - dateStart).TotalMinutes;
|
|
var xp = _xpConfig.Data.VoiceXpPerMinute * minutes;
|
|
var actualXp = (int)Math.Floor(xp);
|
|
|
|
if (actualXp > 0)
|
|
{
|
|
_addMessageXp.Enqueue(new()
|
|
{
|
|
Guild = channel.Guild,
|
|
User = user,
|
|
XpAmount = actualXp
|
|
});
|
|
}
|
|
}
|
|
*/
|
|
|
|
private bool ShouldTrackXp(SocketGuildUser user, IMessageChannel channel)
|
|
{
|
|
var channelId = channel.Id;
|
|
|
|
if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans)
|
|
&& (chans.Contains(channelId)
|
|
|| (channel is SocketThreadChannel tc && chans.Contains(tc.ParentChannel.Id))))
|
|
return false;
|
|
|
|
if (_excludedServers.Contains(user.Guild.Id))
|
|
return false;
|
|
|
|
if (_excludedRoles.TryGetValue(user.Guild.Id, out var roles) && user.Roles.Any(x => roles.Contains(x.Id)))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
public Task ExecOnNoCommandAsync(IGuild guild, IUserMessage arg)
|
|
{
|
|
if (arg.Author is not SocketGuildUser user || user.IsBot)
|
|
return Task.CompletedTask;
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
if (!ShouldTrackXp(user, arg.Channel))
|
|
return;
|
|
|
|
var xpConf = _xpConfig.Data;
|
|
var xp = 0;
|
|
if (arg.Attachments.Any(a => a.Height >= 128 && a.Width >= 128))
|
|
xp = xpConf.XpFromImage;
|
|
|
|
if (arg.Content.Contains(' ') || arg.Content.Length >= 5)
|
|
xp = Math.Max(xp, xpConf.XpPerMessage);
|
|
|
|
if (xp <= 0)
|
|
return;
|
|
|
|
if (!await SetUserRewardedAsync(user.Id))
|
|
return;
|
|
|
|
await _xpGainQueue.Writer.WriteAsync(new()
|
|
{
|
|
Guild = user.Guild,
|
|
Channel = arg.Channel,
|
|
User = user,
|
|
XpAmount = xp
|
|
});
|
|
});
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// public void AddXpDirectly(IGuildUser user, IMessageChannel channel, int amount)
|
|
// {
|
|
// if (amount <= 0)
|
|
// throw new ArgumentOutOfRangeException(nameof(amount));
|
|
//
|
|
// _xpGainQueue.Writer.WriteAsync(new()
|
|
// {
|
|
// Guild = user.Guild,
|
|
// Channel = channel,
|
|
// User = user,
|
|
// XpAmount = amount
|
|
// });
|
|
// }
|
|
|
|
public async Task<int> AddXpToUsersAsync(ulong guildId, long amount, params ulong[] userIds)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
return await ctx.GetTable<UserXpStats>()
|
|
.Where(x => x.GuildId == guildId && userIds.Contains(x.UserId))
|
|
.UpdateAsync(old => new()
|
|
{
|
|
Xp = old.Xp + amount
|
|
});
|
|
}
|
|
|
|
public void AddXp(ulong userId, ulong guildId, int amount)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
var usr = uow.GetOrCreateUserXpStats(guildId, userId);
|
|
|
|
usr.Xp += amount;
|
|
|
|
uow.SaveChanges();
|
|
}
|
|
|
|
public bool IsServerExcluded(ulong id)
|
|
=> _excludedServers.Contains(id);
|
|
|
|
public IEnumerable<ulong> GetExcludedRoles(ulong id)
|
|
{
|
|
if (_excludedRoles.TryGetValue(id, out var val))
|
|
return val.ToArray();
|
|
|
|
return Enumerable.Empty<ulong>();
|
|
}
|
|
|
|
public IEnumerable<ulong> GetExcludedChannels(ulong id)
|
|
{
|
|
if (_excludedChannels.TryGetValue(id, out var val))
|
|
return val.ToArray();
|
|
|
|
return Enumerable.Empty<ulong>();
|
|
}
|
|
|
|
private TypedKey<bool> GetUserRewKey(ulong userId)
|
|
=> new($"xp:{_client.CurrentUser.Id}:user_gain:{userId}");
|
|
|
|
private async Task<bool> SetUserRewardedAsync(ulong userId)
|
|
=> await _c.AddAsync(GetUserRewKey(userId),
|
|
true,
|
|
expiry: TimeSpan.FromSeconds(_xpConfig.Data.MessageXpCooldown),
|
|
overwrite: false);
|
|
|
|
public async Task<FullUserStats> GetUserStatsAsync(IGuildUser user)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
var du = uow.GetOrCreateUser(user, set => set.Include(x => x.Club));
|
|
var totalXp = du.TotalXp;
|
|
var globalRank = uow.Set<DiscordUser>().GetUserGlobalRank(user.Id);
|
|
var guildRank = await uow.Set<UserXpStats>().GetUserGuildRanking(user.Id, user.GuildId);
|
|
var stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id);
|
|
await uow.SaveChangesAsync();
|
|
|
|
return new(du,
|
|
stats,
|
|
new(totalXp),
|
|
new(stats.Xp),
|
|
globalRank,
|
|
guildRank);
|
|
}
|
|
|
|
public bool ToggleExcludeServer(ulong id)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
var xpSetting = uow.XpSettingsFor(id);
|
|
if (_excludedServers.Add(id))
|
|
{
|
|
xpSetting.ServerExcluded = true;
|
|
uow.SaveChanges();
|
|
return true;
|
|
}
|
|
|
|
_excludedServers.TryRemove(id);
|
|
xpSetting.ServerExcluded = false;
|
|
uow.SaveChanges();
|
|
return false;
|
|
}
|
|
|
|
public bool ToggleExcludeRole(ulong guildId, ulong rId)
|
|
{
|
|
var roles = _excludedRoles.GetOrAdd(guildId, _ => new());
|
|
using var uow = _db.GetDbContext();
|
|
var xpSetting = uow.XpSettingsFor(guildId);
|
|
var excludeObj = new ExcludedItem
|
|
{
|
|
ItemId = rId,
|
|
ItemType = ExcludedItemType.Role
|
|
};
|
|
|
|
if (roles.Add(rId))
|
|
{
|
|
if (xpSetting.ExclusionList.Add(excludeObj))
|
|
uow.SaveChanges();
|
|
|
|
return true;
|
|
}
|
|
|
|
roles.TryRemove(rId);
|
|
|
|
var toDelete = xpSetting.ExclusionList.FirstOrDefault(x => x.Equals(excludeObj));
|
|
if (toDelete is not null)
|
|
{
|
|
uow.Remove(toDelete);
|
|
uow.SaveChanges();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool ToggleExcludeChannel(ulong guildId, ulong chId)
|
|
{
|
|
var channels = _excludedChannels.GetOrAdd(guildId, _ => new());
|
|
using var uow = _db.GetDbContext();
|
|
var xpSetting = uow.XpSettingsFor(guildId);
|
|
var excludeObj = new ExcludedItem
|
|
{
|
|
ItemId = chId,
|
|
ItemType = ExcludedItemType.Channel
|
|
};
|
|
|
|
if (channels.Add(chId))
|
|
{
|
|
if (xpSetting.ExclusionList.Add(excludeObj))
|
|
uow.SaveChanges();
|
|
|
|
return true;
|
|
}
|
|
|
|
channels.TryRemove(chId);
|
|
|
|
if (xpSetting.ExclusionList.Remove(excludeObj))
|
|
uow.SaveChanges();
|
|
|
|
return false;
|
|
}
|
|
|
|
public async Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(IGuildUser user)
|
|
{
|
|
var stats = await GetUserStatsAsync(user);
|
|
return await GenerateXpImageAsync(stats);
|
|
}
|
|
|
|
|
|
public Task<(Stream Image, IImageFormat Format)> GenerateXpImageAsync(FullUserStats stats)
|
|
=> Task.Run(async () =>
|
|
{
|
|
var bgBytes = await GetXpBackgroundAsync(stats.User.UserId);
|
|
|
|
if (bgBytes is null)
|
|
{
|
|
Log.Warning("Xp background image could not be loaded");
|
|
throw new ArgumentNullException(nameof(bgBytes));
|
|
}
|
|
|
|
var outlinePen = new SolidPen(Color.Black, 1f);
|
|
|
|
using var img = Image.Load<Rgba32>(bgBytes);
|
|
if (template.User.Name.Show)
|
|
{
|
|
var fontSize = (int)(template.User.Name.FontSize * 0.9);
|
|
var username = stats.User.ToString();
|
|
var usernameFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
|
|
|
|
var size = TextMeasurer.MeasureSize($"@{username}", new(usernameFont));
|
|
var scale = 400f / size.Width;
|
|
if (scale < 1)
|
|
usernameFont = _fonts.NotoSans.CreateFont(template.User.Name.FontSize * scale, FontStyle.Bold);
|
|
|
|
img.Mutate(x =>
|
|
{
|
|
x.DrawText(new RichTextOptions(usernameFont)
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
FallbackFontFamilies = _fonts.FallBackFonts,
|
|
Origin = new(template.User.Name.Pos.X, template.User.Name.Pos.Y + 8)
|
|
},
|
|
"@" + username,
|
|
Brushes.Solid(template.User.Name.Color),
|
|
outlinePen);
|
|
});
|
|
}
|
|
|
|
//club name
|
|
|
|
if (template.Club.Name.Show)
|
|
{
|
|
var clubName = stats.User.Club?.ToString() ?? "-";
|
|
|
|
var clubFont = _fonts.NotoSans.CreateFont(template.Club.Name.FontSize, FontStyle.Regular);
|
|
|
|
img.Mutate(x => x.DrawText(new RichTextOptions(clubFont)
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
VerticalAlignment = VerticalAlignment.Top,
|
|
FallbackFontFamilies = _fonts.FallBackFonts,
|
|
Origin = new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8)
|
|
},
|
|
clubName,
|
|
Brushes.Solid(template.Club.Name.Color),
|
|
outlinePen));
|
|
}
|
|
|
|
Font GetTruncatedFont(
|
|
FontFamily fontFamily,
|
|
int fontSize,
|
|
FontStyle style,
|
|
string text,
|
|
int maxSize)
|
|
{
|
|
var font = fontFamily.CreateFont(fontSize, style);
|
|
var size = TextMeasurer.MeasureSize(text, new(font));
|
|
var scale = maxSize / size.Width;
|
|
if (scale < 1)
|
|
font = fontFamily.CreateFont(fontSize * scale, style);
|
|
|
|
return font;
|
|
}
|
|
|
|
|
|
if (template.User.GlobalLevel.Show)
|
|
{
|
|
// up to 83 width
|
|
|
|
var globalLevelFont = GetTruncatedFont(
|
|
_fonts.NotoSans,
|
|
template.User.GlobalLevel.FontSize,
|
|
FontStyle.Bold,
|
|
stats.Global.Level.ToString(),
|
|
75);
|
|
|
|
img.Mutate(x =>
|
|
{
|
|
x.DrawText(stats.Global.Level.ToString(),
|
|
globalLevelFont,
|
|
template.User.GlobalLevel.Color,
|
|
new(template.User.GlobalLevel.Pos.X, template.User.GlobalLevel.Pos.Y)); //level
|
|
});
|
|
}
|
|
|
|
if (template.User.GuildLevel.Show)
|
|
{
|
|
var guildLevelFont = GetTruncatedFont(
|
|
_fonts.NotoSans,
|
|
template.User.GuildLevel.FontSize,
|
|
FontStyle.Bold,
|
|
stats.Guild.Level.ToString(),
|
|
75);
|
|
|
|
img.Mutate(x =>
|
|
{
|
|
x.DrawText(stats.Guild.Level.ToString(),
|
|
guildLevelFont,
|
|
template.User.GuildLevel.Color,
|
|
new(template.User.GuildLevel.Pos.X, template.User.GuildLevel.Pos.Y));
|
|
});
|
|
}
|
|
|
|
|
|
var global = stats.Global;
|
|
var guild = stats.Guild;
|
|
|
|
//xp bar
|
|
if (template.User.Xp.Bar.Show)
|
|
{
|
|
var xpPercent = global.LevelXp / (float)global.RequiredXp;
|
|
DrawXpBar(xpPercent, template.User.Xp.Bar.Global, img);
|
|
xpPercent = guild.LevelXp / (float)guild.RequiredXp;
|
|
DrawXpBar(xpPercent, template.User.Xp.Bar.Guild, img);
|
|
}
|
|
|
|
if (template.User.Xp.Global.Show)
|
|
{
|
|
img.Mutate(x => x.DrawText(
|
|
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Global.FontSize, FontStyle.Bold))
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Origin = new(template.User.Xp.Global.Pos.X, template.User.Xp.Global.Pos.Y),
|
|
},
|
|
$"{global.LevelXp}/{global.RequiredXp}",
|
|
Brushes.Solid(template.User.Xp.Global.Color),
|
|
outlinePen));
|
|
}
|
|
|
|
if (template.User.Xp.Guild.Show)
|
|
{
|
|
img.Mutate(x => x.DrawText(
|
|
new RichTextOptions(_fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize, FontStyle.Bold))
|
|
{
|
|
HorizontalAlignment = HorizontalAlignment.Center,
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
Origin = new(template.User.Xp.Guild.Pos.X, template.User.Xp.Guild.Pos.Y)
|
|
},
|
|
$"{guild.LevelXp}/{guild.RequiredXp}",
|
|
Brushes.Solid(template.User.Xp.Guild.Color),
|
|
outlinePen));
|
|
}
|
|
|
|
var rankPen = new SolidPen(Color.White, 1);
|
|
//ranking
|
|
if (template.User.GlobalRank.Show)
|
|
{
|
|
var globalRankStr = stats.GlobalRanking.ToString();
|
|
|
|
var globalRankFont = GetTruncatedFont(
|
|
_fonts.UniSans,
|
|
template.User.GlobalRank.FontSize,
|
|
FontStyle.Bold,
|
|
globalRankStr,
|
|
68);
|
|
|
|
img.Mutate(x => x.DrawText(
|
|
new RichTextOptions(globalRankFont)
|
|
{
|
|
Origin = new(template.User.GlobalRank.Pos.X, template.User.GlobalRank.Pos.Y)
|
|
},
|
|
globalRankStr,
|
|
Brushes.Solid(template.User.GlobalRank.Color),
|
|
rankPen
|
|
));
|
|
}
|
|
|
|
if (template.User.GuildRank.Show)
|
|
{
|
|
var guildRankStr = stats.GuildRanking.ToString();
|
|
|
|
var guildRankFont = GetTruncatedFont(
|
|
_fonts.UniSans,
|
|
template.User.GuildRank.FontSize,
|
|
FontStyle.Bold,
|
|
guildRankStr,
|
|
43);
|
|
|
|
img.Mutate(x => x.DrawText(
|
|
new RichTextOptions(guildRankFont)
|
|
{
|
|
Origin = new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y)
|
|
},
|
|
guildRankStr,
|
|
Brushes.Solid(template.User.GuildRank.Color),
|
|
rankPen
|
|
));
|
|
}
|
|
|
|
//avatar
|
|
if (template.User.Icon.Show)
|
|
{
|
|
try
|
|
{
|
|
var avatarUrl = stats.User.RealAvatarUrl();
|
|
|
|
if (avatarUrl is not null)
|
|
{
|
|
var result = await _c.GetImageDataAsync(avatarUrl);
|
|
if (!result.TryPickT0(out var data, out _))
|
|
{
|
|
using (var http = _httpFactory.CreateClient())
|
|
{
|
|
var avatarData = await http.GetByteArrayAsync(avatarUrl);
|
|
using (var tempDraw = Image.Load<Rgba32>(avatarData))
|
|
{
|
|
tempDraw.Mutate(x => x
|
|
.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y)
|
|
.ApplyRoundedCorners(Math.Max(template.User.Icon.Size.X,
|
|
template.User.Icon.Size.Y)
|
|
/ 2.0f));
|
|
await using (var stream = await tempDraw.ToStreamAsync())
|
|
{
|
|
data = stream.ToArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
await _c.SetImageDataAsync(avatarUrl, data);
|
|
}
|
|
|
|
using var toDraw = Image.Load(data);
|
|
if (toDraw.Size != new Size(template.User.Icon.Size.X, template.User.Icon.Size.Y))
|
|
toDraw.Mutate(x => x.Resize(template.User.Icon.Size.X, template.User.Icon.Size.Y));
|
|
|
|
img.Mutate(x => x.DrawImage(toDraw,
|
|
new Point(template.User.Icon.Pos.X, template.User.Icon.Pos.Y),
|
|
1));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Error drawing avatar image");
|
|
}
|
|
}
|
|
|
|
//club image
|
|
if (template.Club.Icon.Show)
|
|
await DrawClubImage(img, stats);
|
|
|
|
// #if GLOBAL_ELLIE
|
|
await DrawFrame(img, stats.User.UserId);
|
|
// #endif
|
|
|
|
var outputSize = template.OutputSize;
|
|
if (outputSize.X != img.Width || outputSize.Y != img.Height)
|
|
img.Mutate(x => x.Resize(template.OutputSize.X, template.OutputSize.Y));
|
|
|
|
var imageFormat = img.Metadata.DecodedImageFormat;
|
|
var output = ((Stream)await img.ToStreamAsync(imageFormat), imageFormat);
|
|
|
|
return output;
|
|
});
|
|
|
|
private async Task<byte[]?> GetXpBackgroundAsync(ulong userId)
|
|
{
|
|
var item = await GetItemInUse(userId, XpShopItemType.Background);
|
|
if (item is null)
|
|
{
|
|
return await _images.GetXpBackgroundImageAsync();
|
|
}
|
|
|
|
var url = _xpConfig.Data.Shop.GetItemUrl(XpShopItemType.Background, item.ItemKey);
|
|
if (!string.IsNullOrWhiteSpace(url))
|
|
{
|
|
var data = await _images.GetImageDataAsync(new Uri(url));
|
|
return data;
|
|
}
|
|
|
|
return await _images.GetXpBackgroundImageAsync();
|
|
}
|
|
|
|
// #if GLOBAL_ELLIE
|
|
private async Task DrawFrame(Image<Rgba32> img, ulong userId)
|
|
{
|
|
var patron = await _ps.GetPatronAsync(userId);
|
|
|
|
var item = await GetItemInUse(userId, XpShopItemType.Frame);
|
|
|
|
Image? frame = null;
|
|
if (item is null)
|
|
{
|
|
if (patron?.Tier == PatronTier.V)
|
|
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
|
|
else if (patron?.Tier >= PatronTier.X || _creds.IsOwner(userId))
|
|
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
|
|
}
|
|
else
|
|
{
|
|
var url = _xpConfig.Data.Shop.GetItemUrl(XpShopItemType.Frame, item.ItemKey);
|
|
if (!string.IsNullOrWhiteSpace(url))
|
|
{
|
|
var data = await _images.GetImageDataAsync(new Uri(url));
|
|
frame = Image.Load<Rgba32>(data);
|
|
}
|
|
}
|
|
|
|
if (frame is not null)
|
|
img.Mutate(x => x.DrawImage(frame, new Point(0, 0), new GraphicsOptions()));
|
|
}
|
|
// #endif
|
|
|
|
private void DrawXpBar(float percent, XpBar info, Image<Rgba32> img)
|
|
{
|
|
var x1 = info.PointA.X;
|
|
var y1 = info.PointA.Y;
|
|
|
|
var x2 = info.PointB.X;
|
|
var y2 = info.PointB.Y;
|
|
|
|
var length = info.Length * percent;
|
|
|
|
float x3, x4, y3, y4;
|
|
|
|
if (info.Direction == XpTemplateDirection.Down)
|
|
{
|
|
x3 = x1;
|
|
x4 = x2;
|
|
y3 = y1 + length;
|
|
y4 = y2 + length;
|
|
}
|
|
else if (info.Direction == XpTemplateDirection.Up)
|
|
{
|
|
x3 = x1;
|
|
x4 = x2;
|
|
y3 = y1 - length;
|
|
y4 = y2 - length;
|
|
}
|
|
else if (info.Direction == XpTemplateDirection.Left)
|
|
{
|
|
x3 = x1 - length;
|
|
x4 = x2 - length;
|
|
y3 = y1;
|
|
y4 = y2;
|
|
}
|
|
else
|
|
{
|
|
x3 = x1 + length;
|
|
x4 = x2 + length;
|
|
y3 = y1;
|
|
y4 = y2;
|
|
}
|
|
|
|
img.Mutate(x => x.FillPolygon(info.Color,
|
|
new PointF(x1, y1),
|
|
new PointF(x3, y3),
|
|
new PointF(x4, y4),
|
|
new PointF(x2, y2)));
|
|
}
|
|
|
|
private async Task DrawClubImage(Image<Rgba32> img, FullUserStats stats)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl))
|
|
{
|
|
try
|
|
{
|
|
var imgUrl = new Uri(stats.User.Club.ImageUrl);
|
|
var result = await _c.GetImageDataAsync(imgUrl);
|
|
if (!result.TryPickT0(out var data, out _))
|
|
{
|
|
using (var http = _httpFactory.CreateClient())
|
|
using (var temp = await http.GetAsync(imgUrl, HttpCompletionOption.ResponseHeadersRead))
|
|
{
|
|
if (!temp.IsImage() || temp.GetContentLength() > 11 * 1024 * 1024)
|
|
return;
|
|
|
|
var imgData = await temp.Content.ReadAsByteArrayAsync();
|
|
using (var tempDraw = Image.Load<Rgba32>(imgData))
|
|
{
|
|
tempDraw.Mutate(x => x
|
|
.Resize(template.Club.Icon.Size.X, template.Club.Icon.Size.Y)
|
|
.ApplyRoundedCorners(Math.Max(template.Club.Icon.Size.X,
|
|
template.Club.Icon.Size.Y)
|
|
/ 2.0f));
|
|
await using (var tds = await tempDraw.ToStreamAsync())
|
|
{
|
|
data = tds.ToArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
await _c.SetImageDataAsync(imgUrl, data);
|
|
}
|
|
|
|
using var toDraw = Image.Load(data);
|
|
if (toDraw.Size != new Size(template.Club.Icon.Size.X, template.Club.Icon.Size.Y))
|
|
toDraw.Mutate(x => x.Resize(template.Club.Icon.Size.X, template.Club.Icon.Size.Y));
|
|
|
|
img.Mutate(x => x.DrawImage(
|
|
toDraw,
|
|
new Point(template.Club.Icon.Pos.X, template.Club.Icon.Pos.Y),
|
|
1));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Error drawing club image");
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task XpReset(ulong guildId, ulong userId)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
await uow.GetTable<UserXpStats>()
|
|
.DeleteAsync(x => x.UserId == userId && x.GuildId == guildId);
|
|
}
|
|
|
|
public void XpReset(ulong guildId)
|
|
{
|
|
using var uow = _db.GetDbContext();
|
|
uow.Set<UserXpStats>().ResetGuildXp(guildId);
|
|
uow.SaveChanges();
|
|
}
|
|
|
|
public async Task ResetXpRewards(ulong guildId)
|
|
{
|
|
await using var uow = _db.GetDbContext();
|
|
var guildConfig = uow.GuildConfigsForId(guildId,
|
|
set => set.Include(x => x.XpSettings)
|
|
.ThenInclude(x => x.CurrencyRewards)
|
|
.Include(x => x.XpSettings)
|
|
.ThenInclude(x => x.RoleRewards));
|
|
|
|
uow.RemoveRange(guildConfig.XpSettings.RoleRewards);
|
|
uow.RemoveRange(guildConfig.XpSettings.CurrencyRewards);
|
|
await uow.SaveChangesAsync();
|
|
}
|
|
|
|
public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopBgs()
|
|
{
|
|
var data = _xpConfig.Data;
|
|
if (!data.Shop.IsEnabled)
|
|
return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
|
|
|
|
return new(_xpConfig.Data.Shop.Bgs?.Where(x => x.Value.Price >= 0).ToDictionary(x => x.Key, x => x.Value));
|
|
}
|
|
|
|
public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopFrames()
|
|
{
|
|
var data = _xpConfig.Data;
|
|
if (!data.Shop.IsEnabled)
|
|
return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
|
|
|
|
return new(_xpConfig.Data.Shop.Frames?.Where(x => x.Value.Price >= 0).ToDictionary(x => x.Key, x => x.Value));
|
|
}
|
|
|
|
public async Task<BuyResult> BuyShopItemAsync(ulong userId, XpShopItemType type, string key)
|
|
{
|
|
var conf = _xpConfig.Data;
|
|
|
|
if (!conf.Shop.IsEnabled)
|
|
return BuyResult.XpShopDisabled;
|
|
|
|
var req = type == XpShopItemType.Background
|
|
? conf.Shop.BgsTierRequirement
|
|
: conf.Shop.FramesTierRequirement;
|
|
|
|
if (req != PatronTier.None && !_creds.IsOwner(userId))
|
|
{
|
|
var patron = await _ps.GetPatronAsync(userId);
|
|
|
|
if (patron is null || (int)patron.Value.Tier < (int)req)
|
|
return BuyResult.InsufficientPatronTier;
|
|
}
|
|
|
|
await using var ctx = _db.GetDbContext();
|
|
try
|
|
{
|
|
if (await ctx.GetTable<XpShopOwnedItem>()
|
|
.AnyAsyncLinqToDB(x => x.UserId == userId && x.ItemKey == key && x.ItemType == type))
|
|
return BuyResult.AlreadyOwned;
|
|
|
|
var item = GetShopItem(type, key);
|
|
|
|
if (item is null || item.Price < 0)
|
|
return BuyResult.UnknownItem;
|
|
|
|
if (item.Price > 0 && !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}")))
|
|
return BuyResult.InsufficientFunds;
|
|
|
|
|
|
await ctx.GetTable<XpShopOwnedItem>()
|
|
.InsertAsync(() => new XpShopOwnedItem()
|
|
{
|
|
UserId = userId,
|
|
IsUsing = false,
|
|
ItemKey = key,
|
|
ItemType = type,
|
|
DateAdded = DateTime.UtcNow,
|
|
});
|
|
|
|
return BuyResult.Success;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Error buying shop item: {ErrorMessage}", ex.Message);
|
|
return BuyResult.UnknownItem;
|
|
}
|
|
}
|
|
|
|
private XpConfig.ShopItemInfo? GetShopItem(XpShopItemType type, string key)
|
|
{
|
|
var data = _xpConfig.Data;
|
|
if (type == XpShopItemType.Background)
|
|
{
|
|
if (data.Shop.Bgs is { } bgs && bgs.TryGetValue(key, out var item))
|
|
return item;
|
|
|
|
return null;
|
|
}
|
|
|
|
if (type == XpShopItemType.Frame)
|
|
{
|
|
if (data.Shop.Frames is { } fs && fs.TryGetValue(key, out var item))
|
|
return item;
|
|
|
|
return null;
|
|
}
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(type));
|
|
}
|
|
|
|
public async Task<bool> OwnsItemAsync(
|
|
ulong userId,
|
|
XpShopItemType itemType,
|
|
string key)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
return await ctx.GetTable<XpShopOwnedItem>()
|
|
.AnyAsyncLinqToDB(x => x.UserId == userId
|
|
&& x.ItemType == itemType
|
|
&& x.ItemKey == key);
|
|
}
|
|
|
|
|
|
public async Task<XpShopOwnedItem?> GetUserItemAsync(
|
|
ulong userId,
|
|
XpShopItemType itemType,
|
|
string key)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
return await ctx.GetTable<XpShopOwnedItem>()
|
|
.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
|
|
&& x.ItemType == itemType
|
|
&& x.ItemKey == key);
|
|
}
|
|
|
|
public async Task<XpShopOwnedItem?> GetItemInUse(
|
|
ulong userId,
|
|
XpShopItemType itemType)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
return await ctx.GetTable<XpShopOwnedItem>()
|
|
.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
|
|
&& x.ItemType == itemType
|
|
&& x.IsUsing);
|
|
}
|
|
|
|
public async Task<bool> UseShopItemAsync(ulong userId, XpShopItemType itemType, string key)
|
|
{
|
|
var data = _xpConfig.Data;
|
|
XpConfig.ShopItemInfo? item = null;
|
|
if (itemType == XpShopItemType.Background)
|
|
{
|
|
data.Shop.Bgs?.TryGetValue(key, out item);
|
|
}
|
|
else
|
|
{
|
|
data.Shop.Frames?.TryGetValue(key, out item);
|
|
}
|
|
|
|
if (item is null)
|
|
return false;
|
|
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
if (await OwnsItemAsync(userId, itemType, key))
|
|
{
|
|
await ctx.GetTable<XpShopOwnedItem>()
|
|
.Where(x => x.UserId == userId && x.ItemType == itemType)
|
|
.UpdateAsync(old => new()
|
|
{
|
|
IsUsing = key == old.ItemKey
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public PatronTier GetXpShopTierRequirement(Xp.XpShopInputType type)
|
|
=> type switch
|
|
{
|
|
Xp.XpShopInputType.F => _xpConfig.Data.Shop.FramesTierRequirement,
|
|
_ => _xpConfig.Data.Shop.BgsTierRequirement,
|
|
};
|
|
|
|
public bool IsShopEnabled()
|
|
=> _xpConfig.Data.Shop.IsEnabled;
|
|
|
|
public async Task<int> GetTotalGuildUsers(ulong requestGuildId, List<ulong>? guildUsers = null)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
return await ctx.GetTable<UserXpStats>()
|
|
.Where(x => x.GuildId == requestGuildId
|
|
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
|
|
.CountAsyncLinqToDB();
|
|
}
|
|
|
|
public async Task SetLevelAsync(ulong guildId, ulong userId, int level)
|
|
{
|
|
var lvlStats = LevelStats.CreateForLevel(level);
|
|
await using var ctx = _db.GetDbContext();
|
|
await ctx.GetTable<UserXpStats>()
|
|
.InsertOrUpdateAsync(() => new()
|
|
{
|
|
GuildId = guildId,
|
|
UserId = userId,
|
|
Xp = lvlStats.TotalXp,
|
|
DateAdded = DateTime.UtcNow
|
|
},
|
|
(old) => new()
|
|
{
|
|
Xp = lvlStats.TotalXp
|
|
},
|
|
() => new()
|
|
{
|
|
GuildId = guildId,
|
|
UserId = userId
|
|
});
|
|
}
|
|
} |