Compare commits

...

40 Commits

Author SHA1 Message Date
Kwoth
7f5e065c4d docs: updated commandlist, version upped to 5.1.20 2024-11-10 15:56:43 +00:00
Kwoth
86b214163a fix: typo 2024-11-10 12:28:06 +00:00
Kwoth
9dbb08d85f add: added .snipe command
add: added .gsreset and .bsreset commands
change: improved .timely rewards for patrons
dev: Improved how blacklist works under the hood
2024-11-08 18:24:50 +00:00
Kwoth
89ca56c77c fix: rich logging fix for userids, updated commandlist
add: added bot.date and changed bot.time placeholder. They use timestamp tags now.
fix: fixed double log on server leave
2024-11-07 05:38:22 +00:00
Kwoth
9e96679099 change: patreon reward bonuses increased slightly 2024-11-06 07:10:12 +00:00
Kwoth
1280d2b397 change: .divorce no longer has a cooldown
add: Added .waifuclaims / .claims command which lists your waifus (name, price and ids)
change: Timely now shows patreon multiplier bonus if there is any, (alongside boost)
2024-11-06 05:20:04 +00:00
Kwoth
c731127607 fix: missing example in commands 2024-11-05 09:40:01 +00:00
Kwoth
c15930306a fix: fix pipeline, missing aliases 2024-11-05 09:34:01 +00:00
Kwoth
701501d678 add: 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
add: slot now has 1 more icon (wheat!), and multipliers have been modified to even out the gains
change: betroll is improved (around 2% better payout), as 66 is now a winning number, not a losing one
2024-11-05 08:24:21 +00:00
Kwoth
fa12fcea58 change: .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 04:27:27 +00:00
Kwoth
274219c40b docs: Upped version to 5.1.19, updated changelog
fix: Fixed timely on different shards
2024-11-05 02:57:01 +00:00
Kwoth
96c9b47da2 add: timely now has an option in gambling whether to use no protection, captcha, or button.
fix: grpc api fix for dashy
2024-11-04 14:35:59 +00:00
Kwoth
b5d1469df1 Merge branch 'v5' of https://gitlab.com/kwoth/nadekobot into v5 2024-11-04 12:28:52 +00:00
Kwoth
d7747bd25a fix: timely fixes 2024-11-04 12:28:42 +00:00
Kwoth
7d162d1f04 fix: timely fixes 2024-11-04 12:28:01 +00:00
Kwoth
704d061d46 fix: fixed pipeline, added missing strings 2024-11-04 10:58:44 +00:00
Kwoth
c39c9061fd add: added timely boost bonus to gambling.yml
change: .betstats renamed to .gamblestats/.gs
add: added .betstats, .betstats <game> and .betstats <user> <game?> command which shows you your stats for gambling commands
2024-11-04 10:42:05 +00:00
Kwoth
619ddba4f8 fix: fixed pagination numbers in xplb and xpglb 2024-11-04 02:03:53 +00:00
Kwoth
3acef04b32 change: strikeout slightly thinner to make password easier to read on plants 2024-11-03 13:45:12 +00:00
Kwoth
83a1d959b1 fix: Added nordic and ugro finnic languages to flag translate 2024-11-03 12:05:51 +00:00
Kwoth
a1632722bc fix: fix timely 2024-11-03 09:32:43 +00:00
Kwoth
ee0a28afab fix: revert patron migration temporarily as ef core is bugging out hard 2024-11-03 08:39:37 +00:00
Kwoth
2b301c0aab fix: possible fix for patron table 2024-11-03 08:31:23 +00:00
Kwoth
b6b6b4e19e fix: Fixed UserId patron table error
fix: Added au and kz countries as en and kz languages respectively
fix: Strikeout is thinner now on plants
2024-11-03 08:24:00 +00:00
Kwoth
32fc8b6e03 docs: Upped version to 5.1.18, updated changelog 2024-11-03 03:44:33 +00:00
Kwoth
297e2fde0e change: timely 'password' is now a button 2024-11-03 03:41:34 +00:00
Kwoth
729f26caab button for timely 2024-11-03 02:48:39 +00:00
Kwoth
4b12e4e923 dev: Removed discrim from the database
add: .translateflags command
add: captcha to timely, configurable in .conf gambling
change: change bonuses for patreon rewards
fix: nunchi message color fix
2024-11-02 16:23:58 +00:00
Kwoth
12f4ce7f2a change: animal race will update more frequently, but animals will move slightly slower. Overall everything will be slightly faster 2024-11-01 04:53:20 +00:00
Kwoth
00944e08c3 fix: .ncs will now show an error if setting a pixel fails 2024-11-01 04:52:29 +00:00
Kwoth
569abd7194 api: work on server xp api 2024-10-31 11:48:31 +00:00
Kwoth
474a1db41d add: timely now has a 3 letter password by default. Configurable via .conf gamb 2024-10-31 11:48:09 +00:00
Kwoth
0f6255947e fix: fixed ubl pagination 2024-10-30 13:16:04 +00:00
Kwoth
f68f219a25 fix: ytdataapiv3 searches will no longer duplicate youtube urls 2024-10-30 07:02:13 +00:00
Kwoth
8f16b11d02 api: finance api implementation 2024-10-29 08:15:53 +00:00
Kwoth
df5eced904 change: Error sending greet dm will now be a warning
change: initial canvas price down to 3 from 10, 10 is way too expensive
2024-10-29 01:53:42 +00:00
Kwoth
1dcd158f43 fix: Bot will now not accept .aar Role if that Role is higher than or equal to bot's role. Previously bot would just fail silently, now there is a proper error message. 2024-10-28 21:42:05 +00:00
Kwoth
757c9b564d Updated changelog. Version upped to 5.1.16 2024-10-28 08:21:27 +00:00
Kwoth
07cef3eb5e Added .nc and related commands.
You can set pixel colors (and text) on a 500x350 canvas, pepega version of r/place
You use currency to set pixels.
see whole canvas: .nc
set pixel: .ncsp <pos> <color> <text?>
get pixel: .ncp <pos>
zoom: .ncz <pos> or .ncz x y
2024-10-28 08:17:23 +00:00
Kwoth
85c525e19b api: added command feed and shard update feed 2024-10-23 21:29:40 +00:00
110 changed files with 32247 additions and 929 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
dotnet ef migrations remove -c SqliteContext -f -p src/NadekoBot/NadekoBot.csproj
dotnet ef migrations remove -c PostgreSqlContext -f -p src/NadekoBot/NadekoBot.csproj

View File

@@ -0,0 +1,47 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/empty.proto";
package ncanvas;
service GrpcNCanvas {
rpc GetCanvas(google.protobuf.Empty) returns (CanvasReply);
rpc GetPixel(GetPixelRequest) returns (GetPixelReply);
rpc SetPixel(SetPixelRequest) returns (SetPixelReply);
}
message CanvasReply {
repeated uint32 pixels = 1;
int32 width = 2;
int32 height = 3;
}
message GetPixelRequest {
int32 x = 1;
int32 y = 2;
}
message GetPixelReply {
string color = 1;
uint32 packedColor = 2;
int32 positionX = 3;
int32 positionY = 4;
int64 price = 5;
string text = 6;
string position = 7;
}
message SetPixelRequest {
string position = 1;
string color = 2;
string text = 3;
int64 price = 4;
}
message SetPixelReply {
string error = 1;
bool success = 2;
optional GetPixelReply pixel = 3;
}

View File

@@ -1,26 +0,0 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package econ;
service GrpcEcon {
rpc GetEconomy(EconomyRequest) returns (EconomyReply);
}
message EconomyRequest {
string guildId = 1;
}
message EconomyReply {
uint64 totalOwned = 1;
uint64 byTopOnePercent = 2;
uint64 plantedAmount = 3;
uint64 ownedByTheBot = 4;
uint64 inTheBank = 5;
uint64 totalEconomy = 6;
}
message CurrencyLbRequest {
int32 page = 1;
}

View File

@@ -0,0 +1,60 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/timestamp.proto";
package fin;
service GrpcFin {
rpc GetTransactions(GetTransactionsRequest) returns (GetTransactionsReply);
rpc GetHoldings(GetHoldingsRequest) returns (GetHoldingsReply);
rpc Withdraw(WithdrawRequest) returns (WithdrawReply);
rpc Deposit(DepositRequest) returns (DepositReply);
}
message GetTransactionsRequest {
int32 page = 1;
uint64 userId = 2;
}
message GetTransactionsReply {
repeated TransactionReply transactions = 1;
int32 total = 2;
}
message TransactionReply {
int64 amount = 1;
string note = 2;
string type = 3;
string extra = 4;
google.protobuf.Timestamp timestamp = 5;
string id = 6;
}
message GetHoldingsRequest {
uint64 userId = 1;
}
message GetHoldingsReply {
int64 cash = 1;
int64 bank = 2;
}
message WithdrawRequest {
uint64 userId = 1;
int64 amount = 2;
}
message WithdrawReply {
bool success = 1;
}
message DepositRequest {
uint64 userId = 1;
int64 amount = 2;
}
message DepositReply {
bool success = 1;
}

View File

@@ -3,13 +3,11 @@ syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi"; option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
package other; package other;
service GrpcOther { service GrpcOther {
rpc BotOnGuild(BotOnGuildRequest) returns (BotOnGuildReply); rpc BotOnGuild(BotOnGuildRequest) returns (BotOnGuildReply);
rpc GetGuilds(google.protobuf.Empty) returns (GetGuildsReply);
rpc GetTextChannels(GetTextChannelsRequest) returns (GetTextChannelsReply); rpc GetTextChannels(GetTextChannelsRequest) returns (GetTextChannelsReply);
rpc GetRoles(GetRolesRequest) returns (GetRolesReply); rpc GetRoles(GetRolesRequest) returns (GetRolesReply);
@@ -17,10 +15,15 @@ service GrpcOther {
rpc GetXpLb(GetLbRequest) returns (XpLbReply); rpc GetXpLb(GetLbRequest) returns (XpLbReply);
rpc GetWaifuLb(GetLbRequest) returns (WaifuLbReply); rpc GetWaifuLb(GetLbRequest) returns (WaifuLbReply);
rpc GetShardStatuses(google.protobuf.Empty) returns (GetShardStatusesReply); rpc GetShardStats(google.protobuf.Empty) returns (stream ShardStatsReply);
rpc GetCommandFeed(google.protobuf.Empty) returns (stream CommandFeedEntry);
rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply); rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply);
} }
message CommandFeedEntry {
string command = 1;
}
message GetRolesRequest { message GetRolesRequest {
uint64 guildId = 1; uint64 guildId = 1;
} }
@@ -37,26 +40,13 @@ message BotOnGuildReply {
bool success = 1; bool success = 1;
} }
message GetGuildsReply { message ShardStatsReply {
repeated GuildReply guilds = 1;
}
message GuildReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
}
message GetShardStatusesReply {
repeated ShardStatusReply shards = 1;
}
message ShardStatusReply {
int32 id = 1; int32 id = 1;
string status = 2; string status = 2;
int32 guildCount = 3; int32 guildCount = 3;
google.protobuf.Timestamp lastUpdate = 4; string uptime = 4;
int64 commands = 5;
} }
message GetTextChannelsRequest{ message GetTextChannelsRequest{

View File

@@ -0,0 +1,120 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package xp;
service GrpcXp {
rpc GetXpLb(GetXpLbRequest) returns (GetXpLbReply);
rpc ResetUserXp(ResetUserXpRequest) returns (ResetUserXpReply);
rpc GetXpSettings(GetXpSettingsRequest) returns (GetXpSettingsReply);
rpc AddExclusion(AddExclusionRequest) returns (AddExclusionReply);
rpc DeleteExclusion(DeleteExclusionRequest) returns (DeleteExclusionReply);
rpc AddReward(AddRewardRequest) returns (AddRewardReply);
rpc DeleteReward(DeleteRewardRequest) returns (DeleteRewardReply);
rpc SetServerExclusion(SetServerExclusionRequest) returns (SetServerExclusionReply);
}
message SetServerExclusionRequest {
uint64 guildId = 1;
bool serverExcluded = 2;
}
message SetServerExclusionReply {
bool success = 1;
}
message GetXpLbRequest {
uint64 guildId = 1;
int32 page = 2;
}
message GetXpLbReply {
repeated XpLbUserReply users = 1;
int32 total = 2;
}
message XpLbUserReply {
uint64 userId = 1;
string username = 2;
int64 xp = 3;
int64 level = 4;
int64 levelPercent = 5;
string avatar = 6;
}
message ResetUserXpRequest {
uint64 guildId = 1;
uint64 userId = 2;
}
message ResetUserXpReply {
bool success = 1;
}
message GetXpSettingsReply {
repeated ExclItemReply exclusions = 1;
repeated RewItemReply rewards = 2;
bool serverExcluded = 3;
}
message GetXpSettingsRequest {
uint64 guildId = 1;
}
message ExclItemReply {
string type = 1;
uint64 id = 2;
string name = 3;
}
message RewItemReply {
int32 level = 1;
string type = 2;
string value = 3;
}
message AddExclusionRequest {
uint64 guildId = 1;
string type = 2;
uint64 id = 3;
}
message AddExclusionReply {
bool success = 1;
}
message DeleteExclusionRequest {
uint64 guildId = 1;
string type = 2;
uint64 id = 3;
}
message DeleteExclusionReply {
bool success = 1;
}
message AddRewardRequest {
uint64 guildId = 1;
int32 level = 2;
string type = 3;
string value = 4;
}
message AddRewardReply {
bool success = 1;
}
message DeleteRewardRequest {
uint64 guildId = 1;
int32 level = 2;
string type = 3;
}
message DeleteRewardReply {
bool success = 1;
}

View File

@@ -25,7 +25,6 @@ public static class DiscordUserExtensions
{ {
UserId = userId, UserId = userId,
Username = username, Username = username,
Discriminator = discrim,
AvatarId = avatarId, AvatarId = avatarId,
TotalXp = 0, TotalXp = 0,
CurrencyAmount = 0 CurrencyAmount = 0
@@ -33,7 +32,6 @@ public static class DiscordUserExtensions
old => new() old => new()
{ {
Username = username, Username = username,
Discriminator = discrim,
AvatarId = avatarId AvatarId = avatarId
}, },
() => new() () => new()
@@ -49,8 +47,7 @@ public static class DiscordUserExtensions
() => new() () => new()
{ {
UserId = userId, UserId = userId,
Username = "Unknown", Username = "??Unknown",
Discriminator = "????",
AvatarId = string.Empty, AvatarId = string.Empty,
TotalXp = 0, TotalXp = 0,
CurrencyAmount = 0 CurrencyAmount = 0

View File

@@ -44,9 +44,6 @@ public static class UserXpExtensions
.CountAsyncLinqToDB() .CountAsyncLinqToDB()
+ 1; + 1;
public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId) public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
=> xps.Delete(x => x.GuildId == guildId); => xps.Delete(x => x.GuildId == guildId);

View File

@@ -7,7 +7,7 @@ public class DiscordUser : DbEntity
{ {
public ulong UserId { get; set; } public ulong UserId { get; set; }
public string Username { get; set; } public string Username { get; set; }
public string Discriminator { get; set; } // public string Discriminator { get; set; }
public string AvatarId { get; set; } public string AvatarId { get; set; }
public int? ClubId { get; set; } public int? ClubId { get; set; }
@@ -27,9 +27,6 @@ public class DiscordUser : DbEntity
public override string ToString() public override string ToString()
{ {
if (string.IsNullOrWhiteSpace(Discriminator) || Discriminator == "0000") return Username;
return Username;
return Username + "#" + Discriminator;
} }
} }

View File

@@ -0,0 +1,8 @@
#nullable disable
namespace NadekoBot.Db.Models;
public class FlagTranslateChannel : DbEntity
{
public ulong GuildId { get; set; }
public ulong ChannelId { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Db.Models;
public class NCPixel
{
[Key]
public int Id { get; set; }
public required int Position { get; init; }
public required long Price { get; init; }
public required ulong OwnerId { get; init; }
public required uint Color { get; init; }
[MaxLength(256)]
public required string Text { get; init; }
}

View File

@@ -1,8 +1,12 @@
#nullable disable #nullable disable
using System.ComponentModel.DataAnnotations;
namespace NadekoBot.Db.Models; namespace NadekoBot.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

@@ -61,6 +61,7 @@ public abstract class NadekoContext : DbContext
public DbSet<TodoModel> Todos { get; set; } public DbSet<TodoModel> Todos { get; set; }
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,40 @@ public abstract class NadekoContext : 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
modelBuilder.Entity<FlagTranslateChannel>()
.HasIndex(x => new { x.GuildId, x.ChannelId })
.IsUnique();
#endregion
#region NCanvas
modelBuilder.Entity<NCPixel>()
.HasAlternateKey(x => x.Position);
modelBuilder.Entity<NCPixel>()
.HasIndex(x => x.OwnerId);
#endregion
#region QUOTES #region QUOTES
var quoteEntity = modelBuilder.Entity<Quote>(); var quoteEntity = modelBuilder.Entity<Quote>();

View File

@@ -5,6 +5,11 @@ namespace NadekoBot.Migrations;
public static class MigrationQueries public static class MigrationQueries
{ {
public static void UpdateUsernames(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE DiscordUser SET Username = '??' + Username WHERE Discriminator = '????';");
}
public static void MigrateRero(MigrationBuilder migrationBuilder) public static void MigrateRero(MigrationBuilder migrationBuilder)
{ {
if (migrationBuilder.IsSqlite()) if (migrationBuilder.IsSqlite())

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class ncanvas : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ncpixel",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
position = table.Column<int>(type: "integer", nullable: false),
price = table.Column<long>(type: "bigint", nullable: false),
ownerid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
color = table.Column<long>(type: "bigint", nullable: false),
text = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_ncpixel", x => x.id);
table.UniqueConstraint("ak_ncpixel_position", x => x.position);
});
migrationBuilder.CreateIndex(
name: "ix_discorduser_username",
table: "discorduser",
column: "username");
migrationBuilder.CreateIndex(
name: "ix_ncpixel_ownerid",
table: "ncpixel",
column: "ownerid");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ncpixel");
migrationBuilder.DropIndex(
name: "ix_discorduser_username",
table: "discorduser");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class nodiscrimandflagtranslate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn(
name: "discriminator",
table: "discorduser");
migrationBuilder.CreateTable(
name: "flagtranslatechannel",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_flagtranslatechannel", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_flagtranslatechannel_guildid_channelid",
table: "flagtranslatechannel",
columns: new[] { "guildid", "channelid" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "flagtranslatechannel");
migrationBuilder.AddColumn<string>(
name: "discriminator",
table: "discorduser",
type: "text",
nullable: true);
}
}
}

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 NadekoBot.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 NadekoBot.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

@@ -751,10 +751,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone") .HasColumnType("timestamp without time zone")
.HasColumnName("dateadded"); .HasColumnName("dateadded");
b.Property<string>("Discriminator")
.HasColumnType("text")
.HasColumnName("discriminator");
b.Property<bool>("IsClubAdmin") b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("boolean") .HasColumnType("boolean")
@@ -799,6 +795,9 @@ namespace NadekoBot.Migrations.PostgreSql
b.HasIndex("UserId") b.HasIndex("UserId")
.HasDatabaseName("ix_discorduser_userid"); .HasDatabaseName("ix_discorduser_userid");
b.HasIndex("Username")
.HasDatabaseName("ix_discorduser_username");
b.ToTable("discorduser", (string)null); b.ToTable("discorduser", (string)null);
}); });
@@ -995,6 +994,37 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("filteredword", (string)null); b.ToTable("filteredword", (string)null);
}); });
modelBuilder.Entity("NadekoBot.Db.Models.FlagTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.HasKey("Id")
.HasName("pk_flagtranslatechannel");
b.HasIndex("GuildId", "ChannelId")
.IsUnique()
.HasDatabaseName("ix_flagtranslatechannel_guildid_channelid");
b.ToTable("flagtranslatechannel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1627,6 +1657,49 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("muteduserid", (string)null); b.ToTable("muteduserid", (string)null);
}); });
modelBuilder.Entity("NadekoBot.Db.Models.NCPixel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("Color")
.HasColumnType("bigint")
.HasColumnName("color");
b.Property<decimal>("OwnerId")
.HasColumnType("numeric(20,0)")
.HasColumnName("ownerid");
b.Property<int>("Position")
.HasColumnType("integer")
.HasColumnName("position");
b.Property<long>("Price")
.HasColumnType("bigint")
.HasColumnName("price");
b.Property<string>("Text")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)")
.HasColumnName("text");
b.HasKey("Id")
.HasName("pk_ncpixel");
b.HasAlternateKey("Position")
.HasName("ak_ncpixel_position");
b.HasIndex("OwnerId")
.HasDatabaseName("ix_ncpixel_ownerid");
b.ToTable("ncpixel", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b => modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -3154,6 +3227,74 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("greetsettings", (string)null); b.ToTable("greetsettings", (string)null);
}); });
modelBuilder.Entity("NadekoBot.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("NadekoBot.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("NadekoBot.Db.Models.AntiAltSetting", b => modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b =>
{ {
b.HasOne("NadekoBot.Db.Models.GuildConfig", null) b.HasOne("NadekoBot.Db.Models.GuildConfig", null)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
/// <inheritdoc />
public partial class ncanvas : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "NCPixel",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Position = table.Column<int>(type: "INTEGER", nullable: false),
Price = table.Column<long>(type: "INTEGER", nullable: false),
OwnerId = table.Column<ulong>(type: "INTEGER", nullable: false),
Color = table.Column<uint>(type: "INTEGER", nullable: false),
Text = table.Column<string>(type: "TEXT", maxLength: 256, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_NCPixel", x => x.Id);
table.UniqueConstraint("AK_NCPixel_Position", x => x.Position);
});
migrationBuilder.CreateIndex(
name: "IX_DiscordUser_Username",
table: "DiscordUser",
column: "Username");
migrationBuilder.CreateIndex(
name: "IX_NCPixel_OwnerId",
table: "NCPixel",
column: "OwnerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "NCPixel");
migrationBuilder.DropIndex(
name: "IX_DiscordUser_Username",
table: "DiscordUser");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,55 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
/// <inheritdoc />
public partial class nodiscrimandflagtranslate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
MigrationQueries.UpdateUsernames(migrationBuilder);
migrationBuilder.DropColumn(
name: "Discriminator",
table: "DiscordUser");
migrationBuilder.CreateTable(
name: "FlagTranslateChannel",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_FlagTranslateChannel", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_FlagTranslateChannel_GuildId_ChannelId",
table: "FlagTranslateChannel",
columns: new[] { "GuildId", "ChannelId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "FlagTranslateChannel");
migrationBuilder.AddColumn<string>(
name: "Discriminator",
table: "DiscordUser",
type: "TEXT",
nullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.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 NadekoBot.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

@@ -560,9 +560,6 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded") b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("Discriminator")
.HasColumnType("TEXT");
b.Property<bool>("IsClubAdmin") b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
@@ -596,6 +593,8 @@ namespace NadekoBot.Migrations
b.HasIndex("UserId"); b.HasIndex("UserId");
b.HasIndex("Username");
b.ToTable("DiscordUser"); b.ToTable("DiscordUser");
}); });
@@ -741,6 +740,29 @@ namespace NadekoBot.Migrations
b.ToTable("FilteredWord"); b.ToTable("FilteredWord");
}); });
modelBuilder.Entity("NadekoBot.Db.Models.FlagTranslateChannel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GuildId", "ChannelId")
.IsUnique();
b.ToTable("FlagTranslateChannel");
});
modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -1213,6 +1235,38 @@ namespace NadekoBot.Migrations
b.ToTable("MutedUserId"); b.ToTable("MutedUserId");
}); });
modelBuilder.Entity("NadekoBot.Db.Models.NCPixel", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<uint>("Color")
.HasColumnType("INTEGER");
b.Property<ulong>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("Position")
.HasColumnType("INTEGER");
b.Property<long>("Price")
.HasColumnType("INTEGER");
b.Property<string>("Text")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasAlternateKey("Position");
b.HasIndex("OwnerId");
b.ToTable("NCPixel");
});
modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b => modelBuilder.Entity("NadekoBot.Db.Models.NadekoExpression", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
@@ -2345,6 +2399,58 @@ namespace NadekoBot.Migrations
b.ToTable("GreetSettings"); b.ToTable("GreetSettings");
}); });
modelBuilder.Entity("NadekoBot.Services.Rakeback", b =>
{
b.Property<ulong>("UserId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal>("Amount")
.HasColumnType("TEXT");
b.HasKey("UserId");
b.ToTable("Rakeback");
});
modelBuilder.Entity("NadekoBot.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("NadekoBot.Db.Models.AntiAltSetting", b => modelBuilder.Entity("NadekoBot.Db.Models.AntiAltSetting", b =>
{ {
b.HasOne("NadekoBot.Db.Models.GuildConfig", null) b.HasOne("NadekoBot.Db.Models.GuildConfig", null)

View File

@@ -24,6 +24,13 @@ public partial class Administration
await Response().Error(strs.hierarchy).SendAsync(); await Response().Error(strs.hierarchy).SendAsync();
return; return;
} }
// the user can't aar the role which is greater or equal to the bot's highest role
if (role.Position >= ((SocketGuild)ctx.Guild).CurrentUser.GetRoles().Max(x => x.Position))
{
await Response().Error(strs.hierarchy).SendAsync();
return;
}
var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id); var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id);
if (roles.Count == 0) if (roles.Count == 0)

View File

@@ -339,7 +339,7 @@ public class GreetService : INService, IReadyExecutor
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Error sending greet dm"); Log.Warning(ex, "Unable to send Greet DM. Probably the user has closed DMs");
return false; return false;
} }

View File

@@ -71,7 +71,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
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
{ {
@@ -453,7 +452,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
{ {
x.UserId, x.UserId,
x.Username, x.Username,
x.Discriminator
}) })
.Where(x => users.Select(y => y.Id).Contains(x.UserId)) .Where(x => users.Select(y => y.Id).Contains(x.UserId))
.ToArrayAsyncEF(); .ToArrayAsyncEF();
@@ -465,12 +463,11 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
UserId = x.Id, UserId = x.Id,
AvatarId = x.AvatarId, AvatarId = x.AvatarId,
Username = x.Username, Username = x.Username,
Discriminator = x.Discriminator
}); });
var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied; var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied;
var toUpdateUserIds = presentDbUsers var toUpdateUserIds = presentDbUsers
.Where(x => x.Username == "Unknown" && x.Discriminator == "????") .Where(x => x.Username.StartsWith("??"))
.Select(x => x.UserId) .Select(x => x.UserId)
.ToArray(); .ToArray();
@@ -481,7 +478,6 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, INService
.UpdateAsync(x => new DiscordUser() .UpdateAsync(x => new DiscordUser()
{ {
Username = user.Username, Username = user.Username,
Discriminator = user.Discriminator,
// .award tends to set AvatarId and DateAdded to NULL, so account for that. // .award tends to set AvatarId and DateAdded to NULL, so account for that.
AvatarId = user.AvatarId, AvatarId = user.AvatarId,

View File

@@ -2,7 +2,6 @@
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.TypeReaders.Models;
using NadekoBot.Modules.Permissions.Services; using NadekoBot.Modules.Permissions.Services;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using Newtonsoft.Json; using Newtonsoft.Json;

View File

@@ -6,6 +6,10 @@ namespace NadekoBot.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);
@@ -116,7 +120,7 @@ public sealed class AnimalRace : IDisposable
{ {
foreach (var user in _users) foreach (var user in _users)
{ {
user.Progress += rng.Next(1, 11); user.Progress += rng.Next(1, 10);
if (user.Progress >= 60) if (user.Progress >= 60)
user.Progress = 60; user.Progress = 60;
} }
@@ -126,13 +130,15 @@ public sealed class AnimalRace : IDisposable
FinishedUsers.AddRange(finished); FinishedUsers.AddRange(finished);
_ = OnStateUpdate?.Invoke(this); _ = OnStateUpdate?.Invoke(this);
await Task.Delay(2500); await Task.Delay(1750);
} }
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 NadekoBot.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()
winner.Animal.Icon, .WithTitle(GetText(strs.animal_race))
(race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign))) .WithDescription(GetText(strs.animal_race_won_money(
Format.Bold(winner.Username),
winner.Animal.Icon,
N(race.FinishedUsers[0].Bet * race.Multi))))
.WithFooter($"x{race.Multi:F2}"))
.SendAsync(); .SendAsync();
} }
@@ -113,14 +117,14 @@ public partial class Gambling
private async Task Ar_OnStateUpdate(AnimalRace race) private async Task Ar_OnStateUpdate(AnimalRace race)
{ {
var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| var text = $@"|🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁 🏁🔚|
{string.Join("\n", race.Users.Select(p => {string.Join("\n", race.Users.Select(p =>
{ {
var index = race.FinishedUsers.IndexOf(p); var index = race.FinishedUsers.IndexOf(p);
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;
@@ -129,10 +133,10 @@ public partial class Gambling
else else
{ {
await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed() await msg.ModifyAsync(x => x.Embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.animal_race)) .WithTitle(GetText(strs.animal_race))
.WithDescription(text) .WithDescription(text)
.WithOkColor() .WithOkColor()
.Build()); .Build());
} }
} }

View File

@@ -0,0 +1,175 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.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 NadekoBot.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 NadekoBot.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 NadekoBot.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 NadekoBot.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 NadekoBot.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 NadekoBot.Modules.Gambling.Rps; using NadekoBot.Modules.Gambling.Rps;
using NadekoBot.Common.TypeReaders; using NadekoBot.Common.TypeReaders;
using NadekoBot.Modules.Patronage; using NadekoBot.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 NadekoBot.Modules.Gambling; namespace NadekoBot.Modules.Gambling;
@@ -26,10 +33,12 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture; private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker; private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService; private readonly GamblingConfigService _configService;
private readonly FontProvider _fonts;
private readonly IBankService _bank; private readonly IBankService _bank;
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,
@@ -38,10 +47,12 @@ public partial class Gambling : GamblingModule<GamblingService>
DiscordSocketClient client, DiscordSocketClient client,
DownloadTracker tracker, DownloadTracker tracker,
GamblingConfigService configService, GamblingConfigService configService,
FontProvider fonts,
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;
@@ -51,13 +62,16 @@ 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 NadekoRandom();
_enUsCulture = new CultureInfo("en-US", false).NumberFormat; _enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0; _enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = ""; _enUsCulture.NumberGroupSeparator = "";
_tracker = tracker; _tracker = tracker;
_configService = configService; _configService = configService;
_fonts = fonts;
} }
public async Task<string> GetBalanceStringAsync(ulong userId) public async Task<string> GetBalanceStringAsync(ulong userId)
@@ -66,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)
{ {
@@ -140,7 +118,21 @@ public partial class Gambling : GamblingModule<GamblingService>
(smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(ms))) (smc) => RemindTimelyAction(smc, DateTime.UtcNow.Add(TimeSpan.FromMilliseconds(ms)))
); );
private NadekoInteractionBase CreateTimelyInteraction()
=> _inter
.Create(ctx.User.Id,
new ButtonBuilder(
label: "Timely",
emote: Emoji.Parse("💰"),
customId: "timely:" + _rng.Next(123456, 999999)),
async (smc) =>
{
await smc.DeferAsync();
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;
@@ -151,6 +143,71 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
if (Config.Timely.ProtType == TimelyProt.Button)
{
var interaction = CreateTimelyInteraction();
var msg = await Response().Pending(strs.timely_button).Interaction(interaction).SendAsync();
await msg.DeleteAsync();
return;
}
else if (Config.Timely.ProtType == TimelyProt.Captcha)
{
var password = _service.GeneratePassword();
var img = new Image<Rgba32>(70, 35);
var font = _fonts.NotoSans.CreateFont(30);
var outlinePen = new SolidPen(Color.Black, 1f);
var strikeoutRun = new RichTextRun
{
Start = 0,
End = password.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 3),
TextDecorations = TextDecorations.Strikeout
};
// draw password on the image
img.Mutate(x =>
{
x.DrawText(new RichTextOptions(font)
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(35, 17),
TextRuns = [strikeoutRun]
},
password,
Brushes.Solid(Color.White),
outlinePen);
});
using var stream = await img.ToStreamAsync();
var captcha = await Response()
// .Embed(_sender.CreateEmbed()
// .WithOkColor()
// .WithImageUrl("attachment://timely.png"))
.File(stream, "timely.png")
.SendAsync();
try
{
var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant())
{
return;
}
}
finally
{
_ = captcha.DeleteAsync();
}
}
await ClaimTimely();
}
private async Task ClaimTimely()
{
var period = Config.Timely.Cooldown;
if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder) if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
{ {
// Get correct time form remainder // Get correct time form remainder
@@ -169,6 +226,30 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
var val = Config.Timely.Amount;
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
var guildUsers = await boostGuilds
.Select(async gid =>
{
try
{
var guild = await _client.Rest.GetGuildAsync(gid, false);
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
return (guild, user);
}
catch
{
return default;
}
})
.WhenAll();
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
var booster = userInfo != default;
if (booster)
val += Config.BoostBonus.BaseTimelyBonus;
var patron = await _ps.GetPatronAsync(ctx.User.Id); var patron = await _ps.GetPatronAsync(ctx.User.Id);
var percentBonus = (_ps.PercentBonus(patron) / 100f); var percentBonus = (_ps.PercentBonus(patron) / 100f);
@@ -179,7 +260,21 @@ 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"));
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync(); var msg = GetText(strs.timely(N(val), period));
if (booster || percentBonus > float.Epsilon)
{
msg += "\n\n";
if (booster)
msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*";
if (percentBonus > float.Epsilon)
msg +=
$"*+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/nadekobot) pledge! <:hart:746995901758832712>*";
await Response().Confirm(msg).Interaction(inter).SendAsync();
}
else
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
} }
[Cmd] [Cmd]
@@ -290,8 +385,9 @@ 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(
?? $"{userId}"))) ((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithOkColor(); .WithOkColor();
var sb = new StringBuilder(); var sb = new StringBuilder();
@@ -547,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();
} }
} }
@@ -568,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();
} }
} }
@@ -762,6 +862,8 @@ public partial class Gambling : GamblingModule<GamblingService>
private static readonly ImmutableArray<string> _emojis = private static readonly ImmutableArray<string> _emojis =
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray(); new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
private readonly NadekoRandom _rng;
[Cmd] [Cmd]
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount) public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)
@@ -900,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 NadekoInteractionBase 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 NadekoBot.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; } = 8; 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,13 +110,26 @@ 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
setting to 0 or less will disable this feature setting to 0 or less will disable this feature
""")] """)]
public int Cooldown { get; set; } = 24; public int Cooldown { get; set; } = 24;
[Comment("""
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 TimelyProt ProtType { get; set; } = TimelyProt.Button;
}
public enum TimelyProt
{
None,
Button,
Captcha
} }
[Cloneable] [Cloneable]
@@ -145,7 +164,7 @@ public partial class BetRollConfig
}, },
new() new()
{ {
WhenAbove = 66, WhenAbove = 65,
MultiplyBy = 2 MultiplyBy = 2
} }
]; ];
@@ -207,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]
@@ -408,4 +427,15 @@ public sealed partial class BotCutConfig
Default 0.1 (10%). Default 0.1 (10%).
""")] """)]
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,6 +144,11 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
ConfigPrinters.ToString, ConfigPrinters.ToString,
val => val >= 0); val => val >= 0);
AddParsedProp("timely.prot",
gs => gs.Timely.ProtType,
ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString);
Migrate(); Migrate();
} }
@@ -167,22 +172,6 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 5)
{
ModifyConfig(c =>
{
c.Version = 5;
});
}
if (data.Version < 6)
{
ModifyConfig(c =>
{
c.Version = 6;
});
}
if (data.Version < 7) if (data.Version < 7)
{ {
ModifyConfig(c => ModifyConfig(c =>
@@ -199,5 +188,18 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
c.Waifu.Decay.UnclaimedDecayPercent = 0; c.Waifu.Decay.UnclaimedDecayPercent = 0;
}); });
} }
if (data.Version < 12)
{
ModifyConfig(c =>
{
c.Version = 12;
if (c.BetRoll.Pairs.Length == 3 && c.BetRoll.Pairs[2].WhenAbove == 66)
{
c.BetRoll.Pairs[2].WhenAbove = 65;
}
});
}
} }
} }

View File

@@ -16,6 +16,7 @@ public class GamblingService : INService, IReadyExecutor
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCache _cache; private readonly IBotCache _cache;
private readonly GamblingConfigService _gss; private readonly GamblingConfigService _gss;
private readonly NadekoRandom _rng;
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay"); private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
@@ -29,11 +30,19 @@ public class GamblingService : INService, IReadyExecutor
_client = client; _client = client;
_cache = cache; _cache = cache;
_gss = gss; _gss = gss;
_rng = new NadekoRandom();
} }
public Task OnReadyAsync() public Task OnReadyAsync()
=> Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync()); => Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync());
public string GeneratePassword()
{
var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3));
return new kwum(num).ToString();
}
private async Task TransactionClearLoopAsync() private async Task TransactionClearLoopAsync()
{ {
if (_client.ShardId != 0) if (_client.ShardId != 0)
@@ -52,7 +61,7 @@ public class GamblingService : INService, IReadyExecutor
var days = TimeSpan.FromDays(lifetime); var days = TimeSpan.FromDays(lifetime);
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.Set<CurrencyTransaction>() await uow.Set<CurrencyTransaction>()
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days); .DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -90,11 +99,11 @@ public class GamblingService : INService, IReadyExecutor
} }
Log.Information(""" Log.Information("""
--- Decaying users' currency --- --- Decaying users' currency ---
| decay: {ConfigDecayPercent}% | decay: {ConfigDecayPercent}%
| max: {MaxDecay} | max: {MaxDecay}
| threshold: {DecayMinTreshold} | threshold: {DecayMinTreshold}
""", """,
config.Decay.Percent * 100, config.Decay.Percent * 100,
maxDecay, maxDecay,
config.Decay.MinThreshold); config.Decay.MinThreshold);
@@ -104,14 +113,14 @@ public class GamblingService : INService, IReadyExecutor
var decay = (double)config.Decay.Percent; var decay = (double)config.Decay.Percent;
await uow.Set<DiscordUser>() await uow.Set<DiscordUser>()
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id) .Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
.UpdateAsync(old => new() .UpdateAsync(old => new()
{ {
CurrencyAmount = CurrencyAmount =
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5) maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5)) ? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
: old.CurrencyAmount - maxDecay : old.CurrencyAmount - maxDecay
}); });
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
@@ -133,6 +142,7 @@ public class GamblingService : INService, IReadyExecutor
private static TypedKey<Dictionary<ulong, long>> _timelyKey private static TypedKey<Dictionary<ulong, long>> _timelyKey
= new("timely:claims"); = new("timely:claims");
public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period) public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period)
{ {
if (period == 0) if (period == 0)
@@ -178,9 +188,10 @@ public class GamblingService : INService, IReadyExecutor
public bool UserHasTimelyReminder(ulong userId) public bool UserHasTimelyReminder(ulong userId)
{ {
var db = _db.GetDbContext(); var db = _db.GetDbContext();
return db.GetTable<Reminder>().Any(x => x.UserId == userId return db.GetTable<Reminder>()
&& x.Type == ReminderType.Timely); .Any(x => x.UserId == userId
} && x.Type == ReminderType.Timely);
}
public async Task RemoveAllTimelyClaimsAsync() public async Task RemoveAllTimelyClaimsAsync()
=> await _cache.RemoveAsync(_timelyKey); => await _cache.RemoveAsync(_timelyKey);

View File

@@ -57,12 +57,4 @@ public abstract class GamblingModule<TService> : NadekoModule<TService>
return Task.FromResult(true); return Task.FromResult(true);
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 NadekoBot.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

@@ -1,8 +1,11 @@
#nullable disable #nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using SixLabors.Fonts; using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@@ -25,6 +28,7 @@ public class PlantPickService : INService, IExecNoCommand
private readonly NadekoRandom _rng; private readonly NadekoRandom _rng;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly GamblingConfigService _gss; private readonly GamblingConfigService _gss;
private readonly GamblingService _gs;
private readonly ConcurrentHashSet<ulong> _generationChannels; private readonly ConcurrentHashSet<ulong> _generationChannels;
private readonly SemaphoreSlim _pickLock = new(1, 1); private readonly SemaphoreSlim _pickLock = new(1, 1);
@@ -37,7 +41,8 @@ public class PlantPickService : INService, IExecNoCommand
ICurrencyService cs, ICurrencyService cs,
CommandHandler cmdHandler, CommandHandler cmdHandler,
DiscordSocketClient client, DiscordSocketClient client,
GamblingConfigService gss) GamblingConfigService gss,
GamblingService gs)
{ {
_db = db; _db = db;
_strings = strings; _strings = strings;
@@ -48,6 +53,7 @@ public class PlantPickService : INService, IExecNoCommand
_rng = new(); _rng = new();
_client = client; _client = client;
_gss = gss; _gss = gss;
_gs = gs;
using var uow = db.GetDbContext(); using var uow = db.GetDbContext();
var guildIds = client.Guilds.Select(x => x.Id).ToList(); var guildIds = client.Guilds.Select(x => x.Id).ToList();
@@ -87,6 +93,7 @@ public class PlantPickService : INService, IExecNoCommand
var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd)); var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
if (toDelete is not null) if (toDelete is not null)
uow.Remove(toDelete); uow.Remove(toDelete);
_generationChannels.TryRemove(cid); _generationChannels.TryRemove(cid);
enabled = false; enabled = false;
} }
@@ -140,7 +147,7 @@ public class PlantPickService : INService, 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
@@ -152,13 +159,31 @@ public class PlantPickService : INService, 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));
var strikeoutRun = new RichTextRun
{
Start = 0,
End = pass.GetGraphemeCount(),
Font = font,
StrikeoutPen = new SolidPen(Color.White, 2),
TextDecorations = TextDecorations.Strikeout
};
// draw the password over the background // draw the password over the background
x.DrawText(pass, font, Color.White, new(0, 0)); x.DrawText(new RichTextOptions(font)
{
Origin = new(0, 0),
TextRuns =
[
strikeoutRun
]
},
pass,
new SolidBrush(Color.White));
}); });
// return image as a stream for easy sending // return image as a stream for easy sending
var format = img.Metadata.DecodedImageFormat; var format = img.Metadata.DecodedImageFormat;
@@ -208,7 +233,7 @@ public class PlantPickService : INService, IExecNoCommand
+ " " + " "
+ GetText(channel.GuildId, strs.pick_pl(prefix)); + GetText(channel.GuildId, strs.pick_pl(prefix));
var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null; var pw = config.Generation.HasPassword ? _gs.GeneratePassword().ToUpperInvariant() : null;
IUserMessage sent; IUserMessage sent;
var (stream, ext) = await GetRandomCurrencyImageAsync(pw); var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
@@ -232,67 +257,44 @@ public class PlantPickService : INService, IExecNoCommand
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Generate a hexadecimal string from 1000 to ffff.
/// </summary>
/// <returns>A hexadecimal string from 1000 to ffff</returns>
private string GenerateCurrencyPassword()
{
// generate a number from 1000 to ffff
var num = _rng.Next(4096, 65536);
// convert it to hexadecimal
return num.ToString("x4");
}
public async Task<long> PickAsync( public async Task<long> PickAsync(
ulong gid, ulong gid,
ITextChannel ch, ITextChannel ch,
ulong uid, ulong uid,
string pass) string pass)
{ {
await _pickLock.WaitAsync(); long amount;
ulong[] ids;
await using (var uow = _db.GetDbContext())
{
// this method will sum all plants with that password,
// remove them, and get messageids of the removed plants
pass = pass?.Trim().TrimTo(10, true)?.ToUpperInvariant();
// gets all plants in this channel with the same password
var entries = await uow.GetTable<PlantedCurrency>()
.Where(x => x.ChannelId == ch.Id && pass == x.Password)
.DeleteWithOutputAsync();
if (!entries.Any())
return 0;
amount = entries.Sum(x => x.Amount);
ids = entries.Select(x => x.MessageId).ToArray();
}
if (amount > 0)
await _cs.AddAsync(uid, amount, new("currency", "collect"));
try try
{ {
long amount; _ = ch.DeleteMessagesAsync(ids);
ulong[] ids;
await using (var uow = _db.GetDbContext())
{
// this method will sum all plants with that password,
// remove them, and get messageids of the removed plants
pass = pass?.Trim().TrimTo(10, true).ToUpperInvariant();
// gets all plants in this channel with the same password
var entries = uow.Set<PlantedCurrency>()
.AsQueryable()
.Where(x => x.ChannelId == ch.Id && pass == x.Password)
.ToList();
// sum how much currency that is, and get all of the message ids (so that i can delete them)
amount = entries.Sum(x => x.Amount);
ids = entries.Select(x => x.MessageId).ToArray();
// remove them from the database
uow.RemoveRange(entries);
if (amount > 0)
// give the picked currency to the user
await _cs.AddAsync(uid, amount, new("currency", "collect"));
await uow.SaveChangesAsync();
}
try
{
// delete all of the plant messages which have just been picked
_ = ch.DeleteMessagesAsync(ids);
}
catch { }
// return the amount of currency the user picked
return amount;
}
finally
{
_pickLock.Release();
} }
catch { }
// return the amount of currency the user picked
return amount;
} }
public async Task<ulong?> SendPlantMessageAsync( public async Task<ulong?> SendPlantMessageAsync(

View File

@@ -10,7 +10,7 @@ namespace NadekoBot.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 NadekoBot.Db.Models;
namespace NadekoBot.Modules.Gambling.Services;
public sealed class UserBetStatsService : INService
{
private const long RESET_MIN_PRICE = 1000;
private const decimal RESET_TOTAL_MULTIPLIER = 0.02m;
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 NadekoBot.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)
@@ -74,7 +113,7 @@ public partial class Gambling
Format.Bold(ctx.User.ToString()), Format.Bold(ctx.User.ToString()),
Format.Bold(target.ToString()), Format.Bold(target.ToString()),
N(amount))); N(amount)));
if (w.Affinity?.UserId == ctx.User.Id) if (w.Affinity?.UserId == ctx.User.Id)
msg += "\n" + GetText(strs.waifu_fulfilled(target, N(w.Price))); msg += "\n" + GetText(strs.waifu_fulfilled(target, N(w.Price)));
else else
@@ -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 : INService, 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 : INService, 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(
@@ -603,7 +598,7 @@ public class WaifuService : INService, IReadyExecutor
.Where(wi => wi.ClaimerId == waifuId) .Where(wi => wi.ClaimerId == waifuId)
.Select(wi => wi.WaifuId) .Select(wi => wi.WaifuId)
.Contains(x.Id)) .Contains(x.Id))
.Select(x => $"{x.Username}#{x.Discriminator}") .Select(x => x.Username)
.ToListAsyncEF(); .ToListAsyncEF();
} }
@@ -615,7 +610,7 @@ public class WaifuService : INService, IReadyExecutor
.Where(wi => wi.AffinityId == waifuId) .Where(wi => wi.AffinityId == waifuId)
.Select(wi => wi.WaifuId) .Select(wi => wi.WaifuId)
.Contains(x.Id)) .Contains(x.Id))
.Select(x => $"{x.Username}#{x.Discriminator}") .Select(x => x.Username)
.ToListAsyncEF(); .ToListAsyncEF();
} }
@@ -630,4 +625,38 @@ public class WaifuService : INService, 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

@@ -42,15 +42,12 @@ public static class WaifuExtensions
{ {
Affinity = x.Affinity == null Affinity = x.Affinity == null
? null ? null
: x.Affinity.Username : x.Affinity.Username,
+ (x.Affinity.Discriminator != "0000" ? "#" + x.Affinity.Discriminator : ""),
ClaimerName = ClaimerName =
x.Claimer == null x.Claimer == null
? null ? null
: x.Claimer.Username : x.Claimer.Username,
+ (x.Claimer.Discriminator != "0000" ? "#" + x.Claimer.Discriminator : ""), WaifuName = x.Waifu.Username,
WaifuName = x.Waifu.Username
+ (x.Waifu.Discriminator != "0000" ? "#" + x.Waifu.Discriminator : ""),
Price = x.Price Price = x.Price
}) })
.ToListAsyncEF(); .ToListAsyncEF();
@@ -62,7 +59,7 @@ public static class WaifuExtensions
public static ulong GetWaifuUserId(this DbSet<WaifuInfo> waifus, ulong ownerId, string name) public static ulong GetWaifuUserId(this DbSet<WaifuInfo> waifus, ulong ownerId, string name)
=> waifus.AsQueryable() => waifus.AsQueryable()
.AsNoTracking() .AsNoTracking()
.Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username + "#" + x.Waifu.Discriminator == name) .Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username == name)
.Select(x => x.Waifu.UserId) .Select(x => x.Waifu.UserId)
.FirstOrDefault(); .FirstOrDefault();
@@ -100,7 +97,7 @@ public static class WaifuExtensions
ctx.Set<DiscordUser>() ctx.Set<DiscordUser>()
.AsQueryable() .AsQueryable()
.Where(u => u.UserId == userId) .Where(u => u.UserId == userId)
.Select(u => u.Username + "#" + u.Discriminator) .Select(u => u.Username)
.FirstOrDefault(), .FirstOrDefault(),
AffinityCount = AffinityCount =
ctx.Set<WaifuUpdate>() ctx.Set<WaifuUpdate>()
@@ -112,14 +109,14 @@ public static class WaifuExtensions
ctx.Set<DiscordUser>() ctx.Set<DiscordUser>()
.AsQueryable() .AsQueryable()
.Where(u => u.Id == w.AffinityId) .Where(u => u.Id == w.AffinityId)
.Select(u => u.Username + "#" + u.Discriminator) .Select(u => u.Username)
.FirstOrDefault(), .FirstOrDefault(),
ClaimCount = ctx.Set<WaifuInfo>().AsQueryable().Count(x => x.ClaimerId == w.WaifuId), ClaimCount = ctx.Set<WaifuInfo>().AsQueryable().Count(x => x.ClaimerId == w.WaifuId),
ClaimerName = ClaimerName =
ctx.Set<DiscordUser>() ctx.Set<DiscordUser>()
.AsQueryable() .AsQueryable()
.Where(u => u.Id == w.ClaimerId) .Where(u => u.Id == w.ClaimerId)
.Select(u => u.Username + "#" + u.Discriminator) .Select(u => u.Username)
.FirstOrDefault(), .FirstOrDefault(),
DivorceCount = DivorceCount =
ctx.Set<WaifuUpdate>() ctx.Set<WaifuUpdate>()

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 NadekoBot.Modules.Gambling.Betdraw; using NadekoBot.Modules.Gambling.Betdraw;
using NadekoBot.Modules.Gambling.Rps; using NadekoBot.Modules.Gambling.Rps;
using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Gambling.Services;
@@ -8,15 +10,15 @@ namespace NadekoBot.Modules.Gambling;
public sealed class NewGamblingService : IGamblingService, INService public sealed class NewGamblingService : IGamblingService, INService
{ {
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;
} }
public async Task<OneOf<LuLaResult, GamblingError>> LulaAsync(ulong userId, long amount) public async Task<OneOf<LuLaResult, GamblingError>> LulaAsync(ulong userId, long amount)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(amount); ArgumentOutOfRangeException.ThrowIfNegative(amount);
@@ -31,13 +33,13 @@ public sealed class NewGamblingService : IGamblingService, INService
} }
} }
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,9 +59,9 @@ public sealed class NewGamblingService : IGamblingService, INService
} }
} }
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());
var result = game.Roll(amount); var result = game.Roll(amount);
@@ -88,19 +90,23 @@ public sealed class NewGamblingService : IGamblingService, INService
} }
} }
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;
if (won > 0) if (won > 0)
{ {
await _cs.AddAsync(userId, won, new("betflip", "win")); await _cs.AddAsync(userId, won, new("betflip", "win"));
} }
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);
@@ -109,7 +115,7 @@ public sealed class NewGamblingService : IGamblingService, INService
if (maybeGuessColor > 1) if (maybeGuessColor > 1)
throw new ArgumentOutOfRangeException(nameof(maybeGuessColor)); throw new ArgumentOutOfRangeException(nameof(maybeGuessColor));
if (maybeGuessValue > 1) if (maybeGuessValue > 1)
throw new ArgumentOutOfRangeException(nameof(maybeGuessValue)); throw new ArgumentOutOfRangeException(nameof(maybeGuessValue));
@@ -125,13 +131,13 @@ public sealed class NewGamblingService : IGamblingService, INService
var game = new BetdrawGame(); var game = new BetdrawGame();
var result = game.Draw((BetdrawValueGuess?)maybeGuessValue, (BetdrawColorGuess?)maybeGuessColor, amount); var result = game.Draw((BetdrawValueGuess?)maybeGuessValue, (BetdrawColorGuess?)maybeGuessColor, amount);
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) if (won > 0)
{ {
await _cs.AddAsync(userId, won, new("betdraw", "win")); await _cs.AddAsync(userId, won, new("betdraw", "win"));
} }
return result; return result;
} }
@@ -155,7 +161,7 @@ public sealed class NewGamblingService : IGamblingService, INService
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;
@@ -178,7 +184,7 @@ public sealed class NewGamblingService : IGamblingService, INService
return Task.FromResult(results); return Task.FromResult(results);
} }
// //
// //
// private readonly ConcurrentDictionary<ulong, Deck> _decks = new ConcurrentDictionary<ulong, Deck>(); // private readonly ConcurrentDictionary<ulong, Deck> _decks = new ConcurrentDictionary<ulong, Deck>();
@@ -236,7 +242,7 @@ public sealed class NewGamblingService : IGamblingService, INService
{ {
ArgumentOutOfRangeException.ThrowIfNegative(amount); ArgumentOutOfRangeException.ThrowIfNegative(amount);
ArgumentOutOfRangeException.ThrowIfGreaterThan(pick, 2); ArgumentOutOfRangeException.ThrowIfGreaterThan(pick, 2);
if (amount > 0) if (amount > 0)
{ {
var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("rps", "bet")); var isTakeSuccess = await _cs.RemoveAsync(userId, amount, new("rps", "bet"));
@@ -249,7 +255,7 @@ public sealed class NewGamblingService : IGamblingService, INService
var rps = new RpsGame(); var rps = new RpsGame();
var result = rps.Play((RpsPick)pick, amount); var result = rps.Play((RpsPick)pick, amount);
var won = (long)result.Won; var won = (long)result.Won;
if (won > 0) if (won > 0)
{ {
@@ -265,4 +271,46 @@ public sealed class NewGamblingService : IGamblingService, INService
return result; return result;
} }
}
public sealed class RakebackService: INService
{
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

@@ -0,0 +1,24 @@
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Games;
public interface INCanvasService
{
Task<uint[]> GetCanvas();
Task<NCPixel[]> GetPixelGroup(int position);
Task<SetPixelResult> SetPixel(
int position,
uint color,
string text,
ulong userId,
long price);
Task<bool> SetImage(uint[] img);
Task<NCPixel?> GetPixel(int x, int y);
Task<NCPixel?> GetPixel(int position);
int GetHeight();
int GetWidth();
Task ResetAsync();
}

View File

@@ -0,0 +1,305 @@
using NadekoBot.Modules.Gambling.Services;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Games;
public partial class Games
{
public sealed class NCanvasCommands : NadekoModule
{
private readonly INCanvasService _service;
private readonly IHttpClientFactory _http;
private readonly FontProvider _fonts;
private readonly GamblingConfigService _gcs;
public NCanvasCommands(
INCanvasService service,
IHttpClientFactory http,
FontProvider fonts,
GamblingConfigService gcs)
{
_service = service;
_http = http;
_fonts = fonts;
_gcs = gcs;
}
[Cmd]
public async Task NCanvas()
{
var pixels = await _service.GetCanvas();
var image = new Image<Rgba32>(_service.GetWidth(), _service.GetHeight());
Parallel.For(0,
image.Height,
y =>
{
var pixelAccessor = image.DangerousGetPixelRowMemory(y);
var row = pixelAccessor.Span;
for (int x = 0; x < image.Width; x++)
{
row[x] = new Rgba32(pixels[(y * image.Width) + x]);
}
});
await using var stream = await image.ToStreamAsync();
var hint = GetText(strs.nc_hint(prefix, _service.GetWidth(), _service.GetHeight()));
await Response()
.File(stream, "ncanvas.png")
.Embed(_sender.CreateEmbed()
.WithOkColor()
#if GLOBAL_NADEKO
.WithDescription("https://dashy.nadeko.bot/ncanvas")
#endif
.WithFooter(hint)
.WithImageUrl("attachment://ncanvas.png"))
.SendAsync();
}
[Cmd]
public Task NCzoom(int row, int col)
=> NCzoom((col * _service.GetWidth()) + row);
[Cmd]
public async Task NCzoom(kwum position)
{
var w = _service.GetWidth();
var h = _service.GetHeight();
if (position < 0 || position >= w * h)
{
await Response().Error(strs.invalid_input).SendAsync();
return;
}
using var img = await GetZoomImage(position);
await using var stream = await img.ToStreamAsync();
await ctx.Channel.SendFileAsync(stream, $"zoom_{position}.png");
}
private async Task<Image<Rgba32>> GetZoomImage(kwum position)
{
var w = _service.GetWidth();
var pixels = await _service.GetPixelGroup(position);
var origX = ((position % w) - 2) * 100;
var origY = ((position / w) - 2) * 100;
var image = new Image<Rgba32>(500, 500);
const float fontSize = 30;
var posFont = _fonts.NotoSans.CreateFont(fontSize, FontStyle.Bold);
var size = TextMeasurer.MeasureSize("wwww", new TextOptions(posFont));
var scale = 100f / size.Width;
if (scale < 1)
posFont = _fonts.NotoSans.CreateFont(fontSize * scale, FontStyle.Bold);
var outlinePen = new SolidPen(SixLabors.ImageSharp.Color.Black, 1f);
Parallel.For(0,
pixels.Length,
i =>
{
var pix = pixels[i];
var startX = pix.Position % w * 100 - origX;
var startY = pix.Position / w * 100 - origY;
var color = new Rgba32(pix.Color);
image.Mutate(x => FillRectangleExtensions.Fill(x,
new SolidBrush(color),
new RectangleF(startX, startY, 100, 100)));
image.Mutate(x =>
{
x.DrawText(new RichTextOptions(posFont)
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Origin = new(startX + 50, startY + 50)
},
((kwum)pix.Position).ToString().PadLeft(2, '2'),
Brushes.Solid(SixLabors.ImageSharp.Color.White),
outlinePen);
});
});
// write the position on each section of the image
return image;
}
[Cmd]
public async Task NcSetPixel(kwum position, string colorHex, [Leftover] string text = "")
{
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
{
await Response().Error(strs.invalid_input).SendAsync();
return;
}
if (colorHex.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
colorHex = colorHex[2..];
if (!Rgba32.TryParseHex(colorHex, out var clr))
{
await Response().Error(strs.invalid_color).SendAsync();
return;
}
var pixel = await _service.GetPixel(position);
if (pixel is null)
{
await Response().Error(strs.nc_pixel_not_found).SendAsync();
return;
}
var prompt = GetText(strs.nc_pixel_set_confirm(Format.Code(position.ToString()),
Format.Bold(CurrencyHelper.N(pixel.Price,
Culture,
_gcs.Data.Currency.Sign))));
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
.WithPendingColor()
.WithDescription(prompt)))
{
return;
}
var result = await _service.SetPixel(position, clr.PackedValue, text, ctx.User.Id, pixel.Price);
if (result == SetPixelResult.NotEnoughMoney)
{
await Response().Error(strs.not_enough(_gcs.Data.Currency.Sign)).SendAsync();
return;
}
else if (result == SetPixelResult.InsufficientPayment)
{
await Response().Error(strs.nc_insuff_payment).SendAsync();
return;
}
else if (result == SetPixelResult.InvalidInput)
{
await Response().Error(strs.invalid_input).SendAsync();
return;
}
using var img = await GetZoomImage(position);
await using var stream = await img.ToStreamAsync();
await Response()
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithDescription(GetText(strs.nc_pixel_set(Format.Code(position.ToString()))))
.WithImageUrl($"attachment://zoom_{position}.png"))
.File(stream, $"zoom_{position}.png")
.SendAsync();
}
[Cmd]
public async Task NcPixel(int x, int y)
=> await NcPixel((y * _service.GetWidth()) + x);
[Cmd]
public async Task NcPixel(kwum position)
{
if (position < 0 || position >= _service.GetWidth() * _service.GetHeight())
{
await Response().Error(strs.invalid_input).SendAsync();
return;
}
var pixel = await _service.GetPixel(position);
if (pixel is null)
{
await Response().Error(strs.nc_pixel_not_found).SendAsync();
return;
}
var image = new Image<Rgba32>(100, 100);
image.Mutate(x
=> x.Fill(new SolidBrush(new Rgba32(pixel.Color)),
new RectangleF(0, 0, 100, 100)));
await using var stream = await image.ToStreamAsync();
var pos = new kwum(pixel.Position);
await Response()
.File(stream, $"{pixel.Position}.png")
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithDescription(string.IsNullOrWhiteSpace(pixel.Text) ? string.Empty : pixel.Text)
.WithTitle(GetText(strs.nc_pixel(pos)))
.AddField(GetText(strs.nc_position),
$"{pixel.Position % _service.GetWidth()} {pixel.Position / _service.GetWidth()}",
true)
.AddField(GetText(strs.price), pixel.Price.ToString(), true)
.AddField(GetText(strs.color), "#" + new Rgba32(pixel.Color).ToHex())
.WithImageUrl($"attachment://{pixel.Position}.png"))
.SendAsync();
}
[Cmd]
[OwnerOnly]
public async Task NcSetImg()
{
var attach = ctx.Message.Attachments.FirstOrDefault();
if (attach is null)
{
await Response().Error(strs.no_attach_found).SendAsync();
return;
}
var w = _service.GetWidth();
var h = _service.GetHeight();
if (attach.Width != w || attach.Height != h)
{
await Response().Error(strs.invalid_img_size(w, h)).SendAsync();
return;
}
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
.WithDescription(
"This will reset the canvas to the specified image. All prices, text and colors will be reset.\n\n"
+ "Are you sure you want to continue?")))
return;
using var http = _http.CreateClient();
await using var stream = await http.GetStreamAsync(attach.Url);
using var img = await Image.LoadAsync<Rgba32>(stream);
var pixels = new uint[_service.GetWidth() * _service.GetHeight()];
Parallel.For(0,
_service.GetWidth() * _service.GetHeight(),
i => pixels[i] = img[i % _service.GetWidth(), i / _service.GetWidth()].PackedValue);
// for (var y = 0; y < _service.GetHeight(); y++)
// for (var x = 0; x < _service.GetWidth(); x++)
// pixels[(y * _service.GetWidth()) + x] = img[x, y].PackedValue;
await _service.SetImage(pixels);
await ctx.OkAsync();
}
[Cmd]
[OwnerOnly]
public async Task NcReset()
{
await _service.ResetAsync();
if (!await PromptUserConfirmAsync(_sender.CreateEmbed()
.WithDescription(
"This will delete all pixels and reset the canvas.\n\n"
+ "Are you sure you want to continue?")))
return;
await ctx.OkAsync();
}
}
}

View File

@@ -0,0 +1,206 @@
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using SixLabors.ImageSharp.ColorSpaces;
using SixLabors.ImageSharp.ColorSpaces.Conversion;
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Modules.Games;
public sealed class NCanvasService : INCanvasService, IReadyExecutor, INService
{
private readonly TypedKey<uint[]> _canvasKey = new("ncanvas");
private readonly DbService _db;
private readonly IBotCache _cache;
private readonly DiscordSocketClient _client;
private readonly ICurrencyService _cs;
public const int CANVAS_WIDTH = 500;
public const int CANVAS_HEIGHT = 350;
public const int INITIAL_PRICE = 3;
public NCanvasService(
DbService db,
IBotCache cache,
DiscordSocketClient client,
ICurrencyService cs)
{
_db = db;
_cache = cache;
_client = client;
_cs = cs;
}
public async Task OnReadyAsync()
{
if (_client.ShardId != 0)
return;
await using var uow = _db.GetDbContext();
if (await uow.GetTable<NCPixel>().CountAsyncLinqToDB() > 0)
return;
await ResetAsync();
}
public async Task ResetAsync()
{
await using var uow = _db.GetDbContext();
await uow.GetTable<NCPixel>().DeleteAsync();
var toAdd = new List<int>();
for (var i = 0; i < CANVAS_WIDTH * CANVAS_HEIGHT; i++)
{
toAdd.Add(i);
}
await uow.GetTable<NCPixel>()
.BulkCopyAsync(toAdd.Select(x =>
{
var clr = ColorSpaceConverter.ToRgb(new Hsv(((float)Random.Shared.NextDouble() * 360),
(float)(0.5 + (Random.Shared.NextDouble() * 0.49)),
(float)(0.4 + (Random.Shared.NextDouble() / 5 + (x % 100 * 0.2)))))
.ToVector3();
var packed = new Rgba32(clr).PackedValue;
return new NCPixel()
{
Color = packed,
Price = 1,
Position = x,
Text = "",
OwnerId = 0
};
}));
}
private async Task<uint[]> InternalGetCanvas()
{
await using var uow = _db.GetDbContext();
var colors = await uow.GetTable<NCPixel>()
.OrderBy(x => x.Position)
.Select(x => x.Color)
.ToArrayAsyncLinqToDB();
return colors;
}
public async Task<uint[]> GetCanvas()
{
return await _cache.GetOrAddAsync(_canvasKey,
async () => await InternalGetCanvas(),
TimeSpan.FromSeconds(15))
?? [];
}
public async Task<SetPixelResult> SetPixel(
int position,
uint color,
string text,
ulong userId,
long price)
{
if (position < 0 || position >= CANVAS_WIDTH * CANVAS_HEIGHT)
return SetPixelResult.InvalidInput;
var wallet = await _cs.GetWalletAsync(userId);
var paid = await wallet.Take(price, new("canvas", "pixel-buy", $"Bought pixel {new kwum(position)}"));
if (!paid)
{
return SetPixelResult.NotEnoughMoney;
}
var success = false;
try
{
await using var uow = _db.GetDbContext();
var updates = await uow.GetTable<NCPixel>()
.Where(x => x.Position == position && x.Price <= price)
.UpdateAsync(old => new NCPixel()
{
Position = position,
Color = color,
Text = text,
OwnerId = userId,
Price = price + 1
});
success = updates > 0;
}
catch
{
}
if (!success)
{
await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel {new kwum(position)} purchase"));
}
return success ? SetPixelResult.Success : SetPixelResult.InsufficientPayment;
}
public async Task<bool> SetImage(uint[] colors)
{
if (colors.Length != CANVAS_WIDTH * CANVAS_HEIGHT)
return false;
await using var uow = _db.GetDbContext();
await uow.GetTable<NCPixel>().DeleteAsync();
await uow.GetTable<NCPixel>()
.BulkCopyAsync(colors.Select((x, i) => new NCPixel()
{
Color = x,
Price = INITIAL_PRICE,
Position = i,
Text = "",
OwnerId = 0
}));
return true;
}
public Task<NCPixel?> GetPixel(int x, int y)
{
ArgumentOutOfRangeException.ThrowIfNegative(x);
ArgumentOutOfRangeException.ThrowIfNegative(y);
if (x >= CANVAS_WIDTH || y >= CANVAS_HEIGHT)
return Task.FromResult<NCPixel?>(null);
return GetPixel(x + (y * CANVAS_WIDTH));
}
public async Task<NCPixel?> GetPixel(int position)
{
ArgumentOutOfRangeException.ThrowIfNegative(position);
await using var uow = _db.GetDbContext();
return await uow.GetTable<NCPixel>().FirstOrDefaultAsync(x => x.Position == position);
}
public async Task<NCPixel[]> GetPixelGroup(int position)
{
ArgumentOutOfRangeException.ThrowIfNegative(position);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(position, CANVAS_WIDTH * CANVAS_HEIGHT);
await using var uow = _db.GetDbContext();
return await uow.GetTable<NCPixel>()
.Where(x => x.Position % CANVAS_WIDTH >= (position % CANVAS_WIDTH) - 2
&& x.Position % CANVAS_WIDTH <= (position % CANVAS_WIDTH) + 2
&& x.Position / CANVAS_WIDTH >= (position / CANVAS_WIDTH) - 2
&& x.Position / CANVAS_WIDTH <= (position / CANVAS_WIDTH) + 2)
.OrderBy(x => x.Position)
.ToArrayAsyncLinqToDB();
}
public int GetHeight()
=> CANVAS_HEIGHT;
public int GetWidth()
=> CANVAS_WIDTH;
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Games;
public enum SetPixelResult
{
Success,
InsufficientPayment,
NotEnoughMoney,
InvalidInput
}

View File

@@ -29,7 +29,7 @@ public partial class Games
if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString())) if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()))
return; return;
await Response().Error(strs.nunchi_joined(nunchi.ParticipantCount)).SendAsync(); await Response().Confirm(strs.nunchi_joined(nunchi.ParticipantCount)).SendAsync();
return; return;
} }

View File

@@ -122,9 +122,9 @@ public sealed class CurrencyRewardService : INService, IReadyExecutor
var dollarValue = pledgeCents / 100; var dollarValue = pledgeCents / 100;
percentBonus = dollarValue switch percentBonus = dollarValue switch
{ {
>= 100 => 100, >= 100 => 25,
>= 50 => 50, >= 50 => 20,
>= 20 => 20, >= 20 => 15,
>= 10 => 10, >= 10 => 10,
>= 5 => 5, >= 5 => 5,
_ => 0 _ => 0

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

View File

@@ -18,6 +18,9 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INServi
if(results.Count == 0) if(results.Count == 0)
return null; return null;
return results.Map(r => new VideoInfo(r)); return results.Map(r => new VideoInfo()
{
Url = r
});
} }
} }

View File

@@ -0,0 +1,191 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using System.Collections.Frozen;
namespace NadekoBot.Modules.Searches;
public sealed partial class FlagTranslateService : IReadyExecutor, INService
{
private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client;
private readonly TranslateService _ts;
private readonly IMessageSenderService _sender;
private IReadOnlyDictionary<string, string> _supportedFlags;
private readonly DbService _db;
private ConcurrentHashSet<ulong> _enabledChannels;
private readonly IBotCache _cache;
// disallow same message being translated multiple times to the same language
private readonly ConcurrentHashSet<(ulong, string)> _msgLangs = new();
public FlagTranslateService(
IBotCreds creds,
DiscordSocketClient client,
TranslateService ts,
IMessageSenderService sender,
DbService db,
IBotCache cache)
{
_creds = creds;
_client = client;
_ts = ts;
_sender = sender;
_db = db;
_cache = cache;
}
public async Task OnReadyAsync()
{
_supportedFlags = COUNTRIES
.Split('\n')
.Select(x => x.Split(' '))
.ToDictionary(x => x[0], x => x[1].TrimEnd())
.ToFrozenDictionary();
await using (var uow = _db.GetDbContext())
{
_enabledChannels = (await uow.GetTable<FlagTranslateChannel>()
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId,
_creds.TotalShards,
_client.ShardId))
.Select(x => new
{
x.ChannelId,
x.GuildId
})
.ToListAsyncLinqToDB())
.Select(x => x.ChannelId)
.ToHashSet()
.ToConcurrentSet();
}
_client.ReactionAdded += OnReactionAdded;
var periodicCleanup = new PeriodicTimer(TimeSpan.FromHours(24));
while (await periodicCleanup.WaitForNextTickAsync())
{
_msgLangs.Clear();
}
}
private const int FLAG_START = 127462;
private static TypedKey<bool> CdKey(ulong userId)
=> new($"flagtranslate:{userId}");
private Task OnReactionAdded(
Cacheable<IUserMessage, ulong> arg1,
Cacheable<IMessageChannel, ulong> arg2,
SocketReaction reaction)
{
if (!_enabledChannels.Contains(reaction.Channel.Id))
return Task.CompletedTask;
var runes = reaction.Emote.Name.EnumerateRunes();
if (!runes.MoveNext()
|| runes.Current is not { Value: >= 127462 and <= 127487 } l1
|| !runes.MoveNext()
|| runes.Current is not { Value: >= 127462 and <= 127487 } l2)
{
return Task.CompletedTask;
}
_ = Task.Run(async () =>
{
if (reaction.Channel is not SocketTextChannel tc)
return;
var user = await ((IGuild)tc.Guild).GetUserAsync(reaction.UserId);
if (user is null)
return;
if (!user.GetPermissions(tc).SendMessages)
return;
if (!tc.Guild.CurrentUser.GetPermissions(tc).SendMessages
|| !tc.Guild.CurrentUser.GetPermissions(tc).EmbedLinks)
{
await Disable(tc.Guild.Id, tc.Id);
return;
}
var c1 = (char)(l1.Value - FLAG_START + 65);
var c2 = (char)(l2.Value - FLAG_START + 65);
var code = $"{c1}{c2}".ToUpper();
if (!_supportedFlags.TryGetValue(code, out var lang))
return;
if (!_msgLangs.Add((reaction.MessageId, lang)))
return;
var result = await _cache.GetAsync(CdKey(reaction.UserId));
if (result.TryPickT0(out _, out _))
return;
await _cache.AddAsync(CdKey(reaction.UserId), true, TimeSpan.FromSeconds(5));
var msg = await arg1.GetOrDownloadAsync();
var response = await _ts.Translate("", lang, msg.Content).ConfigureAwait(false);
await msg.ReplyAsync(embed: _sender.CreateEmbed()
.WithOkColor()
.WithFooter(user.ToString() ?? reaction.UserId.ToString(),
user.RealAvatarUrl().ToString())
.WithDescription(response)
.WithAuthor(reaction.Emote.ToString())
.Build(),
allowedMentions: AllowedMentions.None
);
});
return Task.CompletedTask;
}
public async Task Disable(ulong guildId, ulong tcId)
{
if (!_enabledChannels.TryRemove(tcId))
return;
await using var uow = _db.GetDbContext();
await uow.GetTable<FlagTranslateChannel>()
.Where(x => x.GuildId == guildId
&& x.ChannelId == tcId)
.DeleteAsync();
}
public async Task<bool> Toggle(ulong guildId, ulong tcId)
{
if (_enabledChannels.Contains(tcId))
{
await Disable(guildId, tcId);
return false;
}
await Enable(guildId, tcId);
return true;
}
public async Task Enable(ulong guildId, ulong tcId)
{
if (!_enabledChannels.Add(tcId))
return;
await using var uow = _db.GetDbContext();
await uow.GetTable<FlagTranslateChannel>()
.InsertAsync(() => new FlagTranslateChannel
{
GuildId = guildId,
ChannelId = tcId
});
}
}

View File

@@ -0,0 +1,81 @@
namespace NadekoBot.Modules.Searches;
public partial class FlagTranslateService
{
private const string COUNTRIES = """
CN zh
IN hi
US en
ID id
PK ur
BR pt
NG ha
BD bn
RU ru
JP ja
MX es
PH tl
VN vi
EG ar
ET am
DE de
IR fa
TR tr
TH th
FR fr
CD fr
MM my
UG en
MZ pt
ZA zu
CO es
BG bg
HR hr
MY ms
NL nl
RO ro
CZ cs
GR el
SK sk
PT pt
KR ko
IT it
ES es
RS sr
TN ar
PL pl
SD ar
CM fr
SN fr
ML fr
NE ha
BI fr
AO pt
AF ps
MA ar
DZ ar
GB en
AR es
ZW ny
KE sw
GH en
SA ar
IL he
IQ ar
UA ua
LY ar
KW ar
OM ar
YE ar
AL sq
AE ar
AU en
NZ en
KZ kz
NO no
SE sv
DK da
FI fi
HU hu
""";
}

View File

@@ -44,12 +44,10 @@ public sealed class TranslateService : ITranslateService, IExecNoCommand, IReady
foreach (var c in cs) foreach (var c in cs)
{ {
_atcs[c.ChannelId] = c.AutoDelete; _atcs[c.ChannelId] = c.AutoDelete;
_users[c.ChannelId] = _users[c.ChannelId] = new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
} }
} }
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg) public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{ {
if (string.IsNullOrWhiteSpace(msg.Content)) if (string.IsNullOrWhiteSpace(msg.Content))
@@ -95,7 +93,7 @@ public sealed class TranslateService : ITranslateService, IExecNoCommand, IReady
} }
} }
public async Task<string> Translate(string source, string target, string text = null) public async Task<string> Translate(string source, string target, string text)
{ {
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Text is empty or null", nameof(text)); throw new ArgumentException("Text is empty or null", nameof(text));

View File

@@ -6,6 +6,14 @@ public partial class Searches
[Group] [Group]
public partial class TranslateCommands : NadekoModule<ITranslateService> public partial class TranslateCommands : NadekoModule<ITranslateService>
{ {
private readonly FlagTranslateService _flagSvc;
public TranslateCommands(FlagTranslateService flagSvc)
{
_flagSvc = flagSvc;
}
public enum AutoDeleteAutoTranslate public enum AutoDeleteAutoTranslate
{ {
Del, Del,
@@ -91,5 +99,18 @@ public partial class Searches
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(ChannelPermission.ManageChannels)]
[BotPerm(ChannelPermission.SendMessages | ChannelPermission.EmbedLinks)]
public async Task TranslateFlags()
{
var enabled = await _flagSvc.Toggle(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await Response().Confirm(strs.trfl_enabled).SendAsync();
else
await Response().Confirm(strs.trfl_disabled).SendAsync();
}
} }
} }

View File

@@ -783,4 +783,28 @@ public partial class Utility : NadekoModule
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 : NadekoModule<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 : NadekoModule<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");
} }
@@ -357,7 +357,7 @@ public partial class Xp : NadekoModule<XpService>
if (!await PromptUserConfirmAsync(embed)) if (!await PromptUserConfirmAsync(embed))
return; return;
_service.XpReset(ctx.Guild.Id, userId); await _service.XpReset(ctx.Guild.Id, userId);
await Response().Confirm(strs.reset_user(userId)).SendAsync(); await Response().Confirm(strs.reset_user(userId)).SendAsync();
} }

View File

@@ -20,6 +20,31 @@ using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Xp.Services; namespace NadekoBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}
public sealed class UserService : IUserService, INService
{
private readonly DbService _db;
public UserService(DbService db)
{
_db = db;
}
public async Task<DiscordUser> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}
public class XpService : INService, IReadyExecutor, IExecNoCommand public class XpService : INService, IReadyExecutor, IExecNoCommand
{ {
private readonly DbService _db; private readonly DbService _db;
@@ -1437,11 +1462,11 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
} }
} }
public void XpReset(ulong guildId, ulong userId) public async Task XpReset(ulong guildId, ulong userId)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
uow.Set<UserXpStats>().ResetGuildUserXp(userId, guildId); await uow.GetTable<UserXpStats>()
uow.SaveChanges(); .DeleteAsync(x => x.UserId == userId && x.GuildId == guildId);
} }
public void XpReset(ulong guildId) public void XpReset(ulong guildId)
@@ -1637,6 +1662,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
public bool IsShopEnabled() public bool IsShopEnabled()
=> _xpConfig.Data.Shop.IsEnabled; => _xpConfig.Data.Shop.IsEnabled;
public async Task<int> GetTotalGuildUsers(ulong requestGuildId, List<ulong>? guildUsers = null)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserXpStats>()
.Where(x => x.GuildId == requestGuildId
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
.CountAsyncLinqToDB();
}
} }
public enum BuyResult public enum BuyResult

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

View File

@@ -18,13 +18,10 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, INService
_qs = qs; _qs = qs;
_client = client; _client = client;
} }
public ServerServiceDefinition Bind() public ServerServiceDefinition Bind()
=> GrpcExprs.BindService(this); => GrpcExprs.BindService(this);
private ulong GetUserId(Metadata meta)
=> ulong.Parse(meta.FirstOrDefault(x => x.Key == "userid")!.Value);
public override async Task<AddExprReply> AddExpr(AddExprRequest request, ServerCallContext context) public override async Task<AddExprReply> AddExpr(AddExprRequest request, ServerCallContext context)
{ {
if (string.IsNullOrWhiteSpace(request.Expr.Trigger) || string.IsNullOrWhiteSpace(request.Expr.Response)) if (string.IsNullOrWhiteSpace(request.Expr.Trigger) || string.IsNullOrWhiteSpace(request.Expr.Response))
@@ -109,7 +106,7 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, INService
public override async Task<AddQuoteReply> AddQuote(AddQuoteRequest request, ServerCallContext context) public override async Task<AddQuoteReply> AddQuote(AddQuoteRequest request, ServerCallContext context)
{ {
var userId = GetUserId(context.RequestHeaders); var userId = context.RequestHeaders.GetUserId();
if (string.IsNullOrWhiteSpace(request.Quote.Trigger) || string.IsNullOrWhiteSpace(request.Quote.Response)) if (string.IsNullOrWhiteSpace(request.Quote.Trigger) || string.IsNullOrWhiteSpace(request.Quote.Response))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required")); throw new RpcException(new Status(StatusCode.InvalidArgument, "Trigger and response are required"));
@@ -146,7 +143,7 @@ public class ExprsSvc : GrpcExprs.GrpcExprsBase, IGrpcSvc, INService
public override async Task<Empty> DeleteQuote(DeleteQuoteRequest request, ServerCallContext context) public override async Task<Empty> DeleteQuote(DeleteQuoteRequest request, ServerCallContext context)
{ {
await _qs.DeleteQuoteAsync(request.GuildId, GetUserId(context.RequestHeaders), true, new kwum(request.Id)); await _qs.DeleteQuoteAsync(request.GuildId, context.RequestHeaders.GetUserId(), true, new kwum(request.Id));
return new Empty(); return new Empty();
} }
} }

View File

@@ -0,0 +1,89 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.NadekoExpressions;
using NadekoBot.Modules.Utility;
namespace NadekoBot.GrpcApi;
public class FinSvc : GrpcFin.GrpcFinBase, IGrpcSvc, INService
{
private readonly ICurrencyService _cs;
private readonly IBankService _bank;
public FinSvc(ICurrencyService cs, IBankService bank)
{
_cs = cs;
_bank = bank;
}
public ServerServiceDefinition Bind()
=> GrpcFin.BindService(this);
[GrpcNoAuthRequired]
public override async Task<DepositReply> Deposit(DepositRequest request, ServerCallContext context)
{
if (request.Amount <= 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Amount must be greater than 0"));
var succ = await _bank.DepositAsync(request.UserId, request.Amount);
return new DepositReply
{
Success = succ
};
}
[GrpcNoAuthRequired]
public override async Task<WithdrawReply> Withdraw(WithdrawRequest request, ServerCallContext context)
{
if (request.Amount <= 0)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Amount must be greater than 0"));
var succ = await _bank.WithdrawAsync(request.UserId, request.Amount);
return new WithdrawReply
{
Success = succ
};
}
[GrpcNoAuthRequired]
public override async Task<GetHoldingsReply> GetHoldings(GetHoldingsRequest request, ServerCallContext context)
{
return new GetHoldingsReply
{
Bank = await _bank.GetBalanceAsync(request.UserId),
Cash = await _cs.GetBalanceAsync(request.UserId)
};
}
[GrpcNoAuthRequired]
public override async Task<GetTransactionsReply> GetTransactions(
GetTransactionsRequest request,
ServerCallContext context)
{
if (request.Page < 1)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than 0"));
var trs = await _cs.GetTransactionsAsync(request.UserId, request.Page - 1);
var reply = new GetTransactionsReply
{
Total = await _cs.GetTransactionsCountAsync(request.UserId)
};
reply.Transactions.AddRange(trs.Select(x => new TransactionReply()
{
Id = new kwum(x.Id).ToString(),
Timestamp = Timestamp.FromDateTime(DateTime.UtcNow),
Amount = x.Amount,
Extra = x.Extra ?? string.Empty,
Note = x.Note ?? string.Empty,
Type = x.Type ?? string.Empty,
}));
return reply;
}
}

View File

@@ -17,10 +17,13 @@ public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IGrpcSvc, INService
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, INService
{ {
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

@@ -0,0 +1,95 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Games;
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.GrpcApi;
public class NCanvasSvc : GrpcNCanvas.GrpcNCanvasBase, IGrpcSvc, INService
{
private readonly INCanvasService _nCanvas;
private readonly DiscordSocketClient _client;
public NCanvasSvc(INCanvasService nCanvas, DiscordSocketClient client)
{
_nCanvas = nCanvas;
_client = client;
}
public ServerServiceDefinition Bind()
=> GrpcNCanvas.BindService(this);
[GrpcNoAuthRequired]
public override async Task<CanvasReply> GetCanvas(Empty request, ServerCallContext context)
{
var pixels = await _nCanvas.GetCanvas();
var reply = new CanvasReply()
{
Width = _nCanvas.GetWidth(),
Height = _nCanvas.GetHeight()
};
reply.Pixels.AddRange(pixels);
return reply;
}
[GrpcNoAuthRequired]
public override async Task<GetPixelReply> GetPixel(GetPixelRequest request, ServerCallContext context)
{
var pixel = await _nCanvas.GetPixel(request.X, request.Y);
if (pixel is null)
throw new RpcException(new Status(StatusCode.NotFound, "Pixel not found"));
var reply = MapPixelToGrpcPixel(pixel);
return reply;
}
private GetPixelReply MapPixelToGrpcPixel(NCPixel pixel)
{
var reply = new GetPixelReply
{
Color = "#" + new Rgba32(pixel.Color).ToHex(),
PackedColor = pixel.Color,
Position = new kwum(pixel.Position).ToString(),
PositionX = pixel.Position % _nCanvas.GetWidth(),
PositionY = pixel.Position / _nCanvas.GetWidth(),
// Owner = await ((IDiscordClient)_client).GetUserAsync(pixel.OwnerId)?.ToString() ?? string.Empty,
// OwnerId = pixel.OwnerId.ToString(),
Price = pixel.Price,
Text = pixel.Text
};
return reply;
}
[GrpcNoAuthRequired]
public override async Task<SetPixelReply> SetPixel(SetPixelRequest request, ServerCallContext context)
{
if (!kwum.TryParse(request.Position, out var pos))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Position is invalid"));
if (!Rgba32.TryParseHex(request.Color, out var clr))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Color is invalid"));
var userId = context.RequestHeaders.GetUserId();
var result = await _nCanvas.SetPixel(pos, clr.PackedValue, request.Text, userId, request.Price);
var reply = new SetPixelReply()
{
Success = result == SetPixelResult.Success,
Error = result switch
{
SetPixelResult.Success => string.Empty,
SetPixelResult.InsufficientPayment => "You have to pay equal or more than the price.",
SetPixelResult.NotEnoughMoney => "You don't have enough currency. ",
SetPixelResult.InvalidInput =>
$"Invalid input. Position has to be >= 0 and < {_nCanvas.GetWidth()}x{_nCanvas.GetHeight()}",
_ => throw new ArgumentOutOfRangeException()
}
};
var pixel = await _nCanvas.GetPixel(pos);
if (pixel is not null)
reply.Pixel = MapPixelToGrpcPixel(pixel);
return reply;
}
}

View File

@@ -13,69 +13,67 @@ public static class GrpcApiExtensions
public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
{ {
private readonly IDiscordClient _client; private readonly DiscordSocketClient _client;
private readonly XpService _xp; private readonly XpService _xp;
private readonly ICurrencyService _cur; private readonly ICurrencyService _cur;
private readonly WaifuService _waifus; private readonly WaifuService _waifus;
private readonly ICoordinator _coord;
private readonly IStatsService _stats; private readonly IStatsService _stats;
private readonly IBotCache _cache; private readonly CommandHandler _cmdHandler;
public OtherSvc( public OtherSvc(
DiscordSocketClient client, DiscordSocketClient client,
XpService xp, XpService xp,
ICurrencyService cur, ICurrencyService cur,
WaifuService waifus, WaifuService waifus,
ICoordinator coord,
IStatsService stats, IStatsService stats,
IBotCache cache) CommandHandler cmdHandler)
{ {
_client = client; _client = client;
_xp = xp; _xp = xp;
_cur = cur; _cur = cur;
_waifus = waifus; _waifus = waifus;
_coord = coord;
_stats = stats; _stats = stats;
_cache = cache; _cmdHandler = cmdHandler;
} }
public ServerServiceDefinition Bind() public ServerServiceDefinition Bind()
=> GrpcOther.BindService(this); => GrpcOther.BindService(this);
[GrpcNoAuthRequired] [GrpcNoAuthRequired]
public override async Task<BotOnGuildReply> BotOnGuild(BotOnGuildRequest request, ServerCallContext context) public override Task<BotOnGuildReply> BotOnGuild(BotOnGuildRequest request, ServerCallContext context)
{ {
var guild = await _client.GetGuildAsync(request.GuildId); var guild = _client.GetGuild(request.GuildId);
var reply = new BotOnGuildReply var reply = new BotOnGuildReply
{ {
Success = guild is not null Success = guild is not null
}; };
return reply; return Task.FromResult(reply);
} }
public override async Task<GetRolesReply> GetRoles(GetRolesRequest request, ServerCallContext context) public override Task<GetRolesReply> GetRoles(GetRolesRequest request, ServerCallContext context)
{ {
var g = await _client.GetGuildAsync(request.GuildId); var g = _client.GetGuild(request.GuildId);
var roles = g?.Roles; var roles = g?.Roles;
var reply = new GetRolesReply(); var reply = new GetRolesReply();
reply.Roles.AddRange(roles?.Select(x => new RoleReply() reply.Roles.AddRange(roles?.Select(x => new RoleReply()
{ {
Id = x.Id, Id = x.Id,
Name = x.Name, Name = x.Name,
Color = x.Color.ToString(), Color = x.Color.ToString(),
IconUrl = x.GetIconUrl() ?? string.Empty, IconUrl = x.GetIconUrl() ?? string.Empty,
}) ?? new List<RoleReply>()); })
?? new List<RoleReply>());
return reply; return Task.FromResult(reply);
} }
public override async Task<GetTextChannelsReply> GetTextChannels( public override async Task<GetTextChannelsReply> GetTextChannels(
GetTextChannelsRequest request, GetTextChannelsRequest request,
ServerCallContext context) ServerCallContext context)
{ {
var g = await _client.GetGuildAsync(request.GuildId); IGuild g = _client.GetGuild(request.GuildId);
var reply = new GetTextChannelsReply(); var reply = new GetTextChannelsReply();
var chs = await g.GetTextChannelsAsync(); var chs = await g.GetTextChannelsAsync();
@@ -89,33 +87,6 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
return reply; return reply;
} }
[GrpcNoAuthRequired]
public override async Task<GetGuildsReply> GetGuilds(Empty request, ServerCallContext context)
{
var guilds = await _client.GetGuildsAsync(CacheMode.CacheOnly);
var reply = new GetGuildsReply();
var userId = context.GetUserId();
var toReturn = new List<IGuild>();
foreach (var g in guilds)
{
var user = await g.GetUserAsync(userId);
if (user is not null && user.GuildPermissions.Has(GuildPermission.Administrator))
toReturn.Add(g);
}
reply.Guilds.AddRange(toReturn
.Select(x => new GuildReply()
{
Id = x.Id,
Name = x.Name,
IconUrl = x.IconUrl
}));
return reply;
}
[GrpcNoAuthRequired] [GrpcNoAuthRequired]
public override async Task<CurrencyLbReply> GetCurrencyLb(GetLbRequest request, ServerCallContext context) public override async Task<CurrencyLbReply> GetCurrencyLb(GetLbRequest request, ServerCallContext context)
@@ -123,16 +94,16 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage); var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage);
var reply = new CurrencyLbReply(); var reply = new CurrencyLbReply();
var entries = users.Select(async x => var entries = users.Select(x =>
{ {
var user = await _client.GetUserAsync(x.UserId, CacheMode.CacheOnly); var user = _client.GetUser(x.UserId);
return new CurrencyLbEntryReply() return Task.FromResult(new CurrencyLbEntryReply()
{ {
Amount = x.CurrencyAmount, Amount = x.CurrencyAmount,
User = user?.ToString() ?? x.Username, User = user?.ToString() ?? x.Username,
UserId = x.UserId, UserId = x.UserId,
Avatar = user?.RealAvatarUrl().ToString() ?? x.RealAvatarUrl()?.ToString() Avatar = user?.RealAvatarUrl().ToString() ?? x.RealAvatarUrl()?.ToString()
}; });
}); });
reply.Entries.AddRange(await entries.WhenAll()); reply.Entries.AddRange(await entries.WhenAll());
@@ -182,26 +153,66 @@ public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IGrpcSvc, INService
} }
[GrpcNoAuthRequired] [GrpcNoAuthRequired]
public override async Task<GetShardStatusesReply> GetShardStatuses(Empty request, ServerCallContext context) public override async Task GetShardStats(
Empty request,
IServerStreamWriter<ShardStatsReply> responseStream,
ServerCallContext context)
{ {
var reply = new GetShardStatusesReply(); while (true)
await _cache.GetOrAddAsync<List<ShardStatus>>("coord:statuses",
() => Task.FromResult(_coord.GetAllShardStatuses().ToList())!,
TimeSpan.FromMinutes(1));
var shards = _coord.GetAllShardStatuses();
reply.Shards.AddRange(shards.Select(x => new ShardStatusReply()
{ {
Id = x.ShardId, var stats = new ShardStatsReply()
Status = x.ConnectionState.ToString(), {
GuildCount = x.GuildCount, Id = _client.ShardId,
LastUpdate = Timestamp.FromDateTime(x.LastUpdate), Commands = _stats.CommandsRan,
})); Uptime = _stats.GetUptimeString(),
Status = GetConnectionState(_client.ConnectionState),
GuildCount = _client.Guilds.Count,
};
return reply; await responseStream.WriteAsync(stats);
await Task.Delay(1000);
}
}
[GrpcNoAuthRequired]
public override async Task GetCommandFeed(
Empty request,
IServerStreamWriter<CommandFeedEntry> responseStream,
ServerCallContext context)
{
var taskCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
Task OnCommandExecuted(IUserMessage userMessage, CommandInfo commandInfo)
{
try
{
responseStream.WriteAsync(new()
{
Command = commandInfo.Name
});
}
catch
{
_cmdHandler.CommandExecuted -= OnCommandExecuted;
taskCompletion.TrySetResult(true);
}
return Task.CompletedTask;
}
_cmdHandler.CommandExecuted += OnCommandExecuted;
await taskCompletion.Task;
}
private string GetConnectionState(ConnectionState clientConnectionState)
{
return clientConnectionState switch
{
ConnectionState.Connected => "Connected",
ConnectionState.Connecting => "Connecting",
_ => "Disconnected"
};
} }
public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context) public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context)

View File

@@ -0,0 +1,282 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.NadekoExpressions;
using NadekoBot.Modules.Utility;
using NadekoBot.Modules.Xp.Services;
namespace NadekoBot.GrpcApi;
public class XpSvc : GrpcXp.GrpcXpBase, IGrpcSvc, INService
{
private readonly XpService _xp;
private readonly DiscordSocketClient _client;
private readonly IUserService _duSvc;
public XpSvc(XpService xp, DiscordSocketClient client, IUserService duSvc)
{
_xp = xp;
_client = client;
_duSvc = duSvc;
}
public ServerServiceDefinition Bind()
=> GrpcXp.BindService(this);
public override async Task<GetXpSettingsReply> GetXpSettings(
GetXpSettingsRequest request,
ServerCallContext context)
{
await Task.Yield();
var guild = _client.GetGuild(request.GuildId);
if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
var excludedChannels = _xp.GetExcludedChannels(request.GuildId);
var excludedRoles = _xp.GetExcludedRoles(request.GuildId);
var isServerExcluded = _xp.IsServerExcluded(request.GuildId);
var reply = new GetXpSettingsReply();
reply.Exclusions.AddRange(excludedChannels
.Select(x => new ExclItemReply()
{
Id = x,
Type = "Channel",
Name = guild.GetChannel(x)?.Name ?? "????"
})
.Concat(excludedRoles
.Select(x => new ExclItemReply()
{
Id = x,
Type = "Role",
Name = guild.GetRole(x)?.Name ?? "????"
})));
var curRews = _xp.GetCurrencyRewards(request.GuildId);
var roleRews = _xp.GetRoleRewards(request.GuildId);
var rews = curRews.Select(x => new RewItemReply()
{
Level = x.Level,
Type = "Currency",
Value = x.Amount.ToString()
});
rews = rews.Concat(roleRews.Select(x => new RewItemReply()
{
Level = x.Level,
Type = x.Remove ? "RemoveRole" : "AddRole",
Value = guild.GetRole(x.RoleId)?.ToString() ?? x.RoleId.ToString()
}))
.OrderBy(x => x.Level);
reply.Rewards.AddRange(rews);
reply.ServerExcluded = isServerExcluded;
return reply;
}
public override async Task<AddExclusionReply> AddExclusion(AddExclusionRequest request, ServerCallContext context)
{
await Task.Yield();
var success = false;
var guild = _client.GetGuild(request.GuildId);
if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
if (request.Type == "Role")
{
if (guild.GetRole(request.Id) is null)
return new()
{
Success = false
};
success = _xp.ToggleExcludeRole(request.GuildId, request.Id);
}
else if (request.Type == "Channel")
{
if (guild.GetTextChannel(request.Id) is null)
return new()
{
Success = false
};
success = _xp.ToggleExcludeChannel(request.GuildId, request.Id);
}
return new()
{
Success = success
};
}
public override Task<DeleteExclusionReply> DeleteExclusion(
DeleteExclusionRequest request,
ServerCallContext context)
{
var success = false;
if (request.Type == "Role")
success = _xp.ToggleExcludeRole(request.GuildId, request.Id);
else
success = _xp.ToggleExcludeChannel(request.GuildId, request.Id);
return Task.FromResult(new DeleteExclusionReply
{
Success = success
});
}
public override async Task<AddRewardReply> AddReward(AddRewardRequest request, ServerCallContext context)
{
await Task.Yield();
var success = false;
var guild = _client.GetGuild(request.GuildId);
if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
if (request.Type == "AddRole" || request.Type == "RemoveRole")
{
if (!ulong.TryParse(request.Value, out var rid))
throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid role id"));
var role = guild.GetRole(rid);
if (role is null)
return new()
{
Success = false
};
_xp.SetRoleReward(request.GuildId, request.Level, rid, request.Type == "RemoveRole");
success = true;
}
// else if (request.Type == "Currency")
// {
// if (!int.TryParse(request.Value, out var amount))
// throw new RpcException(new Status(StatusCode.InvalidArgument, "Invalid amount"));
//
// _xp.SetCurrencyReward(request.GuildId, request.Level, amount);
// success = true;
// }
return new()
{
Success = success
};
}
public override Task<DeleteRewardReply> DeleteReward(DeleteRewardRequest request, ServerCallContext context)
{
var success = false;
if (request.Type == "AddRole" || request.Type == "RemoveRole")
{
_xp.ResetRoleReward(request.GuildId, request.Level);
success = true;
}
else if (request.Type == "Currency")
{
_xp.SetCurrencyReward(request.GuildId, request.Level, 0);
success = true;
}
return Task.FromResult(new DeleteRewardReply
{
Success = success
});
}
public override async Task<ResetUserXpReply> ResetUserXp(ResetUserXpRequest request, ServerCallContext context)
{
await _xp.XpReset(request.GuildId, request.UserId);
return new ResetUserXpReply
{
Success = true
};
}
public override async Task<GetXpLbReply> GetXpLb(GetXpLbRequest request, ServerCallContext context)
{
if (request.Page < 1)
throw new RpcException(new Status(StatusCode.InvalidArgument, "Page must be greater than or equal to 1"));
var guild = _client.GetGuild(request.GuildId);
if (guild is null)
throw new RpcException(new Status(StatusCode.NotFound, "Guild not found"));
var data = await _xp.GetGuildUserXps(request.GuildId, request.Page - 1);
var total = await _xp.GetTotalGuildUsers(request.GuildId);
var reply = new GetXpLbReply
{
Total = total
};
var users = await data
.Select(async x =>
{
var user = guild.GetUser(x.UserId);
if (user is null)
{
var du = await _duSvc.GetUserAsync(x.UserId);
if (du is null)
return new XpLbUserReply
{
UserId = x.UserId,
Avatar = string.Empty,
Username = string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
return new XpLbUserReply()
{
UserId = x.UserId,
Avatar = du.RealAvatarUrl()?.ToString() ?? string.Empty,
Username = du.ToString() ?? string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
}
return new XpLbUserReply
{
UserId = x.UserId,
Avatar = user?.GetAvatarUrl() ?? string.Empty,
Username = user?.ToString() ?? string.Empty,
Xp = x.Xp,
Level = new LevelStats(x.Xp).Level
};
})
.WhenAll();
reply.Users.AddRange(users);
return reply;
}
public override async Task<SetServerExclusionReply> SetServerExclusion(
SetServerExclusionRequest request,
ServerCallContext context)
{
await Task.Yield();
var newValue = _xp.ToggleExcludeServer(request.GuildId);
return new()
{
Success = newValue
};
}
}

View File

@@ -14,10 +14,7 @@ public sealed partial class GrpcApiPermsInterceptor : Interceptor
_client = client; _client = client;
} }
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( private async Task RequestHandler(ServerCallContext context)
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{ {
try try
{ {
@@ -42,7 +39,7 @@ public sealed partial class GrpcApiPermsInterceptor : Interceptor
// if the method is explicitly marked as not requiring auth // if the method is explicitly marked as not requiring auth
if (_noAuthRequired.Contains(method)) if (_noAuthRequired.Contains(method))
return await continuation(request, context); return;
// otherwise the method requires auth, and if it requires auth then the guildid has to be specified // otherwise the method requires auth, and if it requires auth then the guildid has to be specified
if (string.IsNullOrWhiteSpace(gidString)) if (string.IsNullOrWhiteSpace(gidString))
@@ -61,8 +58,6 @@ public sealed partial class GrpcApiPermsInterceptor : Interceptor
// if not then use the default, which is Administrator permission // if not then use the default, which is Administrator permission
await EnsureUserHasPermission(guildId, userId, DEFAULT_PERMISSION); await EnsureUserHasPermission(guildId, userId, DEFAULT_PERMISSION);
} }
return await continuation(request, context);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -70,7 +65,7 @@ public sealed partial class GrpcApiPermsInterceptor : Interceptor
throw; throw;
} }
} }
private async Task EnsureUserHasPermission(ulong guildId, ulong userId, GuildPerm perm) private async Task EnsureUserHasPermission(ulong guildId, ulong userId, GuildPerm perm)
{ {
IGuild guild = _client.GetGuild(guildId); IGuild guild = _client.GetGuild(guildId);
@@ -83,4 +78,42 @@ public sealed partial class GrpcApiPermsInterceptor : Interceptor
throw new RpcException(new Status(StatusCode.PermissionDenied, throw new RpcException(new Status(StatusCode.PermissionDenied,
$"You need {perm} permission to use this method")); $"You need {perm} permission to use this method"));
} }
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
ServerCallContext context,
ClientStreamingServerMethod<TRequest, TResponse> continuation)
{
await RequestHandler(context);
return await continuation(requestStream, context);
}
public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
DuplexStreamingServerMethod<TRequest, TResponse> continuation)
{
await RequestHandler(context);
await continuation(requestStream, responseStream, context);
}
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation)
{
await RequestHandler(context);
await continuation(request, responseStream, context);
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
await RequestHandler(context);
return await continuation(request, context);
}
} }

View File

@@ -0,0 +1,9 @@
using Grpc.Core;
namespace NadekoBot.GrpcApi;
public static class SvcExtensions
{
public static ulong GetUserId(this Metadata meta)
=> ulong.Parse(meta.FirstOrDefault(x => x.Key == "userid")!.Value);
}

View File

@@ -76,6 +76,9 @@ public readonly struct kwum : IEquatable<kwum>
public override string ToString() public override string ToString()
{ {
if (_value == 0)
return VALID_CHARACTERS[0].ToString();
var count = VALID_CHARACTERS.Length; var count = VALID_CHARACTERS.Length;
var localValue = _value; var localValue = _value;
var arrSize = (int)Math.Log(localValue, count) + 1; var arrSize = (int)Math.Log(localValue, count) + 1;

View File

@@ -40,4 +40,11 @@ public interface ICurrencyService
TxData? txData); TxData? txData);
Task<IReadOnlyList<DiscordUser>> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9); Task<IReadOnlyList<DiscordUser>> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9);
Task<IReadOnlyList<CurrencyTransaction>> GetTransactionsAsync(
ulong userId,
int page,
int perPage = 15);
Task<int> GetTransactionsCountAsync(ulong userId);
} }

View File

@@ -4,6 +4,6 @@ namespace NadekoBot.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 NadekoBot.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

@@ -201,9 +201,12 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
{ {
string text; string text;
if (!Languages.ContainsKey(sourceLanguage) || !Languages.ContainsKey(targetLanguage)) if (!Languages.ContainsKey(targetLanguage))
throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage)); throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage));
if (string.IsNullOrWhiteSpace(sourceLanguage) || !Languages.ContainsKey(sourceLanguage))
sourceLanguage = "auto";
var url = new Uri(string.Format( var url = new Uri(string.Format(
"https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}", "https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}",
@@ -223,7 +226,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
private string ConvertToLanguageCode(string language) private string ConvertToLanguageCode(string language)
{ {
Languages.TryGetValue(language, out var mode); Languages.TryGetValue(language, out var mode);
return mode; return string.IsNullOrWhiteSpace(mode) ? language : mode;
} }
} }

View File

@@ -154,7 +154,6 @@ public sealed partial class GoogleApiService
} }
Languages = langs; Languages = langs;
} }
} }

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.ShortDate).ToString());
} }
private void WithClient() private void WithClient()

View File

@@ -165,7 +165,7 @@ public class CommandHandler : INService, 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

@@ -55,14 +55,14 @@ public sealed class CurrencyService : ICurrencyService, INService
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await ctx await ctx
.GetTable<DiscordUser>() .GetTable<DiscordUser>()
.Where(x => userIds.Contains(x.UserId)) .Where(x => userIds.Contains(x.UserId))
.UpdateAsync(du => new() .UpdateAsync(du => new()
{ {
CurrencyAmount = du.CurrencyAmount >= amount CurrencyAmount = du.CurrencyAmount >= amount
? du.CurrencyAmount - amount ? du.CurrencyAmount - amount
: 0 : 0
}); });
await ctx.SaveChangesAsync(); await ctx.SaveChangesAsync();
return; return;
} }
@@ -77,7 +77,7 @@ public sealed class CurrencyService : ICurrencyService, INService
{ {
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, INService
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;
} }
@@ -112,4 +112,29 @@ public sealed class CurrencyService : ICurrencyService, INService
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return await uow.Set<DiscordUser>().GetTopRichest(ignoreId, page, perPage); return await uow.Set<DiscordUser>().GetTopRichest(ignoreId, page, perPage);
} }
public async Task<IReadOnlyList<CurrencyTransaction>> GetTransactionsAsync(
ulong userId,
int page,
int perPage = 15)
{
await using var uow = _db.GetDbContext();
var trs = await uow.GetTable<CurrencyTransaction>()
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.DateAdded)
.Skip(perPage * page)
.Take(perPage)
.ToListAsyncLinqToDB();
return trs;
}
public async Task<int> GetTransactionsCountAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
return await uow.GetTable<CurrencyTransaction>()
.Where(x => x.UserId == userId)
.CountAsyncLinqToDB();
}
} }

View File

@@ -77,8 +77,7 @@ public class DefaultWallet : IWallet
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
UserId = userId, UserId = userId,
Username = "Unknown", Username = "??Unknown",
Discriminator = "????",
CurrencyAmount = amount, CurrencyAmount = amount,
}, },
(old) => new() (old) => new()

View File

@@ -1,8 +1,11 @@
using LinqToDB; using LinqToDB;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Currency; using NadekoBot.Services.Currency;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling;
using System.Collections.Concurrent;
namespace NadekoBot.Services; namespace NadekoBot.Services;
@@ -10,15 +13,11 @@ public sealed class GamblingTxTracker : ITxTracker, INService, 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,83 +27,333 @@ public sealed class GamblingTxTracker : ITxTracker, INService, 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()
{ {
Feature = key, Feature = key,
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, {
PaidOut = old.PaidOut + stat.PaidOut, Bet = old.Bet + stat.Bet,
}, () => new() PaidOut = old.PaidOut + stat.PaidOut,
{ },
Feature = key () => new()
}); {
Feature = key
});
} }
} }
} }
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

@@ -4,10 +4,11 @@ using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
using System.Collections.Frozen;
namespace NadekoBot.Modules.Permissions.Services; namespace NadekoBot.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 +16,115 @@ 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; if (newBlacklist is null)
return default;
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 (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 (blacklistedChannels.Contains(usrMsg.Channel.Id))
} {
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
usrMsg.Channel.Name,
usrMsg.Channel.Id.ToString());
}
if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id)
{
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
usrMsg.Channel.Name,
usrMsg.Channel.Id);
return Task.FromResult(true); if (blacklistedUsers.Contains(usrMsg.Author.Id))
} {
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id) usrMsg.Author.ToString(),
{ usrMsg.Author.Id.ToString());
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]", return Task.FromResult(true);
usrMsg.Author.ToString(),
usrMsg.Author.Id);
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)
@@ -88,34 +135,34 @@ public sealed class BlacklistService : IExecOnMessage
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow await uow
.GetTable<BlacklistEntry>() .GetTable<BlacklistEntry>()
.InsertAsync(() => new() .InsertAsync(() => new()
{ {
ItemId = id, ItemId = id,
Type = type, Type = type,
}); });
if (type == BlacklistType.User) if (type == BlacklistType.User)
{ {
await uow.GetTable<DiscordUser>() await uow.GetTable<DiscordUser>()
.Where(x => x.UserId == id) .Where(x => x.UserId == id)
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
CurrencyAmount = 0 CurrencyAmount = 0
}); });
} }
Reload(); await Reload();
} }
public async Task UnBlacklist(BlacklistType type, ulong id) public async Task UnBlacklist(BlacklistType type, ulong id)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<BlacklistEntry>() await uow.GetTable<BlacklistEntry>()
.Where(bi => bi.ItemId == id && bi.Type == type) .Where(bi => bi.ItemId == id && bi.Type == type)
.DeleteAsync(); .DeleteAsync();
Reload(); await Reload();
} }
public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist) public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
@@ -130,12 +177,12 @@ public sealed class BlacklistService : IExecOnMessage
var blList = toBlacklist.ToList(); var blList = toBlacklist.ToList();
await uow.GetTable<DiscordUser>() await uow.GetTable<DiscordUser>()
.Where(x => blList.Contains(x.UserId)) .Where(x => blList.Contains(x.UserId))
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
CurrencyAmount = 0 CurrencyAmount = 0
}); });
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

@@ -193,7 +193,7 @@ public sealed class StatsService : IStatsService, IReadyExecutor, INService
Id = g.Id, Id = g.Id,
IconUrl = g.IconUrl, IconUrl = g.IconUrl,
Name = g.Name, Name = g.Name,
Owner = (await ig.GetUserAsync(g.OwnerId))?.Username ?? "Unknown", Owner = (await ig.GetUserAsync(g.OwnerId))?.Username ?? "??Unknown",
OwnerId = g.OwnerId, OwnerId = g.OwnerId,
CreatedAt = g.CreatedAt.UtcDateTime, CreatedAt = g.CreatedAt.UtcDateTime,
VoiceChannels = g.VoiceChannels.Count, VoiceChannels = g.VoiceChannels.Count,

Some files were not shown because too many files have changed in this diff Show More