diff --git a/src/EllieBot.GrpcApiBase/protos/xp.proto b/src/EllieBot.GrpcApiBase/protos/xp.proto index faf7b16..07ec339 100644 --- a/src/EllieBot.GrpcApiBase/protos/xp.proto +++ b/src/EllieBot.GrpcApiBase/protos/xp.proto @@ -12,6 +12,8 @@ service GrpcXp { rpc AddReward(AddRewardRequest) returns (AddRewardReply); rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply); + + rpc GetUserXp(GetUserXpRequest) returns (GetUserXpReply); } message GetXpLbRequest { @@ -75,4 +77,18 @@ message DeleteRewardRequest { message DeleteRewardReply { bool success = 1; +} + +message GetUserXpRequest { + uint64 guildId = 1; + uint64 userId = 2; +} + +message GetUserXpReply { + int64 xp = 1; + int64 requiredXp = 2; + int64 level = 3; + string club = 4; + string clubIcon = 5; + int32 rank = 6; } \ No newline at end of file diff --git a/src/EllieBot.GrpcApiBase/protos/xpshop.proto b/src/EllieBot.GrpcApiBase/protos/xpshop.proto new file mode 100644 index 0000000..298e700 --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/xpshop.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.GrpcApi"; + +package greet; + +service GrpcXpShop { + rpc AddXpShopItem (AddXpShopItemRequest) returns (AddXpShopItemReply); + rpc GetShopItems (GetShopItemsRequest) returns (GetShopItemsReply); + rpc UseShopItem (UseShopItemRequest) returns (UseShopItemReply); + rpc BuyShopItem (BuyShopItemRequest) returns (BuyShopItemReply); +} + +message UseShopItemRequest { + uint64 userId = 1; + string uniqueName = 2; + GrpcXpShopItemType itemType = 3; +} + +message UseShopItemReply { + bool success = 1; +} + +message BuyShopItemRequest { + uint64 userId = 1; + string uniqueName = 2; + GrpcXpShopItemType itemType = 3; +} + +message BuyShopItemReply { + bool success = 1; + optional BuyShopItemError Error = 2; +} + +enum BuyShopItemError { + NotEnough = 0; + AlreadyOwned = 1; + Unknown = 2; +} + +message AddXpShopItemRequest { + XpShopItem item = 1; + string uniqueName = 2; + GrpcXpShopItemType itemType = 3; +} + +message AddXpShopItemReply { + bool success = 1; +} + +message GetShopItemsRequest { + +} + +message GetShopItemsReply { + repeated XpShopItem bgs = 1; + repeated XpShopItem frames = 2; +} + +message XpShopItem { + string Name = 1; + string Description = 2; + int64 Price = 3; + string FullUrl = 4; + string PreviewUrl = 5; +} + +enum GrpcXpShopItemType { + Bg = 0; + Frame = 1; +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs b/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs index d2a9069..927ca93 100644 --- a/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs +++ b/src/EllieBot/Modules/Administration/Prune/PruneCommands.cs @@ -40,7 +40,9 @@ public partial class Administration var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync(); var progress = GetProgressTracker(progressMsg); - var result = await _service.PruneWhere(ctx.Channel, + var result = await _service.PruneWhere( + ctx.User.Id, + ctx.Channel, 100, x => x.Author.Id == ctx.Client.CurrentUser.Id, progress); @@ -66,13 +68,17 @@ public partial class Administration PruneResult result; if (opts.Safe) - result = await _service.PruneWhere((ITextChannel)ctx.Channel, + result = await _service.PruneWhere( + ctx.User.Id, + (ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id && !x.IsPinned, progress, opts.After); else - result = await _service.PruneWhere((ITextChannel)ctx.Channel, + result = await _service.PruneWhere( + ctx.User.Id, + (ITextChannel)ctx.Channel, 100, x => x.Author.Id == user.Id, progress, @@ -107,13 +113,17 @@ public partial class Administration PruneResult result; if (opts.Safe) - result = await _service.PruneWhere((ITextChannel)ctx.Channel, + result = await _service.PruneWhere( + ctx.User.Id, + ctx.Channel, count, x => !x.IsPinned && x.Id != progressMsg.Id, progress, opts.After); else - result = await _service.PruneWhere((ITextChannel)ctx.Channel, + result = await _service.PruneWhere( + ctx.User.Id, + ctx.Channel, count, x => x.Id != progressMsg.Id, progress, @@ -133,13 +143,14 @@ public partial class Administration await progressMsg.ModifyAsync(props => { props.Embed = CreateEmbed() - .WithPendingColor() - .WithDescription(GetText(strs.prune_progress(deleted, total))) - .Build(); + .WithPendingColor() + .WithDescription(GetText(strs.prune_progress(deleted, total))) + .Build(); }); } catch { + // ignored } }); @@ -182,7 +193,9 @@ public partial class Administration PruneResult result; if (opts.Safe) { - result = await _service.PruneWhere((ITextChannel)ctx.Channel, + result = await _service.PruneWhere( + ctx.User.Id, + ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned, progress, @@ -191,7 +204,9 @@ public partial class Administration } else { - result = await _service.PruneWhere((ITextChannel)ctx.Channel, + result = await _service.PruneWhere( + ctx.User.Id, + ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks, progress, @@ -233,7 +248,7 @@ public partial class Administration msg.DeleteAfter(5); break; case PruneResult.FeatureLimit: - var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync(); + var msg2 = await Response().Pending(strs.prune_patron).SendAsync(); msg2.DeleteAfter(10); break; default: diff --git a/src/EllieBot/Modules/Administration/Prune/PruneService.cs b/src/EllieBot/Modules/Administration/Prune/PruneService.cs index f90ec43..88e7fd2 100644 --- a/src/EllieBot/Modules/Administration/Prune/PruneService.cs +++ b/src/EllieBot/Modules/Administration/Prune/PruneService.cs @@ -1,23 +1,13 @@ #nullable disable -using EllieBot.Modules.Patronage; - namespace EllieBot.Modules.Administration.Services; -public class PruneService : IEService +public class PruneService(ILogCommandService logService) : IEService { - //channelids where prunes are currently occuring private readonly ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new(); private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14); - private readonly ILogCommandService _logService; - private readonly IPatronageService _ps; - - public PruneService(ILogCommandService logService, IPatronageService ps) - { - _logService = logService; - _ps = ps; - } public async Task<PruneResult> PruneWhere( + ulong runningUserId, IMessageChannel channel, int amount, Func<IMessage, bool> predicate, @@ -37,11 +27,6 @@ public class PruneService : IEService try { - if (channel is ITextChannel tc && !await _ps.LimitHitAsync(LimitedFeatureName.Prune, tc.Guild.OwnerId)) - { - return PruneResult.FeatureLimit; - } - var now = DateTime.UtcNow; IMessage[] msgs; IMessage lastMessage = null; @@ -67,7 +52,7 @@ public class PruneService : IEService var singleDeletable = new List<IMessage>(); foreach (var x in msgs) { - _logService.AddDeleteIgnore(x.Id); + logService.AddDeleteIgnore(x.Id); if (now - x.CreatedAt < _twoWeeks) bulkDeletable.Add(x); diff --git a/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs b/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs index 0d947ac..a817b01 100644 --- a/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs +++ b/src/EllieBot/Modules/Administration/Role/ReactionRoleCommands.cs @@ -57,8 +57,7 @@ public partial class Administration _ => ctx.OkAsync(), async fl => { - _ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser); - await Response().Pending(strs.feature_limit_reached_owner).SendAsync(); + await msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser); }); } diff --git a/src/EllieBot/Modules/Xp/BuyResult.cs b/src/EllieBot/Modules/Xp/BuyResult.cs index 3c4c464..e4a2d32 100644 --- a/src/EllieBot/Modules/Xp/BuyResult.cs +++ b/src/EllieBot/Modules/Xp/BuyResult.cs @@ -7,5 +7,4 @@ public enum BuyResult AlreadyOwned, InsufficientFunds, UnknownItem, - InsufficientPatronTier, } \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs index 3934005..78d10cd 100644 --- a/src/EllieBot/Modules/Xp/Xp.cs +++ b/src/EllieBot/Modules/Xp/Xp.cs @@ -327,12 +327,6 @@ public partial class Xp : EllieModule<XpService> if (!string.IsNullOrWhiteSpace(item.Desc)) eb.AddField(GetText(strs.desc), item.Desc); - 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 => @@ -407,7 +401,6 @@ public partial class Xp : EllieModule<XpService> 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; diff --git a/src/EllieBot/Modules/Xp/XpConfig.cs b/src/EllieBot/Modules/Xp/XpConfig.cs index cb0e120..1931cb6 100644 --- a/src/EllieBot/Modules/Xp/XpConfig.cs +++ b/src/EllieBot/Modules/Xp/XpConfig.cs @@ -10,7 +10,7 @@ namespace EllieBot.Modules.Xp; public sealed partial class XpConfig : ICloneable<XpConfig> { [Comment("""DO NOT CHANGE""")] - public int Version { get; set; } = 10; + public int Version { get; set; } = 11; [Comment("""How much XP will the users receive per message""")] public int TextXpPerMessage { get; set; } = 3; @@ -36,18 +36,6 @@ public sealed partial class XpConfig : ICloneable<XpConfig> """)] 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) diff --git a/src/EllieBot/Modules/Xp/XpConfigService.cs b/src/EllieBot/Modules/Xp/XpConfigService.cs index 1f1eb3d..c4459ba 100644 --- a/src/EllieBot/Modules/Xp/XpConfigService.cs +++ b/src/EllieBot/Modules/Xp/XpConfigService.cs @@ -1,5 +1,6 @@ #nullable disable using EllieBot.Common.Configs; +using EllieBot.Db.Models; namespace EllieBot.Modules.Xp.Services; @@ -48,12 +49,27 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig> private void Migrate() { - if (data.Version < 10) + if (data.Version < 11) { - ModifyConfig(c => - { - c.Version = 10; - }); + ModifyConfig(c => { c.Version = 11; }); } } + + public async Task<bool> AddItemAsync(string uniqueName, XpShopItemType itemType, XpConfig.ShopItemInfo shopItemInfo) + { + await Task.Yield(); + + var success = false; + ModifyConfig(c => + { + var items = itemType == XpShopItemType.Background + ? c.Shop.Bgs + : c.Shop.Frames; + + if (items is not null) + success = items.TryAdd(uniqueName, shopItemInfo); + }); + + return success; + } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs index bb6c165..58def50 100644 --- a/src/EllieBot/Modules/Xp/XpService.cs +++ b/src/EllieBot/Modules/Xp/XpService.cs @@ -989,18 +989,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand if (!conf.Shop.IsEnabled) return BuyResult.XpShopDisabled; - var req = type == XpShopItemType.Background - ? conf.Shop.BgsTierRequirement - : conf.Shop.FramesTierRequirement; - - if (req != PatronTier.None && !_creds.IsOwner(userId)) - { - var patron = await _ps.GetPatronAsync(userId); - - if (patron is null || (int)patron.Value.Tier < (int)req) - return BuyResult.InsufficientPatronTier; - } - await using var ctx = _db.GetDbContext(); try { @@ -1127,13 +1115,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand return false; } - public PatronTier GetXpShopTierRequirement(Xp.XpShopInputType type) - => type switch - { - Xp.XpShopInputType.F => _xpConfig.Data.Shop.FramesTierRequirement, - _ => PatronTier.None, - }; - public bool IsShopEnabled() => _xpConfig.Data.Shop.IsEnabled; diff --git a/src/EllieBot/Services/GrpcApi/XpShopSvc.cs b/src/EllieBot/Services/GrpcApi/XpShopSvc.cs new file mode 100644 index 0000000..9f25eb8 --- /dev/null +++ b/src/EllieBot/Services/GrpcApi/XpShopSvc.cs @@ -0,0 +1,93 @@ +using Grpc.Core; +using EllieBot.Db.Models; +using EllieBot.Modules.Xp; +using EllieBot.Modules.Xp.Services; + +namespace EllieBot.GrpcApi; + +public class XpShopSvc(XpService xp, XpConfigService xpConfig) : GrpcXpShop.GrpcXpShopBase, IGrpcSvc, IEService +{ + public ServerServiceDefinition Bind() + => GrpcXpShop.BindService(this); + + public override async Task<BuyShopItemReply> BuyShopItem(BuyShopItemRequest request, ServerCallContext context) + { + var result = await xp.BuyShopItemAsync(request.UserId, (XpShopItemType)request.ItemType, request.UniqueName); + + var res = new BuyShopItemReply(); + + if (result == BuyResult.Success) + { + res.Success = true; + return res; + } + + res.Error = result switch + { + BuyResult.AlreadyOwned => BuyShopItemError.AlreadyOwned, + BuyResult.InsufficientFunds => BuyShopItemError.NotEnough, + _ => BuyShopItemError.Unknown + }; + + return res; + } + + public override async Task<UseShopItemReply> UseShopItem(UseShopItemRequest request, ServerCallContext context) + { + var result = await xp.UseShopItemAsync(request.UserId, (XpShopItemType)request.ItemType, request.UniqueName); + + var res = new UseShopItemReply + { + Success = result + }; + + return res; + } + + public override async Task<GetShopItemsReply> GetShopItems(GetShopItemsRequest request, ServerCallContext context) + { + var bgsTask = Task.Run(async () => await xp.GetShopBgs()); + var frsTask = Task.Run(async () => await xp.GetShopFrames()); + + var bgs = await bgsTask.Fmap(x => x?.Map(y => MapItemToGrpcItem(y.Value, y.Key)) ?? []); + var frs = await frsTask.Fmap(z => z?.Map(y => MapItemToGrpcItem(y.Value, y.Key)) ?? []); + + var res = new GetShopItemsReply(); + + res.Bgs.AddRange(bgs); + res.Frames.AddRange(frs); + + return res; + + static XpShopItem MapItemToGrpcItem(XpConfig.ShopItemInfo item, string uniqueName) + { + return new XpShopItem() + { + Name = item.Name, + Price = item.Price, + Description = item.Desc, + FullUrl = item.Url, + PreviewUrl = item.Preview, + }; + } + } + + public override async Task<AddXpShopItemReply> AddXpShopItem(AddXpShopItemRequest request, + ServerCallContext context) + { + var result = await xpConfig.AddItemAsync(request.UniqueName, (XpShopItemType)request.ItemType, + new XpConfig.ShopItemInfo() + { + Name = request.Item.Name, + Price = 3000, + Desc = request.Item.Description, + Url = request.Item.FullUrl, + Preview = request.Item.PreviewUrl, + }); + + return new AddXpShopItemReply() + { + Success = result, + }; + } +} \ No newline at end of file diff --git a/src/EllieBot/Services/GrpcApi/XpSvc.cs b/src/EllieBot/Services/GrpcApi/XpSvc.cs index 8aced25..01c9b5c 100644 --- a/src/EllieBot/Services/GrpcApi/XpSvc.cs +++ b/src/EllieBot/Services/GrpcApi/XpSvc.cs @@ -193,4 +193,51 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService return reply; } + + /// <summary> + /// Gets XP information for a specific user in a guild + /// </summary> + public override async Task<GetUserXpReply> GetUserXp( + GetUserXpRequest request, + ServerCallContext context) + { + var guild = _client.GetGuild(request.GuildId); + + if (guild is null) + throw new RpcException(new Status(StatusCode.NotFound, "Guild not found")); + + var user = guild.GetUser(request.UserId); + + if (user is null) + throw new RpcException(new Status(StatusCode.NotFound, "User not found")); + + var reply = new GetUserXpReply(); + + // Get user stats from the XP service + var stats = await _xp.GetUserStatsAsync(user); + var levelStats = stats.Guild; + + // Get user's rank in guild + var guildRank = stats.GuildRanking; + + // Fill the response with user XP data + reply.Xp = levelStats.LevelXp; + reply.RequiredXp = levelStats.RequiredXp; + reply.Level = levelStats.Level; + reply.Rank = guildRank; + + // Add club information if available + if (stats.User.Club is not null) + { + reply.Club = stats.User.Club.ToString(); + reply.ClubIcon = stats.User.Club.ImageUrl ?? string.Empty; + } + else + { + reply.Club = string.Empty; + reply.ClubIcon = string.Empty; + } + + return reply; + } } \ No newline at end of file diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json index 8346ad8..6ad7a04 100644 --- a/src/EllieBot/strings/responses/responses.en-US.json +++ b/src/EllieBot/strings/responses/responses.en-US.json @@ -1045,10 +1045,8 @@ "bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.", "cmd_group_commands": "'{0}' command group", "limit_reached": "Feature limit of {0} reached.", - "feature_limit_reached_you": "You've reached the limit of {0} for the {1} feature. You may be able to increase this limit by upgrading your patron tier.", - "feature_limit_reached_owner": "Feature limit reached. Server owner may upgrade patron level to increase the limit.", - "feature_limit_reached_either": "The limit of {0} for the {1} feature has been reached. Either you or the server owner may able to upgrade this limit by upgrading the patron tier.", - "xp_shop_buy_required_tier": "Buying items from this shop requires Patron Tier {0} or higher.", + "feature_limit": "The limit of {0} for the {1} feature has been reached. Server owner may be able to increase the limit by upgrading the Patron Tier.", + "prune_patron": "Deleting messages 2 weeks old or older requires [Patron Tier X](https://patreon.com/join/elliebot) or higher.", "available_commands": "Available Commands", "tier": "Tier", "pledge": "Pledge",