2024-09-21 00:46:59 +12:00
#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 ;
2024-10-05 14:14:33 +13:00
using LinqToDB.Tools ;
2024-09-21 00:46:59 +12:00
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 ;
2024-10-05 11:44:44 +13:00
private readonly IBotCreds _creds ;
2024-09-21 00:46:59 +12:00
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 ;
public XpService (
DiscordSocketClient client ,
IBot bot ,
DbService db ,
IBotStrings strings ,
IImageCache images ,
IBotCache c ,
FontProvider fonts ,
2024-10-05 11:44:44 +13:00
IBotCreds creds ,
2024-09-21 00:46:59 +12:00
ICurrencyService cs ,
IHttpClientFactory http ,
XpConfigService xpConfig ,
IPubSub pubSub ,
IPatronageService ps ,
IMessageSenderService sender )
{
_db = db ;
_images = images ;
_strings = strings ;
_fonts = fonts ;
_creds = creds ;
_cs = cs ;
_httpFactory = http ;
_xpConfig = xpConfig ;
_pubSub = pubSub ;
_sender = sender ;
_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 ) ) ;
2024-09-21 14:43:36 +12:00
#if ! GLOBAL_ELLIE
2024-09-21 00:46:59 +12:00
_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 ( ) ;
}
}
public sealed class MiniGuildXpStats
{
public long Xp { get ; set ; }
public XpNotificationLocation NotifyOnLevelUp { get ; set ; }
public ulong GuildId { get ; set ; }
public ulong UserId { get ; set ; }
}
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 ) ;
await using ( var ctx = _db . GetDbContext ( ) )
{
var conf = _xpConfig . Data ;
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 ( )
2024-10-05 14:14:33 +13:00
{
TotalXp = old . TotalXp + group . Key
} ,
2024-09-21 00:46:59 +12:00
( _ , 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 ( )
2024-10-05 14:14:33 +13:00
{
Xp = old . Xp + group . Key
} ,
2024-09-21 00:46:59 +12:00
( _ , 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 ( )
2024-10-05 14:14:33 +13:00
{
UserId = userId ,
GuildId = guildId ,
Xp = group . Key ,
DateAdded = DateTime . UtcNow ,
AwardedXp = 0 ,
NotifyOnLevelUp = XpNotificationLocation . None
} ,
2024-09-21 00:46:59 +12:00
_ = > 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 ,
du . NotifyOnLevelUp ) ) ;
}
}
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 + du . AwardedXp ) ;
var newLevel = new LevelStats ( du . Xp + du . AwardedXp ) ;
if ( oldLevel . Level < newLevel . Level )
{
await _levelUpQueue . EnqueueAsync (
NotifyUser ( xpGainData . Guild . Id ,
xpGainData . Channel . Id ,
du . UserId ,
true ,
oldLevel . Level ,
newLevel . Level ,
du . NotifyOnLevelUp ) ) ;
}
}
}
}
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 ,
XpNotificationLocation notifyLoc )
= > async ( ) = >
{
if ( isServer )
{
await HandleRewardsInternalAsync ( guildId , userId , oldLevel , newLevel ) ;
}
await HandleNotifyInternalAsync ( guildId , channelId , userId , isServer , newLevel , notifyLoc ) ;
} ;
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 ,
XpNotificationLocation notifyLoc )
{
if ( notifyLoc = = XpNotificationLocation . None )
return ;
var guild = _client . GetGuild ( guildId ) ;
var user = guild ? . GetUser ( userId ) ;
var ch = guild ? . GetTextChannel ( channelId ) ;
if ( guild is null | | user is null )
return ;
if ( isServer )
{
if ( notifyLoc = = XpNotificationLocation . Dm )
{
await _sender . Response ( user )
. Confirm ( _strings . GetText ( strs . level_up_dm ( user . Mention ,
Format . Bold ( newLevel . ToString ( ) ) ,
Format . Bold ( guild . ToString ( ) ? ? "-" ) ) ,
guild . Id ) )
. SendAsync ( ) ;
}
else // channel
{
if ( ch is not null )
{
await _sender . Response ( ch )
. Confirm ( _strings . GetText ( strs . level_up_channel ( user . Mention ,
Format . Bold ( newLevel . ToString ( ) ) ) ,
guild . Id ) )
. SendAsync ( ) ;
}
}
}
else // global level
{
var chan = notifyLoc switch
{
XpNotificationLocation . Dm = > ( IMessageChannel ) await user . CreateDMChannelAsync ( ) ,
XpNotificationLocation . Channel = > ch ,
_ = > null
} ;
if ( chan is null )
return ;
await _sender . Response ( chan )
. Confirm ( _strings . GetText ( strs . level_up_global ( user . Mention ,
Format . Bold ( newLevel . ToString ( ) ) ) ,
guild . Id ) )
. SendAsync ( ) ;
}
}
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 > > GetUserXps ( ulong guildId , int page )
{
await using var uow = _db . GetDbContext ( ) ;
2024-10-05 14:14:33 +13:00
return await uow
. UserXpStats
. Where ( x = > x . GuildId = = guildId )
. OrderByDescending ( x = > x . Xp + x . AwardedXp )
. Skip ( page * 9 )
. Take ( 9 )
. ToArrayAsyncLinqToDB ( ) ;
2024-09-21 00:46:59 +12:00
}
2024-10-05 14:14:33 +13:00
public async Task < IReadOnlyCollection < UserXpStats > > GetTopUserXps ( ulong guildId , List < ulong > users , int curPage )
2024-09-21 00:46:59 +12:00
{
await using var uow = _db . GetDbContext ( ) ;
2024-10-05 14:14:33 +13:00
return await uow . Set < UserXpStats > ( )
. Where ( x = > x . GuildId = = guildId & & x . UserId . In ( users ) )
. OrderByDescending ( x = > x . Xp + x . AwardedXp )
. Skip ( curPage * 9 )
. Take ( 9 )
. ToArrayAsyncLinqToDB ( ) ;
2024-09-21 00:46:59 +12:00
}
public Task < IReadOnlyCollection < DiscordUser > > GetUserXps ( int page , int perPage = 9 )
{
using var uow = _db . GetDbContext ( ) ;
return uow . Set < DiscordUser > ( )
. GetUsersXpLeaderboardFor ( page , perPage ) ;
}
public async Task ChangeNotificationType ( ulong userId , ulong guildId , XpNotificationLocation type )
{
await 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 )
{
await using var uow = _db . GetDbContext ( ) ;
var du = uow . GetOrCreateUser ( user ) ;
du . NotifyOnLevelUp = type ;
await uow . SaveChangesAsync ( ) ;
}
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 . AwardedXp + = 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 . FromMinutes ( _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 + 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 ( ) ) ;
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 )
2024-10-05 14:14:33 +13:00
{
HorizontalAlignment = HorizontalAlignment . Left ,
VerticalAlignment = VerticalAlignment . Center ,
FallbackFontFamilies = _fonts . FallBackFonts ,
Origin = new ( template . User . Name . Pos . X , template . User . Name . Pos . Y + 8 )
} ,
2024-09-21 00:46:59 +12:00
"@" + 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 )
2024-10-05 14:14:33 +13:00
{
HorizontalAlignment = HorizontalAlignment . Right ,
VerticalAlignment = VerticalAlignment . Top ,
FallbackFontFamilies = _fonts . FallBackFonts ,
Origin = new ( template . Club . Name . Pos . X + 50 , template . Club . Name . Pos . Y - 8 )
} ,
2024-09-21 00:46:59 +12:00
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 ) ) ;
}
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 ) ,
outlinePen ,
new ( awX , awY ) ) ) ;
}
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 ) ;
2024-10-05 14:14:33 +13:00
// #if GLOBAL_ELLIE
2024-09-21 00:46:59 +12:00
await DrawFrame ( img , stats . User . UserId ) ;
2024-10-05 14:14:33 +13:00
// #endif
2024-09-21 00:46:59 +12:00
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 ( ) ;
}
2024-09-21 14:43:36 +12:00
// #if GLOBAL_ELLIE
2024-09-21 00:46:59 +12:00
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 ( ) ) ) ;
}
2024-10-05 14:14:33 +13:00
// #endif
2024-09-21 00:46:59 +12:00
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 void XpReset ( ulong guildId , ulong userId )
{
using var uow = _db . GetDbContext ( ) ;
uow . Set < UserXpStats > ( ) . ResetGuildUserXp ( userId , guildId ) ;
uow . SaveChanges ( ) ;
}
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 enum BuyResult
{
Success ,
XpShopDisabled ,
AlreadyOwned ,
InsufficientFunds ,
UnknownItem ,
InsufficientPatronTier ,
}