mirror of
https://github.com/Bubuni-Team/telegram-bot.git
synced 2026-07-31 00:29:24 +03:00
[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:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Kruzya.TelegramBot.Core.Options;
|
||||
|
||||
public enum OptionType
|
||||
{
|
||||
OnOff,
|
||||
Select
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user