diff --git a/Core/Extensions/RepositoryExtension.CoreContext.cs b/Core/Extensions/RepositoryExtension.CoreContext.cs index bf89ce4..197edc0 100644 --- a/Core/Extensions/RepositoryExtension.CoreContext.cs +++ b/Core/Extensions/RepositoryExtension.CoreContext.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Kruzya.TelegramBot.Core.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.Logging; namespace Kruzya.TelegramBot.Core.Extensions { @@ -51,6 +52,9 @@ namespace Kruzya.TelegramBot.Core.Extensions public static async Task FindOrCreateOption(this DbSet repository, long chatId, long userId, string option) { + repository.GetService>().LogCritical("FindOrCreateOption() is deprecated. " + + "Please, use the new API OptionManagerService."); + var opt = await repository.FindOption(chatId, userId, option); if (opt == null) { diff --git a/Core/Option/CoreContextStorageService.cs b/Core/Option/CoreContextStorageService.cs new file mode 100644 index 0000000..3294a26 --- /dev/null +++ b/Core/Option/CoreContextStorageService.cs @@ -0,0 +1,51 @@ +#nullable enable +using System.Linq; +using System.Threading.Tasks; +using Kruzya.TelegramBot.Core.Data; +using Kruzya.TelegramBot.Core.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace Kruzya.TelegramBot.Core.Option; + +public class CoreContextStorageService : IOptionStorageAdapter +{ + private CoreContext _db; + public CoreContextStorageService(CoreContext db) + { + _db = db; + } + + public async Task LoadValueAsync(IOptionKey key) + { + var entity = await BuildQueryForKey(key).FirstOrDefaultAsync(); + + return entity?.Value; + } + + public async Task SaveValueAsync(IOptionKey key, byte[] value) + { + var entity = await BuildQueryForKey(key).FirstOrDefaultAsync(); + if (entity == null) + { + entity = await CreateEntity(key); + } + + entity.Value = value; + _db.AddOrUpdate(entity); + } + + private IQueryable BuildQueryForKey(IOptionKey key) + { + var legacyUserId = key.UserId ?? 0; + + return _db.UserValues.Where(e => e.BotUser.ChatId == key.ChatId && e.BotUser.UserId == legacyUserId + && e.Name == key.Identifier); + } + + private async Task CreateEntity(IOptionKey key) + { + var entity = _db.UserValues.Create()!; + entity.BotUser = await _db.Users.FindOrCreate(key.ChatId, key.UserId ?? 0); + return entity; + } +} diff --git a/Core/Option/IOptionItem.cs b/Core/Option/IOptionItem.cs new file mode 100644 index 0000000..c435894 --- /dev/null +++ b/Core/Option/IOptionItem.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace Kruzya.TelegramBot.Core.Option; + +/// +/// Allows interact with option related with some chat. +/// +public interface IOptionItem +{ + public IOptionKey Key { get; } + public T Value { get; set; } + public bool IsChanged { get; } + + public Task LoadAsync(IOptionStorageAdapter adapter); + public Task SaveAsync(IOptionStorageAdapter adapter); +} diff --git a/Core/Option/IOptionKey.cs b/Core/Option/IOptionKey.cs new file mode 100644 index 0000000..ac150c7 --- /dev/null +++ b/Core/Option/IOptionKey.cs @@ -0,0 +1,22 @@ +namespace Kruzya.TelegramBot.Core.Option; + +/// +/// Explains what +/// +public interface IOptionKey +{ + /// + /// The chat identifier where this key is related. + /// + public long ChatId { get; } + + /// + /// The Telegram user identifier. May be NULL if this is a simple option for all chat members. + /// + public long? UserId { get; } + + /// + /// The unique option identifier. + /// + public string Identifier { get; } +} \ No newline at end of file diff --git a/Core/Option/IOptionManagerService.cs b/Core/Option/IOptionManagerService.cs new file mode 100644 index 0000000..a01ff30 --- /dev/null +++ b/Core/Option/IOptionManagerService.cs @@ -0,0 +1,9 @@ +namespace Kruzya.TelegramBot.Core.Option; + +/// +/// Describes what API should be implemented in Option Manager Service. +/// +public interface IOptionManagerService +{ + +} diff --git a/Core/Option/IOptionStorageAdapter.cs b/Core/Option/IOptionStorageAdapter.cs new file mode 100644 index 0000000..415308f --- /dev/null +++ b/Core/Option/IOptionStorageAdapter.cs @@ -0,0 +1,26 @@ +#nullable enable + +using System.Threading.Tasks; + +namespace Kruzya.TelegramBot.Core.Option; + +/// +/// Implements an interaction with storage for options. +/// +public interface IOptionStorageAdapter +{ + /// + /// Loads a saved value (if exists) from storage. + /// + /// + /// + public Task LoadValueAsync(IOptionKey key); + + /// + /// Saves a passed value to storage. + /// + /// The unique chat key + /// The serialized value for saving + /// + public Task SaveValueAsync(IOptionKey key, byte[] value); +} diff --git a/Core/Option/OptionItem.cs b/Core/Option/OptionItem.cs new file mode 100644 index 0000000..ac2bb99 --- /dev/null +++ b/Core/Option/OptionItem.cs @@ -0,0 +1,53 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Kruzya.TelegramBot.Core.Option; + +public class OptionItem : IOptionItem +{ + public IOptionKey Key { get; } + + public T Value + { + get => _value; + + set + { + _value = value; + _isChanged = true; + } + } + + public bool IsChanged => _isChanged; + + private T _value; + private bool _isChanged; + + public OptionItem(IOptionKey key, T value) + { + Key = key; + Value = value; + } + + public async Task LoadAsync(IOptionStorageAdapter adapter) + { + var savedValue = await adapter.LoadValueAsync(Key); + if (savedValue == null) + { + return; + } + + // This property changed when we're loaded default value. Reset here. + _isChanged = false; + _value = JsonSerializer.Deserialize(new ReadOnlySpan(savedValue)); + } + + public async Task SaveAsync(IOptionStorageAdapter adapter) + { + var data = JsonSerializer.SerializeToUtf8Bytes(Value, new JsonSerializerOptions { WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); + await adapter.SaveValueAsync(Key, data); + } +} \ No newline at end of file diff --git a/Core/Option/OptionKey.cs b/Core/Option/OptionKey.cs new file mode 100644 index 0000000..5e7f1f4 --- /dev/null +++ b/Core/Option/OptionKey.cs @@ -0,0 +1,26 @@ +using Telegram.Bot.Types; + +namespace Kruzya.TelegramBot.Core.Option; + +public class OptionKey : IOptionKey +{ + public long ChatId { get; } + public long? UserId { get; } = null; + public string Identifier { get; } + + public OptionKey(Chat chat, User user, string identifier) : this(chat.Id, user.Id, identifier) + {} + + public OptionKey(Chat chat, string identifier) : this(chat.Id, null, identifier) + {} + + public OptionKey(long chat, string identifier) : this(chat, null, identifier) + {} + + public OptionKey(long chat, long? user, string identifier) + { + ChatId = chat; + UserId = user; + Identifier = identifier; + } +}