Compare commits

...

29 commits
5.1.18 ... v5

Author SHA1 Message Date
af71e88985
fixed time conversion 2024-11-22 20:47:43 +13:00
4af3a9086f
re-added a missing extension method 2024-11-18 17:21:00 +13:00
2d806119b4
fixed nullref in blacklist 2024-11-18 16:55:56 +13:00
f68ff81dce
.bsreset price reduced 10x 2024-11-13 18:56:05 +13:00
3e5bccd215
fixed update usernames query 2024-11-13 18:54:55 +13:00
e851c01c91
fixed command description for betroll 2024-11-13 18:53:12 +13:00
b81de1f103
fixed newline missing in .timely 2024-11-13 18:20:54 +13:00
d0ecff7429
updated commandlist, version upped to 5.1.20
Changelog updated
2024-11-13 00:31:35 +13:00
21572aad19
fixed typo 2024-11-12 22:02:13 +13:00
0b3582e476
added .snipe command
added .gsreset and .bsreset commands
improved .timely rewards for patrons
Improved how blacklist works under the hood
2024-11-09 18:41:00 +13:00
2fe1b94cea
rich logging fix for userids, updated commandlist
added bot.date and changed bot.time placeholder. They use timestamp tags now.
fixed double log on server leave
2024-11-07 19:22:18 +13:00
e40a458dc5
patreon reward bonuses increased slightly 2024-11-07 19:12:00 +13:00
11ed2aaba8
.divorce no longer has a cooldown
Added .waifuclaims / .claims command which lists your waifus (name, price and ids)
Timely now shows patreon multiplier bonus if there is any, (alongside boost)
2024-11-07 19:04:47 +13:00
04a22e5995
Fixed missing example in commands 2024-11-07 18:35:51 +13:00
6c38d803bc
Merge branch 'v5' of https://toastielab.dev/Emotions-stuff/elliebot into v5 2024-11-07 18:29:14 +13:00
66870f6859
added .rakeback to get a part of the house edge back. Rakeback is accumulated by betting (not winning or losing in particular). All games have manually specified rakeback values
slot now has 1 more icon (wheat!), and multipliers have been modified to even out the gains
betroll is improved (around 2% better payout), as 66 is now a winning number, not a losing one
2024-11-07 18:28:18 +13:00
a8bb7e650e Update CHANGELOG.md 2024-11-05 01:34:32 -08:00
14ac3c92bb
Upped version to 5.1.19, updated changelog 2024-11-05 21:12:21 +13:00
fae15a9e0a
Fixed timely on different shards
.race will now have 82-94% payout rate based on the number of players playign (1-12, x0.01 per player). Any player over 12 won't increase payout
2024-11-05 21:11:30 +13:00
e7cfd3a752
timely now has an option in gambling whether to use no protection, captcha, or button.
grpc api fix
2024-11-05 20:38:37 +13:00
c5aeb43046
timely fixes 2024-11-05 20:20:44 +13:00
9f44d6a854
added missing strings 2024-11-05 19:40:19 +13:00
7da8f2c403
added timely boost bonus to gambling.yml
.betstats renamed to .gamblestats/.gs
added .betstats, .betstats <game> and .betstats <user> <game?> command which shows you your stats for gambling commands
2024-11-05 16:11:05 +13:00
39297c6f83
fixed pagination numbers in xplb and xpglb 2024-11-04 19:27:16 +13:00
fca083a8fe
strikeout slightly thinner to make password easier to read on plants 2024-11-04 19:24:53 +13:00
40a71c1134
Added nordic and ugro finnic languages to flag translate 2024-11-04 19:23:58 +13:00
dee2b04dbb
fix timely 2024-11-04 19:23:03 +13:00
ed14c8ce7e
possible fix for patron table 2024-11-04 19:22:13 +13:00
090f50b253
Fixed UserId patron table error
Added au and kz countries as en and kz languages respectively
Strikeout is thinner now on plants
2024-11-04 19:19:53 +13:00
69 changed files with 21920 additions and 6648 deletions

View file

@ -2,6 +2,51 @@
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
### Added
- Added `.betstats`
- See your own stats with .betstats
- Target someone else: .betstats @mai_lanfiel
- You can also specify a game .betstats lula
- Or both! .betstats mai_lanfiel br
- `.timely` can now have a server boost bonus
- 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
### Changed
- `.plant/pick` password font size will be slightly bigger
- `.race` will now have 82-94% payout rate based on the number of players playing (1-12, x0.01 per player).
- Any player over 12 won't increase payout
### Fixed
- `.xplb` and `.xpglb` now have proper ranks after page 1
- Fixed boost bonus on shards different than the specified servers' shard
## [5.1.18] - 04.11.2024
### Added

View file

@ -11,9 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Dockerfile = Dockerfile
ellie-menu.ps1 = ellie-menu.ps1
LICENSE = LICENSE
migrate.ps1 = migrate.ps1
README.md = README.md
remove-migrations.ps1 = remove-migrations.ps1
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{4D9001F7-B3E8-48FE-97AA-CFD36DA65A64}"
@ -30,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Elli
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -1,8 +0,0 @@
if ($args.Length -eq 0) {
Write-Host "Please provide a migration name." -ForegroundColor Red
}
else {
$migrationName = $args[0]
dotnet ef migrations add $migrationName -o Migrations/Mysql -c SqliteContext -p src/EllieBot/EllieBot.csproj
dotnet ef migrations add $migrationName -o Migrations/PostgreSql -c PostgreSqlContext -p src/EllieBot/EllieBot.csproj
}

View file

@ -15,6 +15,17 @@ service GrpcXp {
rpc AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
rpc SetServerExclusion(SetServerExclusionRequest) returns (SetServerExclusionReply);
}
message SetServerExclusionRequest {
uint64 guildId = 1;
bool serverExcluded = 2;
}
message SetServerExclusionReply {
bool success = 1;
}
message GetXpLbRequest {
@ -32,7 +43,8 @@ message XpLbUserReply {
string username = 2;
int64 xp = 3;
int64 level = 4;
string avatar = 5;
int64 levelPercent = 5;
string avatar = 6;
}
message ResetUserXpRequest {

View file

@ -62,6 +62,7 @@ public abstract class EllieContext : DbContext
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// public DbSet<GuildColors> GuildColors { get; set; }
@ -73,6 +74,22 @@ public abstract class EllieContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region Rakeback
modelBuilder.Entity<Rakeback>()
.HasKey(x => x.UserId);
#endregion
#region UserBetStats
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new { x.UserId, x.Game })
.IsUnique();
#endregion
#region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>()
@ -307,10 +324,10 @@ public abstract class EllieContext : DbContext
var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
selfassignableRolesEntity.HasIndex(s => new
{
s.GuildId,
s.RoleId
})
{
s.GuildId,
s.RoleId
})
.IsUnique();
selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
@ -384,10 +401,10 @@ public abstract class EllieContext : DbContext
var xps = modelBuilder.Entity<UserXpStats>();
xps.HasIndex(x => new
{
x.UserId,
x.GuildId
})
{
x.UserId,
x.GuildId
})
.IsUnique();
xps.HasIndex(x => x.UserId);
@ -433,9 +450,9 @@ public abstract class EllieContext : DbContext
.OnDelete(DeleteBehavior.SetNull);
ci.HasIndex(x => new
{
x.Name
})
{
x.Name
})
.IsUnique();
#endregion
@ -554,10 +571,10 @@ public abstract class EllieContext : DbContext
.IsUnique(false);
rr2.HasIndex(x => new
{
x.MessageId,
x.Emote
})
{
x.MessageId,
x.Emote
})
.IsUnique();
});
@ -632,11 +649,11 @@ public abstract class EllieContext : DbContext
{
// user can own only one of each item
x.HasIndex(model => new
{
model.UserId,
model.ItemType,
model.ItemKey
})
{
model.UserId,
model.ItemType,
model.ItemKey
})
.IsUnique();
});
@ -661,10 +678,10 @@ public abstract class EllieContext : DbContext
#region Sticky Roles
modelBuilder.Entity<StickyRole>(sr => sr.HasIndex(x => new
{
x.GuildId,
x.UserId
})
{
x.GuildId,
x.UserId
})
.IsUnique());
#endregion
@ -709,10 +726,10 @@ public abstract class EllieContext : DbContext
modelBuilder
.Entity<GreetSettings>(gs => gs.HasIndex(x => new
{
x.GuildId,
x.GreetType
})
{
x.GuildId,
x.GreetType
})
.IsUnique());
modelBuilder.Entity<GreetSettings>(gs =>

View file

@ -1,8 +1,12 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
namespace EllieBot.Db.Models;
public class PatronUser
{
// [Key]
// public int Id { get; set; }
public string UniquePlatformUserId { get; set; }
public ulong UserId { get; set; }
public int AmountCents { get; set; }

View file

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

View file

@ -7,7 +7,7 @@ public static class MigrationQueries
{
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)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class betstats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "userbetstats",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
game = table.Column<int>(type: "integer", nullable: false),
wincount = table.Column<long>(type: "bigint", nullable: false),
losecount = table.Column<long>(type: "bigint", nullable: false),
totalbet = table.Column<decimal>(type: "numeric", nullable: false),
paidout = table.Column<decimal>(type: "numeric", nullable: false),
maxwin = table.Column<long>(type: "bigint", nullable: false),
maxbet = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_userbetstats", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_userbetstats_userid_game",
table: "userbetstats",
columns: new[] { "userid", "game" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "userbetstats");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
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");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class betstats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "UserBetStats",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Game = table.Column<int>(type: "INTEGER", nullable: false),
WinCount = table.Column<long>(type: "INTEGER", nullable: false),
LoseCount = table.Column<long>(type: "INTEGER", nullable: false),
TotalBet = table.Column<decimal>(type: "TEXT", nullable: false),
PaidOut = table.Column<decimal>(type: "TEXT", nullable: false),
MaxWin = table.Column<long>(type: "INTEGER", nullable: false),
MaxBet = table.Column<long>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_UserBetStats", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_UserBetStats_UserId_Game",
table: "UserBetStats",
columns: new[] { "UserId", "Game" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "UserBetStats");
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,34 @@
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

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

View file

@ -6,6 +6,10 @@ namespace EllieBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable
{
public const double BASE_MULTIPLIER = 0.82;
public const double MAX_MULTIPLIER = 0.94;
public const double MULTI_PER_USER = 0.01;
public enum Phase
{
WaitingForPlayers,
@ -100,7 +104,7 @@ public sealed class AnimalRace : IDisposable
foreach (var user in _users)
{
if (user.Bet > 0)
await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund"));
await _currency.AddAsync(user.UserId, (long)(user.Bet + BASE_MULTIPLIER), new("animalrace", "refund"));
}
_ = OnStartingFailed?.Invoke(this);
@ -131,8 +135,10 @@ public sealed class AnimalRace : IDisposable
if (FinishedUsers[0].Bet > 0)
{
Multi = FinishedUsers.Count
* Math.Min(MAX_MULTIPLIER, BASE_MULTIPLIER + (MULTI_PER_USER * FinishedUsers.Count));
await _currency.AddAsync(FinishedUsers[0].UserId,
FinishedUsers[0].Bet * (_users.Count - 1),
(long)(FinishedUsers[0].Bet * Multi),
new("animalrace", "win"));
}
@ -140,6 +146,8 @@ public sealed class AnimalRace : IDisposable
});
}
public double Multi { get; set; } = BASE_MULTIPLIER;
public void Dispose()
{
CurrentPhase = Phase.Ended;

View file

@ -12,7 +12,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling
{
[Group]
public partial class AnimalRacingCommands : GamblingSubmodule<AnimalRaceService>
public partial class AnimalRacingCommands : GamblingModule<AnimalRaceService>
{
private readonly ICurrencyService _cs;
private readonly DiscordSocketClient _client;
@ -74,10 +74,14 @@ public partial class Gambling
if (race.FinishedUsers[0].Bet > 0)
{
return Response()
.Confirm(GetText(strs.animal_race),
GetText(strs.animal_race_won_money(Format.Bold(winner.Username),
winner.Animal.Icon,
(race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign)))
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.animal_race))
.WithDescription(GetText(strs.animal_race_won_money(
Format.Bold(winner.Username),
winner.Animal.Icon,
N(race.FinishedUsers[0].Bet * race.Multi))))
.WithFooter($"x{race.Multi:F2}"))
.SendAsync();
}
@ -113,14 +117,14 @@ public partial class Gambling
private async Task Ar_OnStateUpdate(AnimalRace race)
{
var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|
var text = $@"|🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁🔚|
{string.Join("\n", race.Users.Select(p =>
{
var index = race.FinishedUsers.IndexOf(p);
var extra = index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}";
return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}";
}))}
|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|";
|🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁🔚|";
var msg = raceMessage;
@ -129,10 +133,10 @@ public partial class Gambling
else
{
await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.animal_race))
.WithDescription(text)
.WithOkColor()
.Build());
.WithTitle(GetText(strs.animal_race))
.WithDescription(text)
.WithOkColor()
.Build());
}
}

View file

@ -0,0 +1,175 @@
#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 BlackJackCommands : GamblingSubmodule<BlackJackService>
public partial class BlackJackCommands : GamblingModule<BlackJackService>
{
public enum BjAction
{

View file

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

View file

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

View file

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

View file

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

View file

@ -14,6 +14,13 @@ using System.Text;
using EllieBot.Modules.Gambling.Rps;
using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Patronage;
using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
namespace EllieBot.Modules.Gambling;
@ -31,6 +38,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker;
private readonly IPatronageService _ps;
private readonly RakebackService _rb;
public Gambling(
IGamblingService gs,
@ -43,7 +51,8 @@ public partial class Gambling : GamblingModule<GamblingService>
IBankService bank,
IRemindService remind,
IPatronageService patronage,
GamblingTxTracker gamblingTxTracker)
GamblingTxTracker gamblingTxTracker,
RakebackService rb)
: base(configService)
{
_gs = gs;
@ -53,6 +62,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_bank = bank;
_remind = remind;
_gamblingTxTracker = gamblingTxTracker;
_rb = rb;
_ps = patronage;
_rng = new EllieRandom();
@ -70,42 +80,6 @@ public partial class Gambling : GamblingModule<GamblingService>
return N(bal);
}
[Cmd]
public async Task BetStats()
{
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)
{
@ -153,10 +127,12 @@ public partial class Gambling : GamblingModule<GamblingService>
customId: "timely:" + _rng.Next(123456, 999999)),
async (smc) =>
{
await smc.DeferAsync();
await ClaimTimely();
});
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timely()
{
var val = Config.Timely.Amount;
@ -167,62 +143,64 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
if (Config.Timely.RequirePassword)
if (Config.Timely.ProtType == TimelyProt.Button)
{
// var password = _service.GeneratePassword();
//
// var img = new Image<Rgba32>(100, 40);
//
// var font = _fonts.NotoSans.CreateFont(30);
// var outlinePen = new SolidPen(Color.Black, 1f);
// var strikeoutRun = new RichTextRun
// {
// Start = 0,
// End = password.GetGraphemeCount(),
// Font = font,
// StrikeoutPen = new SolidPen(Color.White, 3),
// TextDecorations = TextDecorations.Strikeout
// };
// // draw password on the image
// img.Mutate(x =>
// {
// x.DrawText(new RichTextOptions(font)
// {
// HorizontalAlignment = HorizontalAlignment.Center,
// VerticalAlignment = VerticalAlignment.Center,
// FallbackFontFamilies = _fonts.FallBackFonts,
// Origin = new(50, 20),
// TextRuns = [strikeoutRun]
// },
// password,
// Brushes.Solid(Color.White),
// outlinePen);
// });
// using var stream = await img.ToStreamAsync();
// var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
// .File(stream, "timely.png")
// .SendAsync();
// try
// {
// var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
// if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
// {
// return;
// }
// }
// finally
// {
// _ = captcha.DeleteAsync();
// }
var interaction = CreateTimelyInteraction();
var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync();
await msg.DeleteAsync();
return;
}
else if (Config.Timely.ProtType == TimelyProt.Captcha)
{
var password = _service.GeneratePassword();
var img = new Image<Rgba32>(70, 35);
var font = _fonts.NotoSans.CreateFont(30);
var outlinePen = new SolidPen(Color.Black, 1f);
var strikeoutRun = new RichTextRun
{
Start = 0,
End = password.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 3),
TextDecorations = TextDecorations.Strikeout
};
// draw password on the image
img.Mutate(x =>
{
x.DrawText(new RichTextOptions(font)
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(35, 17),
TextRuns = [strikeoutRun]
},
password,
Brushes.Solid(Color.White),
outlinePen);
});
using var stream = await img.ToStreamAsync();
var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
.File(stream, "timely.png")
.SendAsync();
try
{
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
{
return;
}
}
finally
{
_ = captcha.DeleteAsync();
}
}
await ClaimTimely();
}
@ -249,6 +227,29 @@ public partial class Gambling : GamblingModule<GamblingService>
var val = Config.Timely.Amount;
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
var guildUsers = await boostGuilds
.Select(async gid =>
{
try
{
var guild = await _client.Rest.GetGuildAsync(gid, false);
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
return (guild, user);
}
catch
{
return default;
}
})
.WhenAll();
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
var booster = userInfo != default;
if (booster)
val += Config.BoostBonus.BaseTimelyBonus;
var patron = await _ps.GetPatronAsync(ctx.User.Id);
var percentBonus = (_ps.PercentBonus(patron) / 100f);
@ -259,7 +260,21 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
var msg = GetText(strs.timely(N(val), period));
if (booster || percentBonus > float.Epsilon)
{
msg += "\n\n";
if (booster)
msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
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();
}
else
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
}
[Cmd]
@ -370,8 +385,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithTitle(GetText(strs.transactions(
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithOkColor();
var sb = new StringBuilder();
@ -627,7 +643,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await Response().Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign)).SendAsync();
await Response()
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
.SendAsync();
}
}
@ -648,7 +666,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await Response().Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign)).SendAsync();
await Response()
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
.SendAsync();
}
}
@ -982,4 +1002,45 @@ public partial class Gambling : GamblingModule<GamblingService>
footer: $"Total Bet: {tests} | Payout: {payout:F0} | {payout * 1.0M / tests * 100}%")
.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>
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 9;
public int Version { get; set; } = 12;
[Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; }
@ -67,6 +67,11 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
[Comment("""Slot config""")]
public SlotsConfig Slots { get; set; }
[Comment("""
Bonus config for server boosts
""")]
public BoostBonusConfig BoostBonus { get; set; }
public GamblingConfig()
{
BetRoll = new();
@ -79,6 +84,7 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
Slots = new();
LuckyLadder = new();
BotCuts = new();
BoostBonus = new();
}
}
@ -104,7 +110,7 @@ public partial class TimelyConfig
How much currency will the users get every time they run .timely command
setting to 0 or less will disable this feature
""")]
public int Amount { get; set; } = 0;
public long Amount { get; set; } = 0;
[Comment("""
How often (in hours) can users claim currency with .timely command
@ -113,9 +119,17 @@ public partial class TimelyConfig
public int Cooldown { get; set; } = 24;
[Comment("""
Whether the users are required to type a password when they do timely.
How will timely be protected?
None, Button (users have to click the button) or Captcha (users have to type the captcha from an image)
""")]
public bool RequirePassword { get; set; } = true;
public TimelyProt ProtType { get; set; } = TimelyProt.Button;
}
public enum TimelyProt
{
None,
Button,
Captcha
}
[Cloneable]
@ -150,7 +164,7 @@ public partial class BetRollConfig
},
new()
{
WhenAbove = 66,
WhenAbove = 65,
MultiplyBy = 2
}
];
@ -212,7 +226,7 @@ public partial class LuckyLadderSettings
public decimal[] Multipliers { get; set; }
public LuckyLadderSettings()
=> Multipliers = [2.4M, 1.7M, 1.5M, 1.2M, 0.5M, 0.3M, 0.2M, 0.1M];
=> Multipliers = [2.4M, 1.7M, 1.5M, 1.1M, 0.5M, 0.3M, 0.2M, 0.1M];
}
[Cloneable]
@ -414,3 +428,14 @@ public sealed partial class BotCutConfig
""")]
public decimal ShopSaleCut { get; set; } = 0.1m;
}
[Cloneable]
public sealed partial class BoostBonusConfig
{
[Comment("Users will receive a bonus if they boost any of these servers")]
public List<ulong> GuildIds { get; set; } = new();
[Comment("This bonus will be added before any other multiplier is applied to the .timely command")]
public long BaseTimelyBonus { get; set; } = 50;
}

View file

@ -144,9 +144,9 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
ConfigPrinters.ToString,
val => val >= 0);
AddParsedProp("timely.pass",
gs => gs.Timely.RequirePassword,
bool.TryParse,
AddParsedProp("timely.prot",
gs => gs.Timely.ProtType,
ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString);
Migrate();
@ -189,11 +189,16 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
});
}
if (data.Version < 9)
if (data.Version < 12)
{
ModifyConfig(c =>
{
c.Version = 9;
c.Version = 12;
if (c.BetRoll.Pairs.Length == 3 && c.BetRoll.Pairs[2].WhenAbove == 66)
{
c.BetRoll.Pairs[2].WhenAbove = 65;
}
});
}
}

View file

@ -58,11 +58,3 @@ public abstract class GamblingModule<TService> : EllieModule<TService>
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
{
[Group]
public partial class PlantPickCommands : GamblingSubmodule<PlantPickService>
public partial class PlantPickCommands : GamblingModule<PlantPickService>
{
private readonly ILogCommandService _logService;

View file

@ -147,7 +147,7 @@ public class PlantPickService : IEService, IExecNoCommand
pass = pass.TrimTo(10, true).ToLowerInvariant();
using var img = Image.Load<Rgba32>(curImg);
// choose font size based on the image height, so that it's visible
var font = _fonts.NotoSans.CreateFont(img.Height / 12.0f, FontStyle.Bold);
var font = _fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold);
img.Mutate(x =>
{
// measure the size of the text to be drawing
@ -159,7 +159,7 @@ public class PlantPickService : IEService, IExecNoCommand
// fill the background with black, add 5 pixels on each side to make it look better
x.FillPolygon(Color.ParseHex("00000080"),
new PointF(0, 0),
new PointF(1, 1),
new PointF(size.Width + 5, 0),
new PointF(size.Width + 5, size.Height + 10),
new PointF(0, size.Height + 10));
@ -169,7 +169,7 @@ public class PlantPickService : IEService, IExecNoCommand
Start = 0,
End = pass.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 5),
StrikeoutPen = new SolidPen(Color.White, 2),
TextDecorations = TextDecorations.Strikeout
};

View file

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

View file

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

View file

@ -0,0 +1,55 @@
#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
{
[Group]
public partial class WaifuClaimCommands : GamblingSubmodule<WaifuService>
public partial class WaifuClaimCommands : GamblingModule<WaifuService>
{
public WaifuClaimCommands(GamblingConfigService gamblingConfService)
: base(gamblingConfService)
@ -37,6 +37,45 @@ public partial class Gambling
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]
[RequireContext(ContextType.Guild)]
public async Task WaifuClaim(long amount, [Leftover] IUser target)
@ -144,7 +183,7 @@ public partial class Gambling
if (targetId == ctx.User.Id)
return;
var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId);
var (w, result, amount) = await _service.DivorceWaifuAsync(ctx.User, targetId);
if (result == DivorceResult.SucessWithPenalty)
{
@ -157,14 +196,6 @@ public partial class Gambling
await Response().Confirm(strs.waifu_divorced_notlike(N(amount))).SendAsync();
else if (result == DivorceResult.NotYourWife)
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]

View file

@ -318,25 +318,20 @@ public class WaifuService : IEService, IReadyExecutor
private static TypedKey<long> GetAffinityKey(ulong userId)
=> new($"waifu:affinity:{userId}");
public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId)
public async Task<(WaifuInfo, DivorceResult, long)> DivorceWaifuAsync(IUser user, ulong targetId)
{
DivorceResult result;
TimeSpan? remaining = null;
long amount = 0;
WaifuInfo w;
await using (var uow = _db.GetDbContext())
{
w = uow.Set<WaifuInfo>().ByWaifuUserId(targetId);
if (w?.Claimer is null || w.Claimer.UserId != user.Id)
{
result = DivorceResult.NotYourWife;
}
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;
@ -369,7 +364,7 @@ public class WaifuService : IEService, IReadyExecutor
await uow.SaveChangesAsync();
}
return (w, result, amount, remaining);
return (w, result, amount);
}
public async Task<bool> GiftWaifuAsync(
@ -630,4 +625,38 @@ public class WaifuService : IEService, IReadyExecutor
.FirstOrDefault())
.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,5 +13,10 @@ public interface IGamblingService
Task<OneOf<SlotResult, GamblingError>> SlotAsync(ulong userId, long amount);
Task<FlipResult[]> FlipAsync(int count);
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,4 +1,6 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Modules.Gambling.Betdraw;
using EllieBot.Modules.Gambling.Rps;
using EllieBot.Modules.Gambling.Services;
@ -8,12 +10,12 @@ namespace EllieBot.Modules.Gambling;
public sealed class NewGamblingService : IGamblingService, IEService
{
private readonly GamblingConfigService _bcs;
private readonly GamblingConfigService _gcs;
private readonly ICurrencyService _cs;
public NewGamblingService(GamblingConfigService bcs, ICurrencyService cs)
public NewGamblingService(GamblingConfigService gcs, ICurrencyService cs)
{
_bcs = bcs;
_gcs = gcs;
_cs = cs;
}
@ -31,13 +33,13 @@ public sealed class NewGamblingService : IGamblingService, IEService
}
}
var game = new LulaGame(_bcs.Data.LuckyLadder.Multipliers);
var game = new LulaGame(_gcs.Data.LuckyLadder.Multipliers);
var result = game.Spin(amount);
var won = (long)result.Won;
if (won > 0)
{
await _cs.AddAsync(userId, won, new("lula", "win"));
await _cs.AddAsync(userId, won, new("lula", result.Multiplier >= 1 ? "win" : "lose"));
}
return result;
@ -57,9 +59,9 @@ public sealed class NewGamblingService : IGamblingService, IEService
}
}
var game = new BetrollGame(_bcs.Data.BetRoll.Pairs
.Select(x => (x.WhenAbove, (decimal)x.MultiplyBy))
.ToList());
var game = new BetrollGame(_gcs.Data.BetRoll.Pairs
.Select(x => (x.WhenAbove, (decimal)x.MultiplyBy))
.ToList());
var result = game.Roll(amount);
@ -88,7 +90,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
}
}
var game = new BetflipGame(_bcs.Data.BetFlip.Multiplier);
var game = new BetflipGame(_gcs.Data.BetFlip.Multiplier);
var result = game.Flip(guess, amount);
var won = (long)result.Won;
@ -100,7 +102,11 @@ public sealed class NewGamblingService : IGamblingService, IEService
return result;
}
public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(ulong userId, long amount, byte? maybeGuessValue, byte? maybeGuessColor)
public async Task<OneOf<BetdrawResult, GamblingError>> BetDrawAsync(
ulong userId,
long amount,
byte? maybeGuessValue,
byte? maybeGuessColor)
{
ArgumentOutOfRangeException.ThrowIfNegative(amount);
@ -155,7 +161,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
var won = (long)result.Won;
if (won > 0)
{
await _cs.AddAsync(userId, won, new("slot", "won"));
await _cs.AddAsync(userId, won, new("slot", "win"));
}
return result;
@ -266,3 +272,45 @@ public sealed class NewGamblingService : IGamblingService, IEService
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;
percentBonus = dollarValue switch
{
>= 100 => 20,
>= 50 => 10,
>= 20 => 5,
>= 10 => 3,
>= 5 => 1,
>= 100 => 25,
>= 50 => 20,
>= 20 => 15,
>= 10 => 10,
>= 5 => 5,
_ => 0
};
return (long)(modifiedAmount * (1 + (percentBonus / 100.0f)));

View file

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

View file

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

View file

@ -69,5 +69,13 @@ public partial class FlagTranslateService
YE ar
AL sq
AE ar
AU en
NZ en
KZ kz
NO no
SE sv
DK da
FI fi
HU hu
""";
}

View file

@ -783,4 +783,28 @@ public partial class Utility : EllieModule
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

@ -225,7 +225,7 @@ public partial class Xp : EllieModule<XpService>
else if (userXpData.AwardedXp < 0)
awardStr = $"({userXpData.AwardedXp})";
embed.AddField($"#{i + 1 + (curPage * 9)} {user?.ToString() ?? users[i].UserId.ToString()}",
embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}",
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
}
@ -278,7 +278,7 @@ public partial class Xp : EllieModule<XpService>
for (var i = 0; i < users.Count; i++)
{
var user = users[i];
embed.AddField($"#{i + 1 + (curPage * 9)} {user}",
embed.AddField($"#{i + 1 + (curPage * 10)} {user}",
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
}

View file

@ -17,10 +17,13 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, IEService
public ServerServiceDefinition Bind()
=> GrpcGreet.BindService(this);
private static GrpcGreetSettings ToConf(GreetSettings? conf)
private static GrpcGreetSettings ToConf(GreetSettings? conf, GreetType type)
{
if (conf is null)
return new GrpcGreetSettings();
return new GrpcGreetSettings()
{
Type = (GrpcGreetType)type
};
return new GrpcGreetSettings()
{
@ -35,9 +38,10 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, IEService
{
var guildId = request.GuildId;
var conf = await _gs.GetGreetSettingsAsync(guildId, (GreetType)request.Type);
var type = (GreetType)request.Type;
var conf = await _gs.GetGreetSettingsAsync(guildId, type);
return ToConf(conf);
return ToConf(conf, type);
}
public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)

View file

@ -69,10 +69,10 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
rews = rews.Concat(roleRews.Select(x => new RewItemReply()
{
Level = x.Level,
Type = "Role",
Type = x.Remove ? "RemoveRole" : "AddRole",
Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString()
}))
.OrderBy(x => x.Level);
.OrderBy(x => x.Level);
reply.Rewards.AddRange(rews);
@ -159,14 +159,14 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
_xp.SetRoleReward(request.GuildId, request.Level, rid, request.Type == "RemoveRole");
success = true;
}
else if (request.Type == "Currency")
{
if (!int.TryParse(request.Value, out var amount))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid amount"));
_xp.SetCurrencyReward(request.GuildId, request.Level, amount);
success = true;
}
// else if (request.Type == "Currency")
// {
// if (!int.TryParse(request.Value, out var amount))
// throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid amount"));
//
// _xp.SetCurrencyReward(request.GuildId, request.Level, amount);
// success = true;
// }
return new()
{
@ -207,15 +207,15 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context)
{
if (request.Page < 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 0"));
if (request.Page < 1)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 1"));
var guild = _client.GetGuild(request.GuildId);
if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
var data = await _xp.GetGuildUserXps(request.GuildId, request.Page);
var data = await _xp.GetGuildUserXps(request.GuildId, request.Page - 1);
var total = await _xp.GetTotalGuildUsers(request.GuildId);
var reply = new GetXpLbReply
@ -223,45 +223,60 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
Total = total
};
reply.Users.AddRange(await data
.Select(async x =>
{
var user = guild.GetUser(x.UserId);
var users = await data
.Select(async x =>
{
var user = guild.GetUser(x.UserId);
if (user is null)
{
var du = await _duSvc.GetUserAsync(x.UserId);
if (du is null)
return new XpLbUserReply
{
UserId = x.UserId,
Avatar = string.Empty,
Username = string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
if (user is null)
{
var du = await _duSvc.GetUserAsync(x.UserId);
if (du is null)
return new XpLbUserReply
{
UserId = x.UserId,
Avatar = string.Empty,
Username = string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
return new XpLbUserReply()
{
UserId = x.UserId,
Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty,
Username = du.ToString() ?? string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
}
return new XpLbUserReply()
{
UserId = x.UserId,
Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty,
Username = du.ToString() ?? string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
}
return new XpLbUserReply
{
UserId = x.UserId,
Avatar = user?.GetAvatarUrl() ?? string.Empty,
Username = user?.ToString() ?? string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
})
.WhenAll());
return new XpLbUserReply
{
UserId = x.UserId,
Avatar = user?.GetAvatarUrl() ?? string.Empty,
Username = user?.ToString() ?? string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
})
.WhenAll();
reply.Users.AddRange(users);
return reply;
}
public override async Task<SetServerExclusionReply> SetServerExclusion(
SetServerExclusionRequest request,
ServerCallContext context)
{
await Task.Yield();
var newValue = _xp.ToggleExcludeServer(request.GuildId);
return new()
{
Success = newValue
};
}
}

View file

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

View file

@ -4,6 +4,6 @@ namespace EllieBot.Services;
public interface ITxTracker
{
Task TrackAdd(long amount, TxData? txData);
Task TrackRemove(long amount, TxData? txData);
Task TrackAdd(ulong userId, long amount, TxData? txData);
Task TrackRemove(ulong userId, long amount, TxData? txData);
}

View file

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

View file

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

View file

@ -12,7 +12,11 @@ public sealed partial class ReplacementPatternStore
{
Register("%bot.time%",
static ()
=> DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
=> TimestampTag.FromDateTime(DateTime.UtcNow, TimestampTagStyles.ShortTime).ToString());
Register("%bot.date%",
static ()
=> TimestampTag.FromDateTime(DateTime.UtcNow, TimestampTagStyles.ShortTime).ToString());
}
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}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Author.Id.ToString(),
usrMsg.Content.TrimTo(10));
}

View file

@ -77,7 +77,7 @@ public sealed class CurrencyService : ICurrencyService, IEService
{
var wallet = await GetWalletAsync(userId);
await wallet.Add(amount, txData);
await _txTracker.TrackAdd(amount, txData);
await _txTracker.TrackAdd(userId, amount, txData);
}
public async Task AddAsync(
@ -97,7 +97,7 @@ public sealed class CurrencyService : ICurrencyService, IEService
var wallet = await GetWalletAsync(userId);
var result = await wallet.Take(amount, txData);
if (result)
await _txTracker.TrackRemove(amount, txData);
await _txTracker.TrackRemove(userId, amount, txData);
return result;
}

View file

@ -1,8 +1,11 @@
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Services.Currency;
using EllieBot.Db.Models;
using EllieBot.Modules.Gambling;
using System.Collections.Concurrent;
namespace EllieBot.Services;
@ -10,15 +13,11 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
{
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
{
"lula",
"betroll",
"betflip",
"blackjack",
"betdraw",
"slot",
"lula", "betroll", "betflip", "blackjack", "betdraw", "slot",
});
private ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> _stats = new();
private NonBlocking.ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> globalStats = new();
private ConcurrentBag<UserBetStats> userStats = new();
private readonly DbService _db;
@ -28,83 +27,333 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
}
public async Task OnReadyAsync()
=> await Task.WhenAll(RunUserStatsCollector(), RunBetStatsCollector());
public async Task RunBetStatsCollector()
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
await using var ctx = _db.GetDbContext();
await using var trans = await ctx.Database.BeginTransactionAsync();
try
{
var keys = _stats.Keys;
// update betstats
var keys = globalStats.Keys;
foreach (var key in keys)
{
if (_stats.TryRemove(key, out var stat))
if (globalStats.TryRemove(key, out var stat))
{
await ctx.GetTable<GamblingStats>()
.InsertOrUpdateAsync(() => new()
{
Feature = key,
Bet = stat.Bet,
PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow
}, old => new()
{
Bet = old.Bet + stat.Bet,
PaidOut = old.PaidOut + stat.PaidOut,
}, () => new()
{
Feature = key
});
.InsertOrUpdateAsync(() => new()
{
Feature = key,
Bet = stat.Bet,
PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow
},
old => new()
{
Bet = old.Bet + stat.Bet,
PaidOut = old.PaidOut + stat.PaidOut,
},
() => new()
{
Feature = key
});
}
}
}
catch (Exception ex)
{
Log.Error(ex, "An error occurred in gambling tx tracker");
}
finally
{
await trans.CommitAsync();
Log.Error(ex, "An error occurred in betstats gambling tx tracker");
}
}
}
public Task TrackAdd(long amount, TxData? txData)
private async Task RunUserStatsCollector()
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync())
{
try
{
if (userStats.Count == 0)
continue;
var users = new List<UserBetStats>(userStats.Count + 5);
while (userStats.TryTake(out var s))
users.Add(s);
if (users.Count == 0)
continue;
// rakeback
var rakebacks = new Dictionary<ulong, decimal>();
// update userstats
foreach (var (k, x) in users.GroupBy(x => (x.UserId, x.Game))
.ToDictionary(x => x.Key,
x => x.Aggregate((a, b) => new()
{
WinCount = a.WinCount + b.WinCount,
LoseCount = a.LoseCount + b.LoseCount,
TotalBet = a.TotalBet + b.TotalBet,
PaidOut = a.PaidOut + b.PaidOut,
MaxBet = Math.Max(a.MaxBet, b.MaxBet),
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
await using var uow = _db.GetDbContext();
await uow.GetTable<UserBetStats>()
.InsertOrUpdateAsync(() => new()
{
UserId = k.UserId,
Game = k.Game,
WinCount = x.WinCount,
LoseCount = Math.Max(0, x.LoseCount),
TotalBet = x.TotalBet,
PaidOut = x.PaidOut,
MaxBet = x.MaxBet,
MaxWin = x.MaxWin
},
o => new()
{
WinCount = o.WinCount + x.WinCount,
LoseCount = Math.Max(0, o.LoseCount + x.LoseCount),
TotalBet = o.TotalBet + x.TotalBet,
PaidOut = o.PaidOut + x.PaidOut,
MaxBet = Math.Max(o.MaxBet, x.MaxBet),
MaxWin = Math.Max(o.MaxWin, x.MaxWin),
},
() => new()
{
UserId = k.UserId,
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)
{
Log.Error(ex, "An error occurred in UserBetStats gambling tx tracker");
}
}
}
private const decimal BASE_RAKEBACK = 0.05m;
public Task TrackAdd(ulong userId, long amount, TxData? txData)
{
if (txData is null)
return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type))
{
_stats.AddOrUpdate(txData.Type,
globalStats.AddOrUpdate(txData.Type,
_ => (0, amount),
(_, old) => (old.Bet, old.PaidOut + amount));
}
var mType = GetGameType(txData.Type);
if (mType is not { } type)
return Task.CompletedTask;
if (txData.Type == "lula")
{
if (txData.Extra == "lose")
{
userStats.Add(new()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = 0,
TotalBet = 0,
PaidOut = amount,
MaxBet = 0,
MaxWin = amount,
});
return Task.CompletedTask;
}
}
else if (txData.Type == "animalrace")
{
if (txData.Extra == "refund")
{
userStats.Add(new()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = -1,
TotalBet = -amount,
PaidOut = 0,
MaxBet = 0,
MaxWin = 0,
});
return Task.CompletedTask;
}
}
userStats.Add(new UserBetStats()
{
UserId = userId,
Game = type,
WinCount = 1,
LoseCount = -1,
TotalBet = 0,
PaidOut = amount,
MaxBet = 0,
MaxWin = amount,
});
return Task.CompletedTask;
}
public Task TrackRemove(long amount, TxData? txData)
public Task TrackRemove(ulong userId, long amount, TxData? txData)
{
if (txData is null)
return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type))
{
_stats.AddOrUpdate(txData.Type,
globalStats.AddOrUpdate(txData.Type,
_ => (amount, 0),
(_, old) => (old.Bet + amount, old.PaidOut));
}
var mType = GetGameType(txData.Type);
if (mType is not { } type)
return Task.CompletedTask;
userStats.Add(new UserBetStats()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = 1,
TotalBet = amount,
PaidOut = 0,
MaxBet = amount,
MaxWin = 0
});
return Task.CompletedTask;
}
private static GamblingGame? GetGameType(string game)
=> game switch
{
"lula" => GamblingGame.Lula,
"betroll" => GamblingGame.Betroll,
"betflip" => GamblingGame.Betflip,
"blackjack" => GamblingGame.Blackjack,
"betdraw" => GamblingGame.Betdraw,
"slot" => GamblingGame.Slots,
"animalrace" => GamblingGame.Race,
_ => null
};
public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
{
await using var ctx = _db.GetDbContext();
return await ctx.Set<GamblingStats>()
.ToListAsyncEF();
.ToListAsyncEF();
}
public async Task<List<UserBetStats>> GetUserStatsAsync(ulong userId, GamblingGame? game = null)
{
await using var ctx = _db.GetDbContext();
if (game is null)
return await ctx
.GetTable<UserBetStats>()
.Where(x => x.UserId == userId)
.ToListAsync();
return await ctx
.GetTable<UserBetStats>()
.Where(x => x.UserId == userId && x.Game == game)
.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 int Id { get; set; }
public ulong UserId { get; set; }
public GamblingGame Game { get; set; }
public long WinCount { get; set; }
public long LoseCount { get; set; }
public decimal TotalBet { get; set; }
public decimal PaidOut { get; set; }
public long MaxWin { get; set; }
public long MaxBet { get; set; }
}
public enum GamblingGame
{
Betflip = 0,
Bf = 0,
Betroll = 1,
Br = 1,
Betdraw = 2,
Bd = 2,
Slots = 3,
Slot = 3,
Blackjack = 4,
Bj = 4,
Lula = 5,
Race = 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.Data;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using System.Collections.Frozen;
namespace EllieBot.Modules.Permissions.Services;
public sealed class BlacklistService : IExecOnMessage
public sealed class BlacklistService : IExecOnMessage, IReadyExecutor
{
public int Priority
=> int.MaxValue;
@ -15,69 +15,114 @@ public sealed class BlacklistService : IExecOnMessage
private readonly DbService _db;
private readonly IPubSub _pubSub;
private readonly IBotCreds _creds;
private IReadOnlyList<BlacklistEntry> blacklist;
private readonly DiscordSocketClient _client;
private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
private FrozenSet<ulong> blacklistedGuilds = new HashSet<ulong>().ToFrozenSet();
private FrozenSet<ulong> blacklistedUsers = new HashSet<ulong>().ToFrozenSet();
private FrozenSet<ulong> blacklistedChannels = new HashSet<ulong>().ToFrozenSet();
public BlacklistService(DbService db, IPubSub pubSub, IBotCreds creds)
private readonly TypedKey<bool> _blPubKey = new("blacklist.reload");
public BlacklistService(
DbService db,
IPubSub pubSub,
IBotCreds creds,
DiscordSocketClient client)
{
_db = db;
_pubSub = pubSub;
_creds = creds;
_client = client;
Reload(false);
_pubSub.Sub(_blPubKey, OnReload);
_pubSub.Sub(_blPubKey, async _ => await Reload(false));
}
public async Task OnReadyAsync()
{
_client.JoinedGuild += async (g) =>
{
if (blacklistedGuilds.Contains(g.Id))
{
await g.LeaveAsync();
}
};
await Reload(false);
}
private ValueTask OnReload(BlacklistEntry[] newBlacklist)
{
blacklist = newBlacklist;
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;
}
public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
public Task<bool> ExecOnMessageAsync(IGuild? guild, IUserMessage usrMsg)
{
foreach (var bl in blacklist)
if (guild is not null && blacklistedGuilds.Contains(guild.Id))
{
if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id)
{
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id);
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]",
guild.Name,
guild.Id.ToString());
return Task.FromResult(true);
}
return Task.FromResult(true);
}
if (blacklistedChannels.Contains(usrMsg.Channel.Id))
{
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);
return Task.FromResult(true);
}
if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
{
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
usrMsg.Author.ToString(),
usrMsg.Author.Id);
return Task.FromResult(true);
}
if (blacklistedUsers.Contains(usrMsg.Author.Id))
{
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
usrMsg.Author.ToString(),
usrMsg.Author.Id.ToString());
return Task.FromResult(true);
}
return Task.FromResult(false);
}
public IReadOnlyList<BlacklistEntry> GetBlacklist()
=> blacklist;
public void Reload(bool publish = true)
public async Task<IReadOnlyList<BlacklistEntry>> GetBlacklist(BlacklistType type)
{
using var uow = _db.GetDbContext();
var toPublish = uow.GetTable<BlacklistEntry>().ToArray();
blacklist = toPublish;
await using var uow = _db.GetDbContext();
return await uow
.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)
_pubSub.Pub(_blPubKey, toPublish);
{
await _pubSub.Pub(_blPubKey, true);
}
await OnReload(items);
}
public async Task Blacklist(BlacklistType type, ulong id)
@ -88,34 +133,34 @@ public sealed class BlacklistService : IExecOnMessage
await using var uow = _db.GetDbContext();
await uow
.GetTable<BlacklistEntry>()
.InsertAsync(() => new()
{
ItemId = id,
Type = type,
});
.GetTable<BlacklistEntry>()
.InsertAsync(() => new()
{
ItemId = id,
Type = type,
});
if (type == BlacklistType.User)
{
await uow.GetTable<DiscordUser>()
.Where(x => x.UserId == id)
.UpdateAsync(_ => new()
{
CurrencyAmount = 0
});
.Where(x => x.UserId == id)
.UpdateAsync(_ => new()
{
CurrencyAmount = 0
});
}
Reload();
await Reload();
}
public async Task UnBlacklist(BlacklistType type, ulong id)
{
await using var uow = _db.GetDbContext();
await uow.GetTable<BlacklistEntry>()
.Where(bi => bi.ItemId == id && bi.Type == type)
.DeleteAsync();
.Where(bi => bi.ItemId == id && bi.Type == type)
.DeleteAsync();
Reload();
await Reload();
}
public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
@ -130,12 +175,12 @@ public sealed class BlacklistService : IExecOnMessage
var blList = toBlacklist.ToList();
await uow.GetTable<DiscordUser>()
.Where(x => blList.Contains(x.UserId))
.UpdateAsync(_ => new()
{
CurrencyAmount = 0
});
.Where(x => blList.Contains(x.UserId))
.UpdateAsync(_ => new()
{
CurrencyAmount = 0
});
Reload();
await Reload();
}
}

View file

@ -27,5 +27,22 @@ public sealed class ImagesConfig : ConfigServiceBase<ImageUrls>
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,5 +1,4 @@
#nullable disable
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
namespace EllieBot.Common.TypeReaders.Models;
@ -9,8 +8,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)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
public string Input { get; set; }
public TimeSpan Time { get; set; }
public string Input { get; set; } = string.Empty;
public TimeSpan Time { get; set; } = default;
private StoopidTime() { }
@ -53,8 +52,8 @@ public class StoopidTime
};
}
public static implicit operator TimeSpan(StoopidTime st)
=> st.Time;
public static implicit operator TimeSpan?(StoopidTime? st)
=> st?.Time;
public static implicit operator StoopidTime(TimeSpan ts)
=> new()

View file

@ -0,0 +1,49 @@
# 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

@ -848,6 +848,10 @@ eventstart:
- eventstart
betstats:
- betstats
- bs
gamblestats:
- gamblestats
- gs
bettest:
- bettest
slot:
@ -859,6 +863,11 @@ affinity:
waifuclaim:
- waifuclaim
- claim
- wc
waifuclaims:
- waifuclaims
- claims
- wcs
waifureset:
- waifureset
waifutransfer:
@ -1450,3 +1459,17 @@ translateflags:
- trfl
- fltr
- transflags
rakeback:
- rakeback
- rb
betstatsreset:
- betstatsreset
- bsr
- bsreset
gamblestatsreset:
- gamblestatsreset
- gsr
- gsreset
snipe:
- snipe
- sn

View file

@ -2756,19 +2756,6 @@
}
],
"Gambling": [
{
"Aliases": [
".betstats"
],
"Description": "Shows the total stats of several gambling features.\nUpdates once an hour.",
"Usage": [
".betstats"
],
"Submodule": "Gambling",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".timely"
@ -3012,6 +2999,20 @@
"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": [
".race"
@ -3119,6 +3120,70 @@
"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": [
".blackjack",
@ -3583,10 +3648,25 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".waifuclaims",
".claims",
".wcs"
],
"Description": "Shows all of your currently claimed waifus.",
"Usage": [
".waifuclaims"
],
"Submodule": "WaifuClaimCommands",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".waifuclaim",
".claim"
".claim - wc"
],
"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": [
@ -6160,6 +6240,24 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".translateflags",
".trfl",
".fltr",
".transflags"
],
"Description": "Toggles translate flags on the current channel.\nReacting with a country flag will translate the message to that country's language.",
"Usage": [
".translateflags"
],
"Submodule": "TranslateCommands",
"Module": "Searches",
"Options": null,
"Requirements": [
"ManageChannels Channel Permission"
]
},
{
"Aliases": [
".xkcd"
@ -6508,6 +6606,20 @@
"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": [
".prompt"

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE
version: 9
version: 12
# Currency settings
currency:
# What is the emoji/character which represents the currency
@ -28,7 +28,7 @@ betRoll:
multiplyBy: 10
- whenAbove: 90
multiplyBy: 4
- whenAbove: 66
- whenAbove: 65
multiplyBy: 2
# Automatic currency generation settings.
generation:
@ -56,8 +56,9 @@ timely:
# How often (in hours) can users claim currency with .timely command
# setting to 0 or less will disable this feature
cooldown: 12
# Whether the users are required to type a password when they do timely.
requirePassword: true
# How will timely be protected?
# None, Button (users have to click the button) or Captcha (users have to type the captcha from an image)
protType: Button
# How much will each user's owned currency decay over time.
decay:
# Percentage of user's current currency which will be deducted every 24h.
@ -84,7 +85,7 @@ luckyLadder:
- 2.4
- 1.7
- 1.5
- 1.2
- 1.1
- 0.5
- 0.3
- 0.2
@ -273,3 +274,9 @@ voteReward: 100
slots:
# Hex value of the color which the numbers on the slot image will have.
currencyFontColor: ff0000
# Bonus config for server boosts
boostBonus:
# Users will receive a bonus if they boost any of these servers
guildIds: []
# This bonus will be added before any other multiplier is applied to the .timely command
baseTimelyBonus: 50

View file

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

View file

@ -1517,7 +1517,7 @@ take:
betroll:
desc: |-
Bets the specified amount of currency and rolls a dice.
Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10.
Rolling over 65 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.
ex:
- 5
@ -2703,7 +2703,7 @@ eventstart:
desc: "The type of event being started."
options:
desc: "The optional option flags for the event."
betstats:
gamblestats:
desc: |-
Shows the total stats of several gambling features.
Updates once an hour.
@ -2711,6 +2711,13 @@ betstats:
- ''
params:
- { }
gamblestatsreset:
desc: |-
Resets the gamble stats.
ex:
- ''
params:
- { }
slot:
desc: |-
Play Ellie slots by placing your bet.
@ -2738,6 +2745,12 @@ waifuclaim:
desc: "The cost of claiming the waifu."
target:
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:
desc: Resets your waifu stats, except current waifus.
ex:
@ -4642,3 +4655,51 @@ translateflags:
- ''
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:
desc: |-
Shows the current bet stats for yourself, or the targetted user.
You may optionally specify the game to show stats for.
Supported games right now are: bf, br, bd, lula, slot, race
ex:
- ''
- '@someone'
- '@someone lula'
- 'bd'
params:
- {}
- user:
desc: 'The user for who to show the betstats for.'
- user:
desc: 'The user for who to show the betstats for.'
game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined'
- game:
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,5 +1114,9 @@
"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.",
"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}"
}

8
src/EllieBot/migrate.ps1 Normal file
View file

@ -0,0 +1,8 @@
if ($args.Length -eq 0) {
Write-Host "Please provide a migration name." -ForegroundColor Red
}
else {
$migrationName = $args[0]
dotnet ef migrations add $migrationName -c SqliteContext -p EllieBot.csproj
dotnet ef migrations add $migrationName -c PostgreSqlContext -p EllieBot.csproj
}