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

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 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:

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

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

View file

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

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;
}
/// <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.",
"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",