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 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 ## [5.1.18] - 04.11.2024
### Added ### Added

View file

@ -11,9 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Dockerfile = Dockerfile Dockerfile = Dockerfile
ellie-menu.ps1 = ellie-menu.ps1 ellie-menu.ps1 = ellie-menu.ps1
LICENSE = LICENSE LICENSE = LICENSE
migrate.ps1 = migrate.ps1
README.md = README.md README.md = README.md
remove-migrations.ps1 = remove-migrations.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot", "src\EllieBot\EllieBot.csproj", "{4D9001F7-B3E8-48FE-97AA-CFD36DA65A64}" 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 EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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 AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply); 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 { message GetXpLbRequest {
@ -32,7 +43,8 @@ message XpLbUserReply {
string username = 2; string username = 2;
int64 xp = 3; int64 xp = 3;
int64 level = 4; int64 level = 4;
string avatar = 5; int64 levelPercent = 5;
string avatar = 6;
} }
message ResetUserXpRequest { message ResetUserXpRequest {

View file

@ -62,6 +62,7 @@ public abstract class EllieContext : DbContext
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; } public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; } public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// public DbSet<GuildColors> GuildColors { get; set; } // public DbSet<GuildColors> GuildColors { get; set; }
@ -73,6 +74,22 @@ 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
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new { x.UserId, x.Game })
.IsUnique();
#endregion
#region Flag Translate #region Flag Translate
modelBuilder.Entity<FlagTranslateChannel>() modelBuilder.Entity<FlagTranslateChannel>()

View file

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

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.18</Version> <Version>5.1.20</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

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

View file

@ -1,9 +1,9 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using EllieBot.Db;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using EllieBot.Db;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable #nullable disable
@ -801,6 +801,57 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("discorduser", (string)null); b.ToTable("discorduser", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowTarget")
.HasColumnType("boolean")
.HasColumnName("allowtarget");
b.Property<bool>("AutoDeleteTrigger")
.HasColumnType("boolean")
.HasColumnName("autodeletetrigger");
b.Property<bool>("ContainsAnywhere")
.HasColumnType("boolean")
.HasColumnName("containsanywhere");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<bool>("DmResponse")
.HasColumnType("boolean")
.HasColumnName("dmresponse");
b.Property<decimal?>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Reactions")
.HasColumnType("text")
.HasColumnName("reactions");
b.Property<string>("Response")
.HasColumnType("text")
.HasColumnName("response");
b.Property<string>("Trigger")
.HasColumnType("text")
.HasColumnName("trigger");
b.HasKey("Id")
.HasName("pk_expressions");
b.ToTable("expressions", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1700,57 +1751,6 @@ namespace EllieBot.Migrations.PostgreSql
b.ToTable("ncpixel", (string)null); b.ToTable("ncpixel", (string)null);
}); });
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AllowTarget")
.HasColumnType("boolean")
.HasColumnName("allowtarget");
b.Property<bool>("AutoDeleteTrigger")
.HasColumnType("boolean")
.HasColumnName("autodeletetrigger");
b.Property<bool>("ContainsAnywhere")
.HasColumnType("boolean")
.HasColumnName("containsanywhere");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<bool>("DmResponse")
.HasColumnType("boolean")
.HasColumnName("dmresponse");
b.Property<decimal?>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Reactions")
.HasColumnType("text")
.HasColumnName("reactions");
b.Property<string>("Response")
.HasColumnType("text")
.HasColumnName("response");
b.Property<string>("Trigger")
.HasColumnType("text")
.HasColumnName("trigger");
b.HasKey("Id")
.HasName("pk_expressions");
b.ToTable("expressions", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b => modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b =>
{ {
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
@ -3227,6 +3227,74 @@ 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 =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("Game")
.HasColumnType("integer")
.HasColumnName("game");
b.Property<long>("LoseCount")
.HasColumnType("bigint")
.HasColumnName("losecount");
b.Property<long>("MaxBet")
.HasColumnType("bigint")
.HasColumnName("maxbet");
b.Property<long>("MaxWin")
.HasColumnType("bigint")
.HasColumnName("maxwin");
b.Property<decimal>("PaidOut")
.HasColumnType("numeric")
.HasColumnName("paidout");
b.Property<decimal>("TotalBet")
.HasColumnType("numeric")
.HasColumnName("totalbet");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.Property<long>("WinCount")
.HasColumnType("bigint")
.HasColumnName("wincount");
b.HasKey("Id")
.HasName("pk_userbetstats");
b.HasIndex("UserId", "Game")
.IsUnique()
.HasDatabaseName("ix_userbetstats_userid_game");
b.ToTable("userbetstats", (string)null);
});
modelBuilder.Entity("EllieBot.Db.Models.AntiAltSetting", b => modelBuilder.Entity("EllieBot.Db.Models.AntiAltSetting", b =>
{ {
b.HasOne("EllieBot.Db.Models.GuildConfig", null) b.HasOne("EllieBot.Db.Models.GuildConfig", null)

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

@ -1,9 +1,9 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using EllieBot.Db;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using EllieBot.Db;
#nullable disable #nullable disable
@ -598,6 +598,44 @@ namespace EllieBot.Migrations
b.ToTable("DiscordUser"); b.ToTable("DiscordUser");
}); });
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AllowTarget")
.HasColumnType("INTEGER");
b.Property<bool>("AutoDeleteTrigger")
.HasColumnType("INTEGER");
b.Property<bool>("ContainsAnywhere")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("DmResponse")
.HasColumnType("INTEGER");
b.Property<ulong?>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Reactions")
.HasColumnType("TEXT");
b.Property<string>("Response")
.HasColumnType("TEXT");
b.Property<string>("Trigger")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Expressions");
});
modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b => modelBuilder.Entity("EllieBot.Db.Models.ExcludedItem", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@ -1267,44 +1305,6 @@ namespace EllieBot.Migrations
b.ToTable("NCPixel"); b.ToTable("NCPixel");
}); });
modelBuilder.Entity("EllieBot.Db.Models.EllieExpression", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AllowTarget")
.HasColumnType("INTEGER");
b.Property<bool>("AutoDeleteTrigger")
.HasColumnType("INTEGER");
b.Property<bool>("ContainsAnywhere")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("DmResponse")
.HasColumnType("INTEGER");
b.Property<ulong?>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Reactions")
.HasColumnType("TEXT");
b.Property<string>("Response")
.HasColumnType("TEXT");
b.Property<string>("Trigger")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Expressions");
});
modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b => modelBuilder.Entity("EllieBot.Db.Models.PatronUser", b =>
{ {
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
@ -2399,6 +2399,58 @@ 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 =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Game")
.HasColumnType("INTEGER");
b.Property<long>("LoseCount")
.HasColumnType("INTEGER");
b.Property<long>("MaxBet")
.HasColumnType("INTEGER");
b.Property<long>("MaxWin")
.HasColumnType("INTEGER");
b.Property<decimal>("PaidOut")
.HasColumnType("TEXT");
b.Property<decimal>("TotalBet")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.Property<long>("WinCount")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Game")
.IsUnique();
b.ToTable("UserBetStats");
});
modelBuilder.Entity("EllieBot.Db.Models.AntiAltSetting", b => modelBuilder.Entity("EllieBot.Db.Models.AntiAltSetting", b =>
{ {
b.HasOne("EllieBot.Db.Models.GuildConfig", null) b.HasOne("EllieBot.Db.Models.GuildConfig", null)

View file

@ -71,7 +71,6 @@ 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

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

View file

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

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 Gambling
{ {
public partial class BlackJackCommands : GamblingSubmodule<BlackJackService> public partial class BlackJackCommands : GamblingModule<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 : GamblingSubmodule<GamblingService> public partial class Connect4Commands : GamblingModule<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 : GamblingSubmodule<IGamblingService> public partial class DrawCommands : GamblingModule<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 : GamblingSubmodule<CurrencyEventsService> public partial class CurrencyEventsCommands : GamblingModule<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 : GamblingSubmodule<IGamblingService> public partial class FlipCoinCommands : GamblingModule<IGamblingService>
{ {
public enum BetFlipGuess : byte public enum BetFlipGuess : byte
{ {

View file

@ -14,6 +14,13 @@ using System.Text;
using EllieBot.Modules.Gambling.Rps; using EllieBot.Modules.Gambling.Rps;
using EllieBot.Common.TypeReaders; using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Patronage; 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; namespace EllieBot.Modules.Gambling;
@ -31,6 +38,7 @@ 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,
@ -43,7 +51,8 @@ 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;
@ -53,6 +62,7 @@ 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();
@ -70,42 +80,6 @@ public partial class Gambling : GamblingModule<GamblingService>
return N(bal); 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) private async Task RemindTimelyAction(SocketMessageComponent smc, DateTime when)
{ {
@ -153,10 +127,12 @@ public partial class Gambling : GamblingModule<GamblingService>
customId: "timely:" + _rng.Next(123456, 999999)), customId: "timely:" + _rng.Next(123456, 999999)),
async (smc) => async (smc) =>
{ {
await smc.DeferAsync();
await ClaimTimely(); await ClaimTimely();
}); });
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)]
public async Task Timely() public async Task Timely()
{ {
var val = Config.Timely.Amount; var val = Config.Timely.Amount;
@ -167,62 +143,64 @@ public partial class Gambling : GamblingModule<GamblingService>
return; 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 interaction = CreateTimelyInteraction();
var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync(); var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync();
await msg.DeleteAsync(); await msg.DeleteAsync();
return; 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(); await ClaimTimely();
} }
@ -249,6 +227,29 @@ public partial class Gambling : GamblingModule<GamblingService>
var val = Config.Timely.Amount; 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 patron = await _ps.GetPatronAsync(ctx.User.Id);
var percentBonus = (_ps.PercentBonus(patron) / 100f); var percentBonus = (_ps.PercentBonus(patron) / 100f);
@ -259,6 +260,20 @@ 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 || 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(); await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
} }
@ -370,7 +385,8 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
var embed = _sender.CreateEmbed() var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() .WithTitle(GetText(strs.transactions(
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}"))) ?? $"{userId}")))
.WithOkColor(); .WithOkColor();
@ -627,7 +643,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
else 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 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}%") 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; } = 9; public int Version { get; set; } = 12;
[Comment("""Currency settings""")] [Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; } public CurrencyConfig Currency { get; set; }
@ -67,6 +67,11 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
[Comment("""Slot config""")] [Comment("""Slot config""")]
public SlotsConfig Slots { get; set; } public SlotsConfig Slots { get; set; }
[Comment("""
Bonus config for server boosts
""")]
public BoostBonusConfig BoostBonus { get; set; }
public GamblingConfig() public GamblingConfig()
{ {
BetRoll = new(); BetRoll = new();
@ -79,6 +84,7 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
Slots = new(); Slots = new();
LuckyLadder = new(); LuckyLadder = new();
BotCuts = 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 How much currency will the users get every time they run .timely command
setting to 0 or less will disable this feature setting to 0 or less will disable this feature
""")] """)]
public int Amount { get; set; } = 0; public long Amount { get; set; } = 0;
[Comment(""" [Comment("""
How often (in hours) can users claim currency with .timely command 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; public int Cooldown { get; set; } = 24;
[Comment(""" [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] [Cloneable]
@ -150,7 +164,7 @@ public partial class BetRollConfig
}, },
new() new()
{ {
WhenAbove = 66, WhenAbove = 65,
MultiplyBy = 2 MultiplyBy = 2
} }
]; ];
@ -212,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.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] [Cloneable]
@ -414,3 +428,14 @@ public sealed partial class BotCutConfig
""")] """)]
public decimal ShopSaleCut { get; set; } = 0.1m; 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, ConfigPrinters.ToString,
val => val >= 0); val => val >= 0);
AddParsedProp("timely.pass", AddParsedProp("timely.prot",
gs => gs.Timely.RequirePassword, gs => gs.Timely.ProtType,
bool.TryParse, ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString); ConfigPrinters.ToString);
Migrate(); Migrate();
@ -189,11 +189,16 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 9) if (data.Version < 12)
{ {
ModifyConfig(c => 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); 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 : GamblingSubmodule<PlantPickService> public partial class PlantPickCommands : GamblingModule<PlantPickService>
{ {
private readonly ILogCommandService _logService; private readonly ILogCommandService _logService;

View file

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

View file

@ -10,7 +10,7 @@ namespace EllieBot.Modules.Gambling;
public partial class Gambling public partial class Gambling
{ {
[Group] [Group]
public partial class ShopCommands : GamblingSubmodule<IShopService> public partial class ShopCommands : GamblingModule<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 : GamblingSubmodule<IGamblingService> public partial class SlotCommands : GamblingModule<IGamblingService>
{ {
private readonly IImageCache _images; private readonly IImageCache _images;
private readonly FontProvider _fonts; 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 public partial class Gambling
{ {
[Group] [Group]
public partial class WaifuClaimCommands : GamblingSubmodule<WaifuService> public partial class WaifuClaimCommands : GamblingModule<WaifuService>
{ {
public WaifuClaimCommands(GamblingConfigService gamblingConfService) public WaifuClaimCommands(GamblingConfigService gamblingConfService)
: base(gamblingConfService) : base(gamblingConfService)
@ -37,6 +37,45 @@ 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)
@ -144,7 +183,7 @@ public partial class Gambling
if (targetId == ctx.User.Id) if (targetId == ctx.User.Id)
return; 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) if (result == DivorceResult.SucessWithPenalty)
{ {
@ -157,14 +196,6 @@ 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,25 +318,20 @@ 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, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId) public async Task<(WaifuInfo, DivorceResult, long)> 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;
@ -369,7 +364,7 @@ public class WaifuService : IEService, IReadyExecutor
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
} }
return (w, result, amount, remaining); return (w, result, amount);
} }
public async Task<bool> GiftWaifuAsync( public async Task<bool> GiftWaifuAsync(
@ -630,4 +625,38 @@ 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,5 +13,10 @@ 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,4 +1,6 @@
#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;
@ -8,12 +10,12 @@ namespace EllieBot.Modules.Gambling;
public sealed class NewGamblingService : IGamblingService, IEService public sealed class NewGamblingService : IGamblingService, IEService
{ {
private readonly GamblingConfigService _bcs; private readonly GamblingConfigService _gcs;
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
public NewGamblingService(GamblingConfigService bcs, ICurrencyService cs) public NewGamblingService(GamblingConfigService gcs, ICurrencyService cs)
{ {
_bcs = bcs; _gcs = gcs;
_cs = cs; _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 result = game.Spin(amount);
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) 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; return result;
@ -57,7 +59,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
} }
} }
var game = new BetrollGame(_bcs.Data.BetRoll.Pairs var game = new BetrollGame(_gcs.Data.BetRoll.Pairs
.Select(x => (x.WhenAbove, (decimal)x.MultiplyBy)) .Select(x => (x.WhenAbove, (decimal)x.MultiplyBy))
.ToList()); .ToList());
@ -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 result = game.Flip(guess, amount);
var won = (long)result.Won; var won = (long)result.Won;
@ -100,7 +102,11 @@ public sealed class NewGamblingService : IGamblingService, IEService
return result; 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); ArgumentOutOfRangeException.ThrowIfNegative(amount);
@ -155,7 +161,7 @@ public sealed class NewGamblingService : IGamblingService, IEService
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) if (won > 0)
{ {
await _cs.AddAsync(userId, won, new("slot", "won")); await _cs.AddAsync(userId, won, new("slot", "win"));
} }
return result; return result;
@ -266,3 +272,45 @@ 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 => 20, >= 100 => 25,
>= 50 => 10, >= 50 => 20,
>= 20 => 5, >= 20 => 15,
>= 10 => 3, >= 10 => 10,
>= 5 => 1, >= 5 => 5,
_ => 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 => 20, >= 2000 => 30,
>= 1000 => 10, >= 1000 => 20,
>= 500 => 5, >= 500 => 10,
_ => 0 _ => 0
}; };

View file

@ -18,13 +18,13 @@ public partial class Permissions
{ {
ArgumentOutOfRangeException.ThrowIfNegative(page); ArgumentOutOfRangeException.ThrowIfNegative(page);
var list = _service.GetBlacklist(); var list = await _service.GetBlacklist(type);
var allItems = await list.Where(x => x.Type == type) var allItems = await list
.Select(i => .Select(i =>
{ {
try try
{ {
return Task.FromResult(i.Type switch return Task.FromResult(type switch
{ {
BlacklistType.Channel => Format.Code(i.ItemId.ToString()) BlacklistType.Channel => Format.Code(i.ItemId.ToString())
+ " " + " "

View file

@ -69,5 +69,13 @@ public partial class FlagTranslateService
YE ar YE ar
AL sq AL sq
AE ar 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(); 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) else if (userXpData.AwardedXp < 0)
awardStr = $"({userXpData.AwardedXp})"; 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}"); $"{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++) for (var i = 0; i < users.Count; i++)
{ {
var user = users[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"); $"{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() public ServerServiceDefinition Bind()
=> GrpcGreet.BindService(this); => GrpcGreet.BindService(this);
private static GrpcGreetSettings ToConf(GreetSettings? conf) private static GrpcGreetSettings ToConf(GreetSettings? conf, GreetType type)
{ {
if (conf is null) if (conf is null)
return new GrpcGreetSettings(); return new GrpcGreetSettings()
{
Type = (GrpcGreetType)type
};
return new GrpcGreetSettings() return new GrpcGreetSettings()
{ {
@ -35,9 +38,10 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, IEService
{ {
var guildId = request.GuildId; 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) public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)

View file

@ -69,7 +69,7 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
rews = rews.Concat(roleRews.Select(x => new RewItemReply() rews = rews.Concat(roleRews.Select(x => new RewItemReply()
{ {
Level = x.Level, Level = x.Level,
Type = "Role", Type = x.Remove ? "RemoveRole" : "AddRole",
Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString() Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString()
})) }))
.OrderBy(x => x.Level); .OrderBy(x => x.Level);
@ -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()
{ {
@ -207,15 +207,15 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context) public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context)
{ {
if (request.Page < 0) if (request.Page < 1)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 0")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 1"));
var guild = _client.GetGuild(request.GuildId); var guild = _client.GetGuild(request.GuildId);
if (guild is null) if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found")); 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 total = await _xp.GetTotalGuildUsers(request.GuildId);
var reply = new GetXpLbReply var reply = new GetXpLbReply
@ -223,7 +223,7 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
Total = total Total = total
}; };
reply.Users.AddRange(await data var users = await data
.Select(async x => .Select(async x =>
{ {
var user = guild.GetUser(x.UserId); var user = guild.GetUser(x.UserId);
@ -260,8 +260,23 @@ public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, IEService
Level = new LevelStats(x.Xp).Level Level = new LevelStats(x.Xp).Level
}; };
}) })
.WhenAll()); .WhenAll();
reply.Users.AddRange(users);
return reply; 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; using EllieBot.Db.Models;
namespace EllieBot.Db; namespace EllieBot.Db;
@ -17,6 +16,8 @@ public static class SelfAssignableRolesExtensions
return true; 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(); => roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray();
} }

View file

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

View file

@ -11,9 +11,9 @@ public class SlotGame
{ {
var rolls = new[] var rolls = new[]
{ {
(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) (byte)_rng.Next(0, 7)
}; };
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 == 5) if (a == 6)
{ {
winType = SlotWinType.TrippleJoker; winType = SlotWinType.TrippleJoker;
multi = 30; multi = 25;
} }
else else
{ {
winType = SlotWinType.TrippleNormal; winType = SlotWinType.TrippleNormal;
multi = 10; multi = 15;
} }
} }
else if (a == 5 && (b == 5 || c == 5) else if (a == 6 && (b == 6 || c == 6)
|| (b == 5 && c == 5)) || (b == 6 && c == 6))
{ {
winType = SlotWinType.DoubleJoker; 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; 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; } = 5; public int Version { get; set; } = 6;
public CoinData Coins { get; set; } public CoinData Coins { get; set; }
public Uri[] Currency { get; set; } public Uri[] Currency { get; set; }

View file

@ -12,7 +12,11 @@ public sealed partial class ReplacementPatternStore
{ {
Register("%bot.time%", Register("%bot.time%",
static () 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() 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, usrMsg.Author.Id.ToString(),
usrMsg.Content.TrimTo(10)); usrMsg.Content.TrimTo(10));
} }

View file

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

View file

@ -1,8 +1,11 @@
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore; 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;
namespace EllieBot.Services; namespace EllieBot.Services;
@ -10,15 +13,11 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
{ {
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[] private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
{ {
"lula", "lula", "betroll", "betflip", "blackjack", "betdraw", "slot",
"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; private readonly DbService _db;
@ -28,19 +27,22 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
=> await Task.WhenAll(RunUserStatsCollector(), RunBetStatsCollector());
public async Task RunBetStatsCollector()
{ {
using var timer = new PeriodicTimer(TimeSpan.FromHours(1)); using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync()) while (await timer.WaitForNextTickAsync())
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await using var trans = await ctx.Database.BeginTransactionAsync();
try try
{ {
var keys = _stats.Keys; // update betstats
var keys = globalStats.Keys;
foreach (var key in keys) foreach (var key in keys)
{ {
if (_stats.TryRemove(key, out var stat)) if (globalStats.TryRemove(key, out var stat))
{ {
await ctx.GetTable<GamblingStats>() await ctx.GetTable<GamblingStats>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
@ -49,11 +51,13 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
Bet = stat.Bet, Bet = stat.Bet,
PaidOut = stat.PaidOut, PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow DateAdded = DateTime.UtcNow
}, old => new() },
old => new()
{ {
Bet = old.Bet + stat.Bet, Bet = old.Bet + stat.Bet,
PaidOut = old.PaidOut + stat.PaidOut, PaidOut = old.PaidOut + stat.PaidOut,
}, () => new() },
() => new()
{ {
Feature = key Feature = key
}); });
@ -62,49 +66,294 @@ public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "An error occurred in gambling tx tracker"); Log.Error(ex, "An error occurred in betstats gambling tx tracker");
}
finally
{
await trans.CommitAsync();
} }
} }
} }
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) if (txData is null)
return Task.CompletedTask; return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type)) if (_gamblingTypes.Contains(txData.Type))
{ {
_stats.AddOrUpdate(txData.Type, globalStats.AddOrUpdate(txData.Type,
_ => (0, amount), _ => (0, amount),
(_, old) => (old.Bet, old.PaidOut + 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; return Task.CompletedTask;
} }
public Task TrackRemove(long amount, TxData? txData) public Task TrackRemove(ulong userId, long amount, TxData? txData)
{ {
if (txData is null) if (txData is null)
return Task.CompletedTask; return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type)) if (_gamblingTypes.Contains(txData.Type))
{ {
_stats.AddOrUpdate(txData.Type, globalStats.AddOrUpdate(txData.Type,
_ => (amount, 0), _ => (amount, 0),
(_, old) => (old.Bet + amount, old.PaidOut)); (_, 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; 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() public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
return await ctx.Set<GamblingStats>() 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;
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 public sealed class BlacklistService : IExecOnMessage, IReadyExecutor
{ {
public int Priority public int Priority
=> int.MaxValue; => int.MaxValue;
@ -15,69 +15,114 @@ public sealed class BlacklistService : IExecOnMessage
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 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; _db = db;
_pubSub = pubSub; _pubSub = pubSub;
_creds = creds; _creds = creds;
_client = client;
Reload(false); _pubSub.Sub(_blPubKey, async _ => await 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)
{ {
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; 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,
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id); guild.Id.ToString());
return Task.FromResult(true); return Task.FromResult(true);
} }
if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id) if (blacklistedChannels.Contains(usrMsg.Channel.Id))
{ {
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]", Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
usrMsg.Channel.Name, usrMsg.Channel.Name,
usrMsg.Channel.Id); usrMsg.Channel.Id.ToString());
return Task.FromResult(true);
} }
if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
if (blacklistedUsers.Contains(usrMsg.Author.Id))
{ {
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]", Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
usrMsg.Author.ToString(), usrMsg.Author.ToString(),
usrMsg.Author.Id); usrMsg.Author.Id.ToString());
return Task.FromResult(true); return Task.FromResult(true);
} }
}
return Task.FromResult(false); return Task.FromResult(false);
} }
public IReadOnlyList<BlacklistEntry> GetBlacklist() public async Task<IReadOnlyList<BlacklistEntry>> GetBlacklist(BlacklistType type)
=> blacklist;
public void Reload(bool publish = true)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var toPublish = uow.GetTable<BlacklistEntry>().ToArray();
blacklist = toPublish; 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) 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)
@ -105,7 +150,7 @@ public sealed class BlacklistService : IExecOnMessage
}); });
} }
Reload(); await Reload();
} }
public async Task UnBlacklist(BlacklistType type, ulong id) public async Task UnBlacklist(BlacklistType type, ulong id)
@ -115,7 +160,7 @@ public sealed class BlacklistService : IExecOnMessage
.Where(bi => bi.ItemId == id && bi.Type == type) .Where(bi => bi.ItemId == id && bi.Type == type)
.DeleteAsync(); .DeleteAsync();
Reload(); await Reload();
} }
public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist) public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
@ -136,6 +181,6 @@ public sealed class BlacklistService : IExecOnMessage
CurrencyAmount = 0 CurrencyAmount = 0
}); });
Reload(); await Reload();
} }
} }

View file

@ -27,5 +27,22 @@ 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,5 +1,4 @@
#nullable disable using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
namespace EllieBot.Common.TypeReaders.Models; 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)?$", @"^(?:(?<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; } public string Input { get; set; } = string.Empty;
public TimeSpan Time { get; set; } public TimeSpan Time { get; set; } = default;
private StoopidTime() { } private StoopidTime() { }
@ -53,8 +52,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

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

View file

@ -2756,19 +2756,6 @@
} }
], ],
"Gambling": [ "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": [ "Aliases": [
".timely" ".timely"
@ -3012,6 +2999,20 @@
"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"
@ -3119,6 +3120,70 @@
"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",
@ -3583,10 +3648,25 @@
"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" ".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.", "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": [
@ -6160,6 +6240,24 @@
"Options": null, "Options": null,
"Requirements": [] "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": [ "Aliases": [
".xkcd" ".xkcd"
@ -6508,6 +6606,20 @@
"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: 9 version: 12
# 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: 66 - whenAbove: 65
multiplyBy: 2 multiplyBy: 2
# Automatic currency generation settings. # Automatic currency generation settings.
generation: generation:
@ -56,8 +56,9 @@ timely:
# How often (in hours) can users claim currency with .timely command # How often (in hours) can users claim currency with .timely command
# setting to 0 or less will disable this feature # setting to 0 or less will disable this feature
cooldown: 12 cooldown: 12
# Whether the users are required to type a password when they do timely. # How will timely be protected?
requirePassword: true # 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. # How much will each user's owned currency decay over time.
decay: decay:
# Percentage of user's current currency which will be deducted every 24h. # Percentage of user's current currency which will be deducted every 24h.
@ -84,7 +85,7 @@ luckyLadder:
- 2.4 - 2.4
- 1.7 - 1.7
- 1.5 - 1.5
- 1.2 - 1.1
- 0.5 - 0.5
- 0.3 - 0.3
- 0.2 - 0.2
@ -273,3 +274,9 @@ voteReward: 100
slots: slots:
# Hex value of the color which the numbers on the slot image will have. # Hex value of the color which the numbers on the slot image will have.
currencyFontColor: ff0000 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 # DO NOT CHANGE
version: 5 version: 6
coins: coins:
heads: heads:
- https://cdn.nadeko.bot/coins/heads3.png - https://cdn.nadeko.bot/coins/heads3.png
@ -22,15 +22,13 @@ 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/0.png - https://cdn.nadeko.bot/slots/10.png
- https://cdn.nadeko.bot/slots/1.png - https://cdn.nadeko.bot/slots/11.png
- https://cdn.nadeko.bot/slots/2.png - https://cdn.nadeko.bot/slots/12.png
- https://cdn.nadeko.bot/slots/3.png - https://cdn.nadeko.bot/slots/13.png
- https://cdn.nadeko.bot/slots/4.png - https://cdn.nadeko.bot/slots/14.png
- https://cdn.nadeko.bot/slots/5.png - https://cdn.nadeko.bot/slots/15.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 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. You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 5 - 5
@ -2703,7 +2703,7 @@ eventstart:
desc: "The type of event being started." desc: "The type of event being started."
options: options:
desc: "The optional option flags for the event." desc: "The optional option flags for the event."
betstats: gamblestats:
desc: |- desc: |-
Shows the total stats of several gambling features. Shows the total stats of several gambling features.
Updates once an hour. Updates once an hour.
@ -2711,6 +2711,13 @@ betstats:
- '' - ''
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.
@ -2738,6 +2745,12 @@ 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:
@ -4642,3 +4655,51 @@ 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:
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.", "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}"
} }

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
}