Compare commits

...

28 Commits

Author SHA1 Message Date
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
65 changed files with 16256 additions and 528 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,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

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

View File

@@ -44,9 +44,6 @@ public static class UserXpExtensions
.CountAsyncLinqToDB()
+ 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)
=> xps.Delete(x => x.GuildId == guildId);

View File

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

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

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

View File

@@ -61,6 +61,7 @@ public abstract class NadekoContext : DbContext
public DbSet<TodoModel> Todos { get; set; }
public DbSet<ArchivedTodoListModel> TodosArchive { get; set; }
public DbSet<HoneypotChannel> HoneyPotChannels { get; set; }
// public DbSet<GuildColors> GuildColors { get; set; }
@@ -73,6 +74,23 @@ public abstract class NadekoContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#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>()

View File

@@ -5,6 +5,11 @@ namespace NadekoBot.Migrations;
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)
{
if (migrationBuilder.IsSqlite())

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

View File

@@ -751,10 +751,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("timestamp without time zone")
.HasColumnName("dateadded");
b.Property<string>("Discriminator")
.HasColumnType("text")
.HasColumnName("discriminator");
b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
@@ -998,6 +994,37 @@ namespace NadekoBot.Migrations.PostgreSql
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 =>
{
b.Property<int>("Id")
@@ -3200,6 +3227,57 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("greetsettings", (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 =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)

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

View File

@@ -560,9 +560,6 @@ namespace NadekoBot.Migrations
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<string>("Discriminator")
.HasColumnType("TEXT");
b.Property<bool>("IsClubAdmin")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -743,6 +740,29 @@ namespace NadekoBot.Migrations
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 =>
{
b.Property<int>("Id")
@@ -2379,6 +2399,44 @@ namespace NadekoBot.Migrations
b.ToTable("GreetSettings");
});
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 =>
{
b.HasOne("NadekoBot.Db.Models.GuildConfig", null)

View File

@@ -24,6 +24,13 @@ public partial class Administration
await Response().Error(strs.hierarchy).SendAsync();
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);
if (roles.Count == 0)

View File

@@ -339,7 +339,7 @@ public class GreetService : INService, IReadyExecutor
}
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;
}

View File

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

View File

@@ -6,6 +6,10 @@ namespace NadekoBot.Modules.Gambling.Common.AnimalRacing;
public sealed class AnimalRace : IDisposable
{
public const double BASE_MULTIPLIER = 0.82;
public const double MAX_MULTIPLIER = 0.94;
public const double MULTI_PER_USER = 0.01;
public enum Phase
{
WaitingForPlayers,
@@ -100,7 +104,7 @@ public sealed class AnimalRace : IDisposable
foreach (var user in _users)
{
if (user.Bet > 0)
await _currency.AddAsync(user.UserId, user.Bet, new("animalrace", "refund"));
await _currency.AddAsync(user.UserId, (long)(user.Bet * BASE_MULTIPLIER), new("animalrace", "refund"));
}
_ = OnStartingFailed?.Invoke(this);
@@ -116,7 +120,7 @@ public sealed class AnimalRace : IDisposable
{
foreach (var user in _users)
{
user.Progress += rng.Next(1, 11);
user.Progress += rng.Next(1, 10);
if (user.Progress >= 60)
user.Progress = 60;
}
@@ -126,13 +130,15 @@ public sealed class AnimalRace : IDisposable
FinishedUsers.AddRange(finished);
_ = OnStateUpdate?.Invoke(this);
await Task.Delay(2500);
await Task.Delay(1750);
}
if (FinishedUsers[0].Bet > 0)
{
Multi = FinishedUsers.Count
* Math.Min(MAX_MULTIPLIER, BASE_MULTIPLIER + (MULTI_PER_USER * FinishedUsers.Count));
await _currency.AddAsync(FinishedUsers[0].UserId,
FinishedUsers[0].Bet * (_users.Count - 1),
(long)(FinishedUsers[0].Bet * Multi),
new("animalrace", "win"));
}
@@ -140,6 +146,8 @@ public sealed class AnimalRace : IDisposable
});
}
public double Multi { get; set; } = BASE_MULTIPLIER;
public void Dispose()
{
CurrentPhase = Phase.Ended;

View File

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

View File

@@ -14,6 +14,13 @@ using System.Text;
using NadekoBot.Modules.Gambling.Rps;
using NadekoBot.Common.TypeReaders;
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;
@@ -26,6 +33,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
private readonly FontProvider _fonts;
private readonly IBankService _bank;
private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker;
@@ -38,6 +46,7 @@ public partial class Gambling : GamblingModule<GamblingService>
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService,
FontProvider fonts,
IBankService bank,
IRemindService remind,
IPatronageService patronage,
@@ -52,12 +61,14 @@ public partial class Gambling : GamblingModule<GamblingService>
_remind = remind;
_gamblingTxTracker = gamblingTxTracker;
_ps = patronage;
_rng = new NadekoRandom();
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
_tracker = tracker;
_configService = configService;
_fonts = fonts;
}
public async Task<string> GetBalanceStringAsync(ulong userId)
@@ -67,7 +78,65 @@ public partial class Gambling : GamblingModule<GamblingService>
}
[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();
@@ -140,7 +209,21 @@ public partial class Gambling : GamblingModule<GamblingService>
(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]
[RequireContext(ContextType.Guild)]
public async Task Timely()
{
var val = Config.Timely.Amount;
@@ -151,6 +234,71 @@ public partial class Gambling : GamblingModule<GamblingService>
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)
{
// Get correct time form remainder
@@ -169,6 +317,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 percentBonus = (_ps.PercentBonus(patron) / 100f);
@@ -179,7 +351,16 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
if (booster)
{
var msg = GetText(strs.timely(N(val), period))
+ "\n\n"
+ $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*";
await Response().Confirm(msg).Interaction(inter).SendAsync();
}
else
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
}
[Cmd]
@@ -290,8 +471,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
var embed = _sender.CreateEmbed()
.WithTitle(GetText(strs.transactions(((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithTitle(GetText(strs.transactions(
((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString()
?? $"{userId}")))
.WithOkColor();
var sb = new StringBuilder();
@@ -547,7 +729,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await Response().Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign)).SendAsync();
await Response()
.Error(strs.take_fail(N(amount), Format.Bold(user.ToString()), CurrencySign))
.SendAsync();
}
}
@@ -568,7 +752,9 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await Response().Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign)).SendAsync();
await Response()
.Error(strs.take_fail(N(amount), Format.Code(usrId.ToString()), CurrencySign))
.SendAsync();
}
}
@@ -762,6 +948,8 @@ public partial class Gambling : GamblingModule<GamblingService>
private static readonly ImmutableArray<string> _emojis =
new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
private readonly NadekoRandom _rng;
[Cmd]
public async Task LuckyLadder([OverrideTypeReader(typeof(BalanceTypeReader))] long amount)

View File

@@ -11,7 +11,7 @@ namespace NadekoBot.Modules.Gambling.Common;
public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 8;
public int Version { get; set; } = 11;
[Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; }
@@ -67,6 +67,11 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
[Comment("""Slot config""")]
public SlotsConfig Slots { get; set; }
[Comment("""
Bonus config for server boosts
""")]
public BoostBonusConfig BoostBonus { get; set; }
public GamblingConfig()
{
BetRoll = new();
@@ -79,6 +84,7 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
Slots = new();
LuckyLadder = new();
BotCuts = new();
BoostBonus = new();
}
}
@@ -104,13 +110,26 @@ public partial class TimelyConfig
How much currency will the users get every time they run .timely command
setting to 0 or less will disable this feature
""")]
public int Amount { get; set; } = 0;
public long Amount { get; set; } = 0;
[Comment("""
How often (in hours) can users claim currency with .timely command
setting to 0 or less will disable this feature
""")]
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]
@@ -408,4 +427,15 @@ public sealed partial class BotCutConfig
Default 0.1 (10%).
""")]
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,
val => val >= 0);
AddParsedProp("timely.prot",
gs => gs.Timely.ProtType,
ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString);
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)
{
ModifyConfig(c =>
@@ -199,5 +188,13 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
c.Waifu.Decay.UnclaimedDecayPercent = 0;
});
}
if (data.Version < 11)
{
ModifyConfig(c =>
{
c.Version = 11;
});
}
}
}

View File

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

View File

@@ -1,8 +1,11 @@
#nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using SixLabors.Fonts;
using SixLabors.Fonts.Unicode;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
@@ -25,6 +28,7 @@ public class PlantPickService : INService, IExecNoCommand
private readonly NadekoRandom _rng;
private readonly DiscordSocketClient _client;
private readonly GamblingConfigService _gss;
private readonly GamblingService _gs;
private readonly ConcurrentHashSet<ulong> _generationChannels;
private readonly SemaphoreSlim _pickLock = new(1, 1);
@@ -37,7 +41,8 @@ public class PlantPickService : INService, IExecNoCommand
ICurrencyService cs,
CommandHandler cmdHandler,
DiscordSocketClient client,
GamblingConfigService gss)
GamblingConfigService gss,
GamblingService gs)
{
_db = db;
_strings = strings;
@@ -48,6 +53,7 @@ public class PlantPickService : INService, IExecNoCommand
_rng = new();
_client = client;
_gss = gss;
_gs = gs;
using var uow = db.GetDbContext();
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));
if (toDelete is not null)
uow.Remove(toDelete);
_generationChannels.TryRemove(cid);
enabled = false;
}
@@ -140,7 +147,7 @@ public class PlantPickService : INService, IExecNoCommand
pass = pass.TrimTo(10, true).ToLowerInvariant();
using var img = Image.Load<Rgba32>(curImg);
// choose font size based on the image height, so that it's visible
var font = _fonts.NotoSans.CreateFont(img.Height / 12.0f, FontStyle.Bold);
var font = _fonts.NotoSans.CreateFont(img.Height / 11.0f, FontStyle.Bold);
img.Mutate(x =>
{
// measure the size of the text to be drawing
@@ -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
x.FillPolygon(Color.ParseHex("00000080"),
new PointF(0, 0),
new PointF(1, 1),
new PointF(size.Width + 5, 0),
new PointF(size.Width + 5, size.Height + 10),
new PointF(0, size.Height + 10));
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
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
var format = img.Metadata.DecodedImageFormat;
@@ -208,7 +233,7 @@ public class PlantPickService : INService, IExecNoCommand
+ " "
+ 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;
var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
@@ -232,67 +257,44 @@ public class PlantPickService : INService, IExecNoCommand
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(
ulong gid,
ITextChannel ch,
ulong uid,
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
{
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 = 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();
_ = ch.DeleteMessagesAsync(ids);
}
catch { }
// return the amount of currency the user picked
return amount;
}
public async Task<ulong?> SendPlantMessageAsync(

View File

@@ -603,7 +603,7 @@ public class WaifuService : INService, IReadyExecutor
.Where(wi => wi.ClaimerId == waifuId)
.Select(wi => wi.WaifuId)
.Contains(x.Id))
.Select(x => $"{x.Username}#{x.Discriminator}")
.Select(x => x.Username)
.ToListAsyncEF();
}
@@ -615,7 +615,7 @@ public class WaifuService : INService, IReadyExecutor
.Where(wi => wi.AffinityId == waifuId)
.Select(wi => wi.WaifuId)
.Contains(x.Id))
.Select(x => $"{x.Username}#{x.Discriminator}")
.Select(x => x.Username)
.ToListAsyncEF();
}

View File

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

View File

@@ -37,7 +37,7 @@ public sealed class NewGamblingService : IGamblingService, INService
var won = (long)result.Won;
if (won > 0)
{
await _cs.AddAsync(userId, won, new("lula", "win"));
await _cs.AddAsync(userId, won, new("lula", result.Multiplier >= 1 ? "win" : "lose"));
}
return result;
@@ -155,7 +155,7 @@ public sealed class NewGamblingService : IGamblingService, INService
var won = (long)result.Won;
if (won > 0)
{
await _cs.AddAsync(userId, won, new("slot", "won"));
await _cs.AddAsync(userId, won, new("slot", "win"));
}
return result;

View File

@@ -171,7 +171,23 @@ public partial class Games
return;
}
await _service.SetPixel(position, clr.PackedValue, text, ctx.User.Id, pixel.Price);
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();

View File

@@ -20,7 +20,7 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, INService
public const int CANVAS_WIDTH = 500;
public const int CANVAS_HEIGHT = 350;
public const int INITIAL_PRICE = 10;
public const int INITIAL_PRICE = 3;
public NCanvasService(
DbService db,
@@ -110,7 +110,7 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, INService
var wallet = await _cs.GetWalletAsync(userId);
var paid = await wallet.Take(price, new("canvas", "pixel", $"Bought pixel #{position}"));
var paid = await wallet.Take(price, new("canvas", "pixel-buy", $"Bought pixel {new kwum(position)}"));
if (!paid)
{
return SetPixelResult.NotEnoughMoney;
@@ -138,7 +138,7 @@ public sealed class NCanvasService : INCanvasService, IReadyExecutor, INService
if (!success)
{
await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel #{position} purchase"));
await wallet.Add(price, new("canvas", "pixel-refund", $"Refund pixel {new kwum(position)} purchase"));
}
return success ? SetPixelResult.Success : SetPixelResult.InsufficientPayment;

View File

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

View File

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

View File

@@ -67,7 +67,7 @@ public partial class Permissions
return _sender.CreateEmbed()
.WithTitle(title)
.WithDescription(allItems.Join('\n'))
.WithDescription(pageItems.Join('\n'))
.WithOkColor();
})
.SendAsync();

View File

@@ -18,6 +18,9 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INServi
if(results.Count == 0)
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)
{
_atcs[c.ChannelId] = c.AutoDelete;
_users[c.ChannelId] =
new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
_users[c.ChannelId] = new(c.Users.ToDictionary(x => x.UserId, x => (x.Source.ToLower(), x.Target.ToLower())));
}
}
public async Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg)
{
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))
throw new ArgumentException("Text is empty or null", nameof(text));

View File

@@ -6,6 +6,14 @@ public partial class Searches
[Group]
public partial class TranslateCommands : NadekoModule<ITranslateService>
{
private readonly FlagTranslateService _flagSvc;
public TranslateCommands(FlagTranslateService flagSvc)
{
_flagSvc = flagSvc;
}
public enum AutoDeleteAutoTranslate
{
Del,
@@ -91,5 +99,18 @@ public partial class Searches
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

@@ -225,7 +225,7 @@ public partial class Xp : NadekoModule<XpService>
else if (userXpData.AwardedXp < 0)
awardStr = $"({userXpData.AwardedXp})";
embed.AddField($"#{i + 1 + (curPage * 9)} {user?.ToString() ?? users[i].UserId.ToString()}",
embed.AddField($"#{i + 1 + (curPage * 10)} {user?.ToString() ?? users[i].UserId.ToString()}",
$"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}");
}
@@ -278,7 +278,7 @@ public partial class Xp : NadekoModule<XpService>
for (var i = 0; i < users.Count; i++)
{
var user = users[i];
embed.AddField($"#{i + 1 + (curPage * 9)} {user}",
embed.AddField($"#{i + 1 + (curPage * 10)} {user}",
$"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp");
}
@@ -357,7 +357,7 @@ public partial class Xp : NadekoModule<XpService>
if (!await PromptUserConfirmAsync(embed))
return;
_service.XpReset(ctx.Guild.Id, userId);
await _service.XpReset(ctx.Guild.Id, userId);
await Response().Confirm(strs.reset_user(userId)).SendAsync();
}

View File

@@ -20,6 +20,31 @@ using Image = SixLabors.ImageSharp.Image;
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
{
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();
uow.Set<UserXpStats>().ResetGuildUserXp(userId, guildId);
uow.SaveChanges();
await using var uow = _db.GetDbContext();
await uow.GetTable<UserXpStats>()
.DeleteAsync(x => x.UserId == userId && x.GuildId == guildId);
}
public void XpReset(ulong guildId)
@@ -1637,6 +1662,15 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
public bool IsShopEnabled()
=> _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

View File

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

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

View File

@@ -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

@@ -40,4 +40,11 @@ public interface ICurrencyService
TxData? txData);
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
{
Task TrackAdd(long amount, TxData? txData);
Task TrackRemove(long amount, TxData? txData);
Task TrackAdd(ulong userId, long amount, TxData? txData);
Task TrackRemove(ulong userId, long amount, TxData? txData);
}

View File

@@ -201,9 +201,12 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
{
string text;
if (!Languages.ContainsKey(sourceLanguage) || !Languages.ContainsKey(targetLanguage))
if (!Languages.ContainsKey(targetLanguage))
throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage));
if (string.IsNullOrWhiteSpace(sourceLanguage) || !Languages.ContainsKey(sourceLanguage))
sourceLanguage = "auto";
var url = new Uri(string.Format(
"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)
{
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;
}
}

View File

@@ -55,14 +55,14 @@ public sealed class CurrencyService : ICurrencyService, INService
{
await using var ctx = _db.GetDbContext();
await ctx
.GetTable<DiscordUser>()
.Where(x => userIds.Contains(x.UserId))
.UpdateAsync(du => new()
{
CurrencyAmount = du.CurrencyAmount >= amount
? du.CurrencyAmount - amount
: 0
});
.GetTable<DiscordUser>()
.Where(x => userIds.Contains(x.UserId))
.UpdateAsync(du => new()
{
CurrencyAmount = du.CurrencyAmount >= amount
? du.CurrencyAmount - amount
: 0
});
await ctx.SaveChangesAsync();
return;
}
@@ -77,7 +77,7 @@ public sealed class CurrencyService : ICurrencyService, INService
{
var wallet = await GetWalletAsync(userId);
await wallet.Add(amount, txData);
await _txTracker.TrackAdd(amount, txData);
await _txTracker.TrackAdd(userId, amount, txData);
}
public async Task AddAsync(
@@ -97,7 +97,7 @@ public sealed class CurrencyService : ICurrencyService, INService
var wallet = await GetWalletAsync(userId);
var result = await wallet.Take(amount, txData);
if (result)
await _txTracker.TrackRemove(amount, txData);
await _txTracker.TrackRemove(userId, amount, txData);
return result;
}
@@ -112,4 +112,29 @@ public sealed class CurrencyService : ICurrencyService, INService
await using var uow = _db.GetDbContext();
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()
{
UserId = userId,
Username = "Unknown",
Discriminator = "????",
Username = "??Unknown",
CurrencyAmount = amount,
},
(old) => new()

View File

@@ -1,8 +1,10 @@
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Services.Currency;
using NadekoBot.Db.Models;
using System.Collections.Concurrent;
namespace NadekoBot.Services;
@@ -10,15 +12,11 @@ public sealed class GamblingTxTracker : ITxTracker, INService, IReadyExecutor
{
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
{
"lula",
"betroll",
"betflip",
"blackjack",
"betdraw",
"slot",
"lula", "betroll", "betflip", "blackjack", "betdraw", "slot",
});
private ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> _stats = new();
private NonBlocking.ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> globalStats = new();
private ConcurrentBag<UserBetStats> userStats = new();
private readonly DbService _db;
@@ -28,83 +26,283 @@ public sealed class GamblingTxTracker : ITxTracker, INService, IReadyExecutor
}
public async Task OnReadyAsync()
=> await Task.WhenAll(RunUserStatsCollector(), RunBetStatsCollector());
public async Task RunBetStatsCollector()
{
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
while (await timer.WaitForNextTickAsync())
{
await using var ctx = _db.GetDbContext();
await using var trans = await ctx.Database.BeginTransactionAsync();
try
{
var keys = _stats.Keys;
// update betstats
var keys = globalStats.Keys;
foreach (var key in keys)
{
if (_stats.TryRemove(key, out var stat))
if (globalStats.TryRemove(key, out var stat))
{
await ctx.GetTable<GamblingStats>()
.InsertOrUpdateAsync(() => new()
{
Feature = key,
Bet = stat.Bet,
PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow
}, old => new()
{
Bet = old.Bet + stat.Bet,
PaidOut = old.PaidOut + stat.PaidOut,
}, () => new()
{
Feature = key
});
.InsertOrUpdateAsync(() => new()
{
Feature = key,
Bet = stat.Bet,
PaidOut = stat.PaidOut,
DateAdded = DateTime.UtcNow
},
old => new()
{
Bet = old.Bet + stat.Bet,
PaidOut = old.PaidOut + stat.PaidOut,
},
() => new()
{
Feature = key
});
}
}
}
catch (Exception ex)
{
Log.Error(ex, "An error occurred in gambling tx tracker");
}
finally
{
await trans.CommitAsync();
Log.Error(ex, "An error occurred in betstats gambling tx tracker");
}
}
}
public Task TrackAdd(long amount, TxData? txData)
private async Task RunUserStatsCollector()
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync())
{
try
{
if (userStats.Count == 0)
continue;
var users = new List<UserBetStats>(userStats.Count + 5);
while (userStats.TryTake(out var s))
users.Add(s);
if (users.Count == 0)
continue;
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),
})))
{
// 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
});
}
}
catch (Exception ex)
{
Log.Error(ex, "An error occurred in UserBetStats gambling tx tracker");
}
}
}
public Task TrackAdd(ulong userId, long amount, TxData? txData)
{
if (txData is null)
return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type))
{
_stats.AddOrUpdate(txData.Type,
globalStats.AddOrUpdate(txData.Type,
_ => (0, amount),
(_, old) => (old.Bet, old.PaidOut + amount));
}
var mType = GetGameType(txData.Type);
if (mType is not { } type)
return Task.CompletedTask;
if (txData.Type == "lula")
{
if (txData.Extra == "lose")
{
userStats.Add(new()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = 0,
TotalBet = 0,
PaidOut = amount,
MaxBet = 0,
MaxWin = amount,
});
return Task.CompletedTask;
}
}
else if (txData.Type == "animalrace")
{
if (txData.Extra == "refund")
{
userStats.Add(new()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = -1,
TotalBet = -amount,
PaidOut = 0,
MaxBet = 0,
MaxWin = 0,
});
return Task.CompletedTask;
}
}
userStats.Add(new UserBetStats()
{
UserId = userId,
Game = type,
WinCount = 1,
LoseCount = -1,
TotalBet = 0,
PaidOut = amount,
MaxBet = 0,
MaxWin = amount,
});
return Task.CompletedTask;
}
public Task TrackRemove(long amount, TxData? txData)
public Task TrackRemove(ulong userId, long amount, TxData? txData)
{
if (txData is null)
return Task.CompletedTask;
if (_gamblingTypes.Contains(txData.Type))
{
_stats.AddOrUpdate(txData.Type,
globalStats.AddOrUpdate(txData.Type,
_ => (amount, 0),
(_, old) => (old.Bet + amount, old.PaidOut));
}
var mType = GetGameType(txData.Type);
if (mType is not { } type)
return Task.CompletedTask;
userStats.Add(new UserBetStats()
{
UserId = userId,
Game = type,
WinCount = 0,
LoseCount = 1,
TotalBet = amount,
PaidOut = 0,
MaxBet = amount,
MaxWin = 0
});
return Task.CompletedTask;
}
private static GamblingGame? GetGameType(string game)
=> game switch
{
"lula" => GamblingGame.Lula,
"betroll" => GamblingGame.Betroll,
"betflip" => GamblingGame.Betflip,
"blackjack" => GamblingGame.Blackjack,
"betdraw" => GamblingGame.Betdraw,
"slot" => GamblingGame.Slots,
"animalrace" => GamblingGame.Race,
_ => null
};
public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
{
await using var ctx = _db.GetDbContext();
return await ctx.Set<GamblingStats>()
.ToListAsyncEF();
.ToListAsyncEF();
}
public async Task<List<UserBetStats>> GetUserStatsAsync(ulong userId, GamblingGame? game = null)
{
await using var ctx = _db.GetDbContext();
if (game is null)
return await ctx
.GetTable<UserBetStats>()
.Where(x => x.UserId == userId)
.ToListAsync();
return await ctx
.GetTable<UserBetStats>()
.Where(x => x.UserId == userId && x.Game == game)
.ToListAsync();
}
}
public 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
}

View File

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

View File

@@ -848,6 +848,10 @@ eventstart:
- eventstart
betstats:
- betstats
- bs
gamblestats:
- gamblestats
- gs
bettest:
- bettest
slot:
@@ -1444,4 +1448,9 @@ ncpixel:
- ncp
- ncgp
ncreset:
- ncreset
- ncreset
translateflags:
- translateflags
- trfl
- fltr
- transflags

View File

@@ -2758,11 +2758,29 @@
"Gambling": [
{
"Aliases": [
".betstats"
".betstats",
".bs"
],
"Description": "Shows the current bet stats for yourself, or the targetted user.\nYou may optionally specify the game to show stats for.\nSupported games right now are: bf, br, bd, lula, slot, race",
"Usage": [
".betstats",
".betstats @someone",
".betstats @someone lula",
".betstats bd"
],
"Submodule": "Gambling",
"Module": "Gambling",
"Options": null,
"Requirements": []
},
{
"Aliases": [
".gamblestats",
".gs"
],
"Description": "Shows the total stats of several gambling features.\nUpdates once an hour.",
"Usage": [
".betstats"
".gamblestats"
],
"Submodule": "Gambling",
"Module": "Gambling",
@@ -6160,6 +6178,24 @@
"Options": null,
"Requirements": []
},
{
"Aliases": [
".translateflags",
".trfl",
".fltr",
".transflags"
],
"Description": "Toggles translate flags on the current channel.\nReacting with a country flag will translate the message to that country's language.",
"Usage": [
".translateflags"
],
"Submodule": "TranslateCommands",
"Module": "Searches",
"Options": null,
"Requirements": [
"ManageChannels Channel Permission"
]
},
{
"Aliases": [
".xkcd"

View File

@@ -1,5 +1,5 @@
# DO NOT CHANGE
version: 8
version: 11
# Currency settings
currency:
# What is the emoji/character which represents the currency
@@ -56,6 +56,9 @@ timely:
# How often (in hours) can users claim currency with .timely command
# setting to 0 or less will disable this feature
cooldown: 12
# How will timely be protected?
# None, Button (users have to click the button) or Captcha (users have to type the captcha from an image)
protType: Captcha
# How much will each user's owned currency decay over time.
decay:
# Percentage of user's current currency which will be deducted every 24h.
@@ -125,12 +128,13 @@ waifu:
# Settings for periodic waifu price decay.
# Waifu price decays only if the waifu has no claimer.
decay:
# Percentage (0 - 100) of the waifu value to reduce.
# Set 0 to disable
# Unclaimed waifus will decay by this percentage (0 - 100).
# Default is 0 (disabled)
# For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$)
unclaimedDecayPercent: 0
# Claimed waifus will decay by this percentage (0 - 100).
# Default is 0 (disabled)
# For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$)
claimedDecayPercent: 0
# How often to decay waifu values, in hours
hourInterval: 24
@@ -270,3 +274,10 @@ voteReward: 100
slots:
# Hex value of the color which the numbers on the slot image will have.
currencyFontColor: ff0000
# Bonus config for server boosts
boostBonus:
# Users will receive a bonus if they boost any of these servers
guildIds:
- 117523346618318850
# This bonus will be added before any other multiplier is applied to the .timely command
baseTimelyBonus: 50

View File

@@ -2703,7 +2703,7 @@ eventstart:
desc: "The type of event being started."
options:
desc: "The optional option flags for the event."
betstats:
gamblestats:
desc: |-
Shows the total stats of several gambling features.
Updates once an hour.
@@ -4633,4 +4633,32 @@ ncreset:
ex:
- ''
params:
- { }
- { }
translateflags:
desc: |-
Toggles translate flags on the current channel.
Reacting with a country flag will translate the message to that country's language.
ex:
- ''
params:
- { }
betstats:
desc: |-
Shows the current bet stats for yourself, or the targetted user.
You may optionally specify the game to show stats for.
Supported games right now are: bf, br, bd, lula, slot, race
ex:
- ''
- '@someone'
- '@someone lula'
- 'bd'
params:
- {}
- user:
desc: 'The user for who to show the betstats for.'
- user:
desc: 'The user for who to show the betstats for.'
game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined'
- game:
desc: 'The game to show betstats for. Omit to show betstats for all games combined'

View File

@@ -622,6 +622,7 @@
"region": "Region",
"remind2": "I will remind {0} to {1} {2} ({3})",
"remind_timely": "I will remind you about your timely reward {0}",
"timely_button": "Click the button to claim your timely reward.",
"remind_invalid": "Not a valid remind format. Remind must have a target, timer and a reason. Check the command list.",
"remind_too_long": "Remind time has exceeded maximum.",
"repeater_redundant_no": "Repeater **#{0}** won't post redundant messages anymore.",
@@ -1109,6 +1110,9 @@
"invalid_color": "Color you've specified is invalid.",
"nc_pixel_set_confirm": "Are you sure you want to set pixel {0}? It will cost you {1}",
"nc_hint": "Use `{0}nczoom x y` command to zoom in. Image is {1}x{2} pixels.",
"nc_insuff_payment": "Invalid payment.",
"invalid_img_size": "Image must to be {0}x{1} pixels.",
"no_attach_found": "No attachment found. Please send the image along with this command."
"no_attach_found": "No attachment found. Please send the image along with this command." ,
"trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.",
"trfl_disabled": "Flag translation disabled."
}

View File

@@ -3,7 +3,7 @@ if ($args.Length -eq 0) {
}
else {
$migrationName = $args[0]
dotnet ef migrations add $migrationName -c SqliteContext -p src/NadekoBot/NadekoBot.csproj
dotnet ef migrations add $migrationName -c PostgreSqlContext -p src/NadekoBot/NadekoBot.csproj
dotnet ef migrations add $migrationName -c SqliteContext -p NadekoBot.csproj
dotnet ef migrations add $migrationName -c PostgreSqlContext -p NadekoBot.csproj
}

View File

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