Added Ellie.Bot.Modules.Xp
This commit is contained in:
parent
ac9bc86ca1
commit
cdaf7c9ff2
32 changed files with 3679 additions and 7 deletions
14
Ellie.sln
14
Ellie.sln
|
@ -41,8 +41,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Common", "src\Ell
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Modules.Gambling", "src\Ellie.Bot.Modules.Gambling\Ellie.Bot.Modules.Gambling.csproj", "{11910E7D-E373-482F-8207-678DE0A88112}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Generators.Cloneable", "src\Ellie.Bot.Generators.Cloneable\Ellie.Bot.Generators.Cloneable.csproj", "{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Bot.Modules.Searches", "src\Ellie.Bot.Modules.Searches\Ellie.Bot.Modules.Searches.csproj", "{3168DB6A-574F-4ECA-9203-7F54AC3311DE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.Bot.Modules.Music", "src\Ellie.Bot.Modules.Music\Ellie.Bot.Modules.Music.csproj", "{07579F42-E7DD-4FFC-B375-A4EC61EAF012}"
|
||||
|
@ -57,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.Bot.Modules.Permisssi
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.Bot.Modules.Xp", "src\Ellie.Bot.Modules.Xp\Ellie.Bot.Modules.Xp.csproj", "{A8F7CBAE-93EC-44D2-9CDF-EEC7CEDBDC73}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ellie.Bot.Generators.Cloneable", "src\Ellie.Bot.Generators.Cloneable\Ellie.Bot.Generators.Cloneable.csproj", "{D98BF713-F286-4B7A-895B-C9FE8842F3A9}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -115,10 +115,6 @@ Global
|
|||
{11910E7D-E373-482F-8207-678DE0A88112}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11910E7D-E373-482F-8207-678DE0A88112}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11910E7D-E373-482F-8207-678DE0A88112}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3168DB6A-574F-4ECA-9203-7F54AC3311DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3168DB6A-574F-4ECA-9203-7F54AC3311DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3168DB6A-574F-4ECA-9203-7F54AC3311DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
|
@ -147,6 +143,10 @@ Global
|
|||
{A8F7CBAE-93EC-44D2-9CDF-EEC7CEDBDC73}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8F7CBAE-93EC-44D2-9CDF-EEC7CEDBDC73}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8F7CBAE-93EC-44D2-9CDF-EEC7CEDBDC73}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D98BF713-F286-4B7A-895B-C9FE8842F3A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D98BF713-F286-4B7A-895B-C9FE8842F3A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D98BF713-F286-4B7A-895B-C9FE8842F3A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D98BF713-F286-4B7A-895B-C9FE8842F3A9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -166,7 +166,6 @@ Global
|
|||
{D3411F6C-320C-456D-BA86-24481EB000EA} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{3EC0F005-560F-4E90-88CF-199520133BBA} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{11910E7D-E373-482F-8207-678DE0A88112} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{AFA3DD12-0F98-4754-ADD7-9FF3C1A37C90} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{3168DB6A-574F-4ECA-9203-7F54AC3311DE} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{07579F42-E7DD-4FFC-B375-A4EC61EAF012} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{01168E4B-B34A-4C90-83E4-BEC6F03452D6} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
|
@ -174,6 +173,7 @@ Global
|
|||
{6F8102FD-C78E-4AE5-8019-DC6BB4F6D1A8} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{C4269D96-1699-46C8-8461-3263B5668BA5} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{A8F7CBAE-93EC-44D2-9CDF-EEC7CEDBDC73} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
{D98BF713-F286-4B7A-895B-C9FE8842F3A9} = {C5E3EF2E-72CF-41BB-B0C5-EB4C08403E67}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {878761F1-C7B5-4D38-A00D-3377D703EBBA}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
31
src/Ellie.Bot.Modules.Patronage/GlobalUsings.cs
Normal file
31
src/Ellie.Bot.Modules.Patronage/GlobalUsings.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// global using System.Collections.Concurrent;
|
||||
global using NonBlocking;
|
||||
|
||||
// packages
|
||||
global using Serilog;
|
||||
global using Humanizer;
|
||||
|
||||
// ellie
|
||||
global using Ellie;
|
||||
global using Ellie.Services;
|
||||
global using Ellise.Common; // new project
|
||||
global using Ellie.Common; // old + ellie specific things
|
||||
global using Ellie.Common.Attributes;
|
||||
global using Ellie.Extensions;
|
||||
global using Ellie.Marmalade;
|
||||
|
||||
// discord
|
||||
global using Discord;
|
||||
global using Discord.Commands;
|
||||
global using Discord.Net;
|
||||
global using Discord.WebSocket;
|
||||
|
||||
// aliases
|
||||
global using GuildPerm = Discord.GuildPermission;
|
||||
global using ChannelPerm = Discord.ChannelPermission;
|
||||
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
||||
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
||||
global using TypeReaderResult = Ellie.Common.TypeReaders.TypeReaderResult;
|
||||
|
||||
// non-essential
|
||||
global using JetBrains.Annotations;
|
|
@ -0,0 +1,195 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Ellie.Modules.Gambling.Services;
|
||||
using Ellie.Modules.Patronage;
|
||||
using Ellie.Services.Currency;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Modules.Utility;
|
||||
|
||||
public sealed class CurrencyRewardService : IEService, IDisposable
|
||||
{
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly DbService _db;
|
||||
private readonly IEmbedBuilderService _eb;
|
||||
private readonly GamblingConfigService _config;
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public CurrencyRewardService(
|
||||
ICurrencyService cs,
|
||||
IPatronageService ps,
|
||||
DbService db,
|
||||
IEmbedBuilderService eb,
|
||||
GamblingConfigService config,
|
||||
DiscordSocketClient client)
|
||||
{
|
||||
_cs = cs;
|
||||
_ps = ps;
|
||||
_db = db;
|
||||
_eb = eb;
|
||||
_config = config;
|
||||
_client = client;
|
||||
|
||||
_ps.OnNewPatronPayment += OnNewPayment;
|
||||
_ps.OnPatronRefunded += OnPatronRefund;
|
||||
_ps.OnPatronUpdated += OnPatronUpdate;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_ps.OnNewPatronPayment -= OnNewPayment;
|
||||
_ps.OnPatronRefunded -= OnPatronRefund;
|
||||
_ps.OnPatronUpdated -= OnPatronUpdate;
|
||||
}
|
||||
|
||||
private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron)
|
||||
{
|
||||
// if pledge was increased
|
||||
if (oldPatron.Amount < newPatron.Amount)
|
||||
{
|
||||
var conf = _config.Data;
|
||||
var newAmount = (long)(newPatron.Amount * conf.PatreonCurrencyPerCent);
|
||||
|
||||
RewardedUser old;
|
||||
await using (var ctx = _db.GetDbContext())
|
||||
{
|
||||
old = await ctx.GetTable<RewardedUser>()
|
||||
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (old is null)
|
||||
{
|
||||
await OnNewPayment(newPatron);
|
||||
return;
|
||||
}
|
||||
|
||||
// no action as the amount is the same or lower
|
||||
if (old.AmountRewardedThisMonth >= newAmount)
|
||||
return;
|
||||
|
||||
var count = await ctx.GetTable<RewardedUser>()
|
||||
.Where(x => x.PlatformUserId == newPatron.UniquePlatformUserId)
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
PlatformUserId = newPatron.UniquePlatformUserId,
|
||||
UserId = newPatron.UserId,
|
||||
// amount before bonuses
|
||||
AmountRewardedThisMonth = newAmount,
|
||||
LastReward = newPatron.PaidAt
|
||||
});
|
||||
|
||||
// shouldn't ever happen
|
||||
if (count == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
var oldAmount = old.AmountRewardedThisMonth;
|
||||
|
||||
var realNewAmount = GetRealCurrencyReward(
|
||||
(int)(newAmount / conf.PatreonCurrencyPerCent),
|
||||
newAmount,
|
||||
out var percentBonus);
|
||||
|
||||
var realOldAmount = GetRealCurrencyReward(
|
||||
(int)(oldAmount / conf.PatreonCurrencyPerCent),
|
||||
oldAmount,
|
||||
out _);
|
||||
|
||||
var diff = realNewAmount - realOldAmount;
|
||||
if (diff <= 0)
|
||||
return; // no action if new is lower
|
||||
|
||||
// if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars,
|
||||
// up to 100%
|
||||
|
||||
await _cs.AddAsync(newPatron.UserId, diff, new TxData("patron","update"));
|
||||
|
||||
_ = SendMessageToUser(newPatron.UserId,
|
||||
$"You've received an additional **{diff}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!");
|
||||
}
|
||||
}
|
||||
|
||||
private long GetRealCurrencyReward(int pledgeCents, long modifiedAmount, out int percentBonus)
|
||||
{
|
||||
// needs at least 5$ to be eligible for a bonus
|
||||
if (pledgeCents < 500)
|
||||
{
|
||||
percentBonus = 0;
|
||||
return modifiedAmount;
|
||||
}
|
||||
|
||||
var dollarValue = pledgeCents / 100;
|
||||
percentBonus = dollarValue switch
|
||||
{
|
||||
>= 100 => 100,
|
||||
>= 50 => 50,
|
||||
>= 20 => 20,
|
||||
>= 10 => 10,
|
||||
>= 5 => 5,
|
||||
_ => 0
|
||||
};
|
||||
return (long)(modifiedAmount * (1 + (percentBonus / 100.0f)));
|
||||
}
|
||||
|
||||
// on a new payment, always give the full amount.
|
||||
private async Task OnNewPayment(Patron patron)
|
||||
{
|
||||
var amount = (long)(patron.Amount * _config.Data.PatreonCurrencyPerCent);
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.GetTable<RewardedUser>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
PlatformUserId = patron.UniquePlatformUserId,
|
||||
UserId = patron.UserId,
|
||||
AmountRewardedThisMonth = amount,
|
||||
LastReward = patron.PaidAt,
|
||||
},
|
||||
old => new()
|
||||
{
|
||||
AmountRewardedThisMonth = amount,
|
||||
UserId = patron.UserId,
|
||||
LastReward = patron.PaidAt
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
PlatformUserId = patron.UniquePlatformUserId
|
||||
});
|
||||
|
||||
var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus);
|
||||
await _cs.AddAsync(patron.UserId, realAmount, new("patron", "new"));
|
||||
_ = SendMessageToUser(patron.UserId,
|
||||
$"You've received **{realAmount}**{_config.Data.Currency.Sign} as a currency reward (**+{percentBonus}%**)!");
|
||||
}
|
||||
|
||||
private async Task SendMessageToUser(ulong userId, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = (IUser)_client.GetUser(userId) ?? await _client.Rest.GetUserAsync(userId);
|
||||
if (user is null)
|
||||
return;
|
||||
|
||||
var eb = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithDescription(message);
|
||||
|
||||
await user.EmbedAsync(eb);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Warning("Unable to send a \"Currency Reward\" message to the patron {UserId}", userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnPatronRefund(Patron patron)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
_ = await ctx.GetTable<RewardedUser>()
|
||||
.UpdateAsync(old => new()
|
||||
{
|
||||
AmountRewardedThisMonth = old.AmountRewardedThisMonth * 2
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Ellie.Db.Models;
|
||||
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public readonly struct InsufficientTier
|
||||
{
|
||||
public FeatureType FeatureType { get; init; }
|
||||
public string Feature { get; init; }
|
||||
public PatronTier RequiredTier { get; init; }
|
||||
public PatronTier UserTier { get; init; }
|
||||
}
|
19
src/Ellie.Bot.Modules.Xp/CleanupCommands.cs
Normal file
19
src/Ellie.Bot.Modules.Xp/CleanupCommands.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public sealed partial class Xp
|
||||
{
|
||||
public class CleanupCommands : CleanupModuleBase
|
||||
{
|
||||
private readonly IXpCleanupService _service;
|
||||
|
||||
public CleanupCommands(IXpCleanupService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public Task DeleteXp()
|
||||
=> ConfirmActionInternalAsync("Delete Xp", () => _service.DeleteXp());
|
||||
}
|
||||
}
|
424
src/Ellie.Bot.Modules.Xp/Club/Club.cs
Normal file
424
src/Ellie.Bot.Modules.Xp/Club/Club.cs
Normal file
|
@ -0,0 +1,424 @@
|
|||
#nullable disable
|
||||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Modules.Xp.Services;
|
||||
|
||||
namespace Ellie.Modules.Xp.Club;
|
||||
|
||||
public partial class Xp
|
||||
{
|
||||
[Group]
|
||||
public partial class Club : EllieModule<IClubService>
|
||||
{
|
||||
private readonly XpService _xps;
|
||||
|
||||
public Club(XpService xps)
|
||||
=> _xps = xps;
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubTransfer([Leftover] IUser newOwner)
|
||||
{
|
||||
var result = _service.TransferClub(ctx.User, newOwner);
|
||||
|
||||
if (!result.TryPickT0(out var club, out var error))
|
||||
{
|
||||
if (error == ClubTransferError.NotOwner)
|
||||
await ReplyErrorLocalizedAsync(strs.club_owner_only);
|
||||
else
|
||||
await ReplyErrorLocalizedAsync(strs.club_target_not_member);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(
|
||||
strs.club_transfered(
|
||||
Format.Bold(club.Name),
|
||||
Format.Bold(newOwner.ToString())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubAdmin([Leftover] IUser toAdmin)
|
||||
{
|
||||
var result = await _service.ToggleAdminAsync(ctx.User, toAdmin);
|
||||
|
||||
if (result == ToggleAdminResult.AddedAdmin)
|
||||
await ReplyConfirmLocalizedAsync(strs.club_admin_add(Format.Bold(toAdmin.ToString())));
|
||||
else if (result == ToggleAdminResult.RemovedAdmin)
|
||||
await ReplyConfirmLocalizedAsync(strs.club_admin_remove(Format.Bold(toAdmin.ToString())));
|
||||
else if (result == ToggleAdminResult.NotOwner)
|
||||
await ReplyErrorLocalizedAsync(strs.club_owner_only);
|
||||
else if (result == ToggleAdminResult.CantTargetThyself)
|
||||
await ReplyErrorLocalizedAsync(strs.club_admin_invalid_target);
|
||||
else if (result == ToggleAdminResult.TargetNotMember)
|
||||
await ReplyErrorLocalizedAsync(strs.club_target_not_member);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubCreate([Leftover] string clubName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clubName) || clubName.Length > 20)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_name_too_long);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _service.CreateClubAsync(ctx.User, clubName);
|
||||
|
||||
if (result == ClubCreateResult.NameTaken)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_create_error_name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == ClubCreateResult.InsufficientLevel)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_create_insuff_lvl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == ClubCreateResult.AlreadyInAClub)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_already_in);
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.club_created(Format.Bold(clubName)));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubIcon([Leftover] string url = null)
|
||||
{
|
||||
if ((!Uri.IsWellFormedUriString(url, UriKind.Absolute) && url is not null))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_icon_url_format);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await _service.SetClubIconAsync(ctx.User.Id, url);
|
||||
if (result == SetClubIconResult.Success)
|
||||
await ReplyConfirmLocalizedAsync(strs.club_icon_set);
|
||||
else if (result == SetClubIconResult.NotOwner)
|
||||
await ReplyErrorLocalizedAsync(strs.club_owner_only);
|
||||
else if (result == SetClubIconResult.TooLarge)
|
||||
await ReplyErrorLocalizedAsync(strs.club_icon_too_large);
|
||||
else if (result == SetClubIconResult.InvalidFileType)
|
||||
await ReplyErrorLocalizedAsync(strs.club_icon_invalid_filetype);
|
||||
}
|
||||
|
||||
private async Task InternalClubInfoAsync(ClubInfo club)
|
||||
{
|
||||
var lvl = new LevelStats(club.Xp);
|
||||
var users = club.Members.OrderByDescending(x =>
|
||||
{
|
||||
var l = new LevelStats(x.TotalXp).Level;
|
||||
if (club.OwnerId == x.Id)
|
||||
return int.MaxValue;
|
||||
if (x.IsClubAdmin)
|
||||
return (int.MaxValue / 2) + l;
|
||||
return l;
|
||||
});
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(0,
|
||||
page =>
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle($"{club}")
|
||||
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
|
||||
.AddField(GetText(strs.desc),
|
||||
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
|
||||
.AddField(GetText(strs.owner), club.Owner.ToString(), true)
|
||||
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
|
||||
.AddField(GetText(strs.members),
|
||||
string.Join("\n",
|
||||
users.Skip(page * 10)
|
||||
.Take(10)
|
||||
.Select(x =>
|
||||
{
|
||||
var l = new LevelStats(x.TotalXp);
|
||||
var lvlStr = Format.Bold($" ⟪{l.Level}⟫");
|
||||
if (club.OwnerId == x.Id)
|
||||
return x + "🌟" + lvlStr;
|
||||
if (x.IsClubAdmin)
|
||||
return x + "⭐" + lvlStr;
|
||||
return x + lvlStr;
|
||||
})));
|
||||
|
||||
if (Uri.IsWellFormedUriString(club.ImageUrl, UriKind.Absolute))
|
||||
return embed.WithThumbnailUrl(club.ImageUrl);
|
||||
|
||||
return embed;
|
||||
},
|
||||
club.Members.Count,
|
||||
10);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[Priority(1)]
|
||||
public async Task ClubInformation(IUser user = null)
|
||||
{
|
||||
user ??= ctx.User;
|
||||
var club = _service.GetClubByMember(user);
|
||||
if (club is null)
|
||||
{
|
||||
await ErrorLocalizedAsync(strs.club_user_not_in_club(Format.Bold(user.ToString())));
|
||||
return;
|
||||
}
|
||||
|
||||
await InternalClubInfoAsync(club);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[Priority(0)]
|
||||
public async Task ClubInformation([Leftover] string clubName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clubName))
|
||||
{
|
||||
await ClubInformation(ctx.User);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_service.GetClubByName(clubName, out var club))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_not_exists);
|
||||
return;
|
||||
}
|
||||
|
||||
await InternalClubInfoAsync(club);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public Task ClubBans(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var club = _service.GetClubWithBansAndApplications(ctx.User.Id);
|
||||
if (club is null)
|
||||
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
|
||||
|
||||
var bans = club.Bans.Select(x => x.User).ToArray();
|
||||
|
||||
return ctx.SendPaginatedConfirmAsync(page,
|
||||
_ =>
|
||||
{
|
||||
var toShow = string.Join("\n", bans
|
||||
.Skip(page * 10).Take(10)
|
||||
.Select(x => x.ToString()));
|
||||
|
||||
return _eb.Create()
|
||||
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
|
||||
.WithDescription(toShow)
|
||||
.WithOkColor();
|
||||
},
|
||||
bans.Length,
|
||||
10);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public Task ClubApps(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var club = _service.GetClubWithBansAndApplications(ctx.User.Id);
|
||||
if (club is null)
|
||||
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
|
||||
|
||||
var apps = club.Applicants.Select(x => x.User).ToArray();
|
||||
|
||||
return ctx.SendPaginatedConfirmAsync(page,
|
||||
_ =>
|
||||
{
|
||||
var toShow = string.Join("\n", apps.Skip(page * 10).Take(10).Select(x => x.ToString()));
|
||||
|
||||
return _eb.Create()
|
||||
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
|
||||
.WithDescription(toShow)
|
||||
.WithOkColor();
|
||||
},
|
||||
apps.Length,
|
||||
10);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubApply([Leftover] string clubName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clubName))
|
||||
return;
|
||||
|
||||
if (!_service.GetClubByName(clubName, out var club))
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_not_exists);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = _service.ApplyToClub(ctx.User, club);
|
||||
if (result == ClubApplyResult.Success)
|
||||
await ReplyConfirmLocalizedAsync(strs.club_applied(Format.Bold(club.ToString())));
|
||||
else if (result == ClubApplyResult.Banned)
|
||||
await ReplyErrorLocalizedAsync(strs.club_join_banned);
|
||||
else if (result == ClubApplyResult.InsufficientLevel)
|
||||
await ReplyErrorLocalizedAsync(strs.club_insuff_lvl);
|
||||
else if (result == ClubApplyResult.AlreadyInAClub)
|
||||
await ReplyErrorLocalizedAsync(strs.club_already_in);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[Priority(1)]
|
||||
public Task ClubAccept(IUser user)
|
||||
=> ClubAccept(user.ToString());
|
||||
|
||||
[Cmd]
|
||||
[Priority(0)]
|
||||
public async Task ClubAccept([Leftover] string userName)
|
||||
{
|
||||
var result = _service.AcceptApplication(ctx.User.Id, userName, out var discordUser);
|
||||
if (result == ClubAcceptResult.Accepted)
|
||||
await ReplyConfirmLocalizedAsync(strs.club_accepted(Format.Bold(discordUser.ToString())));
|
||||
else if (result == ClubAcceptResult.NoSuchApplicant)
|
||||
await ReplyErrorLocalizedAsync(strs.club_accept_invalid_applicant);
|
||||
else if (result == ClubAcceptResult.NotOwnerOrAdmin)
|
||||
await ReplyErrorLocalizedAsync(strs.club_admin_perms);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubLeave()
|
||||
{
|
||||
var res = _service.LeaveClub(ctx.User);
|
||||
|
||||
if (res == ClubLeaveResult.Success)
|
||||
await ReplyConfirmLocalizedAsync(strs.club_left);
|
||||
else if (res == ClubLeaveResult.NotInAClub)
|
||||
await ReplyErrorLocalizedAsync(strs.club_not_in_a_club);
|
||||
else
|
||||
await ReplyErrorLocalizedAsync(strs.club_owner_cant_leave);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[Priority(1)]
|
||||
public Task ClubKick([Leftover] IUser user)
|
||||
=> ClubKick(user.ToString());
|
||||
|
||||
[Cmd]
|
||||
[Priority(0)]
|
||||
public Task ClubKick([Leftover] string userName)
|
||||
{
|
||||
var result = _service.Kick(ctx.User.Id, userName, out var club);
|
||||
if (result == ClubKickResult.Success)
|
||||
{
|
||||
return ReplyConfirmLocalizedAsync(strs.club_user_kick(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())));
|
||||
}
|
||||
|
||||
if (result == ClubKickResult.Hierarchy)
|
||||
return ReplyErrorLocalizedAsync(strs.club_kick_hierarchy);
|
||||
|
||||
if (result == ClubKickResult.NotOwnerOrAdmin)
|
||||
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
|
||||
|
||||
return ReplyErrorLocalizedAsync(strs.club_target_not_member);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[Priority(1)]
|
||||
public Task ClubBan([Leftover] IUser user)
|
||||
=> ClubBan(user.ToString());
|
||||
|
||||
[Cmd]
|
||||
[Priority(0)]
|
||||
public Task ClubBan([Leftover] string userName)
|
||||
{
|
||||
var result = _service.Ban(ctx.User.Id, userName, out var club);
|
||||
if (result == ClubBanResult.Success)
|
||||
{
|
||||
return ReplyConfirmLocalizedAsync(strs.club_user_banned(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())));
|
||||
}
|
||||
|
||||
if (result == ClubBanResult.Unbannable)
|
||||
return ReplyErrorLocalizedAsync(strs.club_ban_fail_unbannable);
|
||||
|
||||
if (result == ClubBanResult.WrongUser)
|
||||
return ReplyErrorLocalizedAsync(strs.club_ban_fail_user_not_found);
|
||||
|
||||
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[Priority(1)]
|
||||
public Task ClubUnBan([Leftover] IUser user)
|
||||
=> ClubUnBan(user.ToString());
|
||||
|
||||
[Cmd]
|
||||
[Priority(0)]
|
||||
public Task ClubUnBan([Leftover] string userName)
|
||||
{
|
||||
var result = _service.UnBan(ctx.User.Id, userName, out var club);
|
||||
|
||||
if (result == ClubUnbanResult.Success)
|
||||
{
|
||||
return ReplyConfirmLocalizedAsync(strs.club_user_unbanned(Format.Bold(userName),
|
||||
Format.Bold(club.ToString())));
|
||||
}
|
||||
|
||||
if (result == ClubUnbanResult.WrongUser)
|
||||
{
|
||||
return ReplyErrorLocalizedAsync(strs.club_unban_fail_user_not_found);
|
||||
}
|
||||
|
||||
return ReplyErrorLocalizedAsync(strs.club_admin_perms);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubDescription([Leftover] string desc = null)
|
||||
{
|
||||
if (_service.SetDescription(ctx.User.Id, desc))
|
||||
{
|
||||
desc = string.IsNullOrWhiteSpace(desc)
|
||||
? "-"
|
||||
: desc;
|
||||
|
||||
var eb = _eb.Create(ctx)
|
||||
.WithAuthor(ctx.User)
|
||||
.WithTitle(GetText(strs.club_desc_update))
|
||||
.WithOkColor()
|
||||
.WithDescription(desc);
|
||||
|
||||
await ctx.Channel.EmbedAsync(eb);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.club_desc_update_failed);
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task ClubDisband()
|
||||
{
|
||||
if (_service.Disband(ctx.User.Id, out var club))
|
||||
await ReplyConfirmLocalizedAsync(strs.club_disbanded(Format.Bold(club.Name)));
|
||||
else
|
||||
await ReplyErrorLocalizedAsync(strs.club_disband_error);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public Task ClubLeaderboard(int page = 1)
|
||||
{
|
||||
if (--page < 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var clubs = _service.GetClubLeaderboardPage(page);
|
||||
|
||||
var embed = _eb.Create().WithTitle(GetText(strs.club_leaderboard(page + 1))).WithOkColor();
|
||||
|
||||
var i = page * 9;
|
||||
foreach (var club in clubs)
|
||||
embed.AddField($"#{++i} " + club, club.Xp + " xp");
|
||||
|
||||
return ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
}
|
||||
}
|
319
src/Ellie.Bot.Modules.Xp/Club/ClubService.cs
Normal file
319
src/Ellie.Bot.Modules.Xp/Club/ClubService.cs
Normal file
|
@ -0,0 +1,319 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
using OneOf;
|
||||
|
||||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public class ClubService : IEService, IClubService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public ClubService(DbService db, IHttpClientFactory httpFactory)
|
||||
{
|
||||
_db = db;
|
||||
_httpFactory = httpFactory;
|
||||
}
|
||||
|
||||
|
||||
public async Task<ClubCreateResult> CreateClubAsync(IUser user, string clubName)
|
||||
{
|
||||
//must be lvl 5 and must not be in a club already
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user);
|
||||
var xp = new LevelStats(du.TotalXp);
|
||||
|
||||
if (xp.Level < 5)
|
||||
return ClubCreateResult.InsufficientLevel;
|
||||
|
||||
if (du.ClubId is not null)
|
||||
return ClubCreateResult.AlreadyInAClub;
|
||||
|
||||
if (await uow.Set<ClubInfo>().AnyAsyncEF(x => x.Name == clubName))
|
||||
return ClubCreateResult.NameTaken;
|
||||
|
||||
du.IsClubAdmin = true;
|
||||
du.Club = new()
|
||||
{
|
||||
Name = clubName,
|
||||
Owner = du
|
||||
};
|
||||
uow.Set<ClubInfo>().Add(du.Club);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
await uow.GetTable<ClubApplicants>()
|
||||
.DeleteAsync(x => x.UserId == du.Id);
|
||||
|
||||
return ClubCreateResult.Success;
|
||||
}
|
||||
|
||||
public OneOf<ClubInfo, ClubTransferError> TransferClub(IUser from, IUser newOwner)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(@from.Id);
|
||||
var newOwnerUser = uow.GetOrCreateUser(newOwner);
|
||||
|
||||
if (club is null || club.Owner.UserId != from.Id)
|
||||
return ClubTransferError.NotOwner;
|
||||
|
||||
if (!club.Members.Contains(newOwnerUser))
|
||||
return ClubTransferError.TargetNotMember;
|
||||
|
||||
club.Owner.IsClubAdmin = true; // old owner will stay as admin
|
||||
newOwnerUser.IsClubAdmin = true;
|
||||
club.Owner = newOwnerUser;
|
||||
uow.SaveChanges();
|
||||
return club;
|
||||
}
|
||||
|
||||
public async Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin)
|
||||
{
|
||||
if (owner.Id == toAdmin.Id)
|
||||
return ToggleAdminResult.CantTargetThyself;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(owner.Id);
|
||||
var adminUser = uow.GetOrCreateUser(toAdmin);
|
||||
|
||||
if (club is null)
|
||||
return ToggleAdminResult.NotOwner;
|
||||
|
||||
if(!club.Members.Contains(adminUser))
|
||||
return ToggleAdminResult.TargetNotMember;
|
||||
|
||||
var newState = adminUser.IsClubAdmin = !adminUser.IsClubAdmin;
|
||||
await uow.SaveChangesAsync();
|
||||
return newState ? ToggleAdminResult.AddedAdmin : ToggleAdminResult.RemovedAdmin;
|
||||
}
|
||||
|
||||
public ClubInfo GetClubByMember(IUser user)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var member = uow.Set<ClubInfo>().GetByMember(user.Id);
|
||||
return member;
|
||||
}
|
||||
|
||||
public async Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string url)
|
||||
{
|
||||
if (url is not null)
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
using var temp = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (!temp.IsImage())
|
||||
return SetClubIconResult.InvalidFileType;
|
||||
|
||||
if (temp.GetContentLength() > 5.Megabytes().Bytes)
|
||||
return SetClubIconResult.TooLarge;
|
||||
}
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(ownerUserId);
|
||||
|
||||
if (club is null)
|
||||
return SetClubIconResult.NotOwner;
|
||||
|
||||
club.ImageUrl = url;
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
return SetClubIconResult.Success;
|
||||
}
|
||||
|
||||
public bool GetClubByName(string clubName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
club = uow.Set<ClubInfo>().GetByName(clubName);
|
||||
|
||||
return club is not null;
|
||||
}
|
||||
|
||||
public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user);
|
||||
uow.SaveChanges();
|
||||
|
||||
//user banned or a member of a club, or already applied,
|
||||
// or doesn't min minumum level requirement, can't apply
|
||||
if (du.Club is not null)
|
||||
return ClubApplyResult.AlreadyInAClub;
|
||||
|
||||
if (club.Bans.Any(x => x.UserId == du.Id))
|
||||
return ClubApplyResult.Banned;
|
||||
|
||||
if (club.Applicants.Any(x => x.UserId == du.Id))
|
||||
return ClubApplyResult.InsufficientLevel;
|
||||
|
||||
var app = new ClubApplicants
|
||||
{
|
||||
ClubId = club.Id,
|
||||
UserId = du.Id
|
||||
};
|
||||
|
||||
uow.Set<ClubApplicants>().Add(app);
|
||||
uow.SaveChanges();
|
||||
return ClubApplyResult.Success;
|
||||
}
|
||||
|
||||
|
||||
public ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser)
|
||||
{
|
||||
discordUser = null;
|
||||
using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(clubOwnerUserId);
|
||||
if (club is null)
|
||||
return ClubAcceptResult.NotOwnerOrAdmin;
|
||||
|
||||
var applicant =
|
||||
club.Applicants.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
|
||||
if (applicant is null)
|
||||
return ClubAcceptResult.NoSuchApplicant;
|
||||
|
||||
applicant.User.Club = club;
|
||||
applicant.User.IsClubAdmin = false;
|
||||
club.Applicants.Remove(applicant);
|
||||
|
||||
//remove that user's all other applications
|
||||
uow.Set<ClubApplicants>()
|
||||
.RemoveRange(uow.Set<ClubApplicants>().AsQueryable().Where(x => x.UserId == applicant.User.Id));
|
||||
|
||||
discordUser = applicant.User;
|
||||
uow.SaveChanges();
|
||||
return ClubAcceptResult.Accepted;
|
||||
}
|
||||
|
||||
public ClubInfo GetClubWithBansAndApplications(ulong ownerUserId)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
return uow.Set<ClubInfo>().GetByOwnerOrAdmin(ownerUserId);
|
||||
}
|
||||
|
||||
public ClubLeaveResult LeaveClub(IUser user)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var du = uow.GetOrCreateUser(user, x => x.Include(u => u.Club));
|
||||
if (du.Club is null)
|
||||
return ClubLeaveResult.NotInAClub;
|
||||
if (du.Club.OwnerId == du.Id)
|
||||
return ClubLeaveResult.OwnerCantLeave;
|
||||
|
||||
du.Club = null;
|
||||
du.IsClubAdmin = false;
|
||||
uow.SaveChanges();
|
||||
return ClubLeaveResult.Success;
|
||||
}
|
||||
|
||||
public bool SetDescription(ulong userId, string desc)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var club = uow.Set<ClubInfo>().GetByOwner(userId);
|
||||
if (club is null)
|
||||
return false;
|
||||
|
||||
club.Description = desc?.TrimTo(150, true);
|
||||
uow.SaveChanges();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Disband(ulong userId, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
club = uow.Set<ClubInfo>().GetByOwner(userId);
|
||||
if (club is null)
|
||||
return false;
|
||||
|
||||
uow.Set<ClubInfo>().Remove(club);
|
||||
uow.SaveChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ClubBanResult Ban(ulong bannerId, string userName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(bannerId);
|
||||
if (club is null)
|
||||
return ClubBanResult.NotOwnerOrAdmin;
|
||||
|
||||
var usr = club.Members.FirstOrDefault(x => x.ToString().ToUpperInvariant() == userName.ToUpperInvariant())
|
||||
?? club.Applicants
|
||||
.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant())
|
||||
?.User;
|
||||
if (usr is null)
|
||||
return ClubBanResult.WrongUser;
|
||||
|
||||
if (club.OwnerId == usr.Id
|
||||
|| (usr.IsClubAdmin && club.Owner.UserId != bannerId)) // can't ban the owner kek, whew
|
||||
return ClubBanResult.Unbannable;
|
||||
|
||||
club.Bans.Add(new()
|
||||
{
|
||||
Club = club,
|
||||
User = usr
|
||||
});
|
||||
club.Members.Remove(usr);
|
||||
|
||||
var app = club.Applicants.FirstOrDefault(x => x.UserId == usr.Id);
|
||||
if (app is not null)
|
||||
club.Applicants.Remove(app);
|
||||
|
||||
uow.SaveChanges();
|
||||
|
||||
return ClubBanResult.Success;
|
||||
}
|
||||
|
||||
public ClubUnbanResult UnBan(ulong ownerUserId, string userName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(ownerUserId);
|
||||
if (club is null)
|
||||
return ClubUnbanResult.NotOwnerOrAdmin;
|
||||
|
||||
var ban = club.Bans.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
|
||||
if (ban is null)
|
||||
return ClubUnbanResult.WrongUser;
|
||||
|
||||
club.Bans.Remove(ban);
|
||||
uow.SaveChanges();
|
||||
|
||||
return ClubUnbanResult.Success;
|
||||
}
|
||||
|
||||
|
||||
public ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(kickerId);
|
||||
if (club is null)
|
||||
return ClubKickResult.NotOwnerOrAdmin;
|
||||
|
||||
var usr = club.Members.FirstOrDefault(x => x.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
|
||||
if (usr is null)
|
||||
return ClubKickResult.TargetNotAMember;
|
||||
|
||||
if (club.OwnerId == usr.Id || (usr.IsClubAdmin && club.Owner.UserId != kickerId))
|
||||
return ClubKickResult.Hierarchy;
|
||||
|
||||
club.Members.Remove(usr);
|
||||
var app = club.Applicants.FirstOrDefault(x => x.UserId == usr.Id);
|
||||
if (app is not null)
|
||||
club.Applicants.Remove(app);
|
||||
uow.SaveChanges();
|
||||
|
||||
return ClubKickResult.Success;
|
||||
}
|
||||
|
||||
public List<ClubInfo> GetClubLeaderboardPage(int page)
|
||||
{
|
||||
if (page < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(page));
|
||||
|
||||
using var uow = _db.GetDbContext();
|
||||
return uow.Set<ClubInfo>().GetClubLeaderboardPage(page);
|
||||
}
|
||||
}
|
33
src/Ellie.Bot.Modules.Xp/Club/IClubService.cs
Normal file
33
src/Ellie.Bot.Modules.Xp/Club/IClubService.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using Ellie.Db.Models;
|
||||
using OneOf;
|
||||
|
||||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public interface IClubService
|
||||
{
|
||||
Task<ClubCreateResult> CreateClubAsync(IUser user, string clubName);
|
||||
OneOf<ClubInfo,ClubTransferError> TransferClub(IUser from, IUser newOwner);
|
||||
Task<ToggleAdminResult> ToggleAdminAsync(IUser owner, IUser toAdmin);
|
||||
ClubInfo? GetClubByMember(IUser user);
|
||||
Task<SetClubIconResult> SetClubIconAsync(ulong ownerUserId, string? url);
|
||||
bool GetClubByName(string clubName, out ClubInfo club);
|
||||
ClubApplyResult ApplyToClub(IUser user, ClubInfo club);
|
||||
ClubAcceptResult AcceptApplication(ulong clubOwnerUserId, string userName, out DiscordUser discordUser);
|
||||
ClubInfo? GetClubWithBansAndApplications(ulong ownerUserId);
|
||||
ClubLeaveResult LeaveClub(IUser user);
|
||||
bool SetDescription(ulong userId, string? desc);
|
||||
bool Disband(ulong userId, out ClubInfo club);
|
||||
ClubBanResult Ban(ulong bannerId, string userName, out ClubInfo club);
|
||||
ClubUnbanResult UnBan(ulong ownerUserId, string userName, out ClubInfo club);
|
||||
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
|
||||
List<ClubInfo> GetClubLeaderboardPage(int page);
|
||||
}
|
||||
|
||||
public enum ClubApplyResult
|
||||
{
|
||||
Success,
|
||||
|
||||
AlreadyInAClub,
|
||||
Banned,
|
||||
InsufficientLevel
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubAcceptResult
|
||||
{
|
||||
Accepted,
|
||||
NotOwnerOrAdmin,
|
||||
NoSuchApplicant,
|
||||
}
|
9
src/Ellie.Bot.Modules.Xp/Club/Results/ClubBanResult.cs
Normal file
9
src/Ellie.Bot.Modules.Xp/Club/Results/ClubBanResult.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubBanResult
|
||||
{
|
||||
Success,
|
||||
NotOwnerOrAdmin,
|
||||
WrongUser,
|
||||
Unbannable
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubCreateResult
|
||||
{
|
||||
Success,
|
||||
AlreadyInAClub,
|
||||
NameTaken,
|
||||
InsufficientLevel,
|
||||
}
|
9
src/Ellie.Bot.Modules.Xp/Club/Results/ClubKickResult.cs
Normal file
9
src/Ellie.Bot.Modules.Xp/Club/Results/ClubKickResult.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubKickResult
|
||||
{
|
||||
Success,
|
||||
NotOwnerOrAdmin,
|
||||
TargetNotAMember,
|
||||
Hierarchy
|
||||
}
|
8
src/Ellie.Bot.Modules.Xp/Club/Results/ClubLeaveResult.cs
Normal file
8
src/Ellie.Bot.Modules.Xp/Club/Results/ClubLeaveResult.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubLeaveResult
|
||||
{
|
||||
Success,
|
||||
OwnerCantLeave,
|
||||
NotInAClub
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubTransferError
|
||||
{
|
||||
NotOwner,
|
||||
TargetNotMember
|
||||
}
|
8
src/Ellie.Bot.Modules.Xp/Club/Results/ClubUnbanResult.cs
Normal file
8
src/Ellie.Bot.Modules.Xp/Club/Results/ClubUnbanResult.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ClubUnbanResult
|
||||
{
|
||||
Success,
|
||||
NotOwnerOrAdmin,
|
||||
WrongUser
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum SetClubIconResult
|
||||
{
|
||||
Success,
|
||||
InvalidFileType,
|
||||
TooLarge,
|
||||
NotOwner,
|
||||
}
|
10
src/Ellie.Bot.Modules.Xp/Club/Results/ToggleAdminResult.cs
Normal file
10
src/Ellie.Bot.Modules.Xp/Club/Results/ToggleAdminResult.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public enum ToggleAdminResult
|
||||
{
|
||||
AddedAdmin,
|
||||
RemovedAdmin,
|
||||
NotOwner,
|
||||
TargetNotMember,
|
||||
CantTargetThyself,
|
||||
}
|
21
src/Ellie.Bot.Modules.Xp/Ellie.Bot.Modules.Xp.csproj
Normal file
21
src/Ellie.Bot.Modules.Xp/Ellie.Bot.Modules.Xp.csproj
Normal file
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="linq2db.EntityFrameworkCore" Version="7.3.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ellie.Bot.Common\Ellie.Bot.Common.csproj"/>
|
||||
<ProjectReference Include="..\Ellie.Bot.Db\Ellie.Bot.Db.csproj"/>
|
||||
|
||||
|
||||
<ProjectReference Include="..\Ellie.Bot.Generators.Cloneable\Ellie.Bot.Generators.Cloneable.csproj" OutputItemType="Analyzer"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
32
src/Ellie.Bot.Modules.Xp/GlobalUsings.cs
Normal file
32
src/Ellie.Bot.Modules.Xp/GlobalUsings.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
// // global using System.Collections.Concurrent;
|
||||
global using NonBlocking;
|
||||
//
|
||||
// // packages
|
||||
global using Serilog;
|
||||
global using Humanizer;
|
||||
global using Newtonsoft;
|
||||
//
|
||||
// // ellie
|
||||
// global using Ellie;
|
||||
global using Ellie.Services;
|
||||
global using Ellise.Common; // new project
|
||||
global using Ellie.Common; // old + ellie specific things
|
||||
global using Ellie.Common.Attributes;
|
||||
global using Ellie.Extensions;
|
||||
// global using Nadeko.Snake;
|
||||
|
||||
// discord
|
||||
global using Discord;
|
||||
global using Discord.Commands;
|
||||
global using Discord.Net;
|
||||
global using Discord.WebSocket;
|
||||
|
||||
// aliases
|
||||
global using GuildPerm = Discord.GuildPermission;
|
||||
global using ChannelPerm = Discord.ChannelPermission;
|
||||
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
||||
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
||||
global using TypeReaderResult = Ellie.Common.TypeReaders.TypeReaderResult;
|
||||
|
||||
// non-essential
|
||||
// global using JetBrains.Annotations;
|
559
src/Ellie.Bot.Modules.Xp/Xp.cs
Normal file
559
src/Ellie.Bot.Modules.Xp/Xp.cs
Normal file
|
@ -0,0 +1,559 @@
|
|||
#nullable disable warnings
|
||||
using Ellie.Modules.Xp.Services;
|
||||
using Ellie.Services.Database.Models;
|
||||
using Ellie.Bot.Common;
|
||||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Modules.Patronage;
|
||||
|
||||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public partial class Xp : EllieModule<XpService>
|
||||
{
|
||||
public enum Channel
|
||||
{
|
||||
Channel
|
||||
}
|
||||
|
||||
public enum NotifyPlace
|
||||
{
|
||||
Server = 0,
|
||||
Guild = 0,
|
||||
Global = 1
|
||||
}
|
||||
|
||||
public enum Role
|
||||
{
|
||||
Role
|
||||
}
|
||||
|
||||
public enum Server
|
||||
{
|
||||
Server
|
||||
}
|
||||
|
||||
private readonly DownloadTracker _tracker;
|
||||
private readonly ICurrencyProvider _gss;
|
||||
|
||||
public Xp(DownloadTracker tracker, ICurrencyProvider gss)
|
||||
{
|
||||
_tracker = tracker;
|
||||
_gss = gss;
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task Experience([Leftover] IUser user = null)
|
||||
{
|
||||
user ??= ctx.User;
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
var (img, fmt) = await _service.GenerateXpImageAsync((IGuildUser)user);
|
||||
await using (img)
|
||||
{
|
||||
await ctx.Channel.SendFileAsync(img, $"{ctx.Guild.Id}_{user.Id}_xp.{fmt.FileExtensions.FirstOrDefault()}");
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpNotify()
|
||||
{
|
||||
var globalSetting = _service.GetNotificationType(ctx.User);
|
||||
var serverSetting = _service.GetNotificationType(ctx.User.Id, ctx.Guild.Id);
|
||||
|
||||
var embed = _eb.Create()
|
||||
.WithOkColor()
|
||||
.AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting))
|
||||
.AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting));
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpNotify(NotifyPlace place, XpNotificationLocation type)
|
||||
{
|
||||
if (place == NotifyPlace.Guild)
|
||||
await _service.ChangeNotificationType(ctx.User.Id, ctx.Guild.Id, type);
|
||||
else
|
||||
await _service.ChangeNotificationType(ctx.User, type);
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpExclude(Server _)
|
||||
{
|
||||
var ex = _service.ToggleExcludeServer(ctx.Guild.Id);
|
||||
|
||||
if (ex)
|
||||
await ReplyConfirmLocalizedAsync(strs.excluded(Format.Bold(ctx.Guild.ToString())));
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync(strs.not_excluded(Format.Bold(ctx.Guild.ToString())));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.ManageRoles)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpExclude(Role _, [Leftover] IRole role)
|
||||
{
|
||||
var ex = _service.ToggleExcludeRole(ctx.Guild.Id, role.Id);
|
||||
|
||||
if (ex)
|
||||
await ReplyConfirmLocalizedAsync(strs.excluded(Format.Bold(role.ToString())));
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync(strs.not_excluded(Format.Bold(role.ToString())));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.ManageChannels)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpExclude(Channel _, [Leftover] IChannel channel = null)
|
||||
{
|
||||
if (channel is null)
|
||||
channel = ctx.Channel;
|
||||
|
||||
var ex = _service.ToggleExcludeChannel(ctx.Guild.Id, channel.Id);
|
||||
|
||||
if (ex)
|
||||
await ReplyConfirmLocalizedAsync(strs.excluded(Format.Bold(channel.ToString())));
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync(strs.not_excluded(Format.Bold(channel.ToString())));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpExclusionList()
|
||||
{
|
||||
var serverExcluded = _service.IsServerExcluded(ctx.Guild.Id);
|
||||
var roles = _service.GetExcludedRoles(ctx.Guild.Id)
|
||||
.Select(x => ctx.Guild.GetRole(x))
|
||||
.Where(x => x is not null)
|
||||
.Select(x => $"`role` {x.Mention}")
|
||||
.ToList();
|
||||
|
||||
var chans = (await _service.GetExcludedChannels(ctx.Guild.Id)
|
||||
.Select(x => ctx.Guild.GetChannelAsync(x))
|
||||
.WhenAll()).Where(x => x is not null)
|
||||
.Select(x => $"`channel` <#{x.Id}>")
|
||||
.ToList();
|
||||
|
||||
var rolesStr = roles.Any() ? string.Join("\n", roles) + "\n" : string.Empty;
|
||||
var chansStr = chans.Count > 0 ? string.Join("\n", chans) + "\n" : string.Empty;
|
||||
var desc = Format.Code(serverExcluded
|
||||
? GetText(strs.server_is_excluded)
|
||||
: GetText(strs.server_is_not_excluded));
|
||||
|
||||
desc += "\n\n" + rolesStr + chansStr;
|
||||
|
||||
var lines = desc.Split('\n');
|
||||
await ctx.SendPaginatedConfirmAsync(0,
|
||||
curpage =>
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithTitle(GetText(strs.exclusion_list))
|
||||
.WithDescription(string.Join('\n', lines.Skip(15 * curpage).Take(15)))
|
||||
.WithOkColor();
|
||||
|
||||
return embed;
|
||||
},
|
||||
lines.Length,
|
||||
15);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[EllieOptions<LbOpts>]
|
||||
[Priority(0)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public Task XpLeaderboard(params string[] args)
|
||||
=> XpLeaderboard(1, args);
|
||||
|
||||
[Cmd]
|
||||
[EllieOptions<LbOpts>]
|
||||
[Priority(1)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpLeaderboard(int page = 1, params string[] args)
|
||||
{
|
||||
if (--page < 0 || page > 100)
|
||||
return;
|
||||
|
||||
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
|
||||
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
|
||||
var socketGuild = (SocketGuild)ctx.Guild;
|
||||
var allUsers = new List<UserXpStats>();
|
||||
if (opts.Clean)
|
||||
{
|
||||
await ctx.Channel.TriggerTypingAsync();
|
||||
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
|
||||
|
||||
allUsers = _service.GetTopUserXps(ctx.Guild.Id, 1000)
|
||||
.Where(user => socketGuild.GetUser(user.UserId) is not null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync(page,
|
||||
curPage =>
|
||||
{
|
||||
var embed = _eb.Create().WithTitle(GetText(strs.server_leaderboard)).WithOkColor();
|
||||
|
||||
List<UserXpStats> users;
|
||||
if (opts.Clean)
|
||||
users = allUsers.Skip(curPage * 9).Take(9).ToList();
|
||||
else
|
||||
users = _service.GetUserXps(ctx.Guild.Id, curPage);
|
||||
|
||||
if (!users.Any())
|
||||
return embed.WithDescription("-");
|
||||
|
||||
for (var i = 0; i < users.Count; i++)
|
||||
{
|
||||
var levelStats = new LevelStats(users[i].Xp + users[i].AwardedXp);
|
||||
var user = ((SocketGuild)ctx.Guild).GetUser(users[i].UserId);
|
||||
|
||||
var userXpData = users[i];
|
||||
|
||||
var awardStr = string.Empty;
|
||||
if (userXpData.AwardedXp > 0)
|
||||
awardStr = $"(+{userXpData.AwardedXp})";
|
||||
else if (userXpData.AwardedXp < 0)
|
||||
awardStr = $"({userXpData.AwardedXp})";
|
||||
|
||||
embed.AddField($"#{i + 1 + (curPage * 9)} {user?.ToString() ?? users[i].UserId.ToString()}",
|
||||
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
|
||||
}
|
||||
|
||||
return embed;
|
||||
},
|
||||
900,
|
||||
9,
|
||||
false);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public async Task XpGlobalLeaderboard(int page = 1)
|
||||
{
|
||||
if (--page < 0 || page > 99)
|
||||
return;
|
||||
var users = _service.GetUserXps(page);
|
||||
|
||||
var embed = _eb.Create().WithTitle(GetText(strs.global_leaderboard)).WithOkColor();
|
||||
|
||||
if (!users.Any())
|
||||
embed.WithDescription("-");
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < users.Length; i++)
|
||||
{
|
||||
var user = users[i];
|
||||
embed.AddField($"#{i + 1 + (page * 9)} {user.ToString()}",
|
||||
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.Channel.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[Priority(2)]
|
||||
public async Task XpAdd(long amount, [Remainder] SocketRole role)
|
||||
{
|
||||
if (amount == 0)
|
||||
return;
|
||||
|
||||
if (role.IsManaged)
|
||||
return;
|
||||
|
||||
var count = await _service.AddXpToUsersAsync(ctx.Guild.Id, amount, role.Members.Select(x => x.Id).ToArray());
|
||||
await ReplyConfirmLocalizedAsync(
|
||||
strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString())));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[Priority(3)]
|
||||
public async Task XpAdd(int amount, ulong userId)
|
||||
{
|
||||
if (amount == 0)
|
||||
return;
|
||||
|
||||
_service.AddXp(userId, ctx.Guild.Id, amount);
|
||||
var usr = ((SocketGuild)ctx.Guild).GetUser(userId)?.ToString() ?? userId.ToString();
|
||||
await ReplyConfirmLocalizedAsync(strs.modified(Format.Bold(usr), Format.Bold(amount.ToString())));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[Priority(4)]
|
||||
public Task XpAdd(int amount, [Leftover] IGuildUser user)
|
||||
=> XpAdd(amount, user.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
public async Task XpTemplateReload()
|
||||
{
|
||||
_service.ReloadXpTemplate();
|
||||
await Task.Delay(1000);
|
||||
await ReplyConfirmLocalizedAsync(strs.template_reloaded);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public Task XpReset(IGuildUser user)
|
||||
=> XpReset(user.Id);
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpReset(ulong userId)
|
||||
{
|
||||
var embed = _eb.Create().WithTitle(GetText(strs.reset)).WithDescription(GetText(strs.reset_user_confirm));
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
|
||||
_service.XpReset(ctx.Guild.Id, userId);
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.reset_user(userId));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpReset()
|
||||
{
|
||||
var embed = _eb.Create().WithTitle(GetText(strs.reset)).WithDescription(GetText(strs.reset_server_confirm));
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
|
||||
_service.XpReset(ctx.Guild.Id);
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.reset_server);
|
||||
}
|
||||
|
||||
public enum XpShopInputType
|
||||
{
|
||||
Backgrounds = 0,
|
||||
B = 0,
|
||||
Bg = 0,
|
||||
Bgs = 0,
|
||||
Frames = 1,
|
||||
F = 1,
|
||||
Fr = 1,
|
||||
Frs = 1,
|
||||
Fs = 1,
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task XpShop()
|
||||
{
|
||||
if (!_service.IsShopEnabled())
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.xp_shop_disabled);
|
||||
return;
|
||||
}
|
||||
|
||||
await SendConfirmAsync(GetText(strs.available_commands),
|
||||
$@"`{prefix}xpshop bgs`
|
||||
`{prefix}xpshop frames`
|
||||
|
||||
*{GetText(strs.xpshop_website)}*");
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task XpShop(XpShopInputType type, int page = 1)
|
||||
{
|
||||
--page;
|
||||
|
||||
if (page < 0)
|
||||
return;
|
||||
|
||||
var items = type == XpShopInputType.Backgrounds
|
||||
? await _service.GetShopBgs()
|
||||
: await _service.GetShopFrames();
|
||||
|
||||
if (items is null)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.xp_shop_disabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_found);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.SendPaginatedConfirmAsync<(string, XpShopItemType)?>(page,
|
||||
current =>
|
||||
{
|
||||
var (key, item) = items.Skip(current).First();
|
||||
|
||||
var eb = _eb.Create(ctx)
|
||||
.WithOkColor()
|
||||
.WithTitle(item.Name)
|
||||
.AddField(GetText(strs.price), CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()), true)
|
||||
.WithImageUrl(string.IsNullOrWhiteSpace(item.Preview)
|
||||
? item.Url
|
||||
: item.Preview);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Desc))
|
||||
eb.AddField(GetText(strs.desc), item.Desc);
|
||||
|
||||
if (key == "default")
|
||||
eb.WithDescription(GetText(strs.xpshop_website));
|
||||
|
||||
|
||||
var tier = _service.GetXpShopTierRequirement(type);
|
||||
if (tier != PatronTier.None)
|
||||
{
|
||||
eb.WithFooter(GetText(strs.xp_shop_buy_required_tier(tier.ToString())));
|
||||
}
|
||||
|
||||
return Task.FromResult(eb);
|
||||
},
|
||||
async current =>
|
||||
{
|
||||
var (key, _) = items.Skip(current).First();
|
||||
|
||||
var itemType = type == XpShopInputType.Backgrounds
|
||||
? XpShopItemType.Background
|
||||
: XpShopItemType.Frame;
|
||||
|
||||
var ownedItem = await _service.GetUserItemAsync(ctx.User.Id, itemType, key);
|
||||
if (ownedItem is not null)
|
||||
{
|
||||
var button = new ButtonBuilder(ownedItem.IsUsing
|
||||
? GetText(strs.in_use)
|
||||
: GetText(strs.use),
|
||||
"xpshop:use",
|
||||
emote: Emoji.Parse("👐"),
|
||||
isDisabled: ownedItem.IsUsing);
|
||||
|
||||
var inter = new SimpleInteraction<(string key, XpShopItemType type)?>(
|
||||
button,
|
||||
OnShopUse,
|
||||
(key, itemType));
|
||||
|
||||
return inter;
|
||||
}
|
||||
else
|
||||
{
|
||||
var button = new ButtonBuilder(GetText(strs.buy),
|
||||
"xpshop:buy",
|
||||
emote: Emoji.Parse("💰"));
|
||||
|
||||
var inter = new SimpleInteraction<(string key, XpShopItemType type)?>(
|
||||
button,
|
||||
OnShopBuy,
|
||||
(key, itemType));
|
||||
|
||||
return inter;
|
||||
}
|
||||
},
|
||||
items.Count,
|
||||
1,
|
||||
addPaginatedFooter: false);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task XpShopBuy(XpShopInputType type, string key)
|
||||
{
|
||||
var result = await _service.BuyShopItemAsync(ctx.User.Id, (XpShopItemType)type, key);
|
||||
|
||||
EllieInteraction GetUseInteraction()
|
||||
{
|
||||
return _inter.Create(ctx.User.Id,
|
||||
new SimpleInteraction<object>(
|
||||
new ButtonBuilder(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")),
|
||||
async (smc, _) => await XpShopUse(type, key)
|
||||
));
|
||||
}
|
||||
|
||||
if (result != BuyResult.Success)
|
||||
{
|
||||
var _ = result switch
|
||||
{
|
||||
BuyResult.XpShopDisabled => await ReplyErrorLocalizedAsync(strs.xp_shop_disabled),
|
||||
BuyResult.InsufficientFunds => await ReplyErrorLocalizedAsync(strs.not_enough(_gss.GetCurrencySign())),
|
||||
BuyResult.AlreadyOwned =>
|
||||
await ReplyErrorLocalizedAsync(strs.xpshop_already_owned, GetUseInteraction()),
|
||||
BuyResult.UnknownItem => await ReplyErrorLocalizedAsync(strs.xpshop_item_not_found),
|
||||
BuyResult.InsufficientPatronTier => await ReplyErrorLocalizedAsync(strs.patron_insuff_tier),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await ReplyConfirmLocalizedAsync(strs.xpshop_buy_success(type.ToString().ToLowerInvariant(),
|
||||
key.ToLowerInvariant()),
|
||||
GetUseInteraction());
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task XpShopUse(XpShopInputType type, string key)
|
||||
{
|
||||
var result = await _service.UseShopItemAsync(ctx.User.Id, (XpShopItemType)type, key);
|
||||
|
||||
if (!result)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.xp_shop_item_cant_use);
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
private async Task OnShopUse(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState)
|
||||
{
|
||||
if (maybeState is not { } state)
|
||||
return;
|
||||
|
||||
var (key, type) = state;
|
||||
|
||||
var result = await _service.UseShopItemAsync(ctx.User.Id, type, key);
|
||||
|
||||
|
||||
if (!result)
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.xp_shop_item_cant_use);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnShopBuy(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState)
|
||||
{
|
||||
if (maybeState is not { } state)
|
||||
return;
|
||||
|
||||
var (key, type) = state;
|
||||
|
||||
var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key);
|
||||
|
||||
if (result == BuyResult.InsufficientFunds)
|
||||
{
|
||||
await ReplyErrorLocalizedAsync(strs.not_enough(_gss.GetCurrencySign()));
|
||||
}
|
||||
}
|
||||
|
||||
private string GetNotifLocationString(XpNotificationLocation loc)
|
||||
{
|
||||
if (loc == XpNotificationLocation.Channel)
|
||||
return GetText(strs.xpn_notif_channel);
|
||||
|
||||
if (loc == XpNotificationLocation.Dm)
|
||||
return GetText(strs.xpn_notif_dm);
|
||||
|
||||
return GetText(strs.xpn_notif_disabled);
|
||||
}
|
||||
}
|
109
src/Ellie.Bot.Modules.Xp/XpConfig.cs
Normal file
109
src/Ellie.Bot.Modules.Xp/XpConfig.cs
Normal file
|
@ -0,0 +1,109 @@
|
|||
#nullable disable
|
||||
using Cloneable;
|
||||
using Ellie.Common.Yml;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Modules.Patronage;
|
||||
|
||||
namespace Ellie.Modules.Xp;
|
||||
|
||||
[Cloneable]
|
||||
public sealed partial class XpConfig : ICloneable<XpConfig>
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; } = 5;
|
||||
|
||||
[Comment("""How much XP will the users receive per message""")]
|
||||
public int XpPerMessage { get; set; } = 3;
|
||||
|
||||
[Comment("""How often can the users receive XP in minutes""")]
|
||||
public int MessageXpCooldown { get; set; } = 5;
|
||||
|
||||
[Comment("""Amount of xp users gain from posting an image""")]
|
||||
public int XpFromImage { get; set; } = 0;
|
||||
|
||||
[Comment("""Average amount of xp earned per minute in VC""")]
|
||||
public double VoiceXpPerMinute { get; set; } = 0;
|
||||
|
||||
[Comment("""The maximum amount of minutes the bot will keep track of a user in a voice channel""")]
|
||||
public int VoiceMaxMinutes { get; set; } = 720;
|
||||
|
||||
[Comment("""The amount of currency users will receive for each point of global xp that they earn""")]
|
||||
public float CurrencyPerXp { get; set; } = 0;
|
||||
|
||||
[Comment("""Xp Shop config""")]
|
||||
public ShopConfig Shop { get; set; } = new();
|
||||
|
||||
public sealed class ShopConfig
|
||||
{
|
||||
[Comment("""
|
||||
Whether the xp shop is enabled
|
||||
True -> Users can access the xp shop using .xpshop command
|
||||
False -> Users can't access the xp shop
|
||||
""")]
|
||||
public bool IsEnabled { get; set; } = false;
|
||||
|
||||
[Comment("""
|
||||
Which patron tier do users need in order to use the .xpshop bgs command
|
||||
Leave at 'None' if patron system is disabled or you don't want any restrictions
|
||||
""")]
|
||||
public PatronTier BgsTierRequirement { get; set; } = PatronTier.None;
|
||||
|
||||
[Comment("""
|
||||
Which patron tier do users need in order to use the .xpshop frames command
|
||||
Leave at 'None' if patron system is disabled or you don't want any restrictions
|
||||
""")]
|
||||
public PatronTier FramesTierRequirement { get; set; } = PatronTier.None;
|
||||
|
||||
[Comment("""
|
||||
Frames available for sale. Keys are unique IDs.
|
||||
Do not change keys as they are not publicly visible. Only change properties (name, price, id)
|
||||
Removing a key which previously existed means that all previous purchases will also be unusable.
|
||||
To remove an item from the shop, but keep previous purchases, set the price to -1
|
||||
""")]
|
||||
public Dictionary<string, ShopItemInfo>? Frames { get; set; } = new()
|
||||
{
|
||||
{"default", new() {Name = "No frame", Price = 0, Url = string.Empty}}
|
||||
};
|
||||
|
||||
[Comment("""
|
||||
Backgrounds available for sale. Keys are unique IDs.
|
||||
Do not change keys as they are not publicly visible. Only change properties (name, price, id)
|
||||
Removing a key which previously existed means that all previous purchases will also be unusable.
|
||||
To remove an item from the shop, but keep previous purchases, set the price to -1
|
||||
""")]
|
||||
public Dictionary<string, ShopItemInfo>? Bgs { get; set; } = new()
|
||||
{
|
||||
{"default", new() {Name = "Default Background", Price = 0, Url = string.Empty}}
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class ShopItemInfo
|
||||
{
|
||||
[Comment("""Visible name of the item""")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Comment("""Price of the item. Set to -1 if you no longer want to sell the item but want the users to be able to keep their old purchase""")]
|
||||
public int Price { get; set; }
|
||||
|
||||
[Comment("""Direct url to the .png image which will be applied to the user's XP card""")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[Comment("""Optional preview url which will show instead of the real URL in the shop """)]
|
||||
public string Preview { get; set; }
|
||||
|
||||
[Comment("""Optional description of the item""")]
|
||||
public string Desc { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public static class XpShopConfigExtensions
|
||||
{
|
||||
public static string? GetItemUrl(this XpConfig.ShopConfig sc, XpShopItemType type, string key)
|
||||
=> (type switch
|
||||
{
|
||||
XpShopItemType.Background => sc.Bgs,
|
||||
_ => sc.Frames
|
||||
})?.TryGetValue(key, out var item) ?? false
|
||||
? item.Url
|
||||
: null;
|
||||
}
|
63
src/Ellie.Bot.Modules.Xp/XpConfigService.cs
Normal file
63
src/Ellie.Bot.Modules.Xp/XpConfigService.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Configs;
|
||||
|
||||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
public sealed class XpConfigService : ConfigServiceBase<XpConfig>
|
||||
{
|
||||
private const string FILE_PATH = "data/xp.yml";
|
||||
private static readonly TypedKey<XpConfig> _changeKey = new("config.xp.updated");
|
||||
|
||||
public override string Name
|
||||
=> "xp";
|
||||
|
||||
public XpConfigService(IConfigSeria serializer, IPubSub pubSub)
|
||||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
AddParsedProp("txt.cooldown",
|
||||
conf => conf.MessageXpCooldown,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
x => x > 0);
|
||||
AddParsedProp("txt.per_msg", conf => conf.XpPerMessage, int.TryParse, ConfigPrinters.ToString, x => x >= 0);
|
||||
AddParsedProp("txt.per_image", conf => conf.XpFromImage, int.TryParse, ConfigPrinters.ToString, x => x > 0);
|
||||
|
||||
AddParsedProp("voice.per_minute",
|
||||
conf => conf.VoiceXpPerMinute,
|
||||
double.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
x => x >= 0);
|
||||
AddParsedProp("voice.max_minutes",
|
||||
conf => conf.VoiceMaxMinutes,
|
||||
int.TryParse,
|
||||
ConfigPrinters.ToString,
|
||||
x => x > 0);
|
||||
|
||||
AddParsedProp("shop.is_enabled",
|
||||
conf => conf.Shop.IsEnabled,
|
||||
bool.TryParse,
|
||||
ConfigPrinters.ToString);
|
||||
|
||||
Migrate();
|
||||
}
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
if (data.Version < 2)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 2;
|
||||
c.XpFromImage = 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (data.Version < 6)
|
||||
{
|
||||
ModifyConfig(c =>
|
||||
{
|
||||
c.Version = 6;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
138
src/Ellie.Bot.Modules.Xp/XpRewards.cs
Normal file
138
src/Ellie.Bot.Modules.Xp/XpRewards.cs
Normal file
|
@ -0,0 +1,138 @@
|
|||
using Ellie.Bot.Common;
|
||||
using Ellie.Modules.Xp.Services;
|
||||
|
||||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public partial class Xp
|
||||
{
|
||||
public partial class XpRewards : EllieModule<XpService>
|
||||
{
|
||||
private readonly ICurrencyProvider _cp;
|
||||
|
||||
public XpRewards(ICurrencyProvider cp)
|
||||
=> _cp = cp;
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
public async Task XpRewsReset()
|
||||
{
|
||||
var promptEmbed = _eb.Create()
|
||||
.WithPendingColor()
|
||||
.WithDescription(GetText(strs.xprewsreset_confirm));
|
||||
|
||||
var reply = await PromptUserConfirmAsync(promptEmbed);
|
||||
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
await _service.ResetXpRewards(ctx.Guild.Id);
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
public Task XpLevelUpRewards(int page = 1)
|
||||
{
|
||||
page--;
|
||||
|
||||
if (page is < 0 or > 100)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var allRewards = _service.GetRoleRewards(ctx.Guild.Id)
|
||||
.OrderBy(x => x.Level)
|
||||
.Select(x =>
|
||||
{
|
||||
var sign = !x.Remove ? "✅ " : "❌ ";
|
||||
|
||||
var str = ctx.Guild.GetRole(x.RoleId)?.ToString();
|
||||
|
||||
if (str is null)
|
||||
str = GetText(strs.role_not_found(Format.Code(x.RoleId.ToString())));
|
||||
else
|
||||
{
|
||||
if (!x.Remove)
|
||||
str = GetText(strs.xp_receive_role(Format.Bold(str)));
|
||||
else
|
||||
str = GetText(strs.xp_lose_role(Format.Bold(str)));
|
||||
}
|
||||
|
||||
return (x.Level, Text: sign + str);
|
||||
})
|
||||
.Concat(_service.GetCurrencyRewards(ctx.Guild.Id)
|
||||
.OrderBy(x => x.Level)
|
||||
.Select(x => (x.Level,
|
||||
Format.Bold(x.Amount + _cp.GetCurrencySign()))))
|
||||
.GroupBy(x => x.Level)
|
||||
.OrderBy(x => x.Key)
|
||||
.ToList();
|
||||
|
||||
return Context.SendPaginatedConfirmAsync(page,
|
||||
cur =>
|
||||
{
|
||||
var embed = _eb.Create().WithTitle(GetText(strs.level_up_rewards)).WithOkColor();
|
||||
|
||||
var localRewards = allRewards.Skip(cur * 9).Take(9).ToList();
|
||||
|
||||
if (!localRewards.Any())
|
||||
return embed.WithDescription(GetText(strs.no_level_up_rewards));
|
||||
|
||||
foreach (var reward in localRewards)
|
||||
embed.AddField(GetText(strs.level_x(reward.Key)),
|
||||
string.Join("\n", reward.Select(y => y.Item2)));
|
||||
|
||||
return embed;
|
||||
},
|
||||
allRewards.Count,
|
||||
9);
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(2)]
|
||||
public async Task XpRoleReward(int level)
|
||||
{
|
||||
_service.ResetRoleReward(ctx.Guild.Id, level);
|
||||
await ReplyConfirmLocalizedAsync(strs.xp_role_reward_cleared(level));
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[UserPerm(GuildPerm.Administrator)]
|
||||
[BotPerm(GuildPerm.ManageRoles)]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[Priority(1)]
|
||||
public async Task XpRoleReward(int level, AddRemove action, [Leftover] IRole role)
|
||||
{
|
||||
if (level < 1)
|
||||
return;
|
||||
|
||||
_service.SetRoleReward(ctx.Guild.Id, level, role.Id, action == AddRemove.Remove);
|
||||
if (action == AddRemove.Add)
|
||||
await ReplyConfirmLocalizedAsync(strs.xp_role_reward_add_role(level, Format.Bold(role.ToString())));
|
||||
else
|
||||
{
|
||||
await ReplyConfirmLocalizedAsync(strs.xp_role_reward_remove_role(Format.Bold(level.ToString()),
|
||||
Format.Bold(role.ToString())));
|
||||
}
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[OwnerOnly]
|
||||
public async Task XpCurrencyReward(int level, int amount = 0)
|
||||
{
|
||||
if (level < 1 || amount < 0)
|
||||
return;
|
||||
|
||||
// todo, shouldn't all amount + sign be N(amount)
|
||||
_service.SetCurrencyReward(ctx.Guild.Id, level, amount);
|
||||
if (amount == 0)
|
||||
await ReplyConfirmLocalizedAsync(strs.cur_reward_cleared(level, _cp.GetCurrencySign()));
|
||||
else
|
||||
await ReplyConfirmLocalizedAsync(strs.cur_reward_added(level,
|
||||
Format.Bold(amount + _cp.GetCurrencySign())));
|
||||
}
|
||||
}
|
||||
}
|
1252
src/Ellie.Bot.Modules.Xp/XpService.cs
Normal file
1252
src/Ellie.Bot.Modules.Xp/XpService.cs
Normal file
File diff suppressed because it is too large
Load diff
7
src/Ellie.Bot.Modules.Xp/_common/Extensions.cs
Normal file
7
src/Ellie.Bot.Modules.Xp/_common/Extensions.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Modules.Xp.Extensions;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
|
||||
}
|
32
src/Ellie.Bot.Modules.Xp/_common/FullUserStats.cs
Normal file
32
src/Ellie.Bot.Modules.Xp/_common/FullUserStats.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
#nullable disable
|
||||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public class FullUserStats
|
||||
{
|
||||
public DiscordUser User { get; }
|
||||
public UserXpStats FullGuildStats { get; }
|
||||
public LevelStats Global { get; }
|
||||
public LevelStats Guild { get; }
|
||||
public int GlobalRanking { get; }
|
||||
public int GuildRanking { get; }
|
||||
|
||||
public FullUserStats(
|
||||
DiscordUser user,
|
||||
UserXpStats fullGuildStats,
|
||||
LevelStats global,
|
||||
LevelStats guild,
|
||||
int globalRanking,
|
||||
int guildRanking)
|
||||
{
|
||||
User = user;
|
||||
Global = global;
|
||||
Guild = guild;
|
||||
GlobalRanking = globalRanking;
|
||||
GuildRanking = guildRanking;
|
||||
FullGuildStats = fullGuildStats;
|
||||
}
|
||||
}
|
6
src/Ellie.Bot.Modules.Xp/_common/IXpCleanupService.cs
Normal file
6
src/Ellie.Bot.Modules.Xp/_common/IXpCleanupService.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public interface IXpCleanupService
|
||||
{
|
||||
Task DeleteXp();
|
||||
}
|
13
src/Ellie.Bot.Modules.Xp/_common/UserCacheItem.cs
Normal file
13
src/Ellie.Bot.Modules.Xp/_common/UserCacheItem.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#nullable disable warnings
|
||||
using Cloneable;
|
||||
|
||||
namespace Ellie.Modules.Xp.Services;
|
||||
|
||||
[Cloneable]
|
||||
public sealed partial class UserCacheItem : ICloneable<UserXpGainData>
|
||||
{
|
||||
public IGuildUser User { get; init; }
|
||||
public IGuild Guild { get; init; }
|
||||
public IMessageChannel Channel { get; init; }
|
||||
public int XpAmount { get; set; }
|
||||
}
|
32
src/Ellie.Bot.Modules.Xp/_common/XpCleanupService.cs
Normal file
32
src/Ellie.Bot.Modules.Xp/_common/XpCleanupService.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using LinqToDB;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public sealed class XpCleanupService : IXpCleanupService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
|
||||
public XpCleanupService(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task DeleteXp()
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.Set<DiscordUser>().UpdateAsync(_ => new DiscordUser()
|
||||
{
|
||||
ClubId = null,
|
||||
// IsClubAdmin = false
|
||||
TotalXp = 0
|
||||
});
|
||||
|
||||
await uow.Set<UserXpStats>().DeleteAsync();
|
||||
await uow.Set<ClubApplicants>().DeleteAsync();
|
||||
await uow.Set<ClubBans>().DeleteAsync();
|
||||
await uow.Set<ClubInfo>().DeleteAsync();
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
}
|
271
src/Ellie.Bot.Modules.Xp/_common/XpTemplate.cs
Normal file
271
src/Ellie.Bot.Modules.Xp/_common/XpTemplate.cs
Normal file
|
@ -0,0 +1,271 @@
|
|||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
namespace Ellie.Modules.Xp;
|
||||
|
||||
public class XpTemplate
|
||||
{
|
||||
public int Version { get; set; } = 0;
|
||||
|
||||
[JsonProperty("output_size")]
|
||||
public XpTemplatePos OutputSize { get; set; } = new()
|
||||
{
|
||||
X = 800,
|
||||
Y = 392
|
||||
};
|
||||
|
||||
public XpTemplateUser User { get; set; } = new()
|
||||
{
|
||||
Name = new()
|
||||
{
|
||||
FontSize = 50,
|
||||
Show = true,
|
||||
Pos = new()
|
||||
{
|
||||
X = 130,
|
||||
Y = 17
|
||||
}
|
||||
},
|
||||
Icon = new()
|
||||
{
|
||||
Show = true,
|
||||
Pos = new()
|
||||
{
|
||||
X = 14,
|
||||
Y = 14
|
||||
},
|
||||
Size = new()
|
||||
{
|
||||
X = 72,
|
||||
Y = 71
|
||||
}
|
||||
},
|
||||
GuildLevel = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 45,
|
||||
Pos = new()
|
||||
{
|
||||
X = 47,
|
||||
Y = 308
|
||||
}
|
||||
},
|
||||
GlobalLevel = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 45,
|
||||
Pos = new()
|
||||
{
|
||||
X = 47,
|
||||
Y = 160
|
||||
}
|
||||
},
|
||||
GuildRank = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 30,
|
||||
Pos = new()
|
||||
{
|
||||
X = 148,
|
||||
Y = 326
|
||||
}
|
||||
},
|
||||
GlobalRank = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 30,
|
||||
Pos = new()
|
||||
{
|
||||
X = 148,
|
||||
Y = 179
|
||||
}
|
||||
},
|
||||
Xp = new()
|
||||
{
|
||||
Bar = new()
|
||||
{
|
||||
Show = true,
|
||||
Global = new()
|
||||
{
|
||||
Direction = XpTemplateDirection.Right,
|
||||
Length = 450,
|
||||
Color = new(0, 0, 0, 0.4f),
|
||||
PointA = new()
|
||||
{
|
||||
X = 321,
|
||||
Y = 104
|
||||
},
|
||||
PointB = new()
|
||||
{
|
||||
X = 286,
|
||||
Y = 235
|
||||
}
|
||||
},
|
||||
Guild = new()
|
||||
{
|
||||
Direction = XpTemplateDirection.Right,
|
||||
Length = 450,
|
||||
Color = new(0, 0, 0, 0.4f),
|
||||
PointA = new()
|
||||
{
|
||||
X = 282,
|
||||
Y = 248
|
||||
},
|
||||
PointB = new()
|
||||
{
|
||||
X = 247,
|
||||
Y = 379
|
||||
}
|
||||
}
|
||||
},
|
||||
Global = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 50,
|
||||
Pos = new()
|
||||
{
|
||||
X = 528,
|
||||
Y = 170
|
||||
}
|
||||
},
|
||||
Guild = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 50,
|
||||
Pos = new()
|
||||
{
|
||||
X = 490,
|
||||
Y = 313
|
||||
}
|
||||
},
|
||||
Awarded = new()
|
||||
{
|
||||
Show = true,
|
||||
FontSize = 25,
|
||||
Pos = new()
|
||||
{
|
||||
X = 490,
|
||||
Y = 345
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public XpTemplateClub Club { get; set; } = new()
|
||||
{
|
||||
Icon = new()
|
||||
{
|
||||
Show = true,
|
||||
Pos = new()
|
||||
{
|
||||
X = 722,
|
||||
Y = 25
|
||||
},
|
||||
Size = new()
|
||||
{
|
||||
X = 45,
|
||||
Y = 45
|
||||
}
|
||||
},
|
||||
Name = new()
|
||||
{
|
||||
FontSize = 35,
|
||||
Pos = new()
|
||||
{
|
||||
X = 650,
|
||||
Y = 49
|
||||
},
|
||||
Show = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public class XpTemplateIcon
|
||||
{
|
||||
public bool Show { get; set; }
|
||||
public XpTemplatePos Pos { get; set; }
|
||||
public XpTemplatePos Size { get; set; }
|
||||
}
|
||||
|
||||
public class XpTemplatePos
|
||||
{
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
}
|
||||
|
||||
public class XpTemplateUser
|
||||
{
|
||||
public XpTemplateText Name { get; set; }
|
||||
public XpTemplateIcon Icon { get; set; }
|
||||
public XpTemplateText GlobalLevel { get; set; }
|
||||
public XpTemplateText GuildLevel { get; set; }
|
||||
public XpTemplateText GlobalRank { get; set; }
|
||||
public XpTemplateText GuildRank { get; set; }
|
||||
public XpTemplateXp Xp { get; set; }
|
||||
}
|
||||
|
||||
public class XpTemplateClub
|
||||
{
|
||||
public XpTemplateIcon Icon { get; set; }
|
||||
public XpTemplateText Name { get; set; }
|
||||
}
|
||||
|
||||
public class XpTemplateText
|
||||
{
|
||||
[JsonConverter(typeof(XpRgba32Converter))]
|
||||
public Rgba32 Color { get; set; } = SixLabors.ImageSharp.Color.White;
|
||||
|
||||
public bool Show { get; set; }
|
||||
public int FontSize { get; set; }
|
||||
public XpTemplatePos Pos { get; set; }
|
||||
}
|
||||
|
||||
public class XpTemplateXp
|
||||
{
|
||||
public XpTemplateXpBar Bar { get; set; }
|
||||
public XpTemplateText Global { get; set; }
|
||||
public XpTemplateText Guild { get; set; }
|
||||
public XpTemplateText Awarded { get; set; }
|
||||
}
|
||||
|
||||
public class XpTemplateXpBar
|
||||
{
|
||||
public bool Show { get; set; }
|
||||
public XpBar Global { get; set; }
|
||||
public XpBar Guild { get; set; }
|
||||
}
|
||||
|
||||
public class XpBar
|
||||
{
|
||||
[JsonConverter(typeof(XpRgba32Converter))]
|
||||
public Rgba32 Color { get; set; }
|
||||
|
||||
public XpTemplatePos PointA { get; set; }
|
||||
public XpTemplatePos PointB { get; set; }
|
||||
public int Length { get; set; }
|
||||
public XpTemplateDirection Direction { get; set; }
|
||||
}
|
||||
|
||||
public enum XpTemplateDirection
|
||||
{
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right
|
||||
}
|
||||
|
||||
public class XpRgba32Converter : JsonConverter<Rgba32>
|
||||
{
|
||||
public override Rgba32 ReadJson(
|
||||
JsonReader reader,
|
||||
Type objectType,
|
||||
Rgba32 existingValue,
|
||||
bool hasExistingValue,
|
||||
JsonSerializer serializer)
|
||||
=> Color.ParseHex(reader.Value?.ToString());
|
||||
|
||||
public override void WriteJson(JsonWriter writer, Rgba32 value, JsonSerializer serializer)
|
||||
=> writer.WriteValue(value.ToHex().ToLowerInvariant());
|
||||
}
|
18
src/Ellie.Bot.Modules.Xp/_common/db/XpShopOwnedItem.cs
Normal file
18
src/Ellie.Bot.Modules.Xp/_common/db/XpShopOwnedItem.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
#nullable disable
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Db.Models;
|
||||
|
||||
public class XpShopOwnedItem : DbEntity
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
public XpShopItemType ItemType { get; set; }
|
||||
public bool IsUsing { get; set; }
|
||||
public string ItemKey { get; set; }
|
||||
}
|
||||
|
||||
public enum XpShopItemType
|
||||
{
|
||||
Background = 0,
|
||||
Frame = 1,
|
||||
}
|
Loading…
Reference in a new issue