Compare commits

..

No commits in common. "v5" and "5.1.19" have entirely different histories.
v5 ... 5.1.19

52 changed files with 352 additions and 8043 deletions

View file

@ -2,36 +2,15 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.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.20] - 13.11.2024
### Added
- Added `.rakeback` command, get a % of house edge back as claimable currency
- Added `.snipe` command to quickly get a copy of a posted message as an embed
- You can reply to a message to snipe that message
- Or just type .snipe and the bot will snipe the last message in the channel with content or image
- Added `.betstatsreset` / `.bsreset` command to reset your stats for a fee
- Added `.gamblestatsreset` / `.gsreset` owner-only command to reset bot stats for all games
- Added `.waifuclaims` command which lists all of your claimed waifus
- Added and changed `%bot.time%` and `%bot.date%` placeholders. They use timestamp tags now
### Changed
- `.divorce` no longer has a cooldown
- `.betroll` has a 2% better payout
- `.slot` payout balanced out (less volatile), reduced jackpot win but increased other wins,
- now has a new symbol, wheat
- worse around 1% in total (now shares the top spot with .bf)
## [5.1.19] - 05.11.2024 ## [5.1.19] - 05.11.2024
### Added ### Added
- Added `.betstats` - Added `.betstats`
- See your own stats with .betstats - See your own stats with .betstats
- Target someone else: .betstats @mai_lanfiel - Target someone else: .betstats @seraphe
- You can also specify a game .betstats lula - You can also specify a game .betstats lula
- Or both! .betstats mai_lanfiel br - Or both! .betstats seraphe br
- `.timely` can now have a server boost bonus - `.timely` can now have a server boost bonus
- Configure server ids and reward amount in data/gambling.yml - Configure server ids and reward amount in data/gambling.yml
- anyone who boosts one of the sepcified servers gets the amount as base timely bonus - anyone who boosts one of the sepcified servers gets the amount as base timely bonus

View file

@ -74,13 +74,6 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#region Rakeback
modelBuilder.Entity<Rakeback>()
.HasKey(x => x.UserId);
#endregion
#region UserBetStats #region UserBetStats
modelBuilder.Entity<UserBetStats>() modelBuilder.Entity<UserBetStats>()

View file

@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore; #nullable disable
using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models; using EllieBot.Db.Models;
namespace EllieBot.Db; namespace EllieBot.Db;
@ -16,8 +17,6 @@ public static class SelfAssignableRolesExtensions
return true; return true;
} }
public static IReadOnlyCollection<SelfAssignedRole> GetFromGuild( public static IReadOnlyCollection<SelfAssignedRole> GetFromGuild(this DbSet<SelfAssignedRole> roles, ulong guildId)
this DbSet<SelfAssignedRole> roles,
ulong guildId)
=> roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray(); => roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray();
} }

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.20</Version> <Version>5.1.19</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>

View file

@ -7,7 +7,7 @@ public static class MigrationQueries
{ {
public static void UpdateUsernames(MigrationBuilder migrationBuilder) public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' || Username WHERE Discriminator = '????';"); migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' + Username WHERE Discriminator = '????';");
} }
public static void MigrateRero(MigrationBuilder migrationBuilder) public static void MigrateRero(MigrationBuilder migrationBuilder)

File diff suppressed because it is too large Load diff

View file

@ -1,33 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class rakeback : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "rakeback",
columns: table => new
{
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
amount = table.Column<decimal>(type: "numeric", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_rakeback", x => x.userid);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "rakeback");
}
}
}

View file

@ -3227,23 +3227,6 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("greetsettings", (string)null); b.ToTable("greetsettings", (string)null);
}); });
modelBuilder.Entity("EllieBot.Services.Rakeback", b =>
{
b.Property<decimal>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<decimal>("Amount")
.HasColumnType("numeric")
.HasColumnName("amount");
b.HasKey("UserId")
.HasName("pk_rakeback");
b.ToTable("rakeback", (string)null);
});
modelBuilder.Entity("EllieBot.Services.UserBetStats", b => modelBuilder.Entity("EllieBot.Services.UserBetStats", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

File diff suppressed because it is too large Load diff

View file

@ -1,34 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class rakeback : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Rakeback",
columns: table => new
{
UserId = table.Column<ulong>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Amount = table.Column<decimal>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Rakeback", x => x.UserId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Rakeback");
}
}
}

View file

@ -2399,20 +2399,6 @@ namespace EllieBot.Migrations
b.ToTable("GreetSettings"); b.ToTable("GreetSettings");
}); });
modelBuilder.Entity("EllieBot.Services.Rakeback", b =>
{
b.Property<ulong>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Amount")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.ToTable("Rakeback");
});
modelBuilder.Entity("EllieBot.Services.UserBetStats", b => modelBuilder.Entity("EllieBot.Services.UserBetStats", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")

View file

@ -71,6 +71,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
if (server.OwnerId != _client.CurrentUser.Id) if (server.OwnerId != _client.CurrentUser.Id)
{ {
await server.LeaveAsync(); await server.LeaveAsync();
Log.Information("Left server {Name} [{Id}]", server.Name, server.Id);
} }
else else
{ {

View file

@ -12,7 +12,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class AnimalRacingCommands : GamblingModule<AnimalRaceService> public partial class AnimalRacingCommands : GamblingSubmodule<AnimalRaceService>
{ {
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;

View file

@ -1,175 +0,0 @@
#nullable disable
using EllieBot.Modules.Gambling.Common;
using EllieBot.Modules.Gambling.Services;
namespace EllieBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public sealed class BetStatsCommands : GamblingModule<UserBetStatsService>
{
private readonly GamblingTxTracker _gamblingTxTracker;
public BetStatsCommands(
GamblingTxTracker gamblingTxTracker,
GamblingConfigService gcs)
: base(gcs)
{
_gamblingTxTracker = gamblingTxTracker;
}
[Cmd]
public async Task BetStatsReset(GamblingGame? game = null)
{
var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game);
var result = await PromptUserConfirmAsync(_sender.CreateEmbed()
.WithDescription(
$"""
Are you sure you want to reset your bet stats for **{GetGameName(game)}**?
It will cost you {N(price)}
"""));
if (!result)
return;
var success = await _service.ResetStatsAsync(ctx.User.Id, game);
if (success)
{
await ctx.OkAsync();
}
else
{
await Response()
.Error(strs.not_enough(CurrencySign))
.SendAsync();
}
}
private string GetGameName(GamblingGame? game)
{
if (game is null)
return "all games";
return game.ToString();
}
[Cmd]
[Priority(3)]
public async Task BetStats()
=> await BetStats(ctx.User, null);
[Cmd]
[Priority(2)]
public async Task BetStats(GamblingGame game)
=> await BetStats(ctx.User, game);
[Cmd]
[Priority(1)]
public async Task BetStats([Leftover] IUser user)
=> await BetStats(user, null);
[Cmd]
[Priority(0)]
public async Task BetStats(IUser user, GamblingGame? game)
{
var stats = await _gamblingTxTracker.GetUserStatsAsync(user.Id, game);
if (stats.Count == 0)
stats = new()
{
new()
{
TotalBet = 1
}
};
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true);
if (game == null)
{
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
eb.AddField("Favorite Game",
favGame.Game + "\n" + Format.Italics((favGame.WinCount + favGame.LoseCount) + " plays"),
true);
}
else
{
eb.WithDescription(game.ToString())
.AddField("# Wins", stats.Sum(x => x.WinCount), true);
}
await Response()
.Embed(eb)
.SendAsync();
}
[Cmd]
public async Task GambleStats()
{
var stats = await _gamblingTxTracker.GetAllAsync();
var eb = _sender.CreateEmbed()
.WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n";
foreach (var stat in stats)
{
var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture);
str += $"`{stat.Feature.PadBoth(9)}`"
+ $"`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`"
+ $"`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`"
+ $"`{perc.PadLeft(6, ' ')}`\n";
}
var bet = stats.Sum(x => x.Bet);
var paidOut = stats.Sum(x => x.PaidOut);
if (bet == 0)
bet = 1;
var tPerc = (paidOut / bet).ToString("P2", Culture);
str += "――――――――――――――――――――\n";
str += $"` {("TOTAL").PadBoth(7)}` "
+ $"**{N(bet).PadLeft(8, ' ')}**"
+ $"**{N(paidOut).PadLeft(8, ' ')}**"
+ $"`{tPerc.PadLeft(6, ' ')}`";
eb.WithDescription(str);
await Response().Embed(eb).SendAsync();
}
[Cmd]
[OwnerOnly]
public async Task GambleStatsReset()
{
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
.WithDescription(
"""
Are you sure?
This will completely reset Gambling Stats.
This action is irreversible.
""")))
return;
await GambleStats();
await _service.ResetGamblingStatsAsync();
await ctx.OkAsync();
}
}
}

View file

@ -8,7 +8,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
public partial class BlackJackCommands : GamblingModule<BlackJackService> public partial class BlackJackCommands : GamblingSubmodule<BlackJackService>
{ {
public enum BjAction public enum BjAction
{ {

View file

@ -9,7 +9,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class Connect4Commands : GamblingModule<GamblingService> public partial class Connect4Commands : GamblingSubmodule<GamblingService>
{ {
private static readonly string[] _numbers = private static readonly string[] _numbers =
[ [

View file

@ -12,7 +12,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class DrawCommands : GamblingModule<IGamblingService> public partial class DrawCommands : GamblingSubmodule<IGamblingService>
{ {
private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new(); private static readonly ConcurrentDictionary<IGuild, Deck> _allDecks = new();
private readonly IImageCache _images; private readonly IImageCache _images;

View file

@ -9,7 +9,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class CurrencyEventsCommands : GamblingModule<CurrencyEventsService> public partial class CurrencyEventsCommands : GamblingSubmodule<CurrencyEventsService>
{ {
public CurrencyEventsCommands(GamblingConfigService gamblingConf) public CurrencyEventsCommands(GamblingConfigService gamblingConf)
: base(gamblingConf) : base(gamblingConf)

View file

@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class FlipCoinCommands : GamblingModule<IGamblingService> public partial class FlipCoinCommands : GamblingSubmodule<IGamblingService>
{ {
public enum BetFlipGuess : byte public enum BetFlipGuess : byte
{ {

View file

@ -38,7 +38,6 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly IRemindService _remind; private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IPatronageService _ps; private readonly IPatronageService _ps;
private readonly RakebackService _rb;
public Gambling( public Gambling(
IGamblingService gs, IGamblingService gs,
@ -51,8 +50,7 @@ public partial class Gambling : GamblingModule<GamblingService>
IBankService bank, IBankService bank,
IRemindService remind, IRemindService remind,
IPatronageService patronage, IPatronageService patronage,
GamblingTxTracker gamblingTxTracker, GamblingTxTracker gamblingTxTracker)
RakebackService rb)
: base(configService) : base(configService)
{ {
_gs = gs; _gs = gs;
@ -62,7 +60,6 @@ public partial class Gambling : GamblingModule<GamblingService>
_bank = bank; _bank = bank;
_remind = remind; _remind = remind;
_gamblingTxTracker = gamblingTxTracker; _gamblingTxTracker = gamblingTxTracker;
_rb = rb;
_ps = patronage; _ps = patronage;
_rng = new EllieRandom(); _rng = new EllieRandom();
@ -80,6 +77,100 @@ public partial class Gambling : GamblingModule<GamblingService>
return N(bal); return N(bal);
} }
[Cmd]
[Priority(3)]
public async Task BetStats()
=> await BetStats(ctx.User, null);
[Cmd]
[Priority(2)]
public async Task BetStats(GamblingGame game)
=> await BetStats(ctx.User, game);
[Cmd]
[Priority(1)]
public async Task BetStats([Leftover] IUser user)
=> await BetStats(user, null);
[Cmd]
[Priority(0)]
public async Task BetStats(IUser user, GamblingGame? game)
{
var stats = await _gamblingTxTracker.GetUserStatsAsync(user.Id, game);
if (stats.Count == 0)
stats = new()
{
new()
{
TotalBet = 1
}
};
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithAuthor(user)
.AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true)
.AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true)
.AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true)
.AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true)
.AddField("Payout",
(stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture),
true);
if (game == null)
{
var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount);
eb.AddField("Favorite Game",
favGame.Game + "\n" + Format.Italics((favGame.WinCount + favGame.LoseCount) + " plays"),
true);
}
else
{
eb.WithDescription(game.ToString())
.AddField("# Wins", stats.Sum(x => x.WinCount), true);
}
await Response()
.Embed(eb)
.SendAsync();
}
[Cmd]
public async Task GambleStats()
{
var stats = await _gamblingTxTracker.GetAllAsync();
var eb = _sender.CreateEmbed()
.WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n";
foreach (var stat in stats)
{
var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture);
str += $"`{stat.Feature.PadBoth(9)}`"
+ $"`{stat.Bet.ToString("N0").PadLeft(8, '')}`"
+ $"`{stat.PaidOut.ToString("N0").PadLeft(8, '')}`"
+ $"`{perc.PadLeft(6, '')}`\n";
}
var bet = stats.Sum(x => x.Bet);
var paidOut = stats.Sum(x => x.PaidOut);
if (bet == 0)
bet = 1;
var tPerc = (paidOut / bet).ToString("P2", Culture);
str += "――――――――――――――――――――\n";
str += $"` {("TOTAL").PadBoth(7)}` "
+ $"**{N(bet).PadLeft(8, '')}**"
+ $"**{N(paidOut).PadLeft(8, '')}**"
+ $"`{tPerc.PadLeft(6, '')}`";
eb.WithDescription(str);
await Response().Embed(eb).SendAsync();
}
private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when) private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when)
{ {
@ -227,6 +318,7 @@ public partial class Gambling : GamblingModule<GamblingService>
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
var boostGuilds = Config.BoostBonus.GuildIds ?? new(); var boostGuilds = Config.BoostBonus.GuildIds ?? new();
var guildUsers = await boostGuilds var guildUsers = await boostGuilds
.Select(async gid => .Select(async gid =>
@ -260,16 +352,11 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim")); await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
var msg = GetText(strs.timely(N(val), period)); if (booster)
if (booster || percentBonus > float.Epsilon)
{ {
msg += "\n\n"; var msg = GetText(strs.timely(N(val), period))
if (booster) + "\n\n"
msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n"; + $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*";
if (percentBonus > float.Epsilon)
msg +=
$"*+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/elliebot) pledge! <:hart:746995901758832712>*";
await Response().Confirm(msg).Interaction(inter).SendAsync(); await Response().Confirm(msg).Interaction(inter).SendAsync();
} }
@ -1002,45 +1089,4 @@ public partial class Gambling : GamblingModule<GamblingService>
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%") footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
.SendAsync(); .SendAsync();
} }
private EllieInteractionBase CreateRakebackInteraction()
=> _inter.Create(ctx.User.Id,
new ButtonBuilder(
customId: "cash:rakeback",
emote: new Emoji("💸")),
RakebackAction);
private async Task RakebackAction(SocketMessageComponent arg)
{
var rb = await _rb.ClaimRakebackAsync(ctx.User.Id);
if (rb == 0)
{
await arg.DeferAsync();
return;
}
await arg.RespondAsync(_sender, GetText(strs.rakeback_claimed(N(rb))), MsgType.Ok);
}
[Cmd]
public async Task Rakeback()
{
var rb = await _rb.GetRakebackAsync(ctx.User.Id);
if (rb < 1)
{
await Response()
.Error(strs.rakeback_none)
.SendAsync();
return;
}
var inter = CreateRakebackInteraction();
await Response()
.Pending(strs.rakeback_available(N(rb)))
.Interaction(inter)
.SendAsync();
}
} }

View file

@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling.Common;
public sealed partial class GamblingConfig : ICloneable<GamblingConfig> public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 12; public int Version { get; set; } = 11;
[Comment("""Currency settings""")] [Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; } public CurrencyConfig Currency { get; set; }
@ -164,7 +164,7 @@ public partial class BetRollConfig
}, },
new() new()
{ {
WhenAbove = 65, WhenAbove = 66,
MultiplyBy = 2 MultiplyBy = 2
} }
]; ];
@ -226,7 +226,7 @@ public partial class LuckyLadderSettings
public decimal[] Multipliers { get; set; } public decimal[] Multipliers { get; set; }
public LuckyLadderSettings() public LuckyLadderSettings()
=> Multipliers = [2.4M, 1.7M, 1.5M, 1.1M, 0.5M, 0.3M, 0.2M, 0.1M]; => Multipliers = [2.4M, 1.7M, 1.5M, 1.2M, 0.5M, 0.3M, 0.2M, 0.1M];
} }
[Cloneable] [Cloneable]

View file

@ -189,16 +189,11 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 12) if (data.Version < 11)
{ {
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 12; c.Version = 11;
if (c.BetRoll.Pairs.Length == 3 && c.BetRoll.Pairs[2].WhenAbove == 66)
{
c.BetRoll.Pairs[2].WhenAbove = 65;
}
}); });
} }
} }

View file

@ -58,3 +58,11 @@ public abstract class GamblingModule<TService> : EllieModule<TService>
return InternalCheckBet(amount); return InternalCheckBet(amount);
} }
} }
public abstract class GamblingSubmodule<TService> : GamblingModule<TService>
{
protected GamblingSubmodule(GamblingConfigService gamblingConfService)
: base(gamblingConfService)
{
}
}

View file

@ -8,7 +8,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class PlantPickCommands : GamblingModule<PlantPickService> public partial class PlantPickCommands : GamblingSubmodule<PlantPickService>
{ {
private readonly ILogCommandService _logService; private readonly ILogCommandService _logService;

View file

@ -10,7 +10,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class ShopCommands : GamblingModule<IShopService> public partial class ShopCommands : GamblingSubmodule<IShopService>
{ {
public enum List public enum List
{ {

View file

@ -21,7 +21,7 @@ public enum GamblingError
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class SlotCommands : GamblingModule<IGamblingService> public partial class SlotCommands : GamblingSubmodule<IGamblingService>
{ {
private readonly IImageCache _images; private readonly IImageCache _images;
private readonly FontProvider _fonts; private readonly FontProvider _fonts;

View file

@ -1,55 +0,0 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Db.Models;
namespace EllieBot.Modules.Gambling.Services;
public sealed class UserBetStatsService : IEService
{
private const long RESET_MIN_PRICE = 1000;
private const decimal RESET_TOTAL_MULTIPLIER = 0.002m;
private readonly DbService _db;
private readonly ICurrencyService _cs;
public UserBetStatsService(DbService db, ICurrencyService cs)
{
_db = db;
_cs = cs;
}
public async Task<long> GetResetStatsPriceAsync(ulong userId, GamblingGame? game)
{
await using var ctx = _db.GetDbContext();
var totalBet = await ctx.GetTable<UserBetStats>()
.Where(x => x.UserId == userId && (game == null || x.Game == game))
.SumAsyncLinqToDB(x => x.TotalBet);
return Math.Max(RESET_MIN_PRICE, (long)Math.Ceiling(totalBet * RESET_TOTAL_MULTIPLIER));
}
public async Task<bool> ResetStatsAsync(ulong userId, GamblingGame? game)
{
var price = await GetResetStatsPriceAsync(userId, game);
if (!await _cs.RemoveAsync(userId, price, new("betstats", "reset")))
{
return false;
}
await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserBetStats>()
.DeleteAsync(x => x.UserId == userId && (game == null || x.Game == game));
return true;
}
public async Task ResetGamblingStatsAsync()
{
await using var ctx = _db.GetDbContext();
await ctx.GetTable<GamblingStats>()
.DeleteAsync();
}
}

View file

@ -10,7 +10,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class WaifuClaimCommands : GamblingModule<WaifuService> public partial class WaifuClaimCommands : GamblingSubmodule<WaifuService>
{ {
public WaifuClaimCommands(GamblingConfigService gamblingConfService) public WaifuClaimCommands(GamblingConfigService gamblingConfService)
: base(gamblingConfService) : base(gamblingConfService)
@ -37,45 +37,6 @@ public partial class Gambling
await Response().Error(strs.waifu_reset_fail).SendAsync(); await Response().Error(strs.waifu_reset_fail).SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task WaifuClaims()
{
await Response()
.Paginated()
.PageItems(async (page) => await _service.GetClaimsAsync(ctx.User.Id, page))
.Page((items, page) =>
{
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithTitle("Waifus");
if (items.Count == 0)
{
eb
.WithPendingColor()
.WithDescription(GetText(strs.empty_page));
return eb;
}
for (var i = 0; i < items.Count; i++)
{
var item = items[i];
eb.AddField($"`#{(page * 9) + 1 + i}` {N(item.Price)}",
$"""
{item.Username}
||{item.UserId}||
""",
true
);
}
return eb;
})
.SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task WaifuClaim(long amount, [Leftover] IUser target) public async Task WaifuClaim(long amount, [Leftover] IUser target)
@ -183,7 +144,7 @@ public partial class Gambling
if (targetId == ctx.User.Id) if (targetId == ctx.User.Id)
return; return;
var (w, result, amount) = await _service.DivorceWaifuAsync(ctx.User, targetId); var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId);
if (result == DivorceResult.SucessWithPenalty) if (result == DivorceResult.SucessWithPenalty)
{ {
@ -196,6 +157,14 @@ public partial class Gambling
await Response().Confirm(strs.waifu_divorced_notlike(N(amount))).SendAsync(); await Response().Confirm(strs.waifu_divorced_notlike(N(amount))).SendAsync();
else if (result == DivorceResult.NotYourWife) else if (result == DivorceResult.NotYourWife)
await Response().Error(strs.waifu_not_yours).SendAsync(); await Response().Error(strs.waifu_not_yours).SendAsync();
else if (remaining is { } rem)
{
await Response()
.Error(strs.waifu_recent_divorce(
Format.Bold(((int)rem.TotalHours).ToString()),
Format.Bold(rem.Minutes.ToString())))
.SendAsync();
}
} }
[Cmd] [Cmd]

View file

@ -318,20 +318,25 @@ public class WaifuService : IEService, IReadyExecutor
private static TypedKey<long> GetAffinityKey(ulong userId) private static TypedKey<long> GetAffinityKey(ulong userId)
=> new($"waifu:affinity:{userId}"); => new($"waifu:affinity:{userId}");
public async Task<(WaifuInfo, DivorceResult, long)> DivorceWaifuAsync(IUser user, ulong targetId) public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
{ {
DivorceResult result; DivorceResult result;
TimeSpan? remaining = null;
long amount = 0; long amount = 0;
WaifuInfo w; WaifuInfo w;
await using (var uow = _db.GetDbContext()) await using (var uow = _db.GetDbContext())
{ {
w = uow.Set<WaifuInfo>().ByWaifuUserId(targetId); w = uow.Set<WaifuInfo>().ByWaifuUserId(targetId);
if (w?.Claimer is null || w.Claimer.UserId != user.Id) if (w?.Claimer is null || w.Claimer.UserId != user.Id)
{
result = DivorceResult.NotYourWife; result = DivorceResult.NotYourWife;
}
else else
{ {
remaining = await _cache.GetRatelimitAsync(GetDivorceKey(user.Id), 6.Hours());
if (remaining is TimeSpan rem)
{
result = DivorceResult.Cooldown;
return (w, result, amount, rem);
}
amount = w.Price / 2; amount = w.Price / 2;
@ -364,7 +369,7 @@ public class WaifuService : IEService, IReadyExecutor
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
return (w, result, amount); return (w, result, amount, remaining);
} }
public async Task<bool> GiftWaifuAsync( public async Task<bool> GiftWaifuAsync(
@ -625,38 +630,4 @@ public class WaifuService : IEService, IReadyExecutor
.FirstOrDefault()) .FirstOrDefault())
.ToListAsyncEF(); .ToListAsyncEF();
} }
public async Task<IReadOnlyCollection<WaifuClaimsResult>> GetClaimsAsync(ulong userId, int page)
{
await using var ctx = _db.GetDbContext();
var wid = ctx.GetTable<DiscordUser>()
.Where(x => x.UserId == userId)
.Select(x => x.Id)
.FirstOrDefault();
if (wid == 0)
return [];
return await ctx.GetTable<WaifuInfo>()
.Where(x => x.ClaimerId == wid)
.LeftJoin(ctx.GetTable<DiscordUser>(),
(wi, du) => wi.WaifuId == du.Id,
(wi, du) => new WaifuClaimsResult(
du.Username,
du.UserId,
wi.Price
))
.OrderByDescending(x => x.Price)
.Skip(page * 9)
.Take(9)
.ToListAsyncLinqToDB();
}
}
public sealed class WaifuClaimsResult(string username, ulong userId, long price)
{
public string Username { get; } = username;
public ulong UserId { get; } = userId;
public long Price { get; } = price;
} }

View file

@ -13,10 +13,5 @@ public interface IGamblingService
Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount); Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount);
Task<FlipResult[]> FlipAsync(int count); Task<FlipResult[]> FlipAsync(int count);
Task<OneOf<RpsResult, GamblingError>> RpsAsync(ulong userId, long amount, byte pick); Task<OneOf<RpsResult, GamblingError>> RpsAsync(ulong userId, long amount, byte pick);
Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? maybeGuessValue, byte? maybeGuessColor);
Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(
ulong userId,
long amount,
byte? maybeGuessValue,
byte? maybeGuessColor);
} }

View file

@ -1,6 +1,4 @@
#nullable disable #nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Modules.Gambling.Betdraw; using EllieBot.Modules.Gambling.Betdraw;
using EllieBot.Modules.Gambling.Rps; using EllieBot.Modules.Gambling.Rps;
using EllieBot.Modules.Gambling.Services; using EllieBot.Modules.Gambling.Services;
@ -10,12 +8,12 @@ namespace EllieBot.Modules.Gambling;
public sealed class NewGamblingService : IGamblingService, IEService public sealed class NewGamblingService : IGamblingService, IEService
{ {
private readonly GamblingConfigService _gcs; private readonly GamblingConfigService _bcs;
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
public NewGamblingService(GamblingConfigService gcs, ICurrencyService cs) public NewGamblingService(GamblingConfigService bcs, ICurrencyService cs)
{ {
_gcs = gcs; _bcs = bcs;
_cs = cs; _cs = cs;
} }
@ -33,7 +31,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new LulaGame(_gcs.Data.LuckyLadder.Multipliers); var game = new LulaGame(_bcs.Data.LuckyLadder.Multipliers);
var result = game.Spin(amount); var result = game.Spin(amount);
var won = (long)result.Won; var won = (long)result.Won;
@ -59,9 +57,9 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new BetrollGame(_gcs.Data.BetRoll.Pairs var game = new BetrollGame(_bcs.Data.BetRoll.Pairs
.Select(x => (x.WhenAbove, (decimal)x.MultiplyBy)) .Select(x => (x.WhenAbove, (decimal)x.MultiplyBy))
.ToList()); .ToList());
var result = game.Roll(amount); var result = game.Roll(amount);
@ -90,7 +88,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new BetflipGame(_gcs.Data.BetFlip.Multiplier); var game = new BetflipGame(_bcs.Data.BetFlip.Multiplier);
var result = game.Flip(guess, amount); var result = game.Flip(guess, amount);
var won = (long)result.Won; var won = (long)result.Won;
@ -102,11 +100,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
return result; return result;
} }
public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync( public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? maybeGuessValue, byte? maybeGuessColor)
ulong userId,
long amount,
byte? maybeGuessValue,
byte? maybeGuessColor)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(amount); ArgumentOutOfRangeException.ThrowIfNegative(amount);
@ -272,45 +266,3 @@ public sealed class NewGamblingService : IGamblingService, IEService
return result; return result;
} }
} }
public sealed class RakebackService : IEService
{
private readonly DbService _db;
private readonly ICurrencyService _cs;
public RakebackService(DbService db, ICurrencyService cs)
{
_db = db;
_cs = cs;
}
public async Task<long> GetRakebackAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var rb = uow.GetTable<Rakeback>()
.Where(x => x.UserId == userId)
.Select(x => x.Amount)
.FirstOrDefault();
return (long)rb;
}
public async Task<long> ClaimRakebackAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var rbs = await uow.GetTable<Rakeback>()
.Where(x => x.UserId == userId)
.DeleteWithOutputAsync((x) => x.Amount);
if (rbs.Length == 0)
return 0;
var rb = (long)rbs[0];
await _cs.AddAsync(userId, rb, new("rakeback", "claim"));
return rb;
}
}

View file

@ -122,11 +122,11 @@ public sealed class CurrencyRewardService : IEService, IReadyExecutor
var dollarValue = pledgeCents / 100; var dollarValue = pledgeCents / 100;
percentBonus = dollarValue switch percentBonus = dollarValue switch
{ {
>= 100 => 25, >= 100 => 20,
>= 50 => 20, >= 50 => 10,
>= 20 => 15, >= 20 => 5,
>= 10 => 10, >= 10 => 3,
>= 5 => 5, >= 5 => 1,
_ => 0 _ => 0
}; };
return (long)(modifiedAmount * (1 + (percentBonus / 100.0f))); return (long)(modifiedAmount * (1 + (percentBonus / 100.0f)));

View file

@ -404,9 +404,9 @@ public sealed class PatronageService
{ {
>= 10_000 => 100, >= 10_000 => 100,
>= 5000 => 50, >= 5000 => 50,
>= 2000 => 30, >= 2000 => 20,
>= 1000 => 20, >= 1000 => 10,
>= 500 => 10, >= 500 => 5,
_ => 0 _ => 0
}; };

View file

@ -18,39 +18,39 @@ public partial class Permissions
{ {
ArgumentOutOfRangeException.ThrowIfNegative(page); ArgumentOutOfRangeException.ThrowIfNegative(page);
var list = await _service.GetBlacklist(type); var list = _service.GetBlacklist();
var allItems = await list var allItems = await list.Where(x => x.Type == type)
.Select(i => .Select(i =>
{ {
try try
{ {
return Task.FromResult(type switch return Task.FromResult(i.Type switch
{ {
BlacklistType.Channel => Format.Code(i.ItemId.ToString()) BlacklistType.Channel => Format.Code(i.ItemId.ToString())
+ " "
+ (_client.GetChannel(i.ItemId)?.ToString()
?? ""),
BlacklistType.User => Format.Code(i.ItemId.ToString())
+ " "
+ ((_client.GetUser(i.ItemId))
?.ToString()
?? ""),
BlacklistType.Server => Format.Code(i.ItemId.ToString())
+ " " + " "
+ (_client.GetChannel(i.ItemId)?.ToString() + (_client.GetGuild(i.ItemId)?.ToString() ?? ""),
?? ""), _ => Format.Code(i.ItemId.ToString())
BlacklistType.User => Format.Code(i.ItemId.ToString()) });
+ " " }
+ ((_client.GetUser(i.ItemId)) catch
?.ToString() {
?? ""), Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]",
BlacklistType.Server => Format.Code(i.ItemId.ToString()) i.Type,
+ " " i.ItemId);
+ (_client.GetGuild(i.ItemId)?.ToString() ?? ""),
_ => Format.Code(i.ItemId.ToString())
});
}
catch
{
Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]",
i.Type,
i.ItemId);
return Task.FromResult(Format.Code(i.ItemId.ToString())); return Task.FromResult(Format.Code(i.ItemId.ToString()));
} }
}) })
.WhenAll(); .WhenAll();
await Response() await Response()
.Paginated() .Paginated()
@ -61,14 +61,14 @@ public partial class Permissions
{ {
if (pageItems.Count == 0) if (pageItems.Count == 0)
return _sender.CreateEmbed() return _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(title) .WithTitle(title)
.WithDescription(GetText(strs.empty_page)); .WithDescription(GetText(strs.empty_page));
return _sender.CreateEmbed() return _sender.CreateEmbed()
.WithTitle(title) .WithTitle(title)
.WithDescription(pageItems.Join('\n')) .WithDescription(pageItems.Join('\n'))
.WithOkColor(); .WithOkColor();
}) })
.SendAsync(); .SendAsync();
} }

View file

@ -783,28 +783,4 @@ public partial class Utility : EllieModule
await Response().Error(ex.Message).SendAsync(); await Response().Error(ex.Message).SendAsync();
} }
} }
[Cmd]
public async Task Snipe()
{
if (ctx.Message.ReferencedMessage is not { } msg)
{
var msgs = await ctx.Channel.GetMessagesAsync(ctx.Message, Direction.Before, 3).FlattenAsync();
msg = msgs.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Content) || (x.Attachments.FirstOrDefault()?.Width is not null)) as IUserMessage;
if (msg is null)
return;
}
var eb = _sender.CreateEmbed()
.WithOkColor()
.WithDescription(msg.Content)
.WithAuthor(msg.Author)
.WithTimestamp(msg.Timestamp)
.WithImageUrl(msg.Attachments.FirstOrDefault()?.Url)
.WithFooter(GetText(strs.sniped_by(ctx.User.ToString())), ctx.User.GetDisplayAvatarUrl());
ctx.Message.DeleteAfter(1);
await Response().Embed(eb).SendAsync();
}
} }

View file

@ -159,14 +159,14 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
_xp.SetRoleReward(request.GuildId, request.Level, rid, request.Type == "RemoveRole"); _xp.SetRoleReward(request.GuildId, request.Level, rid, request.Type == "RemoveRole");
success = true; success = true;
} }
// else if (request.Type == "Currency") else if (request.Type == "Currency")
// { {
// if (!int.TryParse(request.Value, out var amount)) if (!int.TryParse(request.Value, out var amount))
// throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid amount")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid amount"));
//
// _xp.SetCurrencyReward(request.GuildId, request.Level, amount); _xp.SetCurrencyReward(request.GuildId, request.Level, amount);
// success = true; success = true;
// } }
return new() return new()
{ {

View file

@ -11,9 +11,9 @@ public class SlotGame
{ {
var rolls = new[] var rolls = new[]
{ {
(byte)_rng.Next(0, 7), (byte)_rng.Next(0, 6),
(byte)_rng.Next(0, 7), (byte)_rng.Next(0, 6),
(byte)_rng.Next(0, 7) (byte)_rng.Next(0, 6)
}; };
ref var a = ref rolls[0]; ref var a = ref rolls[0];
@ -24,24 +24,24 @@ public class SlotGame
var winType = SlotWinType.None; var winType = SlotWinType.None;
if (a == b && b == c) if (a == b && b == c)
{ {
if (a == 6) if (a == 5)
{ {
winType = SlotWinType.TrippleJoker; winType = SlotWinType.TrippleJoker;
multi = 25; multi = 30;
} }
else else
{ {
winType = SlotWinType.TrippleNormal; winType = SlotWinType.TrippleNormal;
multi = 15; multi = 10;
} }
} }
else if (a == 6 && (b == 6 || c == 6) else if (a == 5 && (b == 5 || c == 5)
|| (b == 6 && c == 6)) || (b == 5 && c == 5))
{ {
winType = SlotWinType.DoubleJoker; winType = SlotWinType.DoubleJoker;
multi = 6; multi = 4;
} }
else if (a == 6 || b == 6 || c == 6) else if (a == 5 || b == 5 || c == 5)
{ {
winType = SlotWinType.SingleJoker; winType = SlotWinType.SingleJoker;
multi = 1; multi = 1;

View file

@ -8,7 +8,7 @@ namespace EllieBot.Common;
public partial class ImageUrls : ICloneable<ImageUrls> public partial class ImageUrls : ICloneable<ImageUrls>
{ {
[Comment("DO NOT CHANGE")] [Comment("DO NOT CHANGE")]
public int Version { get; set; } = 6; public int Version { get; set; } = 5;
public CoinData Coins { get; set; } public CoinData Coins { get; set; }
public Uri[] Currency { get; set; } public Uri[] Currency { get; set; }

View file

@ -12,11 +12,7 @@ public sealed partial class ReplacementPatternStore
{ {
Register("%bot.time%", Register("%bot.time%",
static () static ()
=> TimestampTag.FromDateTime(DateTime.UtcNow, TimestampTagStyles.ShortTime).ToString()); => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
Register("%bot.date%",
static ()
=> TimestampTag.FromDateTime(DateTime.UtcNow, TimestampTagStyles.ShortTime).ToString());
} }
private void WithClient() private void WithClient()

View file

@ -165,7 +165,7 @@ public class CommandHandler : IEService, IReadyExecutor, ICommandHandler
Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}", Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}",
channel?.Guild.Id.ToString() ?? "-", channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-", channel?.Id.ToString() ?? "-",
usrMsg.Author.Id.ToString(), usrMsg.Author.Id,
usrMsg.Content.TrimTo(10)); usrMsg.Content.TrimTo(10));
} }

View file

@ -4,7 +4,6 @@ using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Services.Currency; using EllieBot.Services.Currency;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Gambling;
using System.Collections.Concurrent; using System.Collections.Concurrent;
namespace EllieBot.Services; namespace EllieBot.Services;
@ -89,10 +88,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
if (users.Count == 0) if (users.Count == 0)
continue; continue;
// rakeback
var rakebacks = new Dictionary<ulong, decimal>();
// update userstats
foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game)) foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game))
.ToDictionary(x => x.Key, .ToDictionary(x => x.Key,
x => x.Aggregate((a, b) => new() x => x.Aggregate((a, b) => new()
@ -105,10 +100,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
MaxWin = Math.Max(a.MaxWin, b.MaxWin), MaxWin = Math.Max(a.MaxWin, b.MaxWin),
}))) })))
{ {
rakebacks.TryAdd(k.UserId, 0m);
rakebacks[k.UserId] += x.TotalBet * GetHouseEdge(k.Game) * BASE_RAKEBACK;
// bulk upsert in the future // bulk upsert in the future
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<UserBetStats>() await uow.GetTable<UserBetStats>()
@ -138,25 +129,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
Game = k.Game Game = k.Game
}); });
} }
foreach (var (k, v) in rakebacks)
{
await _db.GetDbContext()
.GetTable<Rakeback>()
.InsertOrUpdateAsync(() => new()
{
UserId = k,
Amount = v
},
(old) => new()
{
Amount = old.Amount + v
},
() => new()
{
UserId = k
});
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -165,8 +137,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
} }
} }
private const decimal BASE_RAKEBACK = 0.05m;
public Task TrackAdd(ulong userId, long amount, TxData? txData) public Task TrackAdd(ulong userId, long amount, TxData? txData)
{ {
if (txData is null) if (txData is null)
@ -305,21 +275,6 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
.Where(x => x.UserId == userId && x.Game == game) .Where(x => x.UserId == userId && x.Game == game)
.ToListAsync(); .ToListAsync();
} }
public decimal GetHouseEdge(GamblingGame game)
=> game switch
{
GamblingGame.Betflip => 0.025m,
GamblingGame.Betroll => 0.04m,
GamblingGame.Betdraw => 0.04m,
GamblingGame.Slots => 0.034m,
GamblingGame.Blackjack => 0.02m,
GamblingGame.Lula => 0.025m,
GamblingGame.Race => 0.06m,
_ => 0
};
} }
public sealed class UserBetStats public sealed class UserBetStats
@ -351,9 +306,3 @@ public enum GamblingGame
Race = 6, Race = 6,
AnimalRace = 6 AnimalRace = 6
} }
public sealed class Rakeback
{
public ulong UserId { get; set; }
public decimal Amount { get; set; }
}

View file

@ -1,13 +1,13 @@
#nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using System.Collections.Frozen;
namespace EllieBot.Modules.Permissions.Services; namespace EllieBot.Modules.Permissions.Services;
public sealed class BlacklistService : IExecOnMessage, IReadyExecutor public sealed class BlacklistService : IExecOnMessage
{ {
public int Priority public int Priority
=> int.MaxValue; => int.MaxValue;
@ -15,114 +15,69 @@ public sealed class BlacklistService : IExecOnMessage, IReadyExecutor
private readonly DbService _db; private readonly DbService _db;
private readonly IPubSub _pubSub; private readonly IPubSub _pubSub;
private readonly IBotCreds _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private IReadOnlyList<BlacklistEntry> blacklist;
private FrozenSet<ulong> blacklistedGuilds = new HashSet<ulong>().ToFrozenSet(); private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
private FrozenSet<ulong> blacklistedUsers = new HashSet<ulong>().ToFrozenSet();
private FrozenSet<ulong> blacklistedChannels = new HashSet<ulong>().ToFrozenSet();
private readonly TypedKey<bool> _blPubKey = new("blacklist.reload"); public BlacklistService(DbService db, IPubSub pubSub, IBotCreds creds)
public BlacklistService(
DbService db,
IPubSub pubSub,
IBotCreds creds,
DiscordSocketClient client)
{ {
_db = db; _db = db;
_pubSub = pubSub; _pubSub = pubSub;
_creds = creds; _creds = creds;
_client = client;
_pubSub.Sub(_blPubKey, async _ => await Reload(false)); Reload(false);
} _pubSub.Sub(_blPubKey, OnReload);
public async Task OnReadyAsync()
{
_client.JoinedGuild += async (g) =>
{
if (blacklistedGuilds.Contains(g.Id))
{
await g.LeaveAsync();
}
};
await Reload(false);
} }
private ValueTask OnReload(BlacklistEntry[] newBlacklist) private ValueTask OnReload(BlacklistEntry[] newBlacklist)
{ {
newBlacklist ??= []; blacklist = newBlacklist;
blacklistedGuilds =
new HashSet<ulong>(newBlacklist.Where(x => x.Type == BlacklistType.Server).Select(x => x.ItemId))
.ToFrozenSet();
blacklistedChannels =
new HashSet<ulong>(newBlacklist.Where(x => x.Type == BlacklistType.Channel).Select(x => x.ItemId))
.ToFrozenSet();
blacklistedUsers =
new HashSet<ulong>(newBlacklist.Where(x => x.Type == BlacklistType.User).Select(x => x.ItemId))
.ToFrozenSet();
return default; return default;
} }
public Task<bool> ExecOnMessageAsync(IGuild? guild, IUserMessage usrMsg) public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
{ {
if (guild is not null && blacklistedGuilds.Contains(guild.Id)) foreach (var bl in blacklist)
{ {
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id)
guild.Name, {
guild.Id.ToString()); Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id);
return Task.FromResult(true);
}
if (blacklistedChannels.Contains(usrMsg.Channel.Id)) return Task.FromResult(true);
{ }
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
usrMsg.Channel.Name,
usrMsg.Channel.Id.ToString());
}
if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id)
{
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
usrMsg.Channel.Name,
usrMsg.Channel.Id);
if (blacklistedUsers.Contains(usrMsg.Author.Id)) return Task.FromResult(true);
{ }
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
usrMsg.Author.ToString(), if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
usrMsg.Author.Id.ToString()); {
return Task.FromResult(true); Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
usrMsg.Author.ToString(),
usrMsg.Author.Id);
return Task.FromResult(true);
}
} }
return Task.FromResult(false); return Task.FromResult(false);
} }
public async Task<IReadOnlyList<BlacklistEntry>> GetBlacklist(BlacklistType type) public IReadOnlyList<BlacklistEntry> GetBlacklist()
=> blacklist;
public void Reload(bool publish = true)
{ {
await using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
var toPublish = uow.GetTable<BlacklistEntry>().ToArray();
return await uow blacklist = toPublish;
.GetTable<BlacklistEntry>()
.Where(x => x.Type == type)
.ToListAsync();
}
public async Task Reload(bool publish = true)
{
var totalShards = _creds.TotalShards;
await using var uow = _db.GetDbContext();
var items = uow.GetTable<BlacklistEntry>()
.Where(x => x.Type != BlacklistType.Server
|| (x.Type == BlacklistType.Server
&& Linq2DbExpressions.GuildOnShard(x.ItemId, totalShards, _client.ShardId)))
.ToArray();
if (publish) if (publish)
{ _pubSub.Pub(_blPubKey, toPublish);
await _pubSub.Pub(_blPubKey, true);
}
await OnReload(items);
} }
public async Task Blacklist(BlacklistType type, ulong id) public async Task Blacklist(BlacklistType type, ulong id)
@ -133,34 +88,34 @@ public sealed class BlacklistService : IExecOnMessage, IReadyExecutor
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow await uow
.GetTable<BlacklistEntry>() .GetTable<BlacklistEntry>()
.InsertAsync(() => new() .InsertAsync(() => new()
{ {
ItemId = id, ItemId = id,
Type = type, Type = type,
}); });
if (type == BlacklistType.User) if (type == BlacklistType.User)
{ {
await uow.GetTable<DiscordUser>() await uow.GetTable<DiscordUser>()
.Where(x => x.UserId == id) .Where(x => x.UserId == id)
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
CurrencyAmount = 0 CurrencyAmount = 0
}); });
} }
await Reload(); Reload();
} }
public async Task UnBlacklist(BlacklistType type, ulong id) public async Task UnBlacklist(BlacklistType type, ulong id)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<BlacklistEntry>() await uow.GetTable<BlacklistEntry>()
.Where(bi => bi.ItemId == id && bi.Type == type) .Where(bi => bi.ItemId == id && bi.Type == type)
.DeleteAsync(); .DeleteAsync();
await Reload(); Reload();
} }
public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist) public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
@ -175,12 +130,12 @@ public sealed class BlacklistService : IExecOnMessage, IReadyExecutor
var blList = toBlacklist.ToList(); var blList = toBlacklist.ToList();
await uow.GetTable<DiscordUser>() await uow.GetTable<DiscordUser>()
.Where(x => blList.Contains(x.UserId)) .Where(x => blList.Contains(x.UserId))
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
CurrencyAmount = 0 CurrencyAmount = 0
}); });
await Reload(); Reload();
} }
} }

View file

@ -27,22 +27,5 @@ public sealed class ImagesConfig : ConfigServiceBase<ImageUrls>
c.Version = 5; c.Version = 5;
}); });
} }
if (data.Version < 6)
{
ModifyConfig(c =>
{
if (c.Slots.Emojis?.Length == 6)
{
c.Slots.Emojis =
[
new("https://cdn.nadeko.bot/slots/15.png"),
..c.Slots.Emojis
];
}
c.Version = 6;
});
}
} }
} }

View file

@ -1,4 +1,5 @@
using System.Text.RegularExpressions; #nullable disable
using System.Text.RegularExpressions;
namespace EllieBot.Common.TypeReaders.Models; namespace EllieBot.Common.TypeReaders.Models;
@ -8,8 +9,8 @@ public class StoopidTime
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$", @"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
RegexOptions.Compiled | RegexOptions.Multiline); RegexOptions.Compiled | RegexOptions.Multiline);
public string Input { get; set; } = string.Empty; public string Input { get; set; }
public TimeSpan Time { get; set; } = default; public TimeSpan Time { get; set; }
private StoopidTime() { } private StoopidTime() { }
@ -52,8 +53,8 @@ public class StoopidTime
}; };
} }
public static implicit operator TimeSpan?(StoopidTime? st) public static implicit operator TimeSpan(StoopidTime st)
=> st?.Time; => st.Time;
public static implicit operator StoopidTime(TimeSpan ts) public static implicit operator StoopidTime(TimeSpan ts)
=> new() => new()

View file

@ -1,49 +0,0 @@
# WORK IN PROGRESS
# Define the folders to search for designer.cs files
$folders = @("Migrations/PostgreSql", "Migrations/Sqlite")
# Loop through each folder
foreach ($folder in $folders) {
# Get all designer.cs files in the folder and subfolders
$files = Get-ChildItem -Path $folder -Filter *.designer.cs -Recurse
$excludedPattern = "cleanup|mysql-init|squash|rero-cascade"
$filteredFiles = $files | Where-Object { $_.Name -notmatch $excludedPattern }
# Loop through each file
foreach ($file in ($files | Where-Object { $_.Name -notmatch $excludedPattern })) {
# Read the contents of the file
$content = Get-Content -Path $file.FullName | Select-Object -First 30
# Find the attribute lines
$attributes = $content | Where-Object { $_ -match '\[.*\]' } | ForEach-Object { ' ' + $_.Trim() }
# Find the namespace
$namespace = $content | Where-Object { $_ -match 'namespace' } | ForEach-Object { $_.Split(' ')[1] }
# Find the class name
$class_name = $content | Where-Object { $_ -match 'partial class' } | ForEach-Object { $_.Trim().Split(' ')[2] }
# Replace the contents with the new template
$new_content = @"
// <auto-generated />
using EllieBot.Db;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace $namespace
{
$($attributes -join "`n")
partial class $class_name
{
}
}
"@
# Write the new contents to the file
Set-Content -Path $file.FullName -Value $new_content
}
}

View file

@ -863,11 +863,6 @@ affinity:
waifuclaim: waifuclaim:
- waifuclaim - waifuclaim
- claim - claim
- wc
waifuclaims:
- waifuclaims
- claims
- wcs
waifureset: waifureset:
- waifureset - waifureset
waifutransfer: waifutransfer:
@ -1459,17 +1454,3 @@ translateflags:
- trfl - trfl
- fltr - fltr
- transflags - transflags
rakeback:
- rakeback
- rb
betstatsreset:
- betstatsreset
- bsr
- bsreset
gamblestatsreset:
- gamblestatsreset
- gsr
- gsreset
snipe:
- snipe
- sn

View file

@ -2756,6 +2756,37 @@
} }
], ],
"Gambling": [ "Gambling": [
{
"Aliases": [
".betstats",
".bs"
],
"Description": "Shows the current bet stats for yourself, or the targetted user.\nYou may optionally specify the game to show stats for.\nSupported games right now are: bf, br, bd, lula, slot, race",
"Usage": [
".betstats",
".betstats @someone",
".betstats @someone lula",
".betstats bd"
],
"Submodule": "Gambling",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".gamblestats",
".gs"
],
"Description": "Shows the total stats of several gambling features.\nUpdates once an hour.",
"Usage": [
".gamblestats"
],
"Submodule": "Gambling",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".timely" ".timely"
@ -2999,20 +3030,6 @@
"Bot Owner Only" "Bot Owner Only"
] ]
}, },
{
"Aliases": [
".rakeback",
".rb"
],
"Description": "Try to claim any rakeback that you have available.\nRakeback is accumulated by betting (not by winning or losing).\nDefault rakeback is 0.05 * house edge\nHouse edge is defined per game",
"Usage": [
".rakeback"
],
"Submodule": "Gambling",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".race" ".race"
@ -3120,70 +3137,6 @@
"Bot Owner Only" "Bot Owner Only"
] ]
}, },
{
"Aliases": [
".betstatsreset",
".bsr",
".bsreset"
],
"Description": "Reset all of your Bet Stats for a fee.\nYou can alternatively reset Bet Stats for the specified game.",
"Usage": [
".betstatsreset",
".betstatsreset game"
],
"Submodule": "BetStatsCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".betstats",
".bs"
],
"Description": "Shows the current bet stats for yourself, or the targetted user.\nYou may optionally specify the game to show stats for.\nSupported games right now are: bf, br, bd, lula, slot, race",
"Usage": [
".betstats",
".betstats @someone",
".betstats @someone lula",
".betstats bd"
],
"Submodule": "BetStatsCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".gamblestats",
".gs"
],
"Description": "Shows the total stats of several gambling features.\nUpdates once an hour.",
"Usage": [
".gamblestats"
],
"Submodule": "BetStatsCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".gamblestatsreset",
".gsr",
".gsreset"
],
"Description": "Resets the gamble stats.",
"Usage": [
".gamblestatsreset"
],
"Submodule": "BetStatsCommands",
"Module": "Gambling",
"Options": null,
"Requirements": [
"Bot Owner Only"
]
},
{ {
"Aliases": [ "Aliases": [
".blackjack", ".blackjack",
@ -3648,25 +3601,10 @@
"Options": null, "Options": null,
"Requirements": [] "Requirements": []
}, },
{
"Aliases": [
".waifuclaims",
".claims",
".wcs"
],
"Description": "Shows all of your currently claimed waifus.",
"Usage": [
".waifuclaims"
],
"Submodule": "WaifuClaimCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".waifuclaim", ".waifuclaim",
".claim - wc" ".claim"
], ],
"Description": "Claim a waifu for yourself by spending currency. You must spend at least 10% more than her current value unless she set `.affinity` towards you.", "Description": "Claim a waifu for yourself by spending currency. You must spend at least 10% more than her current value unless she set `.affinity` towards you.",
"Usage": [ "Usage": [
@ -6606,20 +6544,6 @@
"No Public Bot" "No Public Bot"
] ]
}, },
{
"Aliases": [
".snipe",
".sn"
],
"Description": "Snipe the message you replied to with this command.\nOtherwise, if you don't reply to a message, it will snipe the last message sent in the channel (out of the last few messages) which has text or an image.",
"Usage": [
".snipe"
],
"Submodule": "Utility",
"Module": "Utility",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".prompt" ".prompt"

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 12 version: 11
# Currency settings # Currency settings
currency: currency:
# What is the emoji/character which represents the currency # What is the emoji/character which represents the currency
@ -28,7 +28,7 @@ betRoll:
multiplyBy: 10 multiplyBy: 10
- whenAbove: 90 - whenAbove: 90
multiplyBy: 4 multiplyBy: 4
- whenAbove: 65 - whenAbove: 66
multiplyBy: 2 multiplyBy: 2
# Automatic currency generation settings. # Automatic currency generation settings.
generation: generation:
@ -85,7 +85,7 @@ luckyLadder:
- 2.4 - 2.4
- 1.7 - 1.7
- 1.5 - 1.5
- 1.1 - 1.2
- 0.5 - 0.5
- 0.3 - 0.3
- 0.2 - 0.2

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 6 version: 5
coins: coins:
heads: heads:
- https://cdn.nadeko.bot/coins/heads3.png - https://cdn.nadeko.bot/coins/heads3.png
@ -22,13 +22,15 @@ dice:
- https://cdn.nadeko.bot/other/dice/9.png - https://cdn.nadeko.bot/other/dice/9.png
xp: xp:
bg: https://cdn.nadeko.bot/other/xp/bg_k.png bg: https://cdn.nadeko.bot/other/xp/bg_k.png
rip:
bg: https://cdn.nadeko.bot/other/rip/rip.png
overlay: https://cdn.nadeko.bot/other/rip/overlay.png
slots: slots:
emojis: emojis:
- https://cdn.nadeko.bot/slots/10.png - https://cdn.nadeko.bot/slots/0.png
- https://cdn.nadeko.bot/slots/11.png - https://cdn.nadeko.bot/slots/1.png
- https://cdn.nadeko.bot/slots/12.png - https://cdn.nadeko.bot/slots/2.png
- https://cdn.nadeko.bot/slots/13.png - https://cdn.nadeko.bot/slots/3.png
- https://cdn.nadeko.bot/slots/14.png - https://cdn.nadeko.bot/slots/4.png
- https://cdn.nadeko.bot/slots/15.png - https://cdn.nadeko.bot/slots/5.png
- https://cdn.nadeko.bot/slots/16.png
bg: https://cdn.nadeko.bot/slots/slots_bg.png bg: https://cdn.nadeko.bot/slots/slots_bg.png

View file

@ -1517,7 +1517,7 @@ take:
betroll: betroll:
desc: |- desc: |-
Bets the specified amount of currency and rolls a dice. Bets the specified amount of currency and rolls a dice.
Rolling over 65 yields x2 of your currency, over 90 - x4 and 100 x10. Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10.
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance. You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 5 - 5
@ -2711,13 +2711,6 @@ gamblestats:
- '' - ''
params: params:
- { } - { }
gamblestatsreset:
desc: |-
Resets the gamble stats.
ex:
- ''
params:
- { }
slot: slot:
desc: |- desc: |-
Play Ellie slots by placing your bet. Play Ellie slots by placing your bet.
@ -2745,12 +2738,6 @@ waifuclaim:
desc: "The cost of claiming the waifu." desc: "The cost of claiming the waifu."
target: target:
desc: "The user to whom the claim is being made, allowing the waifu to be claimed from their collection." desc: "The user to whom the claim is being made, allowing the waifu to be claimed from their collection."
waifuclaims:
desc: Shows all of your currently claimed waifus.
ex:
- ''
params:
- { }
waifureset: waifureset:
desc: Resets your waifu stats, except current waifus. desc: Resets your waifu stats, except current waifus.
ex: ex:
@ -4655,16 +4642,6 @@ translateflags:
- '' - ''
params: params:
- { } - { }
betstatsreset:
desc: |-
Reset all of your Bet Stats for a fee.
You can alternatively reset Bet Stats for the specified game.
ex:
- ''
- 'game'
params:
- game:
desc: 'The game to reset betstats for. Omit to reset all games'
betstats: betstats:
desc: |- desc: |-
Shows the current bet stats for yourself, or the targetted user. Shows the current bet stats for yourself, or the targetted user.
@ -4685,21 +4662,3 @@ betstats:
desc: 'The game to show betstats for. Omit to show betstats for all games combined' desc: 'The game to show betstats for. Omit to show betstats for all games combined'
- game: - game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined' desc: 'The game to show betstats for. Omit to show betstats for all games combined'
rakeback:
desc: |-
Try to claim any rakeback that you have available.
Rakeback is accumulated by betting (not by winning or losing).
Default rakeback is 0.05 * house edge
House edge is defined per game
ex:
- ''
params:
- {}
snipe:
desc: |-
Snipe the message you replied to with this command.
Otherwise, if you don't reply to a message, it will snipe the last message sent in the channel (out of the last few messages) which has text or an image.
ex:
- ''
params:
- { }

View file

@ -1114,9 +1114,5 @@
"invalid_img_size": "Image must to be {0}x{1} pixels.", "invalid_img_size": "Image must to be {0}x{1} pixels.",
"no_attach_found": "No attachment found. Please send the image along with this command.", "no_attach_found": "No attachment found. Please send the image along with this command.",
"trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.", "trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.",
"trfl_disabled": "Flag translation disabled.", "trfl_disabled": "Flag translation disabled."
"rakeback_claimed": "You've claimed {0} as rakeback!",
"rakeback_none": "You don't have any rakeback to claim yet.",
"rakeback_available": "You have {0} rakeback available. Click the button to claim.",
"sniped_by": "Sniped by {0}"
} }