Compare commits

...

15 commits

35 changed files with 845 additions and 398 deletions

View file

@ -1,6 +1,29 @@
# Changelog # Changelog
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
## [5.1.4] - 15.07.2024
### Added
- Added `'coins` command which lists top 10 cryptos ordered by marketcap
- Added Clubs rank in the leaderboard to `'clubinfo`
- Bot owners can now check other people's bank balance (Not server owners, only bot owner, the person who is hosting the bot)
- You can now send multiple waifu gifts at once to waifus. For example `'waifugift 3xRose @user` will give that user 3 roses
- The format is `<NUMBER>x<ITEM>`, no spaces
- Added `'boosttest` command
### Changed
- Updated command strings to clarify `'say` and `'send` usages
### Fixed
- Fixed `'waifugift` help string
### Removed
- Removed selfhost button from `'donate` command, no idea why it was there in the first place
## [5.1.3] - 08.07.2024 ## [5.1.3] - 08.07.2024

46
build.ps1 Normal file
View file

@ -0,0 +1,46 @@
Write-Output ""
Write-Output "███████╗██╗ ██╗ ██╗███████╗██████╗ ██████╗ ████████╗"
Write-Output "██╔════╝██║ ██║ ██║██╔════╝██╔══██╗██╔═══██╗╚══██╔══╝"
Write-Output "█████╗ ██║ ██║ ██║█████╗ ██████╔╝██║ ██║ ██║ "
Write-Output "██╔══╝ ██║ ██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║ "
Write-Output "███████╗███████╗███████╗██║███████╗██████╔╝╚██████╔╝ ██║ "
Write-Output "╚══════╝╚══════╝╚══════╝╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ "
Write-Output ""
Write-Output "Copyright © 2024 Toastie_t0ast & EllieBotDevs"
Write-Output ""
Write-Output ""
Write-Output "Publishing EllieBot"
Write-Output ""
Write-Output ""
dotnet publish -c Release -r linux-x64 --self-contained -o elliebot-linux-x64 src/EllieBot/EllieBot.csproj
Write-Output ""
dotnet publish -c Release -r linux-arm64 --self-contained -o elliebot-linux-arm64 src/EllieBot/EllieBot.csproj
Write-Output ""
dotnet publish -c Release -r win-x64 --self-contained -o elliebot-windows-x64 src/EllieBot/EllieBot.csproj
Write-Output ""
dotnet publish -c Release -r win-arm64 --self-contained -o elliebot-windows-arm64 src/EllieBot/EllieBot.csproj
Write-Output ""
dotnet publish -c Release -r osx-x64 --self-contained -o elliebot-osx-x64 src/EllieBot/EllieBot.csproj
Write-Output ""
dotnet publish -c Release -r osx-arm64 --self-contained -o elliebot-osx-arm64 src/EllieBot/EllieBot.csproj
Write-Output ""
Write-Output "Preparing the Windows installer build."
Write-Output ""
dotnet clean
dotnet restore -f --no-cache -v n
dotnet publish -c Release --self-contained --runtime win-x64 /p:Version=5.1.4 src/EllieBot
Write-Output ""
Write-Output ""
Write-Output "Finished the initial build script"
Write-Output ""
Write-Output "To build the Windows installer please install Inno Setup from"
Write-Output "https://jrsoftware.org/isdl.php"
Write-Output "And compile the exe_builder.iss"
Write-Output ""
Write-Output "If you are running on Windows please run the wsl command"
Write-Output "then run build.sh"

18
build.sh Normal file
View file

@ -0,0 +1,18 @@
echo ""
echo "Compressing build files"
echo ""
tar cvf 5.1.4-linux-x64-build.tar elliebot-linux-x64/*
tar cvf 5.1.4-linux-arm64-build.tar elliebot-linux-arm64/*
tar cvf 5.1.4-osx-x64-build.tar elliebot-osx-x64/*
tar cvf 5.1.4-osx-arm64-build.tar elliebot-osx-arm64/*
zip -r 5.1.4-windows-x64-build.zip elliebot-windows-x64/*
zip -r 5.1.4-windows-arm64-build.zip elliebot-windows-arm64/*
echo ""
echo "Moving the installer file you would have generated"
echo "if you followed the instructions in the bottom of the build.ps1 file"
echo "to the directory this script in run in."
echo ""
mv ellie-installers/5.1.4/ellie-setup-5.1.4.exe ellie-setup-5.1.4.exe

View file

@ -181,9 +181,9 @@ dotnet_naming_rule.private_readonly_field.symbols = private_readonly_field
dotnet_naming_rule.private_readonly_field.style = begins_with_underscore dotnet_naming_rule.private_readonly_field.style = begins_with_underscore
dotnet_naming_rule.private_readonly_field.severity = warning dotnet_naming_rule.private_readonly_field.severity = warning
dotnet_naming_rule.private_field.symbols = private_field # dotnet_naming_rule.private_field.symbols = private_field
dotnet_naming_rule.private_field.style = camel_case # dotnet_naming_rule.private_field.style = camel_case
dotnet_naming_rule.private_field.severity = warning # dotnet_naming_rule.private_field.severity = warning
dotnet_naming_rule.const_fields.symbols = const_fields dotnet_naming_rule.const_fields.symbols = const_fields
dotnet_naming_rule.const_fields.style = all_upper dotnet_naming_rule.const_fields.style = all_upper

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.3</Version> <Version>5.1.4</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View file

@ -225,5 +225,19 @@ public partial class Administration
if (!enabled) if (!enabled)
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync(); await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task BoostTest([Leftover] IGuildUser? user = null)
{
user ??= (IGuildUser)ctx.User;
await _service.BoostTest((ITextChannel)ctx.Channel, user);
var enabled = _service.GetBoostEnabled(ctx.Guild.Id);
if (!enabled)
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
}
} }
} }

View file

@ -630,6 +630,13 @@ public class GreetService : IEService, IReadyExecutor
return conf.SendChannelByeMessage; return conf.SendChannelByeMessage;
} }
public bool GetBoostEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendBoostMessage;
}
#endregion #endregion
#region Test Messages #region Test Messages

View file

@ -74,6 +74,27 @@ public partial class Gambling
} }
} }
[Cmd]
[OwnerOnly]
public async Task BankBalance([Leftover] IUser user)
{
var bal = await _bank.GetBalanceAsync(user.Id);
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.bank_balance_other(user.ToString(), N(bal))));
try
{
await Response().User(ctx.User).Embed(eb).SendAsync();
await ctx.OkAsync();
}
catch
{
await Response().Error(strs.cant_dm).SendAsync();
}
}
private async Task BankTakeInternalAsync(long amount, ulong userId) private async Task BankTakeInternalAsync(long amount, ulong userId)
{ {
if (await _bank.TakeAsync(userId, amount)) if (await _bank.TakeAsync(userId, amount))

View file

@ -3,6 +3,7 @@ using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Common.Waifu; using EllieBot.Modules.Gambling.Common.Waifu;
using EllieBot.Modules.Gambling.Services; using EllieBot.Modules.Gambling.Services;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using TwitchLib.Api.Helix.Models.Teams;
namespace EllieBot.Modules.Gambling; namespace EllieBot.Modules.Gambling;
@ -317,7 +318,9 @@ public partial class Gambling
.AddField(GetText(strs.price), N(wi.Price), true) .AddField(GetText(strs.price), N(wi.Price), true)
.AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true) .AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true)
.AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true) .AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true)
.AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true) .AddField(GetText(strs.changes_of_heart),
$"{wi.AffinityCount} - \"the {affInfo}\"",
true)
.AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true) .AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true)
.AddField("\u200B", "\u200B", true) .AddField("\u200B", "\u200B", true)
.AddField(GetText(strs.fans(fansList.Count)), fansStr, true) .AddField(GetText(strs.fans(fansList.Count)), fansStr, true)
@ -364,30 +367,27 @@ public partial class Gambling
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[Priority(0)] [Priority(0)]
public async Task WaifuGift(string itemName, [Leftover] IUser waifu) public async Task WaifuGift(MultipleWaifuItems items, [Leftover] IUser waifu)
{ {
if (waifu.Id == ctx.User.Id) if (waifu.Id == ctx.User.Id)
return; return;
var allItems = _service.GetWaifuItems(); var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, items.Item, items.Count);
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant());
if (item is null)
{
await Response().Error(strs.waifu_gift_not_exist).SendAsync();
return;
}
var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item);
if (sucess) if (sucess)
{ {
await Response() await Response()
.Confirm(strs.waifu_gift(Format.Bold(item + " " + item.ItemEmoji), .Confirm(strs.waifu_gift(Format.Bold($"{GetCountString(items)}{items.Item} {items.Item.ItemEmoji}"),
Format.Bold(waifu.ToString()))) Format.Bold(waifu.ToString())))
.SendAsync(); .SendAsync();
} }
else else
await Response().Error(strs.not_enough(CurrencySign)).SendAsync(); await Response().Error(strs.not_enough(CurrencySign)).SendAsync();
} }
private static string GetCountString(MultipleWaifuItems items)
=> items.Count > 1
? $"{items.Count}x "
: string.Empty;
} }
} }

View file

@ -7,6 +7,7 @@ using EllieBot.Db;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Common.Waifu; using EllieBot.Modules.Gambling.Common.Waifu;
using SixLabors.ImageSharp;
namespace EllieBot.Modules.Gambling.Services; namespace EllieBot.Modules.Gambling.Services;
@ -89,9 +90,14 @@ public class WaifuService : IEService, IReadyExecutor
if (waifu is null) if (waifu is null)
return settings.Waifu.MinPrice; return settings.Waifu.MinPrice;
var divorces = uow.Set<WaifuUpdate>().Count(x var divorces = uow.Set<WaifuUpdate>()
=> x.Old != null && x.Old.UserId == user.Id && x.UpdateType == WaifuUpdateType.Claimed && x.New == null); .Count(x
var affs = uow.Set<WaifuUpdate>().AsQueryable() => x.Old != null
&& x.Old.UserId == user.Id
&& x.UpdateType == WaifuUpdateType.Claimed
&& x.New == null);
var affs = uow.Set<WaifuUpdate>()
.AsQueryable()
.Where(w => w.User.UserId == user.Id .Where(w => w.User.UserId == user.Id
&& w.UpdateType == WaifuUpdateType.AffinityChanged && w.UpdateType == WaifuUpdateType.AffinityChanged
&& w.New != null) && w.New != null)
@ -110,12 +116,14 @@ public class WaifuService : IEService, IReadyExecutor
if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset"))) if (!await _cs.RemoveAsync(user.Id, price, new("waifu", "reset")))
return false; return false;
var affs = uow.Set<WaifuUpdate>().AsQueryable() var affs = uow.Set<WaifuUpdate>()
.AsQueryable()
.Where(w => w.User.UserId == user.Id .Where(w => w.User.UserId == user.Id
&& w.UpdateType == WaifuUpdateType.AffinityChanged && w.UpdateType == WaifuUpdateType.AffinityChanged
&& w.New != null); && w.New != null);
var divorces = uow.Set<WaifuUpdate>().AsQueryable() var divorces = uow.Set<WaifuUpdate>()
.AsQueryable()
.Where(x => x.Old != null .Where(x => x.Old != null
&& x.Old.UserId == user.Id && x.Old.UserId == user.Id
&& x.UpdateType == WaifuUpdateType.Claimed && x.UpdateType == WaifuUpdateType.Claimed
@ -158,14 +166,16 @@ public class WaifuService : IEService, IReadyExecutor
result = WaifuClaimResult.NotEnoughFunds; result = WaifuClaimResult.NotEnoughFunds;
else else
{ {
uow.Set<WaifuInfo>().Add(w = new() uow.Set<WaifuInfo>()
.Add(w = new()
{ {
Waifu = waifu, Waifu = waifu,
Claimer = claimer, Claimer = claimer,
Affinity = null, Affinity = null,
Price = amount Price = amount
}); });
uow.Set<WaifuUpdate>().Add(new() uow.Set<WaifuUpdate>()
.Add(new()
{ {
User = waifu, User = waifu,
Old = null, Old = null,
@ -186,7 +196,8 @@ public class WaifuService : IEService, IReadyExecutor
w.Price = amount + (amount / 4); w.Price = amount + (amount / 4);
result = WaifuClaimResult.Success; result = WaifuClaimResult.Success;
uow.Set<WaifuUpdate>().Add(new() uow.Set<WaifuUpdate>()
.Add(new()
{ {
User = w.Waifu, User = w.Waifu,
Old = oldClaimer, Old = oldClaimer,
@ -206,7 +217,8 @@ public class WaifuService : IEService, IReadyExecutor
w.Price = amount; w.Price = amount;
result = WaifuClaimResult.Success; result = WaifuClaimResult.Success;
uow.Set<WaifuUpdate>().Add(new() uow.Set<WaifuUpdate>()
.Add(new()
{ {
User = w.Waifu, User = w.Waifu,
Old = oldClaimer, Old = oldClaimer,
@ -248,7 +260,8 @@ public class WaifuService : IEService, IReadyExecutor
else if (w is null) else if (w is null)
{ {
var thisUser = uow.GetOrCreateUser(user); var thisUser = uow.GetOrCreateUser(user);
uow.Set<WaifuInfo>().Add(new() uow.Set<WaifuInfo>()
.Add(new()
{ {
Affinity = newAff, Affinity = newAff,
Waifu = thisUser, Waifu = thisUser,
@ -257,7 +270,8 @@ public class WaifuService : IEService, IReadyExecutor
}); });
success = true; success = true;
uow.Set<WaifuUpdate>().Add(new() uow.Set<WaifuUpdate>()
.Add(new()
{ {
User = thisUser, User = thisUser,
Old = null, Old = null,
@ -272,7 +286,8 @@ public class WaifuService : IEService, IReadyExecutor
w.Affinity = newAff; w.Affinity = newAff;
success = true; success = true;
uow.Set<WaifuUpdate>().Add(new() uow.Set<WaifuUpdate>()
.Add(new()
{ {
User = w.Waifu, User = w.Waifu,
Old = oldAff, Old = oldAff,
@ -343,7 +358,8 @@ public class WaifuService : IEService, IReadyExecutor
var oldClaimer = w.Claimer; var oldClaimer = w.Claimer;
w.Claimer = null; w.Claimer = null;
uow.Set<WaifuUpdate>().Add(new() uow.Set<WaifuUpdate>()
.Add(new()
{ {
User = w.Waifu, User = w.Waifu,
Old = oldClaimer, Old = oldClaimer,
@ -358,16 +374,29 @@ public class WaifuService : IEService, IReadyExecutor
return (w, result, amount, remaining); return (w, result, amount, remaining);
} }
public async Task<bool> GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj) public async Task<bool> GiftWaifuAsync(
IUser from,
IUser giftedWaifu,
WaifuItemModel itemObj,
int count)
{ {
if (!await _cs.RemoveAsync(from, itemObj.Price, new("waifu", "item"))) ArgumentOutOfRangeException.ThrowIfLessThan(count, 1, nameof(count));
if (!await _cs.RemoveAsync(from, itemObj.Price * count, new("waifu", "item")))
return false; return false;
var totalValue = itemObj.Price * count;
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var w = uow.Set<WaifuInfo>().ByWaifuUserId(giftedWaifu.Id, set => set.Include(x => x.Items).Include(x => x.Claimer)); var w = uow.Set<WaifuInfo>()
.ByWaifuUserId(giftedWaifu.Id,
set => set
.Include(x => x.Items)
.Include(x => x.Claimer));
if (w is null) if (w is null)
{ {
uow.Set<WaifuInfo>().Add(w = new() uow.Set<WaifuInfo>()
.Add(w = new()
{ {
Affinity = null, Affinity = null,
Claimer = null, Claimer = null,
@ -378,20 +407,21 @@ public class WaifuService : IEService, IReadyExecutor
if (!itemObj.Negative) if (!itemObj.Negative)
{ {
w.Items.Add(new() w.Items.AddRange(Enumerable.Range(0, count)
.Select((_) => new WaifuItem()
{ {
Name = itemObj.Name.ToLowerInvariant(), Name = itemObj.Name.ToLowerInvariant(),
ItemEmoji = itemObj.ItemEmoji ItemEmoji = itemObj.ItemEmoji
}); }));
if (w.Claimer?.UserId == from.Id) if (w.Claimer?.UserId == from.Id)
w.Price += (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect); w.Price += (long)(totalValue * _gss.Data.Waifu.Multipliers.GiftEffect);
else else
w.Price += itemObj.Price / 2; w.Price += totalValue / 2;
} }
else else
{ {
w.Price -= (long)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect); w.Price -= (long)(totalValue * _gss.Data.Waifu.Multipliers.NegativeGiftEffect);
if (w.Price < 1) if (w.Price < 1)
w.Price = 1; w.Price = 1;
} }
@ -492,6 +522,7 @@ public class WaifuService : IEService, IReadyExecutor
} }
private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay"; private static readonly TypedKey<long> _waifuDecayKey = $"waifu:last_decay";
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
// only decay waifu values from shard 0 // only decay waifu values from shard 0
@ -533,7 +564,6 @@ public class WaifuService : IEService, IReadyExecutor
{ {
Price = (long)(old.Price * multi) Price = (long)(old.Price * multi)
}); });
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -557,6 +587,7 @@ public class WaifuService : IEService, IReadyExecutor
.Select(x => $"{x.Username}#{x.Discriminator}") .Select(x => $"{x.Username}#{x.Discriminator}")
.ToListAsyncEF(); .ToListAsyncEF();
} }
public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId) public async Task<IReadOnlyCollection<string>> GetFansNames(int waifuId)
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
@ -573,7 +604,8 @@ public class WaifuService : IEService, IReadyExecutor
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
return await ctx.GetTable<WaifuItem>() return await ctx.GetTable<WaifuItem>()
.Where(x => x.WaifuInfoId == ctx.GetTable<WaifuInfo>() .Where(x => x.WaifuInfoId
== ctx.GetTable<WaifuInfo>()
.Where(x => x.WaifuId == waifuId) .Where(x => x.WaifuId == waifuId)
.Select(x => x.Id) .Select(x => x.Id)
.FirstOrDefault()) .FirstOrDefault())

View file

@ -0,0 +1,6 @@
#nullable disable
using EllieBot.Modules.Gambling.Common;
namespace EllieBot.Modules.Gambling;
public record class MultipleWaifuItems(int Count, WaifuItemModel Item);

View file

@ -0,0 +1,47 @@
#nullable disable
using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Gambling.Services;
using System.Text.RegularExpressions;
namespace EllieBot.Modules.Gambling;
public partial class MultipleWaifuItemsTypeReader : EllieTypeReader<MultipleWaifuItems>
{
private readonly WaifuService _service;
[GeneratedRegex(@"(?:(?<count>\d+)[x*])?(?<item>.+)")]
private static partial Regex ItemRegex();
public MultipleWaifuItemsTypeReader(WaifuService service)
{
_service = service;
}
public override ValueTask<TypeReaderResult<MultipleWaifuItems>> ReadAsync(ICommandContext ctx, string input)
{
input = input.ToLowerInvariant();
var match = ItemRegex().Match(input);
if (!match.Success)
{
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid input."));
}
var count = 1;
if (match.Groups["count"].Success)
{
if (!int.TryParse(match.Groups["count"].Value, out count) || count < 1)
{
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Invalid count."));
}
}
var itemName = match.Groups["item"].Value?.ToLowerInvariant();
var allItems = _service.GetWaifuItems();
var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName);
if (item is null)
{
return new(Discord.Commands.TypeReaderResult.FromError(CommandError.ParseFailed, "Waifu gift does not exist."));
}
return new(Discord.Commands.TypeReaderResult.FromSuccess(new MultipleWaifuItems(count, item)));
}
}

View file

@ -1,6 +1,5 @@
#nullable disable #nullable disable
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Games.Common; using EllieBot.Modules.Games.Common;
using EllieBot.Modules.Games.Common.ChatterBot; using EllieBot.Modules.Games.Common.ChatterBot;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
@ -58,18 +57,21 @@ public class ChatterBotService : IExecOnMessage
Log.Information("Cleverbot will not work as the api key is missing"); Log.Information("Cleverbot will not work as the api key is missing");
return null; return null;
case ChatBotImplementation.Gpt: case ChatBotImplementation.OpenAi:
var data = _gcs.Data;
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
return new OfficialGptSession(_creds.Gpt3ApiKey, return new OpenAiApiSession(
_gcs.Data.ChatGpt.ModelName, data.ChatGpt.ApiUrl,
_gcs.Data.ChatGpt.ChatHistory, _creds.Gpt3ApiKey,
_gcs.Data.ChatGpt.MaxTokens, data.ChatGpt.ModelName,
_gcs.Data.ChatGpt.MinTokens, data.ChatGpt.ChatHistory,
_gcs.Data.ChatGpt.PersonalityPrompt, data.ChatGpt.MaxTokens,
data.ChatGpt.MinTokens,
data.ChatGpt.PersonalityPrompt,
_client.CurrentUser.Username, _client.CurrentUser.Username,
_httpFactory); _httpFactory);
Log.Information("Gpt3 will not work as the api key is missing"); Log.Information("Openai Api will likely not work as the api key is missing");
return null; return null;
default: default:
return null; return null;

View file

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Choice
{
[JsonPropertyName("message")]
public Message Message { get; init; }
}

View file

@ -1,61 +0,0 @@
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiCompletionResponse
{
[JsonPropertyName("choices")]
public Choice[] Choices { get; set; }
[JsonPropertyName("usage")]
public OpenAiUsageData Usage { get; set; }
}
public class OpenAiUsageData
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}
public class Choice
{
[JsonPropertyName("message")]
public Message Message { get; init; }
}
public class Message {
[JsonPropertyName("content")]
public string Content { get; init; }
}
public class Gpt3ApiRequest
{
[JsonPropertyName("model")]
public string Model { get; init; }
[JsonPropertyName("messages")]
public List<GPTMessage> Messages { get; init; }
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("max_tokens")]
public int MaxTokens { get; init; }
}
public class GPTMessage
{
[JsonPropertyName("role")]
public string Role {get; init;}
[JsonPropertyName("content")]
public string Content {get; init;}
[JsonPropertyName("name")]
public string Name {get; init;}
}

View file

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Message
{
[JsonPropertyName("content")]
public string Content { get; init; }
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiMessage
{
[JsonPropertyName("role")]
public string Role { get; init; }
[JsonPropertyName("content")]
public string Content { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; }
}

View file

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiRequest
{
[JsonPropertyName("model")]
public string Model { get; init; }
[JsonPropertyName("messages")]
public List<OpenAiApiMessage> Messages { get; init; }
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("max_tokens")]
public int MaxTokens { get; init; }
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiUsageData
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}

View file

@ -0,0 +1,13 @@
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiCompletionResponse
{
[JsonPropertyName("choices")]
public Choice[] Choices { get; set; }
[JsonPropertyName("usage")]
public OpenAiApiUsageData Usage { get; set; }
}

View file

@ -1,18 +1,15 @@
#nullable disable #nullable disable
using Newtonsoft.Json; using Newtonsoft.Json;
using OneOf.Types; using OneOf.Types;
using System.Net.Http.Json;
using SharpToken; using SharpToken;
using System.CodeDom; using System.Net.Http.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace EllieBot.Modules.Games.Common.ChatterBot; namespace EllieBot.Modules.Games.Common.ChatterBot;
public partial class OfficialGptSession : IChatterBotSession public partial class OpenAiApiSession : IChatterBotSession
{ {
private string Uri private readonly string _baseUrl;
=> $"https://api.openai.com/v1/chat/completions";
private readonly string _apiKey; private readonly string _apiKey;
private readonly string _model; private readonly string _model;
private readonly int _maxHistory; private readonly int _maxHistory;
@ -20,13 +17,14 @@ public partial class OfficialGptSession : IChatterBotSession
private readonly int _minTokens; private readonly int _minTokens;
private readonly string _ellieUsername; private readonly string _ellieUsername;
private readonly GptEncoding _encoding; private readonly GptEncoding _encoding;
private List<GPTMessage> messages = new(); private List<OpenAiApiMessage> messages = new();
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
public OfficialGptSession( public OpenAiApiSession(
string url,
string apiKey, string apiKey,
ChatGptModel model, string model,
int chatHistory, int chatHistory,
int maxTokens, int maxTokens,
int minTokens, int minTokens,
@ -34,21 +32,23 @@ public partial class OfficialGptSession : IChatterBotSession
string ellieUsername, string ellieUsername,
IHttpClientFactory factory) IHttpClientFactory factory)
{ {
_apiKey = apiKey; if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _))
_httpFactory = factory;
_model = model switch
{ {
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo", throw new ArgumentException("Invalid OpenAi api url provided", nameof(url));
ChatGptModel.Gpt4o => "gpt-4o", }
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
};
_baseUrl = url.TrimEnd('/');
_apiKey = apiKey;
_model = model;
_httpFactory = factory;
_maxHistory = chatHistory; _maxHistory = chatHistory;
_maxTokens = maxTokens; _maxTokens = maxTokens;
_minTokens = minTokens; _minTokens = minTokens;
_ellieUsername = UsernameCleaner().Replace(ellieUsername, ""); _ellieUsername = UsernameCleaner().Replace(ellieUsername, "");
_encoding = GptEncoding.GetEncodingForModel(_model); _encoding = GptEncoding.GetEncodingForModel("gpt-4o");
if (!string.IsNullOrWhiteSpace(personality))
{
messages.Add(new() messages.Add(new()
{ {
Role = "system", Role = "system",
@ -56,6 +56,7 @@ public partial class OfficialGptSession : IChatterBotSession
Name = _ellieUsername Name = _ellieUsername
}); });
} }
}
[GeneratedRegex("[^a-zA-Z0-9_-]")] [GeneratedRegex("[^a-zA-Z0-9_-]")]
@ -92,15 +93,16 @@ public partial class OfficialGptSession : IChatterBotSession
} }
else else
{ {
return new Error<string>("Token count exceeded, please increase the number of tokens in the bot config and restart."); return new Error<string>(
"Token count exceeded, please increase the number of tokens in the bot config and restart.");
} }
} }
using var http = _httpFactory.CreateClient(); using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey); http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
var data = await http.PostAsJsonAsync(Uri, var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions",
new Gpt3ApiRequest() new OpenAiApiRequest()
{ {
Model = _model, Model = _model,
Messages = messages, Messages = messages,
@ -113,7 +115,7 @@ public partial class OfficialGptSession : IChatterBotSession
{ {
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString); var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
Log.Information("Received response: {response} ", dataString); // Log.Information("Received response: {Response} ", dataString);
var res = response?.Choices?[0]; var res = response?.Choices?[0];
var message = res?.Message?.Content; var message = res?.Message?.Content;
@ -143,10 +145,3 @@ public partial class OfficialGptSession : IChatterBotSession
} }
} }
} }
public sealed class ThinkResult
{
public string Text { get; set; }
public int TokensIn { get; set; }
public int TokensOut { get; set; }
}

View file

@ -0,0 +1,11 @@
#nullable disable
using System.CodeDom;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public sealed class ThinkResult
{
public string Text { get; set; }
public int TokensIn { get; set; }
public int TokensOut { get; set; }
}

View file

@ -8,7 +8,7 @@ namespace EllieBot.Modules.Games.Common;
public sealed partial class GamesConfig : ICloneable<GamesConfig> public sealed partial class GamesConfig : ICloneable<GamesConfig>
{ {
[Comment("DO NOT CHANGE")] [Comment("DO NOT CHANGE")]
public int Version { get; set; } = 4; public int Version { get; set; } = 5;
[Comment("Hangman related settings (.hangman command)")] [Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new() public HangmanConfig Hangman { get; set; } = new()
@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
} }
]; ];
[Comment(@"Which chatbot API should bot use. [Comment(
"""
Which chatbot API should bot use.
'cleverbot' - bot will use Cleverbot API. 'cleverbot' - bot will use Cleverbot API.
'gpt' - bot will use GPT API")] 'openai' - bot will use OpenAi API
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt; """)]
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.OpenAi;
public ChatGptConfig ChatGpt { get; set; } = new(); public ChatGptConfig ChatGpt { get; set; } = new();
} }
@ -114,19 +117,38 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
[Cloneable] [Cloneable]
public sealed partial class ChatGptConfig public sealed partial class ChatGptConfig
{ {
[Comment(@"Which GPT Model should bot use. [Comment("""
gpt35turbo - cheapest Url to any openai api compatible url.
gpt4o - more expensive, higher quality Make sure to modify the modelName appropriately
")] DO NOT add /v1/chat/completions suffix to the url
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo; """)]
public string ApiUrl { get; set; } = "https://api.openai.com";
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")] [Comment("""
public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything."; Which GPT Model should bot use.
gpt-3.5-turbo - cheapest
gpt-4o - more expensive, higher quality
[Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")] If you are using another openai compatible api, you may use any of the models supported by that api
""")]
public string ModelName { get; set; } = "gpt-3.5-turbo";
[Comment("""
How should the chatbot behave, what's its personality?
This will be sent as a system message.
Usage of this counts towards the max tokens.
""")]
public string PersonalityPrompt { get; set; } =
"You are a chat bot willing to have a conversation with anyone about anything.";
[Comment(
"""
The maximum number of messages in a conversation that can be remembered.
This will increase the number of tokens used.
""")]
public int ChatHistory { get; set; } = 5; public int ChatHistory { get; set; } = 5;
[Comment(@"The maximum number of tokens to use per GPT API call")] [Comment(@"The maximum number of tokens to use per OpenAi API call")]
public int MaxTokens { get; set; } = 100; public int MaxTokens { get; set; } = 100;
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")] [Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
@ -163,18 +185,11 @@ public sealed partial class RaceAnimal
public enum ChatBotImplementation public enum ChatBotImplementation
{ {
Cleverbot, Cleverbot,
OpenAi = 1,
[Obsolete]
Gpt = 1, Gpt = 1,
[Obsolete] [Obsolete]
Gpt3 = 1, Gpt3 = 1,
} }
public enum ChatGptModel
{
[Obsolete]
Gpt4,
[Obsolete]
Gpt432k,
Gpt35Turbo,
Gpt4o,
}

View file

@ -32,29 +32,21 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
gs => gs.ChatBot, gs => gs.ChatBot,
ConfigParsers.InsensitiveEnum, ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString); ConfigPrinters.ToString);
AddParsedProp("gpt.apiUrl",
gs => gs.ChatGpt.ApiUrl,
ConfigParsers.String,
ConfigPrinters.ToString);
AddParsedProp("gpt.modelName", AddParsedProp("gpt.modelName",
gs => gs.ChatGpt.ModelName, gs => gs.ChatGpt.ModelName,
ConfigParsers.InsensitiveEnum, ConfigParsers.String,
ConfigPrinters.ToString); ConfigPrinters.ToString);
AddParsedProp("gpt.personality", AddParsedProp("gpt.personality",
gs => gs.ChatGpt.PersonalityPrompt, gs => gs.ChatGpt.PersonalityPrompt,
ConfigParsers.String, ConfigParsers.String,
ConfigPrinters.ToString); ConfigPrinters.ToString);
AddParsedProp("gpt.chathistory",
gs => gs.ChatGpt.ChatHistory,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
AddParsedProp("gpt.max_tokens",
gs => gs.ChatGpt.MaxTokens,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
AddParsedProp("gpt.min_tokens",
gs => gs.ChatGpt.MinTokens,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
Migrate(); Migrate();
} }
@ -78,7 +70,7 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 3; c.Version = 3;
c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo; c.ChatGpt.ModelName = "gpt35turbo";
}); });
} }
@ -89,11 +81,40 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
c.Version = 4; c.Version = 4;
#pragma warning disable CS0612 // Type or member is obsolete #pragma warning disable CS0612 // Type or member is obsolete
c.ChatGpt.ModelName = c.ChatGpt.ModelName =
c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase)
? ChatGptModel.Gpt4o || c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase)
: c.ChatGpt.ModelName; ? "gpt-4o"
: "gpt-3.5-turbo";
#pragma warning restore CS0612 // Type or member is obsolete #pragma warning restore CS0612 // Type or member is obsolete
}); });
} }
if (data.Version < 5)
{
ModifyConfig(c =>
{
c.Version = 5;
c.ChatBot = c.ChatBot == ChatBotImplementation.OpenAi
? ChatBotImplementation.OpenAi
: c.ChatBot;
if (c.ChatGpt.ModelName.Equals("gpt4o", StringComparison.OrdinalIgnoreCase))
{
c.ChatGpt.ModelName = "gpt-4o";
}
else if (c.ChatGpt.ModelName.Equals("gpt35turbo", StringComparison.OrdinalIgnoreCase))
{
c.ChatGpt.ModelName = "gpt-3.5-turbo";
}
else
{
Log.Warning(
"Unknown OpenAI api model name: {ModelName}. "
+ "It will be reset to 'gpt-3.5-turbo' only this time",
c.ChatGpt.ModelName);
c.ChatGpt.ModelName = "gpt-3.5-turbo";
}
});
}
} }
} }

View file

@ -105,10 +105,7 @@ public partial class Searches
await message.ModifyAsync(mp => await message.ModifyAsync(mp =>
{ {
mp.Attachments = mp.Attachments =
new(new[] new(new[] { attachment });
{
attachment
});
mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build(); mp.Embed = eb.WithImageUrl($"attachment://{fileName}").Build();
}); });
@ -165,7 +162,8 @@ public partial class Searches
.WithAuthor($"#{crypto.CmcRank}") .WithAuthor($"#{crypto.CmcRank}")
.WithTitle($"{crypto.Name} ({crypto.Symbol})") .WithTitle($"{crypto.Name} ({crypto.Symbol})")
.WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/") .WithUrl($"https://coinmarketcap.com/currencies/{crypto.Slug}/")
.WithThumbnailUrl($"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png") .WithThumbnailUrl(
$"https://s3.coinmarketcap.com/static/img/coins/128x128/{crypto.Id}.png")
.AddField(GetText(strs.market_cap), marketCap, true) .AddField(GetText(strs.market_cap), marketCap, true)
.AddField(GetText(strs.price), price, true) .AddField(GetText(strs.price), price, true)
.AddField(GetText(strs.volume_24h), volume, true) .AddField(GetText(strs.volume_24h), volume, true)
@ -192,5 +190,54 @@ public partial class Searches
await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build()); await ctx.Channel.SendFileAsync(sparkline, fileName, embed: toSend.Build());
} }
[Cmd]
public async Task Coins(int page = 1)
{
if (--page < 0)
return;
if (page > 25)
page = 25;
await Response()
.Paginated()
.PageItems(async (page) =>
{
var coins = await _service.GetTopCoins(page);
return coins;
})
.PageSize(10)
.Page((items, _) =>
{
var embed = _sender.CreateEmbed()
.WithOkColor();
if (items.Count > 0)
{
foreach (var coin in items)
{
embed.AddField($"#{coin.MarketCapRank} {coin.Symbol} - {coin.Name}",
$"""
`Price:` {GetArrowEmoji(coin.PercentChange24h)} {coin.CurrentPrice.ToShortString()}$ ({GetSign(coin.PercentChange24h)}{Math.Round(coin.PercentChange24h, 2)}%)
`MarketCap:` {coin.MarketCap.ToShortString()}$
`Supply:` {(coin.CirculatingSupply?.ToShortString() ?? "?")} / {(coin.TotalSupply?.ToShortString() ?? "?")}
""",
inline: false);
}
}
return embed;
})
.CurrentPage(page)
.AddFooter(false)
.SendAsync();
}
private static string GetArrowEmoji(decimal value)
=> value > 0 ? "▲" : "▼";
private static string GetSign(decimal value)
=> value >= 0 ? "+" : "-";
} }
} }

View file

@ -4,8 +4,10 @@ using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Xml; using System.Xml;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
using StringExtensions = EllieBot.Extensions.StringExtensions; using StringExtensions = EllieBot.Extensions.StringExtensions;
@ -212,4 +214,55 @@ public class CryptoService : IEService
var points = GetSparklinePointsFromSvgText(str); var points = GetSparklinePointsFromSvgText(str);
return points; return points;
} }
private static TypedKey<IReadOnlyCollection<GeckoCoinsResult>> GetTopCoinsKey()
=> new($"crypto:top_coins");
public async Task<IReadOnlyCollection<GeckoCoinsResult>?> GetTopCoins(int page)
{
if (page >= 25)
page = 24;
using var http = _httpFactory.CreateClient();
http.AddFakeHeaders();
var result = await _cache.GetOrAddAsync<IReadOnlyCollection<GeckoCoinsResult>>(GetTopCoinsKey(),
async () => await http.GetFromJsonAsync<List<GeckoCoinsResult>>(
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=250")
?? [],
expiry: TimeSpan.FromHours(1));
return result!.Skip(page * 10).Take(10).ToList();
}
}
public sealed class GeckoCoinsResult
{
[JsonPropertyName("id")]
public required string Id { get; init; }
[JsonPropertyName("name")]
public required string Name { get; init; }
[JsonPropertyName("symbol")]
public required string Symbol { get; init; }
[JsonPropertyName("current_price")]
public required decimal CurrentPrice { get; init; }
[JsonPropertyName("price_change_percentage_24h")]
public required decimal PercentChange24h { get; init; }
[JsonPropertyName("market_cap")]
public required decimal MarketCap { get; init; }
[JsonPropertyName("circulating_supply")]
public required decimal? CirculatingSupply { get; init; }
[JsonPropertyName("total_supply")]
public required decimal? TotalSupply { get; init; }
[JsonPropertyName("market_cap_rank")]
public required int MarketCapRank { get; init; }
} }

View file

@ -123,6 +123,8 @@ public partial class Xp
}) })
.ToList(); .ToList();
var rank = await _service.GetClubRankAsync(club.Id);
await Response() await Response()
.Paginated() .Paginated()
.Items(allUsers) .Items(allUsers)
@ -135,6 +137,7 @@ public partial class Xp
.WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)"))) .WithDescription(GetText(strs.level_x(lvl.Level + $" ({club.Xp} xp)")))
.AddField(GetText(strs.desc), .AddField(GetText(strs.desc),
string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description) string.IsNullOrWhiteSpace(club.Description) ? "-" : club.Description)
.AddField(GetText(strs.rank), $"#{rank}", true)
.AddField(GetText(strs.owner), club.Owner.ToString(), true) .AddField(GetText(strs.owner), club.Owner.ToString(), true)
// .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true) // .AddField(GetText(strs.level_req), club.MinimumLevelReq.ToString(), true)
.AddField(GetText(strs.members), .AddField(GetText(strs.members),

View file

@ -134,6 +134,18 @@ public class ClubService : IEService, IClubService
return club is not null; 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) public ClubApplyResult ApplyToClub(IUser user, ClubInfo club)
{ {
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();

View file

@ -23,6 +23,7 @@ public interface IClubService
ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club); ClubKickResult Kick(ulong kickerId, string userName, out ClubInfo club);
List<ClubInfo> GetClubLeaderboardPage(int page); List<ClubInfo> GetClubLeaderboardPage(int page);
Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName); Task<ClubRenameResult> RenameClubAsync(ulong userId, string clubName);
Task<int> GetClubRankAsync(int clubId);
} }
public enum ClubApplyResult public enum ClubApplyResult

View file

@ -147,4 +147,5 @@ public static class StringExtensions
var newString = str.UnescapeUnicodeCodePoint(); var newString = str.UnescapeUnicodeCodePoint();
return newString; return newString;
}); });
} }

View file

@ -1,7 +1,30 @@
using System.Globalization;
namespace EllieBot.Extensions; namespace EllieBot.Extensions;
public static class NumberExtensions public static class NumberExtensions
{ {
public static DateTimeOffset ToUnixTimestamp(this double number) public static DateTimeOffset ToUnixTimestamp(this double number)
=> new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number); => new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number);
public static string ToShortString(this decimal value)
{
if (value <= 1_000)
return Math.Round(value, 2).ToString(CultureInfo.InvariantCulture);
if (value <= 1_000_000)
return Math.Round(value, 1).ToString(CultureInfo.InvariantCulture);
var tokens = " MBtq";
var i = 2;
while (true)
{
var num = (decimal)Math.Pow(1000, i);
if (num > value)
{
var num2 = (decimal)Math.Pow(1000, i - 1);
return $"{Math.Round((value / num2), 1)}{tokens[i - 1]}".Trim();
}
i++;
}
}
} }

View file

@ -30,6 +30,8 @@ greettest:
- greettest - greettest
greetdmtest: greetdmtest:
- greetdmtest - greetdmtest
boosttest:
- boosttest
byetest: byetest:
- byetest - byetest
boost: boost:
@ -1405,3 +1407,7 @@ prompt:
- prompt - prompt
honeypot: honeypot:
- honeypot - honeypot
coins:
- coins
- crypto
- cryptos

View file

@ -112,6 +112,14 @@ greettest:
params: params:
- user: - user:
desc: "The user to impersonate when sending the greeting, defaulting to yourself if not specified." desc: "The user to impersonate when sending the greeting, defaulting to yourself if not specified."
boosttest:
desc: Sends the boost message in the current channel as if you just boosted the server. You can optionally specify a different user.
ex:
- ''
- '@SomeoneElse'
params:
- user:
desc: "The user to impersonate when sending the boost message, defaulting to yourself if not specified."
greetdmtest: greetdmtest:
desc: Sends the greet direct message to you as if you just joined the server. You can optionally specify a different user. desc: Sends the greet direct message to you as if you just joined the server. You can optionally specify a different user.
ex: ex:
@ -2748,8 +2756,10 @@ waifutransfer:
desc: "The user to whom ownership of the waifu is being transferred." desc: "The user to whom ownership of the waifu is being transferred."
waifugift: waifugift:
desc: -| desc: -|
Gift an item to someone. Gift an item to a waifu user.
This will increase their waifu value by a percentage of the gift's value. The waifu's value will be increased by the percentage of the gift's value.
You can optionally prefix the gift with a multiplier to gift the item that many times.
For example, 3xRose will give the waifu 3 roses, 10xBread will give the waifu 10 breads. Do not use plural forms.
Negative gifts will not show up in waifuinfo. Negative gifts will not show up in waifuinfo.
Provide no parameters to see a list of items that you can gift. Provide no parameters to see a list of items that you can gift.
ex: ex:
@ -2757,9 +2767,9 @@ waifugift:
- Rose @Himesama - Rose @Himesama
params: params:
- page: - page:
desc: "The number of pages to display when listing available gifting options." desc: "The number of the page to display."
- itemName: - items:
desc: "The name of an item to be gifted, which is used to determine the percentage increase in waifu value." desc: "The name of an item to be gifted. With an optional multiplier prefix."
waifu: waifu:
desc: "The user who is receiving the gift." desc: "The user who is receiving the gift."
waifulb: waifulb:
@ -4256,8 +4266,10 @@ bankbalance:
Shows how much currency is in your bank account. Shows how much currency is in your bank account.
This differs from your cash amount, as the cash amount is publicly available, but only you have access to your bank balance. This differs from your cash amount, as the cash amount is publicly available, but only you have access to your bank balance.
However, you have to withdraw it first in order to use it. However, you have to withdraw it first in order to use it.
Bot Owner can also check another user's bank balance.
ex: ex:
- '' - ''
- '@User'
params: params:
- {} - {}
banktake: banktake:
@ -4546,3 +4558,14 @@ honeypot:
- '' - ''
params: params:
- {} - {}
coins:
desc: |-
Shows a list of 10 crypto currencies ordered by market cap.
Shows their price, change in the last24h, market cap and circulating and total supply.
Paginated with 10 per page.
ex:
- ''
- '2'
params:
- page:
desc: "Page number to show. Starts at 1."

View file

@ -889,6 +889,7 @@
"club_kick_hierarchy": "Only club owner can kick club admins. Owner can't be kicked.", "club_kick_hierarchy": "Only club owner can kick club admins. Owner can't be kicked.",
"club_renamed": "Club has been renamed to {0}", "club_renamed": "Club has been renamed to {0}",
"club_name_taken": "A club with that name already exists.", "club_name_taken": "A club with that name already exists.",
"rank": "Rank",
"template_reloaded": "Xp template has been reloaded.", "template_reloaded": "Xp template has been reloaded.",
"expr_edited": "Expression Edited", "expr_edited": "Expression Edited",
"self_assign_are_exclusive": "You can only choose 1 role from each group.", "self_assign_are_exclusive": "You can only choose 1 role from each group.",
@ -1039,6 +1040,7 @@
"marmalade_already_loaded": "Marmalade {0} is already loaded", "marmalade_already_loaded": "Marmalade {0} is already loaded",
"marmalade_invalid_not_found": "Marmalade with that name wasn't found or the file was invalid", "marmalade_invalid_not_found": "Marmalade with that name wasn't found or the file was invalid",
"bank_balance": "You have {0} in your bank account.", "bank_balance": "You have {0} in your bank account.",
"bank_balance_other": "User {0} has {1} in the bank.",
"bank_deposited": "You deposited {0} to your bank account.", "bank_deposited": "You deposited {0} to your bank account.",
"bank_withdrew": "You withdrew {0} from your bank account.", "bank_withdrew": "You withdrew {0} from your bank account.",
"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.",