[core] Massive setting system update

- Get rid of attribute crap. Now only interface is required
- Rename canUse to isAdmin in `Bot.IsUserAdminAsync` to match the actual meaning of variable
- Made `AnswerQuery` reusable by moving it from ContentStore to Core's CommonHandler
- Chat Settings edit frontend (inline keyboard)
- Rename NsfwOption to AllowNsfw and move it to ContentStore module
- Remove option backend testing commands
- Add KarmaMode option as an example of Select option type
This commit is contained in:
Andriy
2023-07-21 17:14:11 +03:00
parent 467ccd4fde
commit 27ddfd39cc
21 changed files with 307 additions and 110 deletions
+2 -4
View File
@@ -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>(T? defVal = default)
public T GetValue<T>(T defVal = default)
{
try
{
+3 -3
View File
@@ -22,14 +22,14 @@ namespace Kruzya.TelegramBot.Core.Extensions
public static async Task<bool> 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<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat)
+5
View File
@@ -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);
}
}
}
+184
View File
@@ -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<IOption> _options;
public SettingsHandler(CoreContext db, IServiceProvider serviceProvider) : base(db)
{
_options = serviceProvider.GetServices<IOption>();
}
[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<bool>) 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<string>) 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<string>) 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<InlineKeyboardButton> 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<bool>) option).GetValueAsync(Chat.Id);
button.Text += isEnabled ? " 🟢" : " 🔴";
}
return button;
}
private async Task<InlineKeyboardMarkup> GetChoiceSelectKeyboard(IOption<string> 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);
}
}
+9 -3
View File
@@ -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<TOption, TValue> : IOption<TValue>
where TOption: AbstractOption<TOption, TValue>
{
public abstract string OptionId { get; }
public abstract string OptionName { get; }
public abstract OptionType OptionType { get; }
public abstract TValue DefaultValue { get; }
public List<SelectChoice> ChoiceList { get; } = new();
private readonly IOptionProvider _optionProvider;
protected AbstractOption(IOptionProvider optionProvider)
@@ -15,11 +21,11 @@ public abstract class AbstractOption<TOption, TValue> : IOption<TValue>
public async Task<TValue> GetValueAsync(long chatId)
{
return await _optionProvider.GetValueAsync<TOption, TValue>(chatId);
return await _optionProvider.GetValueAsync(chatId, OptionId, DefaultValue);
}
public async Task SetValueAsync(long chatId, TValue value)
{
await _optionProvider.SetValueAsync<TOption, TValue>(chatId, value);
await _optionProvider.SetValueAsync(chatId, OptionId, value);
}
}
+4 -36
View File
@@ -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<TValue> GetValueAsync<TOption, TValue>(long chatId)
{
return await GetValueAsync(
chatId,
GetOptionId<TOption>(),
GetDefaultValue<TOption, TValue>()
);
}
public async Task<TValue> GetValueAsync<TValue>(long chatId, string optionId, TValue defVal)
{
return (await _db.ChatOptionValues.FindOrCreateOption(chatId, optionId)).GetValue(defVal);
}
public async Task SetValueAsync<TOption, TValue>(long chatId, TValue value)
public async Task SetValueAsync<TValue>(long chatId, string optionId, TValue value)
{
var chatOption = await _db.ChatOptionValues.FindOrCreateOption(chatId, GetOptionId<TOption>());
var chatOption = await _db.ChatOptionValues.FindOrCreateOption(chatId, optionId);
chatOption.SetValue(value);
_db.AddOrUpdate(chatOption);
}
protected OptionAttribute GetOptionAttribute<TOption>()
{
return typeof(TOption).GetCustomAttribute<OptionAttribute>();
}
protected string GetOptionId<TOption>()
{
var optionId = GetOptionAttribute<TOption>()?.OptionId;
if (optionId == null)
{
throw new ArgumentException("Option must implement OptionAttribute");
}
return optionId;
}
protected TValue GetDefaultValue<TOption, TValue>()
{
return (TValue) GetOptionAttribute<TOption>().DefaultValue;
}
}
+12 -2
View File
@@ -1,10 +1,20 @@
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Kruzya.TelegramBot.Core.Options;
public interface IOption<TValue>
public interface IOption
{
public string OptionId { get; }
public string OptionName { get; }
public OptionType OptionType { get; }
public List<SelectChoice> ChoiceList { get; }
}
public interface IOption<TValue> : IOption
{
public TValue DefaultValue { get; }
public Task<TValue> GetValueAsync(long chatId);
public Task SetValueAsync(long chatId, TValue value);
}
+2 -2
View File
@@ -4,6 +4,6 @@ namespace Kruzya.TelegramBot.Core.Options;
public interface IOptionProvider
{
public Task<TValue> GetValueAsync<TOption, TValue>(long chatId);
public Task SetValueAsync<TOption, TValue>(long chatId, TValue value);
public Task<TValue> GetValueAsync<TValue>(long chatId, string optionId, TValue defVal);
public Task SetValueAsync<TValue>(long chatId, string optionId, TValue value);
}
-17
View File
@@ -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;
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace Kruzya.TelegramBot.Core.Options;
public enum OptionType
{
OnOff,
Select
}
+29
View File
@@ -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<InlineKeyboardButton> ToButton(long userId, string optionId, bool isActive = false)
{
return new[]
{
new InlineKeyboardButton((isActive ? "🟢 " : "🔴 ") + FullName)
{
CallbackData = $"option|{userId}|select|{optionId}|{Id}"
}
};
}
}
+4
View File
@@ -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<IOption, AllowNsfw>();
foreach (var serviceType in MediaServiceTypes)
{
services.AddScoped(typeof(IRandomMediaService), serviceType);
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
+4 -9
View File
@@ -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<IReputation>();
_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);
}
}
+15
View File
@@ -0,0 +1,15 @@
using Kruzya.TelegramBot.Core.Options;
namespace West.TelegramBot.ContentStore.Option;
public class AllowNsfw : AbstractOption<AllowNsfw, bool>
{
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)
{
}
}
-2
View File
@@ -5,7 +5,6 @@ global using GuessCache = Kruzya.TelegramBot.Core.Cache.MemoryCache<Telegram.Bot
using Kruzya.TelegramBot.Core;
using Kruzya.TelegramBot.Core.Options;
using Microsoft.Extensions.DependencyInjection;
using West.Entertainment.Options;
namespace West.Entertainment
@@ -17,7 +16,6 @@ namespace West.Entertainment
public override void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IGuessCache, GuessCache>();
services.AddScoped<NsfwOption>();
}
}
}
+1 -19
View File
@@ -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<Common> logger, UserService userService, NsfwOption nsfwOption)
public Common(CoreContext db, ILogger<Common> 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)}");
}
}
}
@@ -1,11 +0,0 @@
using Kruzya.TelegramBot.Core.Options;
namespace West.Entertainment.Options;
[Option("allowNSFW", false)]
public class NsfwOption : AbstractOption<NsfwOption, bool>
{
public NsfwOption(IOptionProvider optionProvider) : base(optionProvider)
{
}
}
+21
View File
@@ -0,0 +1,21 @@
using Kruzya.TelegramBot.Core.Options;
namespace West.TelegramBot.Reputation.Options;
public class KarmaMode : AbstractOption<KarmaMode, string>
{
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")
});
}
}
+3
View File
@@ -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<ReputationContext>(options => options.UseMySql(dsn, ServerVersion.AutoDetect(dsn)));
services.AddScoped<IReputation, ReputationService>();
services.AddScoped<IOption, KarmaMode>();
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>