grpc xpshop api

This commit is contained in:
Toastie 2025-03-22 12:10:17 +13:00
parent 2f740e96b8
commit b3d2785cec
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
13 changed files with 281 additions and 80 deletions

View file

@ -12,6 +12,8 @@ service GrpcXp {
rpc AddReward(AddRewardRequest) returns (AddRewardReply); rpc AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply); rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
rpc GetUserXp(GetUserXpRequest) returns (GetUserXpReply);
} }
message GetXpLbRequest { message GetXpLbRequest {
@ -75,4 +77,18 @@ message DeleteRewardRequest {
message DeleteRewardReply { message DeleteRewardReply {
bool success = 1; 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;
} }

View file

@ -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;
}

View file

@ -40,7 +40,9 @@ public partial class Administration
var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync(); var progressMsg = await Response().Pending(strs.prune_progress(0, 100)).SendAsync();
var progress = GetProgressTracker(progressMsg); var progress = GetProgressTracker(progressMsg);
var result = await _service.PruneWhere(ctx.Channel, var result = await _service.PruneWhere(
ctx.User.Id,
ctx.Channel,
100, 100,
x => x.Author.Id == ctx.Client.CurrentUser.Id, x => x.Author.Id == ctx.Client.CurrentUser.Id,
progress); progress);
@ -66,13 +68,17 @@ public partial class Administration
PruneResult result; PruneResult result;
if (opts.Safe) if (opts.Safe)
result = await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere(
ctx.User.Id,
(ITextChannel)ctx.Channel,
100, 100,
x => x.Author.Id == user.Id && !x.IsPinned, x => x.Author.Id == user.Id && !x.IsPinned,
progress, progress,
opts.After); opts.After);
else else
result = await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere(
ctx.User.Id,
(ITextChannel)ctx.Channel,
100, 100,
x => x.Author.Id == user.Id, x => x.Author.Id == user.Id,
progress, progress,
@ -107,13 +113,17 @@ public partial class Administration
PruneResult result; PruneResult result;
if (opts.Safe) if (opts.Safe)
result = await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere(
ctx.User.Id,
ctx.Channel,
count, count,
x => !x.IsPinned && x.Id != progressMsg.Id, x => !x.IsPinned && x.Id != progressMsg.Id,
progress, progress,
opts.After); opts.After);
else else
result = await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere(
ctx.User.Id,
ctx.Channel,
count, count,
x => x.Id != progressMsg.Id, x => x.Id != progressMsg.Id,
progress, progress,
@ -133,13 +143,14 @@ public partial class Administration
await progressMsg.ModifyAsync(props => await progressMsg.ModifyAsync(props =>
{ {
props.Embed = CreateEmbed() props.Embed = CreateEmbed()
.WithPendingColor() .WithPendingColor()
.WithDescription(GetText(strs.prune_progress(deleted, total))) .WithDescription(GetText(strs.prune_progress(deleted, total)))
.Build(); .Build();
}); });
} }
catch catch
{ {
// ignored
} }
}); });
@ -182,7 +193,9 @@ public partial class Administration
PruneResult result; PruneResult result;
if (opts.Safe) if (opts.Safe)
{ {
result = await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere(
ctx.User.Id,
ctx.Channel,
count, count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks && !m.IsPinned,
progress, progress,
@ -191,7 +204,9 @@ public partial class Administration
} }
else else
{ {
result = await _service.PruneWhere((ITextChannel)ctx.Channel, result = await _service.PruneWhere(
ctx.User.Id,
ctx.Channel,
count, count,
m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < _twoWeeks,
progress, progress,
@ -233,7 +248,7 @@ public partial class Administration
msg.DeleteAfter(5); msg.DeleteAfter(5);
break; break;
case PruneResult.FeatureLimit: 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); msg2.DeleteAfter(10);
break; break;
default: default:

View file

@ -1,23 +1,13 @@
#nullable disable #nullable disable
using EllieBot.Modules.Patronage;
namespace EllieBot.Modules.Administration.Services; 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 ConcurrentDictionary<ulong, CancellationTokenSource> _pruningGuilds = new();
private readonly TimeSpan _twoWeeks = TimeSpan.FromDays(14); 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( public async Task<PruneResult> PruneWhere(
ulong runningUserId,
IMessageChannel channel, IMessageChannel channel,
int amount, int amount,
Func<IMessage, bool> predicate, Func<IMessage, bool> predicate,
@ -37,11 +27,6 @@ public class PruneService : IEService
try try
{ {
if (channel is ITextChannel tc && !await _ps.LimitHitAsync(LimitedFeatureName.Prune, tc.Guild.OwnerId))
{
return PruneResult.FeatureLimit;
}
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
IMessage[] msgs; IMessage[] msgs;
IMessage lastMessage = null; IMessage lastMessage = null;
@ -67,7 +52,7 @@ public class PruneService : IEService
var singleDeletable = new List<IMessage>(); var singleDeletable = new List<IMessage>();
foreach (var x in msgs) foreach (var x in msgs)
{ {
_logService.AddDeleteIgnore(x.Id); logService.AddDeleteIgnore(x.Id);
if (now - x.CreatedAt < _twoWeeks) if (now - x.CreatedAt < _twoWeeks)
bulkDeletable.Add(x); bulkDeletable.Add(x);

View file

@ -57,8 +57,7 @@ public partial class Administration
_ => ctx.OkAsync(), _ => ctx.OkAsync(),
async fl => async fl =>
{ {
_ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser); await msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
}); });
} }

View file

@ -7,5 +7,4 @@ public enum BuyResult
AlreadyOwned, AlreadyOwned,
InsufficientFunds, InsufficientFunds,
UnknownItem, UnknownItem,
InsufficientPatronTier,
} }

View file

@ -327,12 +327,6 @@ public partial class Xp : EllieModule<XpService>
if (!string.IsNullOrWhiteSpace(item.Desc)) if (!string.IsNullOrWhiteSpace(item.Desc))
eb.AddField(GetText(strs.desc), 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; return eb;
}) })
.Interaction(async current => .Interaction(async current =>
@ -407,7 +401,6 @@ public partial class Xp : EllieModule<XpService>
BuyResult.AlreadyOwned => BuyResult.AlreadyOwned =>
await Response().Error(strs.xpshop_already_owned).Interaction(GetUseInteraction()).SendAsync(), await Response().Error(strs.xpshop_already_owned).Interaction(GetUseInteraction()).SendAsync(),
BuyResult.UnknownItem => await Response().Error(strs.xpshop_item_not_found).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() _ => throw new ArgumentOutOfRangeException()
}; };
return; return;

View file

@ -10,7 +10,7 @@ namespace EllieBot.Modules.Xp;
public sealed partial class XpConfig : ICloneable<XpConfig> public sealed partial class XpConfig : ICloneable<XpConfig>
{ {
[Comment("""DO NOT CHANGE""")] [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""")] [Comment("""How much XP will the users receive per message""")]
public int TextXpPerMessage { get; set; } = 3; public int TextXpPerMessage { get; set; } = 3;
@ -36,18 +36,6 @@ public sealed partial class XpConfig : ICloneable<XpConfig>
""")] """)]
public bool IsEnabled { get; set; } = false; 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(""" [Comment("""
Frames available for sale. Keys are unique IDs. Frames available for sale. Keys are unique IDs.
Do not change keys as they are not publicly visible. Only change properties (name, price, id) Do not change keys as they are not publicly visible. Only change properties (name, price, id)

View file

@ -1,5 +1,6 @@
#nullable disable #nullable disable
using EllieBot.Common.Configs; using EllieBot.Common.Configs;
using EllieBot.Db.Models;
namespace EllieBot.Modules.Xp.Services; namespace EllieBot.Modules.Xp.Services;
@ -48,12 +49,27 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig>
private void Migrate() private void Migrate()
{ {
if (data.Version < 10) if (data.Version < 11)
{ {
ModifyConfig(c => ModifyConfig(c => { c.Version = 11; });
{
c.Version = 10;
});
} }
} }
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;
}
} }

View file

@ -989,18 +989,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (!conf.Shop.IsEnabled) if (!conf.Shop.IsEnabled)
return BuyResult.XpShopDisabled; 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(); await using var ctx = _db.GetDbContext();
try try
{ {
@ -1127,13 +1115,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
return false; return false;
} }
public PatronTier GetXpShopTierRequirement(Xp.XpShopInputType type)
=> type switch
{
Xp.XpShopInputType.F => _xpConfig.Data.Shop.FramesTierRequirement,
_ => PatronTier.None,
};
public bool IsShopEnabled() public bool IsShopEnabled()
=> _xpConfig.Data.Shop.IsEnabled; => _xpConfig.Data.Shop.IsEnabled;

View file

@ -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,
};
}
}

View file

@ -193,4 +193,51 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
return reply; 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;
}
} }

View file

@ -1045,10 +1045,8 @@
"bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.", "bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.",
"cmd_group_commands": "'{0}' command group", "cmd_group_commands": "'{0}' command group",
"limit_reached": "Feature limit of {0} reached.", "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": "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.",
"feature_limit_reached_owner": "Feature limit reached. Server owner may upgrade patron level to increase the limit.", "prune_patron": "Deleting messages 2 weeks old or older requires [Patron Tier X](https://patreon.com/join/elliebot) or higher.",
"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.",
"available_commands": "Available Commands", "available_commands": "Available Commands",
"tier": "Tier", "tier": "Tier",
"pledge": "Pledge", "pledge": "Pledge",