diff --git a/Core/Data/ChatOptionValue.cs b/Core/Data/ChatOptionValue.cs index 3e3f010..9fc4027 100644 --- a/Core/Data/ChatOptionValue.cs +++ b/Core/Data/ChatOptionValue.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; using System.Text.Json; using System; @@ -15,7 +13,7 @@ public class ChatOptionValue public byte[] Value { get; set; } - public T GetValue(T? defVal = default) + public T GetValue(T defVal = default) { try { diff --git a/Core/Extensions/BotExtension.cs b/Core/Extensions/BotExtension.cs index 0ca594a..60fffaf 100644 --- a/Core/Extensions/BotExtension.cs +++ b/Core/Extensions/BotExtension.cs @@ -22,14 +22,14 @@ namespace Kruzya.TelegramBot.Core.Extensions public static async Task IsUserAdminAsync(this ITelegramBotClient bot, Chat chat, User user) { var callerMember = await bot.GetChatMemberAsync(chat, user.Id); - var canUse = callerMember is ChatMemberOwner; + var isAdmin = callerMember is ChatMemberOwner; if (callerMember is ChatMemberAdministrator callerAdmin) { - canUse = callerAdmin.CanRestrictMembers; + isAdmin = callerAdmin.CanRestrictMembers; } - return canUse; + return isAdmin; } public static async Task CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat) diff --git a/Core/Handler/CommonHandler.cs b/Core/Handler/CommonHandler.cs index b6758e9..136a1ee 100644 --- a/Core/Handler/CommonHandler.cs +++ b/Core/Handler/CommonHandler.cs @@ -53,5 +53,10 @@ namespace Kruzya.TelegramBot.Core.Handler { return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "warnCount"); } + + protected async Task AnswerQuery(string id, string? message = null) + { + await Bot.AnswerCallbackQueryAsync(id, message); + } } } \ No newline at end of file diff --git a/Core/Handler/SettingsHandler.cs b/Core/Handler/SettingsHandler.cs new file mode 100644 index 0000000..bd50c70 --- /dev/null +++ b/Core/Handler/SettingsHandler.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BotFramework.Attributes; +using BotFramework.Enums; +using Kruzya.TelegramBot.Core.Data; +using Kruzya.TelegramBot.Core.Extensions; +using Kruzya.TelegramBot.Core.Options; +using Microsoft.Extensions.DependencyInjection; +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.ReplyMarkups; + +namespace Kruzya.TelegramBot.Core.Handler; + +public class SettingsHandler : CommonHandler +{ + private readonly IEnumerable _options; + + public SettingsHandler(CoreContext db, IServiceProvider serviceProvider) : base(db) + { + _options = serviceProvider.GetServices(); + } + + [Command(InChat.Public, "settings", CommandParseMode.WithUsername)] + public async Task HandleSettings() + { + if (!await Bot.IsUserAdminAsync(Chat, From)) + { + return; + } + + await Bot.SendTextMessageAsync( + Chat.Id, + "What do you want to change?", + replyMarkup: GetSettingsMarkup() + ); + } + + [Update(InChat.Public, UpdateFlag.CallbackQuery)] + public async Task HandleCallback() + { + var query = CallbackQuery; + var queryData = query.Data; + + if (queryData == null || !queryData.StartsWith("option") || !await Bot.IsUserAdminAsync(Chat, From)) + { + return; + } + + var paramList = queryData.Split('|'); + if (!long.TryParse(paramList[1], out var senderId)) + { + return; + } + + if (senderId != From.Id) + { + await AnswerQuery(query.Id, "Keyboard called by another user"); + return; + } + + if (paramList[2] == "main_menu") + { + await AnswerQuery(query.Id); + await Bot.EditMessageTextAsync(Chat.Id, query.Message!.MessageId, "What do you want to change?", + replyMarkup: GetSettingsMarkup()); + return; + } + + if (paramList[2] == "edit" || paramList[2] == "select") + { + var option = GetOptionById(paramList[3]); + if (option == null) + { + await AnswerQuery(query.Id, "Invalid option ID"); + return; + } + + switch (paramList[2]) + { + case "edit": + await HandleOptionEdit(option, paramList, query); + break; + case "select": + await HandleOptionSelect(option, paramList, query); + break; + } + } + } + + private async Task HandleOptionEdit(IOption option, string[] paramList, CallbackQuery query) + { + var chatId = Chat.Id; + var messageId = query.Message!.MessageId; + switch (option.OptionType) + { + case OptionType.OnOff: + var onOffOpt = (IOption) option; + await onOffOpt.SetValueAsync(chatId, !await onOffOpt.GetValueAsync(chatId)); + await AnswerQuery(query.Id); + + await Bot.EditMessageReplyMarkupAsync(chatId, messageId, GetSettingsMarkup()); + break; + + case OptionType.Select: + var selectOpt = (IOption) option; + await AnswerQuery(query.Id); + + await Bot.EditMessageTextAsync(Chat.Id, messageId, "Here's what you can choose:", + replyMarkup: await GetChoiceSelectKeyboard(selectOpt)); + break; + + default: + await AnswerQuery(query.Id, "Not implemented"); + break; + } + } + + private async Task HandleOptionSelect(IOption option, string[] paramList, CallbackQuery query) + { + var choice = option.ChoiceList.SingleOrDefault(x => x.Id == paramList[4]); + if (choice == null) + { + await AnswerQuery(query.Id, "Invalid option choice"); + return; + } + + var selectOption = (IOption) option; + await selectOption.SetValueAsync(Chat.Id, choice.Id); + await AnswerQuery(query.Id); + + await Bot.EditMessageReplyMarkupAsync(Chat.Id, query.Message!.MessageId, await GetChoiceSelectKeyboard(selectOption)); + } + + private async Task GetKeyboardButton(IOption option) + { + var button = new InlineKeyboardButton(option.OptionName) + { + CallbackData = $"option|{From.Id}|edit|{option.OptionId}" + }; + + if (option.OptionType == OptionType.OnOff) + { + var isEnabled = await ((IOption) option).GetValueAsync(Chat.Id); + button.Text += isEnabled ? " 🟢" : " 🔴"; + } + + return button; + } + + private async Task GetChoiceSelectKeyboard(IOption option) + { + var value = await option.GetValueAsync(Chat.Id); + var choices = option.ChoiceList.Select(x => x.ToButton(From.Id, option.OptionId, x.Id == value)) + .ToList(); + + choices.Add(new [] + { + new InlineKeyboardButton("Main menu") + { + CallbackData = $"option|{From.Id}|main_menu" + } + }); + + return new InlineKeyboardMarkup(choices); + } + + private InlineKeyboardMarkup GetSettingsMarkup() + { + var keyboardLayout = _options.ToAsyncEnumerable() + .SelectAwait(async x => await GetKeyboardButton(x)) + .ToEnumerable() + .Chunk(3); + + return new InlineKeyboardMarkup(keyboardLayout); + } + + private IOption GetOptionById(string optionId) + { + return _options.SingleOrDefault(x => x.OptionId == optionId); + } +} \ No newline at end of file diff --git a/Core/Options/AbstractOption.cs b/Core/Options/AbstractOption.cs index 067e923..bc8dd22 100644 --- a/Core/Options/AbstractOption.cs +++ b/Core/Options/AbstractOption.cs @@ -1,4 +1,4 @@ -using System.Runtime.Serialization; +using System.Collections.Generic; using System.Threading.Tasks; namespace Kruzya.TelegramBot.Core.Options; @@ -6,6 +6,12 @@ namespace Kruzya.TelegramBot.Core.Options; public abstract class AbstractOption : IOption where TOption: AbstractOption { + public abstract string OptionId { get; } + public abstract string OptionName { get; } + public abstract OptionType OptionType { get; } + public abstract TValue DefaultValue { get; } + public List ChoiceList { get; } = new(); + private readonly IOptionProvider _optionProvider; protected AbstractOption(IOptionProvider optionProvider) @@ -15,11 +21,11 @@ public abstract class AbstractOption : IOption public async Task GetValueAsync(long chatId) { - return await _optionProvider.GetValueAsync(chatId); + return await _optionProvider.GetValueAsync(chatId, OptionId, DefaultValue); } public async Task SetValueAsync(long chatId, TValue value) { - await _optionProvider.SetValueAsync(chatId, value); + await _optionProvider.SetValueAsync(chatId, OptionId, value); } } \ No newline at end of file diff --git a/Core/Options/DbOptionProvider.cs b/Core/Options/DbOptionProvider.cs index a67fc0b..4c66ecf 100644 --- a/Core/Options/DbOptionProvider.cs +++ b/Core/Options/DbOptionProvider.cs @@ -1,6 +1,4 @@ -using System; -using System.Reflection; -using System.Threading.Tasks; +using System.Threading.Tasks; using Kruzya.TelegramBot.Core.Data; using Kruzya.TelegramBot.Core.Extensions; @@ -14,46 +12,16 @@ public class DbOptionProvider : IOptionProvider { _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) + public async Task SetValueAsync(long chatId, string optionId, TValue value) { - var chatOption = await _db.ChatOptionValues.FindOrCreateOption(chatId, GetOptionId()); + var chatOption = await _db.ChatOptionValues.FindOrCreateOption(chatId, optionId); 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 index 04fe8a9..8a37b24 100644 --- a/Core/Options/IOption.cs +++ b/Core/Options/IOption.cs @@ -1,10 +1,20 @@ -using System.Runtime.Serialization; +using System.Collections.Generic; using System.Threading.Tasks; namespace Kruzya.TelegramBot.Core.Options; -public interface IOption +public interface IOption { + public string OptionId { get; } + public string OptionName { get; } + public OptionType OptionType { get; } + public List ChoiceList { get; } +} + +public interface IOption : IOption +{ + public TValue DefaultValue { get; } + 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 index f09e690..ba771ed 100644 --- a/Core/Options/IOptionProvider.cs +++ b/Core/Options/IOptionProvider.cs @@ -4,6 +4,6 @@ namespace Kruzya.TelegramBot.Core.Options; public interface IOptionProvider { - public Task GetValueAsync(long chatId); - public Task SetValueAsync(long chatId, TValue value); + public Task GetValueAsync(long chatId, string optionId, TValue defVal); + public Task SetValueAsync(long chatId, string optionId, TValue value); } \ No newline at end of file diff --git a/Core/Options/OptionAttribute.cs b/Core/Options/OptionAttribute.cs deleted file mode 100644 index 7de5136..0000000 --- a/Core/Options/OptionAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -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/Options/OptionType.cs b/Core/Options/OptionType.cs new file mode 100644 index 0000000..6c65c56 --- /dev/null +++ b/Core/Options/OptionType.cs @@ -0,0 +1,7 @@ +namespace Kruzya.TelegramBot.Core.Options; + +public enum OptionType +{ + OnOff, + Select +} \ No newline at end of file diff --git a/Core/Options/SelectChoice.cs b/Core/Options/SelectChoice.cs new file mode 100644 index 0000000..3dc3900 --- /dev/null +++ b/Core/Options/SelectChoice.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Telegram.Bot.Types.ReplyMarkups; + +namespace Kruzya.TelegramBot.Core.Options; + +public class SelectChoice +{ + public string Id { get; } + public string FullName { get; } + public string ShortName { get; } + + public SelectChoice(string id, string fullName, string shortName) + { + Id = id; + FullName = fullName; + ShortName = shortName; + } + + public IEnumerable ToButton(long userId, string optionId, bool isActive = false) + { + return new[] + { + new InlineKeyboardButton((isActive ? "🟢 " : "🔴 ") + FullName) + { + CallbackData = $"option|{userId}|select|{optionId}|{Id}" + } + }; + } +} diff --git a/modules/ContentStore/ContentStore.cs b/modules/ContentStore/ContentStore.cs index 7710d77..294f28a 100644 --- a/modules/ContentStore/ContentStore.cs +++ b/modules/ContentStore/ContentStore.cs @@ -1,5 +1,7 @@ using Kruzya.TelegramBot.Core; +using Kruzya.TelegramBot.Core.Options; using Microsoft.Extensions.DependencyInjection; +using West.TelegramBot.ContentStore.Option; using West.TelegramBot.ContentStore.Service; namespace West.TelegramBot.ContentStore; @@ -22,6 +24,8 @@ public class ContentStore : Module public override void ConfigureServices(IServiceCollection services) { + services.AddScoped(); + foreach (var serviceType in MediaServiceTypes) { services.AddScoped(typeof(IRandomMediaService), serviceType); diff --git a/modules/ContentStore/ContentStore.csproj b/modules/ContentStore/ContentStore.csproj index d2e1a4f..c520860 100644 --- a/modules/ContentStore/ContentStore.csproj +++ b/modules/ContentStore/ContentStore.csproj @@ -1,4 +1,4 @@ - + net6.0 diff --git a/modules/ContentStore/Handler/Store.cs b/modules/ContentStore/Handler/Store.cs index a524a56..6ad2819 100644 --- a/modules/ContentStore/Handler/Store.cs +++ b/modules/ContentStore/Handler/Store.cs @@ -1,26 +1,26 @@ #nullable enable -using BotFramework; using BotFramework.Attributes; using BotFramework.Enums; +using Kruzya.TelegramBot.Core.Data; using Kruzya.TelegramBot.Core.Extensions; +using Kruzya.TelegramBot.Core.Handler; using Kruzya.TelegramBot.Core.Service; using Microsoft.Extensions.DependencyInjection; using Telegram.Bot; -using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; using West.TelegramBot.ContentStore.Service; namespace West.TelegramBot.ContentStore.Handler; -public class Store : BotEventHandler +public class Store : CommonHandler { private readonly IReputation? _reputation; private readonly IServiceProvider _provider; private readonly ContentStore _store; - public Store(IServiceProvider provider, ContentStore store) + public Store(CoreContext db, IServiceProvider provider, ContentStore store) : base(db) { _reputation = provider.GetService(); _provider = provider; @@ -164,9 +164,4 @@ public class Store : BotEventHandler return mediaService != null; } - - private async Task AnswerQuery(string id, string? message = null) - { - await Bot.AnswerCallbackQueryAsync(id, message); - } } \ No newline at end of file diff --git a/modules/ContentStore/Option/AllowNsfw.cs b/modules/ContentStore/Option/AllowNsfw.cs new file mode 100644 index 0000000..11f047d --- /dev/null +++ b/modules/ContentStore/Option/AllowNsfw.cs @@ -0,0 +1,15 @@ +using Kruzya.TelegramBot.Core.Options; + +namespace West.TelegramBot.ContentStore.Option; + +public class AllowNsfw : AbstractOption +{ + public override string OptionId => "allowNSFW"; + public override string OptionName => "NSFW"; + public override OptionType OptionType => OptionType.OnOff; + public override bool DefaultValue => false; + + public AllowNsfw(IOptionProvider optionProvider) : base(optionProvider) + { + } +} \ No newline at end of file diff --git a/modules/Entertainment/Entertainment.cs b/modules/Entertainment/Entertainment.cs index 8e419f3..0203fdd 100644 --- a/modules/Entertainment/Entertainment.cs +++ b/modules/Entertainment/Entertainment.cs @@ -5,7 +5,6 @@ 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 2788fac..b3bf726 100644 --- a/modules/Entertainment/Handler/Common.cs +++ b/modules/Entertainment/Handler/Common.cs @@ -12,23 +12,20 @@ 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, NsfwOption nsfwOption) + public Common(CoreContext db, ILogger logger, UserService userService) { _db = db; _logger = logger; _userService = userService; - _nsfwOption = nsfwOption; } [Command(InChat.Public, "cooldowns", CommandParseMode.Both)] @@ -62,20 +59,5 @@ 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 deleted file mode 100644 index 2307941..0000000 --- a/modules/Entertainment/Options/NsfwOption.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 diff --git a/modules/Reputation/Options/KarmaMode.cs b/modules/Reputation/Options/KarmaMode.cs new file mode 100644 index 0000000..404ae0f --- /dev/null +++ b/modules/Reputation/Options/KarmaMode.cs @@ -0,0 +1,21 @@ +using Kruzya.TelegramBot.Core.Options; + +namespace West.TelegramBot.Reputation.Options; + +public class KarmaMode : AbstractOption +{ + public override string OptionId => "karmaMode"; + public override string OptionName => "Karma Mode"; + public override OptionType OptionType => OptionType.Select; + public override string DefaultValue => "geometric"; + + public KarmaMode(IOptionProvider optionProvider) : base(optionProvider) + { + ChoiceList.AddRange(new [] + { + new SelectChoice("geometric", "Geometric", "G"), + new SelectChoice("linear", "Linear", "L") + }); + } +} + diff --git a/modules/Reputation/Reputation.cs b/modules/Reputation/Reputation.cs index ff524b8..5ef8769 100644 --- a/modules/Reputation/Reputation.cs +++ b/modules/Reputation/Reputation.cs @@ -1,9 +1,11 @@ using Kruzya.TelegramBot.Core; +using Kruzya.TelegramBot.Core.Options; using Kruzya.TelegramBot.Core.Service; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using West.TelegramBot.Reputation.Data; +using West.TelegramBot.Reputation.Options; using West.TelegramBot.Reputation.Service; namespace West.TelegramBot.Reputation; @@ -20,5 +22,6 @@ public class Reputation : Module services.AddDbContext(options => options.UseMySql(dsn, ServerVersion.AutoDetect(dsn))); services.AddScoped(); + services.AddScoped(); } } \ No newline at end of file diff --git a/modules/Reputation/Reputation.csproj b/modules/Reputation/Reputation.csproj index 7d3549f..28d1b18 100644 --- a/modules/Reputation/Reputation.csproj +++ b/modules/Reputation/Reputation.csproj @@ -1,4 +1,4 @@ - + net6.0