From 8de8eac95944b3d1e927f792633fbad9029e1897 Mon Sep 17 00:00:00 2001 From: Andriy <30056636+West14@users.noreply.github.com> Date: Thu, 20 Jul 2023 20:24:16 +0300 Subject: [PATCH] initial chat options implementation --- Core/Core.csproj | 2 +- Core/Data/ChatOptionValue.cs | 38 ++++++++ Core/Data/CoreContext.cs | 2 + .../RepositoryExtension.ChatOption.cs | 26 +++++ .../20230720171645_AddChatOptions.Designer.cs | 95 +++++++++++++++++++ .../20230720171645_AddChatOptions.cs | 58 +++++++++++ Core/Migrations/CoreContextModelSnapshot.cs | 27 +++++- Core/Options/AbstractOption.cs | 25 +++++ Core/Options/DbOptionProvider.cs | 59 ++++++++++++ Core/Options/IOption.cs | 10 ++ Core/Options/IOptionProvider.cs | 9 ++ Core/Options/OptionAttribute.cs | 17 ++++ Core/Startup.cs | 3 + Core/Test.cs | 20 ++++ modules/Entertainment/Entertainment.cs | 3 + modules/Entertainment/Handler/Common.cs | 20 +++- modules/Entertainment/Options/NsfwOption.cs | 11 +++ 17 files changed, 420 insertions(+), 5 deletions(-) create mode 100644 Core/Data/ChatOptionValue.cs create mode 100644 Core/Extensions/RepositoryExtension.ChatOption.cs create mode 100644 Core/Migrations/20230720171645_AddChatOptions.Designer.cs create mode 100644 Core/Migrations/20230720171645_AddChatOptions.cs create mode 100644 Core/Options/AbstractOption.cs create mode 100644 Core/Options/DbOptionProvider.cs create mode 100644 Core/Options/IOption.cs create mode 100644 Core/Options/IOptionProvider.cs create mode 100644 Core/Options/OptionAttribute.cs create mode 100644 Core/Test.cs create mode 100644 modules/Entertainment/Options/NsfwOption.cs diff --git a/Core/Core.csproj b/Core/Core.csproj index cadaad8..f28bf9a 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/Core/Data/ChatOptionValue.cs b/Core/Data/ChatOptionValue.cs new file mode 100644 index 0000000..3e3f010 --- /dev/null +++ b/Core/Data/ChatOptionValue.cs @@ -0,0 +1,38 @@ +#nullable enable + +using Microsoft.EntityFrameworkCore; +using System.Text.Json.Serialization; +using System.Text.Json; +using System; + +namespace Kruzya.TelegramBot.Core.Data; + +[PrimaryKey(nameof(ChatId), nameof(OptionId))] +public class ChatOptionValue +{ + public long ChatId { get; set; } + public string OptionId { get; set; } + public byte[] Value { get; set; } + + + public T GetValue(T? defVal = default) + { + try + { + return JsonSerializer.Deserialize(new ReadOnlySpan(Value)); + } + catch (Exception) + { + return defVal; + } + } + + public void SetValue(T value) + { + Value = JsonSerializer.SerializeToUtf8Bytes(value, new JsonSerializerOptions + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + } +} \ No newline at end of file diff --git a/Core/Data/CoreContext.cs b/Core/Data/CoreContext.cs index 286e98c..6843a11 100644 --- a/Core/Data/CoreContext.cs +++ b/Core/Data/CoreContext.cs @@ -15,6 +15,8 @@ namespace Kruzya.TelegramBot.Core.Data /// public DbSet UserValues { get; set; } + public DbSet ChatOptionValues { get; set; } + public CoreContext(DbContextOptions ctx) : base(ctx) { Database.Migrate(); diff --git a/Core/Extensions/RepositoryExtension.ChatOption.cs b/Core/Extensions/RepositoryExtension.ChatOption.cs new file mode 100644 index 0000000..c45a733 --- /dev/null +++ b/Core/Extensions/RepositoryExtension.ChatOption.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Kruzya.TelegramBot.Core.Data; +using Microsoft.EntityFrameworkCore; + +namespace Kruzya.TelegramBot.Core.Extensions; + +public partial class RepositoryExtension +{ + public static async Task FindOption(this DbSet repository, long chatId, + string optionId) + { + return await repository.SingleOrDefaultAsync(x => x.ChatId == chatId && x.OptionId == optionId); + } + + public static async Task FindOrCreateOption(this DbSet repository, long chatId, + string optionId) + { + var opt = await repository.FindOption(chatId, optionId) ?? new ChatOptionValue + { + ChatId = chatId, + OptionId = optionId + }; + + return opt; + } +} \ No newline at end of file diff --git a/Core/Migrations/20230720171645_AddChatOptions.Designer.cs b/Core/Migrations/20230720171645_AddChatOptions.Designer.cs new file mode 100644 index 0000000..751cbe8 --- /dev/null +++ b/Core/Migrations/20230720171645_AddChatOptions.Designer.cs @@ -0,0 +1,95 @@ +// +using System; +using Kruzya.TelegramBot.Core.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Kruzya.TelegramBot.Core.Migrations +{ + [DbContext(typeof(CoreContext))] + [Migration("20230720171645_AddChatOptions")] + partial class AddChatOptions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUser", b => + { + b.Property("BotUserId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChatId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("BotUserId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b => + { + b.Property("BotUserValueId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("BotUserId") + .HasColumnType("char(36)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Value") + .HasColumnType("longblob"); + + b.Property("ValueType") + .HasColumnType("longtext"); + + b.HasKey("BotUserValueId"); + + b.HasIndex("BotUserId"); + + b.ToTable("UserValues"); + }); + + modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.ChatOptionValue", b => + { + b.Property("ChatId") + .HasColumnType("bigint"); + + b.Property("OptionId") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("ChatId", "OptionId"); + + b.ToTable("ChatOptionValues"); + }); + + modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b => + { + b.HasOne("Kruzya.TelegramBot.Core.Data.BotUser", "BotUser") + .WithMany() + .HasForeignKey("BotUserId"); + + b.Navigation("BotUser"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Core/Migrations/20230720171645_AddChatOptions.cs b/Core/Migrations/20230720171645_AddChatOptions.cs new file mode 100644 index 0000000..29c59d0 --- /dev/null +++ b/Core/Migrations/20230720171645_AddChatOptions.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Kruzya.TelegramBot.Core.Migrations +{ + /// + public partial class AddChatOptions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ChatOptionValues", + columns: table => new + { + ChatId = table.Column(type: "bigint", nullable: false), + OptionId = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longblob", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ChatOptionValues", x => new { x.ChatId, x.OptionId }); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ChatOptionValues"); + + migrationBuilder.AlterColumn( + name: "ValueType", + table: "UserValues", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "UserValues", + type: "longtext CHARACTER SET utf8mb4", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Core/Migrations/CoreContextModelSnapshot.cs b/Core/Migrations/CoreContextModelSnapshot.cs index 8545ed9..971a65a 100644 --- a/Core/Migrations/CoreContextModelSnapshot.cs +++ b/Core/Migrations/CoreContextModelSnapshot.cs @@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +#nullable disable + namespace Kruzya.TelegramBot.Core.Migrations { [DbContext(typeof(CoreContext))] @@ -14,7 +16,7 @@ namespace Kruzya.TelegramBot.Core.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.2") + .HasAnnotation("ProductVersion", "7.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUser", b => @@ -44,13 +46,13 @@ namespace Kruzya.TelegramBot.Core.Migrations .HasColumnType("char(36)"); b.Property("Name") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.Property("Value") .HasColumnType("longblob"); b.Property("ValueType") - .HasColumnType("longtext CHARACTER SET utf8mb4"); + .HasColumnType("longtext"); b.HasKey("BotUserValueId"); @@ -59,11 +61,30 @@ namespace Kruzya.TelegramBot.Core.Migrations b.ToTable("UserValues"); }); + modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.ChatOptionValue", b => + { + b.Property("ChatId") + .HasColumnType("bigint"); + + b.Property("OptionId") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("ChatId", "OptionId"); + + b.ToTable("ChatOptionValues"); + }); + modelBuilder.Entity("Kruzya.TelegramBot.Core.Data.BotUserValue", b => { b.HasOne("Kruzya.TelegramBot.Core.Data.BotUser", "BotUser") .WithMany() .HasForeignKey("BotUserId"); + + b.Navigation("BotUser"); }); #pragma warning restore 612, 618 } diff --git a/Core/Options/AbstractOption.cs b/Core/Options/AbstractOption.cs new file mode 100644 index 0000000..067e923 --- /dev/null +++ b/Core/Options/AbstractOption.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Kruzya.TelegramBot.Core.Options; + +public abstract class AbstractOption : IOption + where TOption: AbstractOption +{ + private readonly IOptionProvider _optionProvider; + + protected AbstractOption(IOptionProvider optionProvider) + { + _optionProvider = optionProvider; + } + + public async Task GetValueAsync(long chatId) + { + return await _optionProvider.GetValueAsync(chatId); + } + + public async Task SetValueAsync(long chatId, TValue value) + { + await _optionProvider.SetValueAsync(chatId, value); + } +} \ No newline at end of file diff --git a/Core/Options/DbOptionProvider.cs b/Core/Options/DbOptionProvider.cs new file mode 100644 index 0000000..a67fc0b --- /dev/null +++ b/Core/Options/DbOptionProvider.cs @@ -0,0 +1,59 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using Kruzya.TelegramBot.Core.Data; +using Kruzya.TelegramBot.Core.Extensions; + +namespace Kruzya.TelegramBot.Core.Options; + +public class DbOptionProvider : IOptionProvider +{ + private readonly CoreContext _db; + + public DbOptionProvider(CoreContext db) + { + _db = db; + } + + public async Task GetValueAsync(long chatId) + { + return await GetValueAsync( + chatId, + GetOptionId(), + GetDefaultValue() + ); + } + + public async Task GetValueAsync(long chatId, string optionId, TValue defVal) + { + return (await _db.ChatOptionValues.FindOrCreateOption(chatId, optionId)).GetValue(defVal); + } + + public async Task SetValueAsync(long chatId, TValue value) + { + var chatOption = await _db.ChatOptionValues.FindOrCreateOption(chatId, GetOptionId()); + chatOption.SetValue(value); + _db.AddOrUpdate(chatOption); + } + + protected OptionAttribute GetOptionAttribute() + { + return typeof(TOption).GetCustomAttribute(); + } + + protected string GetOptionId() + { + var optionId = GetOptionAttribute()?.OptionId; + if (optionId == null) + { + throw new ArgumentException("Option must implement OptionAttribute"); + } + + return optionId; + } + + protected TValue GetDefaultValue() + { + return (TValue) GetOptionAttribute().DefaultValue; + } +} \ No newline at end of file diff --git a/Core/Options/IOption.cs b/Core/Options/IOption.cs new file mode 100644 index 0000000..04fe8a9 --- /dev/null +++ b/Core/Options/IOption.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Kruzya.TelegramBot.Core.Options; + +public interface IOption +{ + public Task GetValueAsync(long chatId); + public Task SetValueAsync(long chatId, TValue value); +} \ No newline at end of file diff --git a/Core/Options/IOptionProvider.cs b/Core/Options/IOptionProvider.cs new file mode 100644 index 0000000..f09e690 --- /dev/null +++ b/Core/Options/IOptionProvider.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Kruzya.TelegramBot.Core.Options; + +public interface IOptionProvider +{ + public Task GetValueAsync(long chatId); + public Task SetValueAsync(long chatId, TValue value); +} \ No newline at end of file diff --git a/Core/Options/OptionAttribute.cs b/Core/Options/OptionAttribute.cs new file mode 100644 index 0000000..7de5136 --- /dev/null +++ b/Core/Options/OptionAttribute.cs @@ -0,0 +1,17 @@ +using System; +using System.Runtime.Serialization; + +namespace Kruzya.TelegramBot.Core.Options; + +[AttributeUsage(AttributeTargets.Class)] +public class OptionAttribute : Attribute +{ + public string OptionId { get; } + public object DefaultValue { get; } + + public OptionAttribute(string optionId, object defaultValue = default) + { + OptionId = optionId; + DefaultValue = defaultValue; + } +} \ No newline at end of file diff --git a/Core/Startup.cs b/Core/Startup.cs index b5e18f4..ca3f127 100644 --- a/Core/Startup.cs +++ b/Core/Startup.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; +using Kruzya.TelegramBot.Core.Options; using Telegram.Bot.Types; namespace Kruzya.TelegramBot.Core @@ -53,6 +54,8 @@ namespace Kruzya.TelegramBot.Core services.AddSingleton>(); services.AddHostedService(); + services.AddScoped(); + // Trigger module handlers. _core.Modules.ConfigureServices(services); } diff --git a/Core/Test.cs b/Core/Test.cs new file mode 100644 index 0000000..aadf958 --- /dev/null +++ b/Core/Test.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using BotFramework; +using BotFramework.Attributes; +using BotFramework.Enums; +using Telegram.Bot; + +namespace Kruzya.TelegramBot.Core; + +public class Test : BotEventHandler +{ + public uint Counter { get; set; } = 5; + + + [Command(InChat.All, "test_state")] + public async Task TestState() + { + Counter++; + await Bot.SendTextMessageAsync(Chat.Id, Counter.ToString()); + } +} \ No newline at end of file diff --git a/modules/Entertainment/Entertainment.cs b/modules/Entertainment/Entertainment.cs index d184b4b..8e419f3 100644 --- a/modules/Entertainment/Entertainment.cs +++ b/modules/Entertainment/Entertainment.cs @@ -3,7 +3,9 @@ global using GuessCache = Kruzya.TelegramBot.Core.Cache.MemoryCache(); + services.AddScoped(); } } } \ No newline at end of file diff --git a/modules/Entertainment/Handler/Common.cs b/modules/Entertainment/Handler/Common.cs index b3bf726..2788fac 100644 --- a/modules/Entertainment/Handler/Common.cs +++ b/modules/Entertainment/Handler/Common.cs @@ -12,20 +12,23 @@ using Kruzya.TelegramBot.Core.Service; using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types.Enums; +using West.Entertainment.Options; namespace West.Entertainment.Handler { public class Common : BotEventHandler { private readonly UserService _userService; + private readonly NsfwOption _nsfwOption; private readonly ILogger _logger; private readonly CoreContext _db; - public Common(CoreContext db, ILogger logger, UserService userService) + public Common(CoreContext db, ILogger logger, UserService userService, NsfwOption nsfwOption) { _db = db; _logger = logger; _userService = userService; + _nsfwOption = nsfwOption; } [Command(InChat.Public, "cooldowns", CommandParseMode.Both)] @@ -59,5 +62,20 @@ namespace West.Entertainment.Handler await Bot.SendTextMessageAsync(Chat.Id, msg.ToString(), parseMode: ParseMode.Html); } + + [Command("test_option_get")] + public async Task HandleTestOptionGet() + { + await Bot.SendTextMessageAsync(Chat.Id, $"nsfw option is: {await _nsfwOption.GetValueAsync(Chat.Id)}"); + } + + [Command("test_option_set")] + public async Task HandleTestOptionSet() + { + var value = await _nsfwOption.GetValueAsync(Chat.Id); + await _nsfwOption.SetValueAsync(Chat.Id, !value); + + await Bot.SendTextMessageAsync(Chat.Id, $"nsfw option is: {await _nsfwOption.GetValueAsync(Chat.Id)}"); + } } } \ No newline at end of file diff --git a/modules/Entertainment/Options/NsfwOption.cs b/modules/Entertainment/Options/NsfwOption.cs new file mode 100644 index 0000000..2307941 --- /dev/null +++ b/modules/Entertainment/Options/NsfwOption.cs @@ -0,0 +1,11 @@ +using Kruzya.TelegramBot.Core.Options; + +namespace West.Entertainment.Options; + +[Option("allowNSFW", false)] +public class NsfwOption : AbstractOption +{ + public NsfwOption(IOptionProvider optionProvider) : base(optionProvider) + { + } +} \ No newline at end of file