Added Xp module

This commit is contained in:
Toastie 2024-09-21 00:46:59 +12:00
parent eb17820a50
commit eb9ed57547
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
24 changed files with 3893 additions and 0 deletions

View file

@ -0,0 +1,485 @@
#nullable disable
using EllieBot.Db.Models;
using EllieBot.Modules.Xp.Services;
namespace EllieBot.Modules.Xp;
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 Response().Error(strs.club_owner_only).SendAsync();
else
await Response().Error(strs.club_target_not_member).SendAsync();
}
else
{
await Response()
.Confirm(
strs.club_transfered(
Format.Bold(club.Name),
Format.Bold(newOwner.ToString())
)
)
.SendAsync();
}
}
[Cmd]
public async Task ClubAdmin([Leftover] IUser toAdmin)
{
var result = await _service.ToggleAdminAsync(ctx.User, toAdmin);
if (result == ToggleAdminResult.AddedAdmin)
await Response().Confirm(strs.club_admin_add(Format.Bold(toAdmin.ToString()))).SendAsync();
else if (result == ToggleAdminResult.RemovedAdmin)
await Response().Confirm(strs.club_admin_remove(Format.Bold(toAdmin.ToString()))).SendAsync();
else if (result == ToggleAdminResult.NotOwner)
await Response().Error(strs.club_owner_only).SendAsync();
else if (result == ToggleAdminResult.CantTargetThyself)
await Response().Error(strs.club_admin_invalid_target).SendAsync();
else if (result == ToggleAdminResult.TargetNotMember)
await Response().Error(strs.club_target_not_member).SendAsync();
}
[Cmd]
public async Task ClubCreate([Leftover] string clubName)
{
var result = await _service.CreateClubAsync(ctx.User, clubName);
if (result == ClubCreateResult.NameTooLong)
{
await Response().Error(strs.club_name_too_long).SendAsync();
return;
}
if (result == ClubCreateResult.NameTaken)
{
await Response().Error(strs.club_name_taken).SendAsync();
return;
}
if (result == ClubCreateResult.InsufficientLevel)
{
await Response().Error(strs.club_create_insuff_lvl).SendAsync();
return;
}
if (result == ClubCreateResult.AlreadyInAClub)
{
await Response().Error(strs.club_already_in).SendAsync();
return;
}
await Response().Confirm(strs.club_created(Format.Bold(clubName))).SendAsync();
}
[Cmd]
public async Task ClubIcon([Leftover] string url = null)
{
if ((!Uri.IsWellFormedUriString(url, UriKind.Absolute) && url is not null))
{
await Response().Error(strs.club_icon_url_format).SendAsync();
return;
}
var result = await _service.SetClubIconAsync(ctx.User.Id, url);
if (result == SetClubIconResult.Success)
await Response().Confirm(strs.club_icon_set).SendAsync();
else if (result == SetClubIconResult.NotOwner)
await Response().Error(strs.club_owner_only).SendAsync();
else if (result == SetClubIconResult.TooLarge)
await Response().Error(strs.club_icon_too_large).SendAsync();
else if (result == SetClubIconResult.InvalidFileType)
await Response().Error(strs.club_icon_invalid_filetype).SendAsync();
}
private async Task InternalClubInfoAsync(ClubInfo club)
{
var lvl = new LevelStats(club.Xp);
var allUsers = 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;
})
.ToList();
var rank = await _service.GetClubRankAsync(club.Id);
await Response()
.Paginated()
.Items(allUsers)
.PageSize(10)
.Page((users, _) =>
{
var embed = _sender.CreateEmbed()
.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.rank), $"#{rank}", true)
.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
.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;
})
.SendAsync();
}
[Cmd]
[Priority(1)]
public async Task ClubInformation(IUser user = null)
{
user ??= ctx.User;
var club = _service.GetClubByMember(user);
if (club is null)
{
await Response().Error(strs.club_user_not_in_club(Format.Bold(user.ToString()))).SendAsync();
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 Response().Error(strs.club_not_exists).SendAsync();
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 Response().Error(strs.club_admin_perms).SendAsync();
var bans = club.Bans.Select(x => x.User).ToArray();
return Response()
.Paginated()
.Items(bans)
.PageSize(10)
.CurrentPage(page)
.Page((items, _) =>
{
var toShow = string.Join("\n", items.Select(x => x.ToString()));
return _sender.CreateEmbed()
.WithTitle(GetText(strs.club_bans_for(club.ToString())))
.WithDescription(toShow)
.WithOkColor();
})
.SendAsync();
}
[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 Response().Error(strs.club_admin_perms).SendAsync();
var apps = club.Applicants.Select(x => x.User).ToArray();
return Response()
.Paginated()
.Items(apps)
.PageSize(10)
.CurrentPage(page)
.Page((items, _) =>
{
var toShow = string.Join("\n", items.Select(x => x.ToString()));
return _sender.CreateEmbed()
.WithTitle(GetText(strs.club_apps_for(club.ToString())))
.WithDescription(toShow)
.WithOkColor();
})
.SendAsync();
}
[Cmd]
public async Task ClubApply([Leftover] string clubName)
{
if (string.IsNullOrWhiteSpace(clubName))
return;
if (!_service.GetClubByName(clubName, out var club))
{
await Response().Error(strs.club_not_exists).SendAsync();
return;
}
var result = _service.ApplyToClub(ctx.User, club);
if (result == ClubApplyResult.Success)
await Response().Confirm(strs.club_applied(Format.Bold(club.ToString()))).SendAsync();
else if (result == ClubApplyResult.Banned)
await Response().Error(strs.club_join_banned).SendAsync();
else if (result == ClubApplyResult.AlreadyApplied)
await Response().Error(strs.club_already_applied).SendAsync();
else if (result == ClubApplyResult.AlreadyInAClub)
await Response().Error(strs.club_already_in).SendAsync();
}
[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 Response().Confirm(strs.club_accepted(Format.Bold(discordUser.ToString()))).SendAsync();
else if (result == ClubAcceptResult.NoSuchApplicant)
await Response().Error(strs.club_accept_invalid_applicant).SendAsync();
else if (result == ClubAcceptResult.NotOwnerOrAdmin)
await Response().Error(strs.club_admin_perms).SendAsync();
}
[Cmd]
[Priority(1)]
public Task ClubReject(IUser user)
=> ClubReject(user.ToString());
[Cmd]
[Priority(0)]
public async Task ClubReject([Leftover] string userName)
{
var result = _service.RejectApplication(ctx.User.Id, userName, out var discordUser);
if (result == ClubDenyResult.Rejected)
await Response().Confirm(strs.club_rejected(Format.Bold(discordUser.ToString()))).SendAsync();
else if (result == ClubDenyResult.NoSuchApplicant)
await Response().Error(strs.club_accept_invalid_applicant).SendAsync();
else if (result == ClubDenyResult.NotOwnerOrAdmin)
await Response().Error(strs.club_admin_perms).SendAsync();
}
[Cmd]
public async Task ClubLeave()
{
var res = _service.LeaveClub(ctx.User);
if (res == ClubLeaveResult.Success)
await Response().Confirm(strs.club_left).SendAsync();
else if (res == ClubLeaveResult.NotInAClub)
await Response().Error(strs.club_not_in_a_club).SendAsync();
else
await Response().Error(strs.club_owner_cant_leave).SendAsync();
}
[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 Response()
.Confirm(strs.club_user_kick(Format.Bold(userName),
Format.Bold(club.ToString())))
.SendAsync();
}
if (result == ClubKickResult.Hierarchy)
return Response().Error(strs.club_kick_hierarchy).SendAsync();
if (result == ClubKickResult.NotOwnerOrAdmin)
return Response().Error(strs.club_admin_perms).SendAsync();
return Response().Error(strs.club_target_not_member).SendAsync();
}
[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 Response()
.Confirm(strs.club_user_banned(Format.Bold(userName),
Format.Bold(club.ToString())))
.SendAsync();
}
if (result == ClubBanResult.Unbannable)
return Response().Error(strs.club_ban_fail_unbannable).SendAsync();
if (result == ClubBanResult.WrongUser)
return Response().Error(strs.club_ban_fail_user_not_found).SendAsync();
return Response().Error(strs.club_admin_perms).SendAsync();
}
[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 Response()
.Confirm(strs.club_user_unbanned(Format.Bold(userName),
Format.Bold(club.ToString())))
.SendAsync();
}
if (result == ClubUnbanResult.WrongUser)
{
return Response().Error(strs.club_unban_fail_user_not_found).SendAsync();
}
return Response().Error(strs.club_admin_perms).SendAsync();
}
[Cmd]
public async Task ClubDescription([Leftover] string desc = null)
{
if (_service.SetDescription(ctx.User.Id, desc))
{
desc = string.IsNullOrWhiteSpace(desc)
? "-"
: desc;
var eb = _sender.CreateEmbed()
.WithAuthor(ctx.User)
.WithTitle(GetText(strs.club_desc_update))
.WithOkColor()
.WithDescription(desc);
await Response().Embed(eb).SendAsync();
}
else
{
await Response().Error(strs.club_desc_update_failed).SendAsync();
}
}
[Cmd]
public async Task ClubDisband()
{
if (_service.Disband(ctx.User.Id, out var club))
await Response().Confirm(strs.club_disbanded(Format.Bold(club.Name))).SendAsync();
else
await Response().Error(strs.club_disband_error).SendAsync();
}
[Cmd]
public Task ClubLeaderboard(int page = 1)
{
if (--page < 0)
return Task.CompletedTask;
var clubs = _service.GetClubLeaderboardPage(page);
var embed = _sender.CreateEmbed().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 Response().Embed(embed).SendAsync();
}
[Cmd]
public async Task ClubRename([Leftover] string clubName)
{
var res = await _service.RenameClubAsync(ctx.User.Id, clubName);
switch (res)
{
case ClubRenameResult.NameTooLong:
await Response().Error(strs.club_name_too_long).SendAsync();
return;
case ClubRenameResult.Success:
{
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.club_renamed(clubName))).WithOkColor();
await Response().Embed(embed).SendAsync();
return;
}
case ClubRenameResult.NameTaken:
await Response().Error(strs.club_name_taken).SendAsync();
return;
case ClubRenameResult.NotOwnerOrAdmin:
await Response().Error(strs.club_admin_perms).SendAsync();
return;
default:
return;
}
}
}
}

View file

@ -0,0 +1,377 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models;
using OneOf;
namespace EllieBot.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)
{
if (!CheckClubName(clubName))
return ClubCreateResult.NameTooLong;
//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 (!string.IsNullOrWhiteSpace(url))
{
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())
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 async Task<int> GetClubRankAsync(int clubId)
{
await using var uow = _db.GetDbContext();
var rank = await uow.Clubs
.ToLinqToDBTable()
.Where(x => x.Xp > (uow.Clubs.First(c => c.Id == clubId).Xp))
.CountAsyncLinqToDB();
return rank + 1;
}
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.ClubId 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.AlreadyApplied;
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 ClubDenyResult RejectApplication(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 ClubDenyResult.NotOwnerOrAdmin;
var applicant =
club.Applicants.FirstOrDefault(x => x.User.ToString().ToUpperInvariant() == userName.ToUpperInvariant());
if (applicant is null)
return ClubDenyResult.NoSuchApplicant;
club.Applicants.Remove(applicant);
discordUser = applicant.User;
uow.SaveChanges();
return ClubDenyResult.Rejected;
}
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)
{
ArgumentOutOfRangeException.ThrowIfNegative(page);
using var uow = _db.GetDbContext();
return uow.Set<ClubInfo>().GetClubLeaderboardPage(page);
}
public async Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName)
{
if (!CheckClubName(clubName))
return ClubRenameResult.NameTooLong;
await using var uow = _db.GetDbContext();
var club = uow.Set<ClubInfo>().GetByOwnerOrAdmin(userId);
if (club is null)
return ClubRenameResult.NotOwnerOrAdmin;
if (await uow.Set<ClubInfo>().AnyAsyncEF(x => x.Name == clubName))
return ClubRenameResult.NameTaken;
club.Name = clubName;
await uow.SaveChangesAsync();
return ClubRenameResult.Success;
}
private static bool CheckClubName(string clubName)
{
return !(string.IsNullOrWhiteSpace(clubName) || clubName.Length > 20);
}
}

View file

@ -0,0 +1,35 @@
using EllieBot.Db.Models;
using OneOf;
namespace EllieBot.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);
ClubDenyResult RejectApplication(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);
Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName);
Task<int> GetClubRankAsync(int clubId);
}
public enum ClubApplyResult
{
Success,
AlreadyInAClub,
Banned,
AlreadyApplied
}

View file

@ -0,0 +1,15 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubAcceptResult
{
Accepted,
NotOwnerOrAdmin,
NoSuchApplicant,
}
public enum ClubDenyResult
{
Rejected,
NoSuchApplicant,
NotOwnerOrAdmin
}

View file

@ -0,0 +1,10 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubBanResult
{
Success,
NotOwnerOrAdmin,
WrongUser,
Unbannable,
}

View file

@ -0,0 +1,10 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubCreateResult
{
Success,
AlreadyInAClub,
NameTaken,
InsufficientLevel,
NameTooLong
}

View file

@ -0,0 +1,9 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubKickResult
{
Success,
NotOwnerOrAdmin,
TargetNotAMember,
Hierarchy
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubLeaveResult
{
Success,
OwnerCantLeave,
NotInAClub
}

View file

@ -0,0 +1,9 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubRenameResult
{
NotOwnerOrAdmin,
Success,
NameTaken,
NameTooLong
}

View file

@ -0,0 +1,7 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubTransferError
{
NotOwner,
TargetNotMember
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Xp.Services;
public enum ClubUnbanResult
{
Success,
NotOwnerOrAdmin,
WrongUser
}

View file

@ -0,0 +1,9 @@
namespace EllieBot.Modules.Xp.Services;
public enum SetClubIconResult
{
Success,
InvalidFileType,
TooLarge,
NotOwner,
}

View file

@ -0,0 +1,10 @@
namespace EllieBot.Modules.Xp.Services;
public enum ToggleAdminResult
{
AddedAdmin,
RemovedAdmin,
NotOwner,
TargetNotMember,
CantTargetThyself,
}

View file

@ -0,0 +1,599 @@
#nullable disable warnings
using EllieBot.Modules.Xp.Services;
using EllieBot.Db.Models;
using EllieBot.Modules.Patronage;
namespace EllieBot.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 = _sender.CreateEmbed()
.WithOkColor()
.AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting))
.AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting));
await Response().Embed(embed).SendAsync();
}
[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 Response().Confirm(strs.excluded(Format.Bold(ctx.Guild.ToString()))).SendAsync();
else
await Response().Confirm(strs.not_excluded(Format.Bold(ctx.Guild.ToString()))).SendAsync();
}
[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 Response().Confirm(strs.excluded(Format.Bold(role.ToString()))).SendAsync();
else
await Response().Confirm(strs.not_excluded(Format.Bold(role.ToString()))).SendAsync();
}
[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 Response().Confirm(strs.excluded(Format.Bold(channel.ToString()))).SendAsync();
else
await Response().Confirm(strs.not_excluded(Format.Bold(channel.ToString()))).SendAsync();
}
[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 Response()
.Paginated()
.Items(lines)
.PageSize(15)
.CurrentPage(0)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.exclusion_list))
.WithDescription(string.Join('\n', items))
.WithOkColor();
return embed;
})
.SendAsync();
}
[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 allCleanUsers = new List<UserXpStats>();
if (opts.Clean)
{
await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000))
.Where(user => socketGuild.GetUser(user.UserId) is not null)
.ToList();
}
var res = opts.Clean
? Response()
.Paginated()
.Items(allCleanUsers)
: Response()
.Paginated()
.PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage));
await res
.PageSize(9)
.CurrentPage(page)
.Page((users, curPage) =>
{
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.server_leaderboard)).WithOkColor();
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;
})
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task XpGlobalLeaderboard(int page = 1)
{
if (--page < 0 || page > 99)
return;
await Response()
.Paginated()
.PageItems(async curPage => await _service.GetUserXps(curPage))
.PageSize(9)
.Page((users, curPage) =>
{
var embed = _sender.CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.global_leaderboard));
if (!users.Any())
{
embed.WithDescription("-");
return embed;
}
for (var i = 0; i < users.Count; i++)
{
var user = users[i];
embed.AddField($"#{i + 1 + (curPage * 9)} {user}",
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
}
return embed;
})
.SendAsync();
}
[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 Response()
.Confirm(
strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString())))
.SendAsync();
}
[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 Response().Confirm(strs.modified(Format.Bold(usr), Format.Bold(amount.ToString()))).SendAsync();
}
[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 Response().Confirm(strs.template_reloaded).SendAsync();
}
[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 = _sender.CreateEmbed()
.WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_user_confirm));
if (!await PromptUserConfirmAsync(embed))
return;
_service.XpReset(ctx.Guild.Id, userId);
await Response().Confirm(strs.reset_user(userId)).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task XpReset()
{
var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.reset))
.WithDescription(GetText(strs.reset_server_confirm));
if (!await PromptUserConfirmAsync(embed))
return;
_service.XpReset(ctx.Guild.Id);
await Response().Confirm(strs.reset_server).SendAsync();
}
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 Response().Error(strs.xp_shop_disabled).SendAsync();
return;
}
await Response()
.Confirm(GetText(strs.available_commands),
$"""
`{prefix}xpshop bgs`
`{prefix}xpshop frames`
*{GetText(strs.xpshop_website)}*
""")
.SendAsync();
}
[Cmd]
public async Task XpShop(XpShopInputType type, int page = 1)
{
--page;
if (page < 0)
return;
var allItems = type == XpShopInputType.Backgrounds
? await _service.GetShopBgs()
: await _service.GetShopFrames();
if (allItems is null)
{
await Response().Error(strs.xp_shop_disabled).SendAsync();
return;
}
if (allItems.Count == 0)
{
await Response().Error(strs.not_found).SendAsync();
return;
}
await Response()
.Paginated()
.Items(allItems)
.PageSize(1)
.CurrentPage(page)
.AddFooter(false)
.Page((items, _) =>
{
if (!items.Any())
return _sender.CreateEmbed()
.WithDescription(GetText(strs.not_found))
.WithErrorColor();
var (key, item) = items.FirstOrDefault();
var eb = _sender.CreateEmbed()
.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 GLOBAL_NADEKO
if (key == "default")
eb.WithDescription(GetText(strs.xpshop_website));
#endif
var tier = _service.GetXpShopTierRequirement(type);
if (tier != PatronTier.None)
{
eb.WithFooter(GetText(strs.xp_shop_buy_required_tier(tier.ToString())));
}
return eb;
})
.Interaction(async current =>
{
var (key, _) = allItems.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 = _inter.Create(
ctx.User.Id,
button,
OnShopUse,
(key, itemType),
clearAfter: false);
return inter;
}
else
{
var button = new ButtonBuilder(GetText(strs.buy),
"xpshop:buy",
emote: Emoji.Parse("💰"));
var inter = _inter.Create(
ctx.User.Id,
button,
OnShopBuy,
(key, itemType),
singleUse: true,
clearAfter: false);
return inter;
}
})
.SendAsync();
}
[Cmd]
public async Task XpShopBuy(XpShopInputType type, string key)
{
var result = await _service.BuyShopItemAsync(ctx.User.Id, (XpShopItemType)type, key);
EllieInteractionBase GetUseInteraction()
{
return _inter.Create(ctx.User.Id,
new(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")),
async (_, state) => await XpShopUse(state.type, state.key),
(type, key)
);
}
if (result != BuyResult.Success)
{
var _ = result switch
{
BuyResult.XpShopDisabled => await Response().Error(strs.xp_shop_disabled).SendAsync(),
BuyResult.InsufficientFunds => await Response()
.Error(strs.not_enough(_gss.GetCurrencySign()))
.SendAsync(),
BuyResult.AlreadyOwned =>
await Response().Error(strs.xpshop_already_owned).Interaction(GetUseInteraction()).SendAsync(),
BuyResult.UnknownItem => await Response().Error(strs.xpshop_item_not_found).SendAsync(),
BuyResult.InsufficientPatronTier => await Response().Error(strs.patron_insuff_tier).SendAsync(),
_ => throw new ArgumentOutOfRangeException()
};
return;
}
await Response()
.Confirm(strs.xpshop_buy_success(type.ToString().ToLowerInvariant(),
key.ToLowerInvariant()))
.Interaction(GetUseInteraction())
.SendAsync();
}
[Cmd]
public async Task XpShopUse(XpShopInputType type, string key)
{
var result = await _service.UseShopItemAsync(ctx.User.Id, (XpShopItemType)type, key);
if (!result)
{
await Response().Confirm(strs.xp_shop_item_cant_use).SendAsync();
return;
}
await ctx.OkAsync();
}
private async Task OnShopUse(SocketMessageComponent smc, (string key, XpShopItemType type) state)
{
var (key, type) = state;
var result = await _service.UseShopItemAsync(ctx.User.Id, type, key);
if (!result)
{
await Response().Confirm(strs.xp_shop_item_cant_use).SendAsync();
}
}
private async Task OnShopBuy(SocketMessageComponent smc, (string key, XpShopItemType type) state)
{
var (key, type) = state;
var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key);
if (result == BuyResult.InsufficientFunds)
{
await Response().Error(strs.not_enough(_gss.GetCurrencySign())).SendAsync();
}
else if (result == BuyResult.Success)
{
await _service.UseShopItemAsync(ctx.User.Id, type, key);
}
}
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);
}
}

View file

@ -0,0 +1,109 @@
#nullable disable warnings
using Cloneable;
using EllieBot.Common.Yml;
using EllieBot.Db.Models;
using EllieBot.Modules.Patronage;
namespace EllieBot.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;
}

View file

@ -0,0 +1,63 @@
#nullable disable
using EllieBot.Common.Configs;
namespace EllieBot.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;
});
}
}
}

View file

@ -0,0 +1,141 @@
using EllieBot.Modules.Xp.Services;
namespace EllieBot.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 = _sender.CreateEmbed()
.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 Response()
.Paginated()
.Items(allRewards)
.PageSize(9)
.CurrentPage(page)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed().WithTitle(GetText(strs.level_up_rewards)).WithOkColor();
if (!items.Any())
return embed.WithDescription(GetText(strs.no_level_up_rewards));
foreach (var reward in items)
embed.AddField(GetText(strs.level_x(reward.Key)),
string.Join("\n", reward.Select(y => y.Item2)));
return embed;
})
.SendAsync();
}
[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 Response().Confirm(strs.xp_role_reward_cleared(level)).SendAsync();
}
[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 Response().Confirm(strs.xp_role_reward_add_role(level, Format.Bold(role.ToString()))).SendAsync();
else
{
await Response()
.Confirm(strs.xp_role_reward_remove_role(Format.Bold(level.ToString()),
Format.Bold(role.ToString())))
.SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[OwnerOnly]
public async Task XpCurrencyReward(int level, int amount = 0)
{
if (level < 1 || amount < 0)
return;
_service.SetCurrencyReward(ctx.Guild.Id, level, amount);
if (amount == 0)
await Response().Confirm(strs.cur_reward_cleared(level, _cp.GetCurrencySign())).SendAsync();
else
await Response()
.Confirm(strs.cur_reward_added(level,
Format.Bold(amount + _cp.GetCurrencySign())))
.SendAsync();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
#nullable disable
using EllieBot.Db.Models;
namespace EllieBot.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 usr,
UserXpStats fullGuildStats,
LevelStats global,
LevelStats guild,
int globalRanking,
int guildRanking)
{
User = usr;
Global = global;
Guild = guild;
GlobalRanking = globalRanking;
GuildRanking = guildRanking;
FullGuildStats = fullGuildStats;
}
}

View file

@ -0,0 +1,6 @@
namespace EllieBot.Modules.Xp;
public interface IXpCleanupService
{
Task DeleteXp();
}

View file

@ -0,0 +1,13 @@
#nullable disable warnings
using Cloneable;
namespace EllieBot.Modules.Xp.Services;
[Cloneable]
public sealed partial class UserXpGainData : ICloneable<UserXpGainData>
{
public IGuildUser User { get; init; }
public IGuild Guild { get; init; }
public IMessageChannel Channel { get; init; }
public int XpAmount { get; set; }
}

View file

@ -0,0 +1,31 @@
using LinqToDB;
using EllieBot.Db.Models;
namespace EllieBot.Modules.Xp;
public sealed class XpCleanupService : IXpCleanupService, IEService
{
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();
}
}

View file

@ -0,0 +1,271 @@
#nullable disable
using Newtonsoft.Json;
using SixLabors.ImageSharp.PixelFormats;
using Color = SixLabors.ImageSharp.Color;
namespace EllieBot.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());
}

View file

@ -0,0 +1,16 @@
#nullable disable warnings
namespace EllieBot.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,
}