using Discord; using Discord.WebSocket; using NadekoBot.Common; using NadekoBot.Common.Collections; using NadekoBot.Db.Models; using NadekoBot.Extensions; 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; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; using Serilog; using StackExchange.Redis; using Image = SixLabors.ImageSharp.Image; namespace NadekoBot.Modules.Xp.Services { // todo improve xp with linqtodb public class XpService : INService { private enum NotifOf { Server, Global } // is it a server level-up or global level-up notification private readonly DbService _db; private readonly CommandHandler _cmd; private readonly IImageCache _images; private readonly IBotStrings _strings; private readonly IDataCache _cache; private readonly FontProvider _fonts; private readonly IBotCredentials _creds; private readonly ICurrencyService _cs; private readonly Task updateXpTask; private readonly IHttpClientFactory _httpFactory; private readonly XpConfigService _xpConfig; private readonly IPubSub _pubSub; private readonly IEmbedBuilderService _eb; public const int XP_REQUIRED_LVL_1 = 36; private readonly ConcurrentDictionary> _excludedRoles; private readonly ConcurrentDictionary> _excludedChannels; private readonly ConcurrentHashSet _excludedServers; private readonly ConcurrentQueue _addMessageXp = new ConcurrentQueue(); private XpTemplate _template; private readonly DiscordSocketClient _client; private readonly TypedKey _xpTemplateReloadKey; public XpService( DiscordSocketClient client, CommandHandler cmd, Bot bot, DbService db, IBotStrings strings, IDataCache cache, FontProvider fonts, IBotCredentials creds, ICurrencyService cs, IHttpClientFactory http, XpConfigService xpConfig, IPubSub pubSub, IEmbedBuilderService eb) { _db = db; _cmd = cmd; _images = cache.LocalImages; _strings = strings; _cache = cache; _fonts = fonts; _creds = creds; _cs = cs; _httpFactory = http; _xpConfig = xpConfig; _pubSub = pubSub; _eb = eb; _excludedServers = new ConcurrentHashSet(); _excludedChannels = new ConcurrentDictionary>(); _client = client; _xpTemplateReloadKey = new("xp.template.reload"); InternalReloadXpTemplate(); if (client.ShardId == 0) { _pubSub.Sub(_xpTemplateReloadKey, _ => { InternalReloadXpTemplate(); return default; }); } //load settings var allGuildConfigs = bot.AllGuildConfigs .Where(x => x.XpSettings != null) .ToList(); _excludedChannels = allGuildConfigs .ToDictionary( x => x.GuildId, x => new ConcurrentHashSet(x.XpSettings .ExclusionList .Where(ex => ex.ItemType == ExcludedItemType.Channel) .Select(ex => ex.ItemId) .Distinct())) .ToConcurrent(); _excludedRoles = allGuildConfigs .ToDictionary( x => x.GuildId, x => new ConcurrentHashSet(x.XpSettings .ExclusionList .Where(ex => ex.ItemType == ExcludedItemType.Role) .Select(ex => ex.ItemId) .Distinct())) .ToConcurrent(); _excludedServers = new ConcurrentHashSet( allGuildConfigs.Where(x => x.XpSettings.ServerExcluded) .Select(x => x.GuildId)); _cmd.OnMessageNoTrigger += _cmd_OnMessageNoTrigger; #if !GLOBAL_NADEKO _client.UserVoiceStateUpdated += _client_OnUserVoiceStateUpdated; // Scan guilds on startup. _client.GuildAvailable += _client_OnGuildAvailable; foreach (var guild in _client.Guilds) { _client_OnGuildAvailable(guild); } #endif updateXpTask = Task.Run(UpdateLoop); } private async Task UpdateLoop() { while (true) { await Task.Delay(TimeSpan.FromSeconds(5)); try { var toNotify = new List<(IGuild Guild, IMessageChannel MessageChannel, IUser User, int Level, XpNotificationLocation NotifyType, NotifOf NotifOf)>(); var roleRewards = new Dictionary>(); var curRewards = new Dictionary>(); var toAddTo = new List(); while (_addMessageXp.TryDequeue(out var usr)) toAddTo.Add(usr); var group = toAddTo.GroupBy(x => (GuildId: x.Guild.Id, x.User)); if (toAddTo.Count == 0) continue; using (var uow = _db.GetDbContext()) { foreach (var item in group) { var xp = item.Sum(x => x.XpAmount); //1. Mass query discord users and userxpstats and get them from local dict //2. (better but much harder) Move everything to the database, and get old and new xp // amounts for every user (in order to give rewards) var usr = uow.GetOrCreateUserXpStats(item.Key.GuildId, item.Key.User.Id); var du = uow.GetOrCreateUser(item.Key.User); var globalXp = du.TotalXp; var oldGlobalLevelData = new LevelStats(globalXp); var newGlobalLevelData = new LevelStats(globalXp + xp); var oldGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp); usr.Xp += xp; du.TotalXp += xp; if (du.Club != null) du.Club.Xp += xp; var newGuildLevelData = new LevelStats(usr.Xp + usr.AwardedXp); if (oldGlobalLevelData.Level < newGlobalLevelData.Level) { du.LastLevelUp = DateTime.UtcNow; var first = item.First(); if (du.NotifyOnLevelUp != XpNotificationLocation.None) toNotify.Add((first.Guild, first.Channel, first.User, newGlobalLevelData.Level, du.NotifyOnLevelUp, NotifOf.Global)); } if (oldGuildLevelData.Level < newGuildLevelData.Level) { usr.LastLevelUp = DateTime.UtcNow; //send level up notification var first = item.First(); if (usr.NotifyOnLevelUp != XpNotificationLocation.None) toNotify.Add((first.Guild, first.Channel, first.User, newGuildLevelData.Level, usr.NotifyOnLevelUp, NotifOf.Server)); //give role if (!roleRewards.TryGetValue(usr.GuildId, out var rrews)) { rrews = uow.XpSettingsFor(usr.GuildId).RoleRewards.ToList(); roleRewards.Add(usr.GuildId, rrews); } if (!curRewards.TryGetValue(usr.GuildId, out var crews)) { crews = uow.XpSettingsFor(usr.GuildId).CurrencyRewards.ToList(); curRewards.Add(usr.GuildId, crews); } //loop through levels since last level up, so if a high amount of xp is gained, reward are still applied. for (var i = oldGuildLevelData.Level + 1; i <= newGuildLevelData.Level; i++) { var rrew = rrews.FirstOrDefault(x => x.Level == i); if (rrew != null) { var role = first.User.Guild.GetRole(rrew.RoleId); if (role is not null) { if (rrew.Remove) { _ = first.User.RemoveRoleAsync(role); } else { _ = first.User.AddRoleAsync(role); } } } //get currency reward for this level var crew = crews.FirstOrDefault(x => x.Level == i); if (crew != null) { //give the user the reward if it exists await _cs.AddAsync(item.Key.User.Id, "Level-up Reward", crew.Amount); } } } } uow.SaveChanges(); } await Task.WhenAll(toNotify.Select(async x => { if (x.NotifOf == NotifOf.Server) { if (x.NotifyType == XpNotificationLocation.Dm) { var chan = await x.User.GetOrCreateDMChannelAsync(); if (chan != null) await chan.SendConfirmAsync(_eb, _strings.GetText(strs.level_up_dm( x.User.Mention, Format.Bold(x.Level.ToString()), Format.Bold(x.Guild.ToString() ?? "-")), x.Guild.Id)); } else if (x.MessageChannel != null) // channel { await x.MessageChannel.SendConfirmAsync(_eb, _strings.GetText(strs.level_up_channel( x.User.Mention, Format.Bold(x.Level.ToString())), x.Guild.Id)); } } else { IMessageChannel chan; if (x.NotifyType == XpNotificationLocation.Dm) { chan = await x.User.GetOrCreateDMChannelAsync(); } else // channel { chan = x.MessageChannel; } await chan.SendConfirmAsync(_eb, _strings.GetText(strs.level_up_global( x.User.Mention, Format.Bold(x.Level.ToString())), x.Guild.Id)); } })); } catch (Exception ex) { Log.Error(ex, "Error In the XP update loop"); } } } private void InternalReloadXpTemplate() { try { var settings = new JsonSerializerSettings { ContractResolver = new RequireObjectPropertiesContractResolver() }; _template = JsonConvert.DeserializeObject( File.ReadAllText("./data/xp_template.json"), settings); } catch (Exception ex) { Log.Error(ex, "Xp template is invalid. Loaded default values"); _template = new XpTemplate(); File.WriteAllText("./data/xp_template_backup.json", JsonConvert.SerializeObject(_template, Formatting.Indented)); } } 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 != null) { uow.Remove(toRemove); settings.CurrencyRewards.Remove(toRemove); } } else { var rew = settings.CurrencyRewards.FirstOrDefault(x => x.Level == level); if (rew != null) rew.Amount = amount; else settings.CurrencyRewards.Add(new XpCurrencyReward() { Level = level, Amount = amount, }); } uow.SaveChanges(); } } public IEnumerable GetCurrencyRewards(ulong id) { using (var uow = _db.GetDbContext()) { return uow.XpSettingsFor(id) .CurrencyRewards .ToArray(); } } public IEnumerable 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 != 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 != null) { rew.RoleId = roleId; rew.Remove = remove; } else { settings.RoleRewards.Add(new XpRoleReward() { Level = level, RoleId = roleId, Remove = remove, }); } uow.SaveChanges(); } public List GetUserXps(ulong guildId, int page) { using (var uow = _db.GetDbContext()) { return uow.UserXpStats.GetUsersFor(guildId, page); } } public List GetTopUserXps(ulong guildId, int count) { using (var uow = _db.GetDbContext()) { return uow.UserXpStats.GetTopUserXps(guildId, count); } } public DiscordUser[] GetUserXps(int page) { using (var uow = _db.GetDbContext()) { return uow.DiscordUser.GetUsersXpLeaderboardFor(page); } } public async Task ChangeNotificationType(ulong userId, ulong guildId, XpNotificationLocation type) { using (var uow = _db.GetDbContext()) { var user = uow.GetOrCreateUserXpStats(guildId, userId); user.NotifyOnLevelUp = type; await uow.SaveChangesAsync(); } } public XpNotificationLocation GetNotificationType(ulong userId, ulong guildId) { using (var uow = _db.GetDbContext()) { var user = uow.GetOrCreateUserXpStats(guildId, userId); return user.NotifyOnLevelUp; } } public XpNotificationLocation GetNotificationType(IUser user) { using (var uow = _db.GetDbContext()) { return uow.GetOrCreateUser(user).NotifyOnLevelUp; } } public async Task ChangeNotificationType(IUser user, XpNotificationLocation type) { using (var uow = _db.GetDbContext()) { var du = uow.GetOrCreateUser(user); du.NotifyOnLevelUp = type; await uow.SaveChangesAsync(); } } private Task _client_OnGuildAvailable(SocketGuild guild) { Task.Run(() => { foreach (var channel in guild.VoiceChannels) { ScanChannelForVoiceXp(channel); } }); return Task.CompletedTask; } private Task _client_OnUserVoiceStateUpdated(SocketUser socketUser, SocketVoiceState before, SocketVoiceState after) { if (!(socketUser is SocketGuildUser user) || user.IsBot) return Task.CompletedTask; var _ = Task.Run(() => { if (before.VoiceChannel != null) { ScanChannelForVoiceXp(before.VoiceChannel); } if (after.VoiceChannel != null && after.VoiceChannel != before.VoiceChannel) { ScanChannelForVoiceXp(after.VoiceChannel); } else if (after.VoiceChannel is 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. UserLeftVoiceChannel(user, before.VoiceChannel); } }); return Task.CompletedTask; } private void ScanChannelForVoiceXp(SocketVoiceChannel channel) { if (ShouldTrackVoiceChannel(channel)) { foreach (var user in channel.Users) { ScanUserForVoiceXp(user, channel); } } else { foreach (var user in channel.Users) { UserLeftVoiceChannel(user, channel); } } } /// /// Assumes that the channel itself is valid and adding xp. /// /// /// private void ScanUserForVoiceXp(SocketGuildUser user, SocketVoiceChannel channel) { if (UserParticipatingInVoiceChannel(user) && ShouldTrackXp(user, channel.Id)) { UserJoinedVoiceChannel(user); } else { UserLeftVoiceChannel(user, channel); } } private bool ShouldTrackVoiceChannel(SocketVoiceChannel channel) { return channel.Users.Where(UserParticipatingInVoiceChannel).Take(2).Count() >= 2; } private bool UserParticipatingInVoiceChannel(SocketGuildUser user) { return !user.IsDeafened && !user.IsMuted && !user.IsSelfDeafened && !user.IsSelfMuted; } 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.NotExists); } 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 UserCacheItem { Guild = channel.Guild, User = user, XpAmount = actualXp }); } } private bool ShouldTrackXp(SocketGuildUser user, ulong channelId) { if (_excludedChannels.TryGetValue(user.Guild.Id, out var chans) && chans.Contains(channelId)) 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; } private Task _cmd_OnMessageNoTrigger(IUserMessage arg) { if (!(arg.Author is SocketGuildUser user) || user.IsBot) return Task.CompletedTask; var _ = Task.Run(() => { if (!ShouldTrackXp(user, arg.Channel.Id)) 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 (!SetUserRewarded(user.Id)) return; _addMessageXp.Enqueue(new UserCacheItem { 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)); _addMessageXp.Enqueue(new UserCacheItem { Guild = user.Guild, Channel = channel, User = user, XpAmount = amount }); } public void AddXp(ulong userId, ulong guildId, int amount) { using (var uow = _db.GetDbContext()) { var usr = uow.GetOrCreateUserXpStats(guildId, userId); usr.AwardedXp += amount; uow.SaveChanges(); } } public bool IsServerExcluded(ulong id) { return _excludedServers.Contains(id); } public IEnumerable GetExcludedRoles(ulong id) { if (_excludedRoles.TryGetValue(id, out var val)) return val.ToArray(); return Enumerable.Empty(); } public IEnumerable GetExcludedChannels(ulong id) { if (_excludedChannels.TryGetValue(id, out var val)) return val.ToArray(); return Enumerable.Empty(); } private bool SetUserRewarded(ulong userId) { var r = _cache.Redis.GetDatabase(); var key = $"{_creds.RedisKey()}_user_xp_gain_{userId}"; return r.StringSet(key, true, TimeSpan.FromMinutes(_xpConfig.Data.MessageXpCooldown), StackExchange.Redis.When.NotExists); } public async Task GetUserStatsAsync(IGuildUser user) { DiscordUser du; UserXpStats stats = null; int totalXp; int globalRank; int guildRank; using (var uow = _db.GetDbContext()) { du = uow.GetOrCreateUser(user); totalXp = du.TotalXp; globalRank = uow.DiscordUser.GetUserGlobalRank(user.Id); guildRank = uow.UserXpStats.GetUserGuildRanking(user.Id, user.GuildId); stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id); await uow.SaveChangesAsync(); } return new FullUserStats(du, stats, new LevelStats(totalXp), new LevelStats(stats.Xp + stats.AwardedXp), 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 ConcurrentHashSet()); 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; } else { roles.TryRemove(rId); var toDelete = xpSetting.ExclusionList.FirstOrDefault(x => x.Equals(excludeObj)); if (toDelete != null) { uow.Remove(toDelete); uow.SaveChanges(); } return false; } } } public bool ToggleExcludeChannel(ulong guildId, ulong chId) { var channels = _excludedChannels.GetOrAdd(guildId, _ => new ConcurrentHashSet()); 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; } else { 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 usernameTextOptions = new TextGraphicsOptions() { TextOptions = new TextOptions() { HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center, } }.WithFallbackFonts(_fonts.FallBackFonts); var clubTextOptions = new TextGraphicsOptions() { TextOptions = new TextOptions() { HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top, } }.WithFallbackFonts(_fonts.FallBackFonts); using (var img = Image.Load(_images.XpBackground, out var imageFormat)) { 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.Measure($"@{username}", new RendererOptions(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(usernameTextOptions, "@" + username, usernameFont, _template.User.Name.Color, new PointF(_template.User.Name.Pos.X, _template.User.Name.Pos.Y + 8)); }); } //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(clubTextOptions, clubName, clubFont, _template.Club.Name.Color, new PointF(_template.Club.Name.Pos.X + 50, _template.Club.Name.Pos.Y - 8)) ); } if (_template.User.GlobalLevel.Show) { img.Mutate(x => { x.DrawText( stats.Global.Level.ToString(), _fonts.NotoSans.CreateFont(_template.User.GlobalLevel.FontSize, FontStyle.Bold), _template.User.GlobalLevel.Color, new PointF(_template.User.GlobalLevel.Pos.X, _template.User.GlobalLevel.Pos.Y) ); //level }); } if (_template.User.GuildLevel.Show) { img.Mutate(x => { x.DrawText( stats.Guild.Level.ToString(), _fonts.NotoSans.CreateFont(_template.User.GuildLevel.FontSize, FontStyle.Bold), _template.User.GuildLevel.Color, new PointF(_template.User.GuildLevel.Pos.X, _template.User.GuildLevel.Pos.Y) ); }); } var pen = new Pen(SixLabors.ImageSharp.Color.Black, 1); 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($"{global.LevelXp}/{global.RequiredXp}", _fonts.NotoSans.CreateFont(_template.User.Xp.Global.FontSize, FontStyle.Bold), Brushes.Solid(_template.User.Xp.Global.Color), pen, new PointF(_template.User.Xp.Global.Pos.X, _template.User.Xp.Global.Pos.Y))); } if (_template.User.Xp.Guild.Show) { img.Mutate(x => x.DrawText($"{guild.LevelXp}/{guild.RequiredXp}", _fonts.NotoSans.CreateFont(_template.User.Xp.Guild.FontSize, FontStyle.Bold), Brushes.Solid(_template.User.Xp.Guild.Color), pen, new PointF(_template.User.Xp.Guild.Pos.X, _template.User.Xp.Guild.Pos.Y))); } if (stats.FullGuildStats.AwardedXp != 0 && _template.User.Xp.Awarded.Show) { var sign = stats.FullGuildStats.AwardedXp > 0 ? "+ " : ""; var awX = _template.User.Xp.Awarded.Pos.X - (Math.Max(0, (stats.FullGuildStats.AwardedXp.ToString().Length - 2)) * 5); var awY = _template.User.Xp.Awarded.Pos.Y; img.Mutate(x => x.DrawText($"({sign}{stats.FullGuildStats.AwardedXp})", _fonts.NotoSans.CreateFont(_template.User.Xp.Awarded.FontSize, FontStyle.Bold), Brushes.Solid(_template.User.Xp.Awarded.Color), pen, new PointF(awX, awY))); } //ranking if (_template.User.GlobalRank.Show) { img.Mutate(x => x.DrawText(stats.GlobalRanking.ToString(), _fonts.UniSans.CreateFont(_template.User.GlobalRank.FontSize, FontStyle.Bold), _template.User.GlobalRank.Color, new PointF(_template.User.GlobalRank.Pos.X, _template.User.GlobalRank.Pos.Y))); } if (_template.User.GuildRank.Show) { img.Mutate(x => x.DrawText(stats.GuildRanking.ToString(), _fonts.UniSans.CreateFont(_template.User.GuildRank.FontSize, FontStyle.Bold), _template.User.GuildRank.Color, new PointF(_template.User.GuildRank.Pos.X, _template.User.GuildRank.Pos.Y))); } //time on this level string GetTimeSpent(DateTime time, string format) { var offset = DateTime.UtcNow - time; return string.Format(format, offset.Days, offset.Hours, offset.Minutes); } if (_template.User.TimeOnLevel.Global.Show) { img.Mutate(x => x.DrawText(GetTimeSpent(stats.User.LastLevelUp, _template.User.TimeOnLevel.Format), _fonts.NotoSans.CreateFont(_template.User.TimeOnLevel.Global.FontSize, FontStyle.Bold), _template.User.TimeOnLevel.Global.Color, new PointF(_template.User.TimeOnLevel.Global.Pos.X, _template.User.TimeOnLevel.Global.Pos.Y))); } if (_template.User.TimeOnLevel.Guild.Show) { img.Mutate(x => x.DrawText( GetTimeSpent(stats.FullGuildStats.LastLevelUp, _template.User.TimeOnLevel.Format), _fonts.NotoSans.CreateFont(_template.User.TimeOnLevel.Guild.FontSize, FontStyle.Bold), _template.User.TimeOnLevel.Guild.Color, new PointF(_template.User.TimeOnLevel.Guild.Pos.X, _template.User.TimeOnLevel.Guild.Pos.Y))); } //avatar if (stats.User.AvatarId != null && _template.User.Icon.Show) { try { var avatarUrl = stats.User.RealAvatarUrl(); var (succ, data) = await _cache.TryGetImageDataAsync(avatarUrl); if (!succ) { using (var http = _httpFactory.CreateClient()) { var avatarData = await http.GetByteArrayAsync(avatarUrl); using (var tempDraw = Image.Load(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)); using (var stream = tempDraw.ToStream()) { data = stream.ToArray(); } } } await _cache.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); } img.Mutate(x => x.Resize(_template.OutputSize.X, _template.OutputSize.Y)); return ((Stream) img.ToStream(imageFormat), imageFormat); } }); void DrawXpBar(float percent, XpBar info, Image 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 = 0, x4 = 0, y3 = 0, y4 = 0; 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[] { new PointF(x1, y1), new PointF(x3, y3), new PointF(x4, y4), new PointF(x2, y2), })); } private async Task DrawClubImage(Image img, FullUserStats stats) { if (!string.IsNullOrWhiteSpace(stats.User.Club?.ImageUrl)) { try { var imgUrl = new Uri(stats.User.Club.ImageUrl); var (succ, data) = await _cache.TryGetImageDataAsync(imgUrl); if (!succ) { using (var http = _httpFactory.CreateClient()) using (var temp = await http.GetAsync(imgUrl, HttpCompletionOption.ResponseHeadersRead)) { if (!temp.IsImage() || temp.GetImageSize() > 11) return; var imgData = await temp.Content.ReadAsByteArrayAsync(); using (var tempDraw = Image.Load(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)); ; using (var tds = tempDraw.ToStream()) { data = tds.ToArray(); } } } await _cache.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 void XpReset(ulong guildId, ulong userId) { using (var uow = _db.GetDbContext()) { uow.UserXpStats.ResetGuildUserXp(userId, guildId); uow.SaveChanges(); } } public void XpReset(ulong guildId) { using (var uow = _db.GetDbContext()) { uow.UserXpStats.ResetGuildXp(guildId); uow.SaveChanges(); } } public async Task ResetXpRewards(ulong guildId) { 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(); } } }