From 8ab067a027132f53b3d4d628698d56a7170d97df Mon Sep 17 00:00:00 2001 From: Andriy <30056636+West14@users.noreply.github.com> Date: Sat, 29 Jul 2023 15:14:52 +0300 Subject: [PATCH] Use file-scoped namespaces --- Core/Cache/ICacheStorage.cs | 13 +- Core/Cache/MemoryCache.cs | 177 +++--- Core/Core.cs | 343 ++++++------ Core/Data/BotUser.cs | 37 +- Core/Data/BotUserValue.cs | 53 +- Core/Data/CoreContext.cs | 35 +- Core/DbResolverParameter.cs | 57 +- Core/Extensions/BotExtension.cs | 61 +-- Core/Extensions/HttpClientExtension.cs | 27 +- Core/Extensions/ModuleArrayExtension.cs | 25 +- Core/Extensions/RepositoryExtension.cs | 19 +- Core/Extensions/StringExtension.cs | 23 +- Core/Extensions/UserExtension.cs | 41 +- Core/Handler/CommonHandler.cs | 81 ++- Core/Handler/GeneralHandler.cs | 99 ++-- Core/Module.cs | 55 +- Core/Program.cs | 27 +- Core/Service/AbstractTimedHostedService.cs | 113 ++-- Core/Service/IReputation.cs | 19 +- Core/Service/UserService.cs | 125 +++-- Core/Startup.cs | 101 ++-- modules/ChatQuotes/ChatQuotes.cs | 33 +- .../Json/NullToEmptyObjectResolver.cs | 23 +- modules/CodeWatcher/CodeWatcher.cs | 25 +- modules/CombotAntiSpam/UserJoinHandler.cs | 1 - .../Provider/IXurPlaceProvider.cs | 11 +- .../Destiny2.WhereIsXur/Provider/XurWiki.cs | 33 +- modules/Destiny2.WhereIsXur/RequestHandler.cs | 43 +- modules/Destiny2.WhereIsXur/WhereIsXur.cs | 21 +- modules/Entertainment/Entertainment.cs | 17 +- modules/Entertainment/Handler/Common.cs | 73 ++- .../Handler/Fun/AbstractAnimationReply.cs | 181 +++--- modules/Entertainment/Handler/Fun/Bayan.cs | 65 ++- .../Entertainment/Handler/Fun/JavaScript.cs | 35 +- modules/Entertainment/Handler/Fun/Python.cs | 43 +- modules/RichSiteSummary/Data/Feed.cs | 151 +++-- modules/RichSiteSummary/Data/Post.cs | 175 +++--- .../Data/RepresentableEntity.cs | 87 ++- .../Data/RichSiteSummaryContext.cs | 93 ++-- modules/RichSiteSummary/Data/Subscriber.cs | 81 ++- .../Handler/AbstractHandler.cs | 17 +- modules/RichSiteSummary/Handler/FeedList.cs | 515 +++++++++--------- .../RichSiteSummary/Handler/StatsHandler.cs | 49 +- modules/RichSiteSummary/RichSiteSummary.cs | 39 +- .../RichSiteSummaryRepositoryExtension.cs | 121 ++-- .../RichSiteSummary/Service/MessageSender.cs | 123 +++-- modules/RichSiteSummary/Service/RssFetch.cs | 413 +++++++------- .../RichSiteSummary/Service/Unsubscriber.cs | 55 +- .../UrchinTrackingModule/UtmParameters.cs | 17 +- .../UrchinTrackingModule/UtmUtility.cs | 69 ++- modules/RichSiteSummary/UserMessage.cs | 17 +- modules/RichSiteSummary/UserUnsubscribe.cs | 9 +- modules/UrlLimitations/MessageExtension.cs | 65 ++- modules/UrlLimitations/MessageHandler.cs | 71 ++- modules/UrlLimitations/UrlLimitations.cs | 9 +- modules/UrlLimitations/UserJoinHandler.cs | 35 +- modules/UserGroupTag/Handler.cs | 147 +++-- modules/UserGroupTag/UserGroupTag.cs | 9 +- 58 files changed, 2222 insertions(+), 2280 deletions(-) diff --git a/Core/Cache/ICacheStorage.cs b/Core/Cache/ICacheStorage.cs index 691e44b..7cc0e6d 100644 --- a/Core/Cache/ICacheStorage.cs +++ b/Core/Cache/ICacheStorage.cs @@ -1,9 +1,8 @@ -namespace Kruzya.TelegramBot.Core.Cache +namespace Kruzya.TelegramBot.Core.Cache; + +public interface ICacheStorage { - public interface ICacheStorage - { - public bool TryGetValue(TKey key, out TValue value); - public void SetValue(TKey key, TValue value, int timeToLive); - public TValue GetOr(TKey key, TValue value); - } + public bool TryGetValue(TKey key, out TValue value); + public void SetValue(TKey key, TValue value, int timeToLive); + public TValue GetOr(TKey key, TValue value); } \ No newline at end of file diff --git a/Core/Cache/MemoryCache.cs b/Core/Cache/MemoryCache.cs index bed4a6b..f613700 100644 --- a/Core/Cache/MemoryCache.cs +++ b/Core/Cache/MemoryCache.cs @@ -2,103 +2,102 @@ using System.Collections.Generic; using System.Threading; -namespace Kruzya.TelegramBot.Core.Cache +namespace Kruzya.TelegramBot.Core.Cache; + +public class MemoryCache : ICacheStorage { - public class MemoryCache : ICacheStorage + /// + /// Represents a cache entry in internal dictionary. + /// + /// + internal struct CacheEntry { - /// - /// Represents a cache entry in internal dictionary. - /// - /// - internal struct CacheEntry - { - public TVal Value; - public DateTime Created; - public int ExpiresAt; + public TVal Value; + public DateTime Created; + public int ExpiresAt; - public bool IsExpired() - { - if (ExpiresAt == Timeout.Infinite) - { - return false; - } - - return (Created.AddSeconds(ExpiresAt)) < DateTime.Now; - } - } - - /// - /// - /// - private readonly Dictionary> _dict = new(); - - /// - /// The TTL for all entries if not set. - /// - private int _defaultTtl; - - public MemoryCache() : this(Timeout.Infinite) + public bool IsExpired() { - } - - public MemoryCache(int defaultTtl) - { - _defaultTtl = defaultTtl; - } - - public bool TryGetValue(TKey key, out TValue value) - { - value = default; - if (!_dict.ContainsKey(key)) + if (ExpiresAt == Timeout.Infinite) { return false; } - CacheEntry temp; - if (!_dict.TryGetValue(key, out temp)) - { - return false; - } - - if (temp.IsExpired()) - { - _dict.Remove(key); - return false; - } - - value = temp.Value; - return true; - } - - public TValue GetOr(TKey key, TValue defValue = default) - { - var doesExist = TryGetValue(key, out var value); - if (!doesExist) - { - value = defValue; - } - - return value; - } - - public void SetValue(TKey key, TValue value) - { - SetValue(key, value, _defaultTtl); - } - - public void SetValue(TKey key, TValue value, int timeToLive) - { - if (_dict.ContainsKey(key)) - { - _dict.Remove(key); - } - - _dict.Add(key, new CacheEntry() - { - Created = DateTime.Now, - ExpiresAt = timeToLive, - Value = value - }); + return (Created.AddSeconds(ExpiresAt)) < DateTime.Now; } } + + /// + /// + /// + private readonly Dictionary> _dict = new(); + + /// + /// The TTL for all entries if not set. + /// + private int _defaultTtl; + + public MemoryCache() : this(Timeout.Infinite) + { + } + + public MemoryCache(int defaultTtl) + { + _defaultTtl = defaultTtl; + } + + public bool TryGetValue(TKey key, out TValue value) + { + value = default; + if (!_dict.ContainsKey(key)) + { + return false; + } + + CacheEntry temp; + if (!_dict.TryGetValue(key, out temp)) + { + return false; + } + + if (temp.IsExpired()) + { + _dict.Remove(key); + return false; + } + + value = temp.Value; + return true; + } + + public TValue GetOr(TKey key, TValue defValue = default) + { + var doesExist = TryGetValue(key, out var value); + if (!doesExist) + { + value = defValue; + } + + return value; + } + + public void SetValue(TKey key, TValue value) + { + SetValue(key, value, _defaultTtl); + } + + public void SetValue(TKey key, TValue value, int timeToLive) + { + if (_dict.ContainsKey(key)) + { + _dict.Remove(key); + } + + _dict.Add(key, new CacheEntry() + { + Created = DateTime.Now, + ExpiresAt = timeToLive, + Value = value + }); + } } \ No newline at end of file diff --git a/Core/Core.cs b/Core/Core.cs index 81b714b..b2e7cf9 100644 --- a/Core/Core.cs +++ b/Core/Core.cs @@ -7,201 +7,200 @@ using System.Linq; using System.Reflection; using Microsoft.Extensions.Configuration; -namespace Kruzya.TelegramBot.Core +namespace Kruzya.TelegramBot.Core; + +public sealed class Core { - public sealed class Core - { - /// - /// The Core API version. - /// - public UInt32 ApiVersion => 1; + /// + /// The Core API version. + /// + public UInt32 ApiVersion => 1; - /// - /// The array that contains all modules instances. - /// - public Module[] Modules => _modules.ToArray(); + /// + /// The array that contains all modules instances. + /// + public Module[] Modules => _modules.ToArray(); - /// - /// The current bot configuration. - /// - public IConfiguration Configuration => _configuration; + /// + /// The current bot configuration. + /// + public IConfiguration Configuration => _configuration; - #region Private properties + #region Private properties - /// - /// The array that contains all modules instances. - /// - private List _modules = new List(); + /// + /// The array that contains all modules instances. + /// + private List _modules = new List(); - /// - /// The current bot configuration. - /// - private IConfiguration _configuration; + /// + /// The current bot configuration. + /// + private IConfiguration _configuration; - /// - /// The all directories for loading our module assemblies. - /// - private List _directories + /// + /// The all directories for loading our module assemblies. + /// + private List _directories + { + get { - get + // For start, load all assemblies in required directory. + // And register in internal temporary array. + var modulesDirectory = new List(new string[] { - // For start, load all assemblies in required directory. - // And register in internal temporary array. - var modulesDirectory = new List(new string[] - { - Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "modules"), - Path.Join(Environment.CurrentDirectory, "modules") - }); + Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "modules"), + Path.Join(Environment.CurrentDirectory, "modules") + }); - // Also lookup load directories from config. - modulesDirectory.AddRange(_configuration.GetSection("ModuleDirectories").GetChildren().Select(q => q.Value) - .ToList()); + // Also lookup load directories from config. + modulesDirectory.AddRange(_configuration.GetSection("ModuleDirectories").GetChildren().Select(q => q.Value) + .ToList()); - return modulesDirectory; - } + return modulesDirectory; } + } - /// - /// List of filenames (without extension) what should be not loaded. - /// - private List _ignoredAssemblyFilenames => _configuration - .GetSection("IgnoredModuleFilenames").GetChildren() - .Select(q => q.Value).ToList(); + /// + /// List of filenames (without extension) what should be not loaded. + /// + private List _ignoredAssemblyFilenames => _configuration + .GetSection("IgnoredModuleFilenames").GetChildren() + .Select(q => q.Value).ToList(); - #endregion + #endregion - public Core(IConfiguration configuration) + public Core(IConfiguration configuration) + { + _configuration = configuration; + + HookAppDomain(); + Start(); + } + + /// + /// Initializes all required submodules. + /// + private void Start() + { + var types = new List(); + foreach (var directory in _directories) { - _configuration = configuration; - - HookAppDomain(); - Start(); - } - - /// - /// Initializes all required submodules. - /// - private void Start() - { - var types = new List(); - foreach (var directory in _directories) + if (!Directory.Exists(directory)) { - if (!Directory.Exists(directory)) + continue; + } + + foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories)) + { + var assemblyTypes = LoadAssembly(file); + if (assemblyTypes == null) { continue; } - foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories)) - { - var assemblyTypes = LoadAssembly(file); - if (assemblyTypes == null) - { - continue; - } - - types.AddRange(assemblyTypes); - } + types.AddRange(assemblyTypes); } + } - // Instantiate them. - foreach (var module in types) - { - EnsureLoaded(module); - } - } - - /// - /// Loads the assembly and appends all Module types to list. - /// - /// The full path to assembly file - /// if success, null otherwise. - private IEnumerable? LoadAssembly(string path) + // Instantiate them. + foreach (var module in types) { - var fileName = Path.GetFileNameWithoutExtension(path).ToString(); - if (_ignoredAssemblyFilenames.Contains(fileName)) - { - return null; - } - - Assembly assembly; - try - { - assembly = Assembly.LoadFile(path); - } - catch (Exception) - { - // don't load any invalid module. - return null; - } - - return assembly.GetExportedTypes().Where(x => !x.IsAbstract && x.IsSubclassOf(typeof(Module))); - } - - /// - /// Ensures if module is loaded and started. Note that call can change module event chain. - /// - /// The Module type. - /// Module instance. - public T EnsureLoaded() - where T : Module - { - return (T) EnsureLoaded(typeof(T)); - } - - /// - /// Ensures if module is loaded and started. Note that call can change module event chain. - /// - /// The Module type. - /// Module instance. - /// The given type is not extends the Module class - public Module EnsureLoaded(Type moduleType) - { - if (!moduleType.IsSubclassOf(typeof(Module))) - { - throw new Exception("Received incorrect module type"); - } - - var module = _modules.FirstOrDefault(module => module.GetType() == moduleType); - if (module == null) - { - module = (Module) Activator.CreateInstance(moduleType, this)!; - Console.WriteLine($"Started module {module.GetType().Name}"); - _modules.Add(module); - } - - return module; - } - - private void HookAppDomain() - { - var appDomain = AppDomain.CurrentDomain; - - appDomain.AssemblyResolve += delegate(object? sender, ResolveEventArgs args) - { - var assemblyName = new AssemblyName(args.Name).Name + ".dll"; - - var possiblePaths = new List(); - if (args.RequestingAssembly != null) - { - var modulePath = Path.GetDirectoryName(args.RequestingAssembly.Location)!; - possiblePaths.Add(modulePath); - possiblePaths.Add(args.RequestingAssembly.Location.Replace(".dll", "")); - } - - possiblePaths.Add(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!); - possiblePaths.Add(Environment.CurrentDirectory); - - foreach (var basePath in possiblePaths) - { - var assemblyPath = $"{basePath}/{assemblyName}"; - if (File.Exists(assemblyPath)) - { - return Assembly.LoadFrom(assemblyPath); - } - } - - return null; - }; + EnsureLoaded(module); } } -} + + /// + /// Loads the assembly and appends all Module types to list. + /// + /// The full path to assembly file + /// if success, null otherwise. + private IEnumerable? LoadAssembly(string path) + { + var fileName = Path.GetFileNameWithoutExtension(path).ToString(); + if (_ignoredAssemblyFilenames.Contains(fileName)) + { + return null; + } + + Assembly assembly; + try + { + assembly = Assembly.LoadFile(path); + } + catch (Exception) + { + // don't load any invalid module. + return null; + } + + return assembly.GetExportedTypes().Where(x => !x.IsAbstract && x.IsSubclassOf(typeof(Module))); + } + + /// + /// Ensures if module is loaded and started. Note that call can change module event chain. + /// + /// The Module type. + /// Module instance. + public T EnsureLoaded() + where T : Module + { + return (T) EnsureLoaded(typeof(T)); + } + + /// + /// Ensures if module is loaded and started. Note that call can change module event chain. + /// + /// The Module type. + /// Module instance. + /// The given type is not extends the Module class + public Module EnsureLoaded(Type moduleType) + { + if (!moduleType.IsSubclassOf(typeof(Module))) + { + throw new Exception("Received incorrect module type"); + } + + var module = _modules.FirstOrDefault(module => module.GetType() == moduleType); + if (module == null) + { + module = (Module) Activator.CreateInstance(moduleType, this)!; + Console.WriteLine($"Started module {module.GetType().Name}"); + _modules.Add(module); + } + + return module; + } + + private void HookAppDomain() + { + var appDomain = AppDomain.CurrentDomain; + + appDomain.AssemblyResolve += delegate(object? sender, ResolveEventArgs args) + { + var assemblyName = new AssemblyName(args.Name).Name + ".dll"; + + var possiblePaths = new List(); + if (args.RequestingAssembly != null) + { + var modulePath = Path.GetDirectoryName(args.RequestingAssembly.Location)!; + possiblePaths.Add(modulePath); + possiblePaths.Add(args.RequestingAssembly.Location.Replace(".dll", "")); + } + + possiblePaths.Add(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!); + possiblePaths.Add(Environment.CurrentDirectory); + + foreach (var basePath in possiblePaths) + { + var assemblyPath = $"{basePath}/{assemblyName}"; + if (File.Exists(assemblyPath)) + { + return Assembly.LoadFrom(assemblyPath); + } + } + + return null; + }; + } +} \ No newline at end of file diff --git a/Core/Data/BotUser.cs b/Core/Data/BotUser.cs index 220d471..4d141fa 100644 --- a/Core/Data/BotUser.cs +++ b/Core/Data/BotUser.cs @@ -1,24 +1,23 @@ using System; using System.ComponentModel.DataAnnotations; -namespace Kruzya.TelegramBot.Core.Data -{ - public class BotUser - { - /// - /// The unique bot user identifier. - /// - [Key] - public Guid BotUserId { get; set; } - - /// - /// The chat identifier where this user is related. - /// - public long ChatId { get; set; } +namespace Kruzya.TelegramBot.Core.Data; - /// - /// The Telegram User identifier. - /// - public long UserId { get; set; } - } +public class BotUser +{ + /// + /// The unique bot user identifier. + /// + [Key] + public Guid BotUserId { get; set; } + + /// + /// The chat identifier where this user is related. + /// + public long ChatId { get; set; } + + /// + /// The Telegram User identifier. + /// + public long UserId { get; set; } } \ No newline at end of file diff --git a/Core/Data/BotUserValue.cs b/Core/Data/BotUserValue.cs index 39ae94c..62dc75d 100644 --- a/Core/Data/BotUserValue.cs +++ b/Core/Data/BotUserValue.cs @@ -4,38 +4,37 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json; using System.Text.Json.Serialization; -namespace Kruzya.TelegramBot.Core.Data +namespace Kruzya.TelegramBot.Core.Data; + +public class BotUserValue { - public class BotUserValue + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [Key] + public Guid BotUserValueId { get; set; } + + public BotUser BotUser { get; set; } + public string Name { get; set; } + + public string ValueType { get; set; } + + public byte[] Value { get; set; } + + public T GetValue(T defVal = default) { - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - [Key] - public Guid BotUserValueId { get; set; } - - public BotUser BotUser { get; set; } - public string Name { get; set; } - - public string ValueType { get; set; } - - public byte[] Value { get; set; } - - public T GetValue(T defVal = default) + try { - try - { - return JsonSerializer.Deserialize(new ReadOnlySpan(Value)); - } - catch (Exception) - { - return defVal; - } + return JsonSerializer.Deserialize(new ReadOnlySpan(Value)); } - - public void SetValue(T value) + catch (Exception) { - Value = JsonSerializer.SerializeToUtf8Bytes(value, new JsonSerializerOptions { WriteIndented = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); - ValueType = value.GetType().FullName; + return defVal; } } + + public void SetValue(T value) + { + Value = JsonSerializer.SerializeToUtf8Bytes(value, new JsonSerializerOptions { WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }); + ValueType = value.GetType().FullName; + } } \ No newline at end of file diff --git a/Core/Data/CoreContext.cs b/Core/Data/CoreContext.cs index 6843a11..bb605f0 100644 --- a/Core/Data/CoreContext.cs +++ b/Core/Data/CoreContext.cs @@ -1,25 +1,24 @@ using Kruzya.TelegramBot.Core.EF; using Microsoft.EntityFrameworkCore; -namespace Kruzya.TelegramBot.Core.Data +namespace Kruzya.TelegramBot.Core.Data; + +public class CoreContext : AutoSaveDbContext { - public class CoreContext : AutoSaveDbContext + /// + /// The all known users. + /// + public DbSet Users { get; set; } + + /// + /// The all knows user options and values. + /// + public DbSet UserValues { get; set; } + + public DbSet ChatOptionValues { get; set; } + + public CoreContext(DbContextOptions ctx) : base(ctx) { - /// - /// The all known users. - /// - public DbSet Users { get; set; } - - /// - /// The all knows user options and values. - /// - public DbSet UserValues { get; set; } - - public DbSet ChatOptionValues { get; set; } - - public CoreContext(DbContextOptions ctx) : base(ctx) - { - Database.Migrate(); - } + Database.Migrate(); } } \ No newline at end of file diff --git a/Core/DbResolverParameter.cs b/Core/DbResolverParameter.cs index 58e8e51..7e58950 100644 --- a/Core/DbResolverParameter.cs +++ b/Core/DbResolverParameter.cs @@ -2,40 +2,39 @@ using BotFramework; using Microsoft.EntityFrameworkCore; -namespace Kruzya.TelegramBot.Core +namespace Kruzya.TelegramBot.Core; + +/// +/// Resolves the GUIDs from database to entities. +/// +/// Entity type +/// Database context where entity should be finded +public class DbResolverParameter : IParameterParser + where TAppContext : DbContext + where TResolvedEntity : class { - /// - /// Resolves the GUIDs from database to entities. - /// - /// Entity type - /// Database context where entity should be finded - public class DbResolverParameter : IParameterParser - where TAppContext : DbContext - where TResolvedEntity : class + private TAppContext _dbContext; + + public DbResolverParameter(TAppContext dbContext) { - private TAppContext _dbContext; - - public DbResolverParameter(TAppContext dbContext) - { - _dbContext = dbContext; - } + _dbContext = dbContext; + } - public TResolvedEntity DefaultInstance() + public TResolvedEntity DefaultInstance() + { + return default(TResolvedEntity); + } + + public bool TryGetValue(string text, out TResolvedEntity result) + { + result = null; + Guid id; + if (Guid.TryParse(text, out id)) { - return default(TResolvedEntity); + result = _dbContext.Find(id); + return (result != null); } - public bool TryGetValue(string text, out TResolvedEntity result) - { - result = null; - Guid id; - if (Guid.TryParse(text, out id)) - { - result = _dbContext.Find(id); - return (result != null); - } - - return false; - } + return false; } } \ No newline at end of file diff --git a/Core/Extensions/BotExtension.cs b/Core/Extensions/BotExtension.cs index 60fffaf..2bd90ac 100644 --- a/Core/Extensions/BotExtension.cs +++ b/Core/Extensions/BotExtension.cs @@ -2,45 +2,44 @@ using Telegram.Bot; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.Core.Extensions +namespace Kruzya.TelegramBot.Core.Extensions; + +public static class BotExtension { - public static class BotExtension + public static async Task CanPunishMember(this ITelegramBotClient bot, Chat chat, User user, User victim = null) { - public static async Task CanPunishMember(this ITelegramBotClient bot, Chat chat, User user, User victim = null) + var canUse = await IsUserAdminAsync(bot, chat, user); + + if (victim != null && canUse) { - var canUse = await IsUserAdminAsync(bot, chat, user); - - if (victim != null && canUse) - { - var victimMember = await bot.GetChatMemberAsync(chat, victim.Id); - canUse = victimMember is not ChatMemberOwner && victimMember is not ChatMemberAdministrator && !victim.IsBot; - } - - return canUse; + var victimMember = await bot.GetChatMemberAsync(chat, victim.Id); + canUse = victimMember is not ChatMemberOwner && victimMember is not ChatMemberAdministrator && !victim.IsBot; } - public static async Task IsUserAdminAsync(this ITelegramBotClient bot, Chat chat, User user) + return canUse; + } + + public static async Task IsUserAdminAsync(this ITelegramBotClient bot, Chat chat, User user) + { + var callerMember = await bot.GetChatMemberAsync(chat, user.Id); + var isAdmin = callerMember is ChatMemberOwner; + + if (callerMember is ChatMemberAdministrator callerAdmin) { - var callerMember = await bot.GetChatMemberAsync(chat, user.Id); - var isAdmin = callerMember is ChatMemberOwner; - - if (callerMember is ChatMemberAdministrator callerAdmin) - { - isAdmin = callerAdmin.CanRestrictMembers; - } - - return isAdmin; + isAdmin = callerAdmin.CanRestrictMembers; } - public static async Task CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat) - { - return await CanDeleteMessagesAsync(bot, chat, await bot.GetMeAsync()); - } + return isAdmin; + } - public static async Task CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat, User user) - { - return await bot.GetChatMemberAsync(chat.Id, user.Id) is ChatMemberOwner - or ChatMemberAdministrator {CanRestrictMembers: true}; - } + public static async Task CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat) + { + return await CanDeleteMessagesAsync(bot, chat, await bot.GetMeAsync()); + } + + public static async Task CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat, User user) + { + return await bot.GetChatMemberAsync(chat.Id, user.Id) is ChatMemberOwner + or ChatMemberAdministrator {CanRestrictMembers: true}; } } \ No newline at end of file diff --git a/Core/Extensions/HttpClientExtension.cs b/Core/Extensions/HttpClientExtension.cs index 452da7c..793472b 100644 --- a/Core/Extensions/HttpClientExtension.cs +++ b/Core/Extensions/HttpClientExtension.cs @@ -3,20 +3,19 @@ using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json; -namespace Kruzya.TelegramBot.Core.Extensions -{ - public static class HttpClientExtension - { - public static async Task GetJsonAsync(this HttpClient client, Uri requestUri) - { - var response = await client.GetStringAsync(requestUri); - return JsonConvert.DeserializeObject(response); - } +namespace Kruzya.TelegramBot.Core.Extensions; - public static async Task GetJsonAsync(this HttpClient client, string requestUri) - { - var response = await client.GetStringAsync(requestUri); - return JsonConvert.DeserializeObject(response); - } +public static class HttpClientExtension +{ + public static async Task GetJsonAsync(this HttpClient client, Uri requestUri) + { + var response = await client.GetStringAsync(requestUri); + return JsonConvert.DeserializeObject(response); + } + + public static async Task GetJsonAsync(this HttpClient client, string requestUri) + { + var response = await client.GetStringAsync(requestUri); + return JsonConvert.DeserializeObject(response); } } \ No newline at end of file diff --git a/Core/Extensions/ModuleArrayExtension.cs b/Core/Extensions/ModuleArrayExtension.cs index 2e615a0..25eb5aa 100644 --- a/Core/Extensions/ModuleArrayExtension.cs +++ b/Core/Extensions/ModuleArrayExtension.cs @@ -2,18 +2,17 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; -namespace Kruzya.TelegramBot.Core.Extensions -{ - internal static class ModuleArrayExtension - { - public static void ConfigureServices(this Module[] modules, IServiceCollection services) - { - foreach (var module in modules) module.ConfigureServices(services); - } +namespace Kruzya.TelegramBot.Core.Extensions; - public static void Configure(this Module[] modules, IApplicationBuilder app, IWebHostEnvironment env) - { - foreach (var module in modules) module.Configure(app, env); - } +internal static class ModuleArrayExtension +{ + public static void ConfigureServices(this Module[] modules, IServiceCollection services) + { + foreach (var module in modules) module.ConfigureServices(services); } -} + + public static void Configure(this Module[] modules, IApplicationBuilder app, IWebHostEnvironment env) + { + foreach (var module in modules) module.Configure(app, env); + } +} \ No newline at end of file diff --git a/Core/Extensions/RepositoryExtension.cs b/Core/Extensions/RepositoryExtension.cs index a9863a1..d9345d6 100644 --- a/Core/Extensions/RepositoryExtension.cs +++ b/Core/Extensions/RepositoryExtension.cs @@ -1,15 +1,14 @@ using Microsoft.EntityFrameworkCore; -namespace Kruzya.TelegramBot.Core.Extensions -{ - public static partial class RepositoryExtension - { - public static TEntity Create(this DbSet repository) where TEntity : class, new() - { - var entity = new TEntity(); - repository.Add(entity); +namespace Kruzya.TelegramBot.Core.Extensions; - return entity; - } +public static partial class RepositoryExtension +{ + public static TEntity Create(this DbSet repository) where TEntity : class, new() + { + var entity = new TEntity(); + repository.Add(entity); + + return entity; } } \ No newline at end of file diff --git a/Core/Extensions/StringExtension.cs b/Core/Extensions/StringExtension.cs index 62f9ab5..3fa055f 100644 --- a/Core/Extensions/StringExtension.cs +++ b/Core/Extensions/StringExtension.cs @@ -1,17 +1,16 @@ using System.Web; -namespace Kruzya.TelegramBot.Core.Extensions -{ - public static class StringExtension - { - public static string HtmlEncode(this string content) - { - return HttpUtility.HtmlEncode(content); - } +namespace Kruzya.TelegramBot.Core.Extensions; - public static string HtmlDecode(this string content) - { - return HttpUtility.HtmlDecode(content); - } +public static class StringExtension +{ + public static string HtmlEncode(this string content) + { + return HttpUtility.HtmlEncode(content); + } + + public static string HtmlDecode(this string content) + { + return HttpUtility.HtmlDecode(content); } } \ No newline at end of file diff --git a/Core/Extensions/UserExtension.cs b/Core/Extensions/UserExtension.cs index 6940c3d..1e63d24 100644 --- a/Core/Extensions/UserExtension.cs +++ b/Core/Extensions/UserExtension.cs @@ -1,31 +1,30 @@ using System.Text; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.Core.Extensions +namespace Kruzya.TelegramBot.Core.Extensions; + +public static class UserExtension { - public static class UserExtension + public static string ToHtml(this User user, bool byUsername = false) { - public static string ToHtml(this User user, bool byUsername = false) - { - var text = new StringBuilder(); - text.Append($""); + var text = new StringBuilder(); + text.Append($""); - if (byUsername && !string.IsNullOrWhiteSpace(user.Username)) - { - text.Append(user.Username); - } - else - { - text.Append($"{user.FirstName} {user.LastName}".Trim().HtmlEncode()); - } - return text.Append("").ToString(); - } - - public static string ToLog(this User user) + if (byUsername && !string.IsNullOrWhiteSpace(user.Username)) { - var name = $"{user.FirstName} {user.LastName}".Trim(); - - return $"{name} (ID: {user.Id}, Username: {user.Username ?? "N/A"})"; + text.Append(user.Username); } + else + { + text.Append($"{user.FirstName} {user.LastName}".Trim().HtmlEncode()); + } + return text.Append("").ToString(); + } + + public static string ToLog(this User user) + { + var name = $"{user.FirstName} {user.LastName}".Trim(); + + return $"{name} (ID: {user.Id}, Username: {user.Username ?? "N/A"})"; } } \ No newline at end of file diff --git a/Core/Handler/CommonHandler.cs b/Core/Handler/CommonHandler.cs index 136a1ee..e9b5313 100644 --- a/Core/Handler/CommonHandler.cs +++ b/Core/Handler/CommonHandler.cs @@ -9,54 +9,53 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.Core.Handler +namespace Kruzya.TelegramBot.Core.Handler; + +public abstract class CommonHandler : BotEventHandler { - public abstract class CommonHandler : BotEventHandler + protected readonly CoreContext Db; + + protected Message? Message => RawUpdate.Message; + + protected User? RepliedUser => Message?.ReplyToMessage?.From; + + protected CommonHandler(CoreContext db) { - protected readonly CoreContext Db; + Db = db; + } - protected Message? Message => RawUpdate.Message; + protected virtual async Task Reply(string text) + { + return await Bot.SendTextMessageAsync(Chat.Id, text, + replyToMessageId: Message?.MessageId, parseMode: ParseMode.Html); + } - protected User? RepliedUser => Message?.ReplyToMessage?.From; - - protected CommonHandler(CoreContext db) - { - Db = db; - } - - protected virtual async Task Reply(string text) - { - return await Bot.SendTextMessageAsync(Chat.Id, text, - replyToMessageId: Message?.MessageId, parseMode: ParseMode.Html); - } - - protected async Task CanPunish() - { - return RepliedUser != null && await Bot.CanPunishMember(Chat, From, RepliedUser); - } + protected async Task CanPunish() + { + return RepliedUser != null && await Bot.CanPunishMember(Chat, From, RepliedUser); + } - protected async Task BanMember(long userId, DateTime banEnd) - { - await Bot.RestrictChatMemberAsync( - Chat.Id, - userId, - new ChatPermissions - { - CanSendMessages = false - }, - banEnd - ); - } + protected async Task BanMember(long userId, DateTime banEnd) + { + await Bot.RestrictChatMemberAsync( + Chat.Id, + userId, + new ChatPermissions + { + CanSendMessages = false + }, + banEnd + ); + } - protected async Task GetWarnOption(User user) - { - return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "warnCount"); - } + protected async Task GetWarnOption(User user) + { + return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "warnCount"); + } - protected async Task AnswerQuery(string id, string? message = null) - { - await Bot.AnswerCallbackQueryAsync(id, message); - } + 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/GeneralHandler.cs b/Core/Handler/GeneralHandler.cs index 15a76e0..b44f13f 100644 --- a/Core/Handler/GeneralHandler.cs +++ b/Core/Handler/GeneralHandler.cs @@ -6,71 +6,70 @@ using Kruzya.TelegramBot.Core.Data; using Microsoft.EntityFrameworkCore; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.Core.Handler +namespace Kruzya.TelegramBot.Core.Handler; + +public class GeneralHandler : BotEventHandler { - public class GeneralHandler : BotEventHandler + [AttributeUsage(AttributeTargets.Method)] + public sealed class HandleEverythingAttribute : HandlerAttribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class HandleEverythingAttribute : HandlerAttribute + protected override bool CanHandle(HandlerParams param) { - protected override bool CanHandle(HandlerParams param) - { - return true; - } - } - - protected readonly CoreContext databaseContext; - - public GeneralHandler(CoreContext dbCtx) - { - databaseContext = dbCtx; - } - - [HandleEverything] - [Priority(short.MaxValue - 10)] - public async Task Listener() - { - foreach (var message in new Message[] { RawUpdate.Message, RawUpdate.EditedMessage, RawUpdate.ChannelPost, RawUpdate.EditedChannelPost }) - { - if (message == null) - { - continue; - } - - await VerifyChatPair(message); - } - return true; } + } - public async Task VerifyChatPair(Message message) - { - await VerifyChatPair(message.Chat, message.From); - } + protected readonly CoreContext databaseContext; - public async Task VerifyChatPair(Chat chat, User user) - { - await VerifyChatPair(chat.Id, user.Id); - } + public GeneralHandler(CoreContext dbCtx) + { + databaseContext = dbCtx; + } - public async Task VerifyChatPair(long chat, long user) + [HandleEverything] + [Priority(short.MaxValue - 10)] + public async Task Listener() + { + foreach (var message in new Message[] { RawUpdate.Message, RawUpdate.EditedMessage, RawUpdate.ChannelPost, RawUpdate.EditedChannelPost }) { - var hasEntry = await databaseContext.Users.AnyAsync(e => e.ChatId == chat && e.UserId == user); - if (hasEntry) + if (message == null) { - return; + continue; } - await MakeChatPair(chat, user); + await VerifyChatPair(message); } - public async Task MakeChatPair(long chat, long user) + return true; + } + + public async Task VerifyChatPair(Message message) + { + await VerifyChatPair(message.Chat, message.From); + } + + public async Task VerifyChatPair(Chat chat, User user) + { + await VerifyChatPair(chat.Id, user.Id); + } + + public async Task VerifyChatPair(long chat, long user) + { + var hasEntry = await databaseContext.Users.AnyAsync(e => e.ChatId == chat && e.UserId == user); + if (hasEntry) { - await databaseContext.Users.AddAsync(new BotUser() - { - ChatId = chat, - UserId = user - }); + return; } + + await MakeChatPair(chat, user); + } + + public async Task MakeChatPair(long chat, long user) + { + await databaseContext.Users.AddAsync(new BotUser() + { + ChatId = chat, + UserId = user + }); } } \ No newline at end of file diff --git a/Core/Module.cs b/Core/Module.cs index 69828d7..d9c422b 100644 --- a/Core/Module.cs +++ b/Core/Module.cs @@ -3,34 +3,33 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Kruzya.TelegramBot.Core +namespace Kruzya.TelegramBot.Core; + +public abstract class Module { - public abstract class Module + protected readonly Core Core; + protected IConfiguration Configuration => Core.Configuration; + + public Module(Core core) { - protected readonly Core Core; - protected IConfiguration Configuration => Core.Configuration; - - public Module(Core core) - { - Core = core; - } - - public virtual void ConfigureServices(IServiceCollection services) - { - } - - public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - } - - /// - /// Ensures if module is loaded and started. Note that call can change module event chain. - /// - /// The Module type. - /// Module instance. - protected T EnsureLoaded() where T : Module - { - return (T) Core.EnsureLoaded(typeof(T)); - } + Core = core; } -} + + public virtual void ConfigureServices(IServiceCollection services) + { + } + + public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + } + + /// + /// Ensures if module is loaded and started. Note that call can change module event chain. + /// + /// The Module type. + /// Module instance. + protected T EnsureLoaded() where T : Module + { + return (T) Core.EnsureLoaded(typeof(T)); + } +} \ No newline at end of file diff --git a/Core/Program.cs b/Core/Program.cs index 103d924..df46d88 100644 --- a/Core/Program.cs +++ b/Core/Program.cs @@ -2,19 +2,18 @@ using System.Globalization; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; -namespace Kruzya.TelegramBot.Core -{ - public class Program - { - public static void Main(string[] args) - { - CultureInfo.CurrentCulture = new CultureInfo("ru-RU"); - CreateHostBuilder(args).Build().Run(); - } +namespace Kruzya.TelegramBot.Core; - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .UseSystemd() - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); +public class Program +{ + public static void Main(string[] args) + { + CultureInfo.CurrentCulture = new CultureInfo("ru-RU"); + CreateHostBuilder(args).Build().Run(); } -} + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseSystemd() + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); +} \ No newline at end of file diff --git a/Core/Service/AbstractTimedHostedService.cs b/Core/Service/AbstractTimedHostedService.cs index 847b613..3f93b0d 100644 --- a/Core/Service/AbstractTimedHostedService.cs +++ b/Core/Service/AbstractTimedHostedService.cs @@ -5,70 +5,69 @@ using BotFramework.Abstractions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Kruzya.TelegramBot.Core.Service +namespace Kruzya.TelegramBot.Core.Service; + +public abstract class AbstractTimedHostedService : IHostedService, IDisposable { - public abstract class AbstractTimedHostedService : IHostedService, IDisposable + private Timer _timer; + + protected readonly ILogger _logger; + protected readonly IBotInstance _bot; + + protected virtual TimeSpan TimerPeriod => TimeSpan.FromSeconds(45); + + protected AbstractTimedHostedService(ILogger logger, IBotInstance bot) { - private Timer _timer; + _bot = bot; + _logger = logger; + } + + #region IHostedService + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Message sender service is starting."); + _timer = new Timer(Run, null, TimeSpan.Zero, TimerPeriod); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Message sender service is stopping."); + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } - protected readonly ILogger _logger; - protected readonly IBotInstance _bot; - - protected virtual TimeSpan TimerPeriod => TimeSpan.FromSeconds(45); + #endregion + #region IDisposable + /// + /// Disposes the timer. + /// + public void Dispose() + { + _timer?.Dispose(); + } + #endregion - protected AbstractTimedHostedService(ILogger logger, IBotInstance bot) + private async void Run(object state) + { + _timer?.Change(Timeout.Infinite, 0); + + try { - _bot = bot; - _logger = logger; + await OnRun(); } - - #region IHostedService - - public Task StartAsync(CancellationToken cancellationToken) + finally { - _logger.LogInformation("Message sender service is starting."); - _timer = new Timer(Run, null, TimeSpan.Zero, TimerPeriod); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Message sender service is stopping."); - _timer?.Change(Timeout.Infinite, 0); - - return Task.CompletedTask; - } - - #endregion - #region IDisposable - /// - /// Disposes the timer. - /// - public void Dispose() - { - _timer?.Dispose(); - } - #endregion - - private async void Run(object state) - { - _timer?.Change(Timeout.Infinite, 0); - - try - { - await OnRun(); - } - finally - { - var timerPeriod = TimerPeriod; - _timer?.Change(timerPeriod, timerPeriod); - } - } - - protected virtual async Task OnRun() - { - await Task.Delay(500); + var timerPeriod = TimerPeriod; + _timer?.Change(timerPeriod, timerPeriod); } } + + protected virtual async Task OnRun() + { + await Task.Delay(500); + } } \ No newline at end of file diff --git a/Core/Service/IReputation.cs b/Core/Service/IReputation.cs index 30181be..71d3204 100644 --- a/Core/Service/IReputation.cs +++ b/Core/Service/IReputation.cs @@ -3,14 +3,13 @@ using System.Threading.Tasks; using Kruzya.TelegramBot.Core.Data; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.Core.Service +namespace Kruzya.TelegramBot.Core.Service; + +public interface IReputation { - public interface IReputation - { - public Task GetUserReputationAsync(Chat chat, User user); - public Task SetUserReputationAsync(Chat chat, User user, double value); - public Task GetReputationDiffAsync(Chat chat, User sender, User receiver); - public Task IncrementReputationAsync(Chat chat, User user, double diff); - public Task> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0); - } -} + public Task GetUserReputationAsync(Chat chat, User user); + public Task SetUserReputationAsync(Chat chat, User user, double value); + public Task GetReputationDiffAsync(Chat chat, User sender, User receiver); + public Task IncrementReputationAsync(Chat chat, User user, double diff); + public Task> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0); +} \ No newline at end of file diff --git a/Core/Service/UserService.cs b/Core/Service/UserService.cs index 5fa261a..06f1c10 100644 --- a/Core/Service/UserService.cs +++ b/Core/Service/UserService.cs @@ -11,81 +11,80 @@ using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.Core.Service +namespace Kruzya.TelegramBot.Core.Service; + +public class UserService { - public class UserService + private readonly List _superUsers; + private readonly Dictionary _statusMap; + private readonly ICacheStorage _userCache; + private readonly ITelegramBotClient _bot; + private readonly ILogger _logger; + + + public UserService( + IConfiguration configuration, + IBotInstance bot, + ICacheStorage userCache, + ILogger logger) { - private readonly List _superUsers; - private readonly Dictionary _statusMap; - private readonly ICacheStorage _userCache; - private readonly ITelegramBotClient _bot; - private readonly ILogger _logger; + _superUsers = configuration.GetSection("SuperUsers") + .GetChildren() + .Select(q => Convert.ToInt64(q.Value)) + .ToList(); + _statusMap = configuration.GetSection("CustomMemberStatus") + .GetChildren() + .ToDictionary(x => Convert.ToInt64(x.Key), x => x.Value); - public UserService( - IConfiguration configuration, - IBotInstance bot, - ICacheStorage userCache, - ILogger logger) + _userCache = userCache; + _bot = bot.BotClient; + _logger = logger; + } + + public async Task GetUser(long userId, long chatId, bool bypassCache = false) + { + if (bypassCache || !_userCache.TryGetValue(userId, out var user)) { - _superUsers = configuration.GetSection("SuperUsers") - .GetChildren() - .Select(q => Convert.ToInt64(q.Value)) - .ToList(); - - _statusMap = configuration.GetSection("CustomMemberStatus") - .GetChildren() - .ToDictionary(x => Convert.ToInt64(x.Key), x => x.Value); - - _userCache = userCache; - _bot = bot.BotClient; - _logger = logger; - } - - public async Task GetUser(long userId, long chatId, bool bypassCache = false) - { - if (bypassCache || !_userCache.TryGetValue(userId, out var user)) + if (bypassCache) { - if (bypassCache) - { - _logger.LogDebug("Bypassing cache for {UserId}.", userId); - } - else - { - _logger.LogDebug("User {UserId} is not found in cache. Fetching from Telegram API.", userId); - } - - user = await GetUserByChat(chatId, userId); - CacheUser(user); + _logger.LogDebug("Bypassing cache for {UserId}.", userId); } + else + { + _logger.LogDebug("User {UserId} is not found in cache. Fetching from Telegram API.", userId); + } + + user = await GetUserByChat(chatId, userId); + CacheUser(user); + } - return user; - } + return user; + } - private void CacheUser(User user) - { - // TODO: config entry for TTL - _userCache.SetValue(user.Id, user, Convert.ToInt32(TimeSpan.FromHours(1).TotalSeconds)); - } + private void CacheUser(User user) + { + // TODO: config entry for TTL + _userCache.SetValue(user.Id, user, Convert.ToInt32(TimeSpan.FromHours(1).TotalSeconds)); + } - private async Task GetUserByChat(long chatId, long userId) - { - return (await _bot.GetChatMemberAsync(chatId, userId)).User; - } + private async Task GetUserByChat(long chatId, long userId) + { + return (await _bot.GetChatMemberAsync(chatId, userId)).User; + } - public bool IsUserSuper(User user) - { - return IsUserSuper(user.Id); - } + public bool IsUserSuper(User user) + { + return IsUserSuper(user.Id); + } - public bool IsUserSuper(long userId) - { - return _superUsers.Contains(userId); - } + public bool IsUserSuper(long userId) + { + return _superUsers.Contains(userId); + } - public string? GetUserCustomStatus(long userId) - { - return _statusMap.ContainsKey(userId) ? _statusMap[userId] : null; - } + public string? GetUserCustomStatus(long userId) + { + return _statusMap.ContainsKey(userId) ? _statusMap[userId] : null; } } \ No newline at end of file diff --git a/Core/Startup.cs b/Core/Startup.cs index c52877f..672e552 100644 --- a/Core/Startup.cs +++ b/Core/Startup.cs @@ -14,57 +14,56 @@ using System.Collections.Concurrent; using Kruzya.TelegramBot.Core.Options; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.Core +namespace Kruzya.TelegramBot.Core; + +public class Startup { - public class Startup + private Core _core; + + public Startup(IConfiguration configuration) { - private Core _core; - - public Startup(IConfiguration configuration) - { - // TODO: add Core to DI. - _core = new Core(configuration); - } - - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 - public void ConfigureServices(IServiceCollection services) - { - services.AddTelegramBot(); - services.AddLogging(builder => builder.AddConsole()); - - // Add all modules to DI with Core instance. - services.AddSingleton(_core); - foreach (var module in _core.Modules) services.AddSingleton(module.GetType(), module); - - // Add database context to DI. - var connectionString = _core.Configuration.GetConnectionString("DefaultConnection"); - services.AddDbContext(ctx => - ctx.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); - - // Cache for users. - services.AddSingleton, MemoryCache>(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - - - services.AddSingleton>(); - services.AddHostedService(); - - services.AddScoped(); - services.AddHttpClient(); - - // Trigger module handlers. - _core.Modules.ConfigureServices(services); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - _core.Modules.Configure(app, env); - } + // TODO: add Core to DI. + _core = new Core(configuration); } -} + + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddTelegramBot(); + services.AddLogging(builder => builder.AddConsole()); + + // Add all modules to DI with Core instance. + services.AddSingleton(_core); + foreach (var module in _core.Modules) services.AddSingleton(module.GetType(), module); + + // Add database context to DI. + var connectionString = _core.Configuration.GetConnectionString("DefaultConnection"); + services.AddDbContext(ctx => + ctx.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + + // Cache for users. + services.AddSingleton, MemoryCache>(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + + services.AddSingleton>(); + services.AddHostedService(); + + services.AddScoped(); + services.AddHttpClient(); + + // Trigger module handlers. + _core.Modules.ConfigureServices(services); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + _core.Modules.Configure(app, env); + } +} \ No newline at end of file diff --git a/modules/ChatQuotes/ChatQuotes.cs b/modules/ChatQuotes/ChatQuotes.cs index 117d91f..95addda 100644 --- a/modules/ChatQuotes/ChatQuotes.cs +++ b/modules/ChatQuotes/ChatQuotes.cs @@ -6,27 +6,26 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using West.TelegramBot.ChatQuotes.Json; -namespace West.TelegramBot.ChatQuotes +namespace West.TelegramBot.ChatQuotes; + +public class ChatQuotes : Module { - public class ChatQuotes : Module + public static readonly JsonSerializerSettings SerializerSettings = new() { - public static readonly JsonSerializerSettings SerializerSettings = new() + MetadataPropertyHandling = MetadataPropertyHandling.Ignore, + DateParseHandling = DateParseHandling.None, + Converters = { - MetadataPropertyHandling = MetadataPropertyHandling.Ignore, - DateParseHandling = DateParseHandling.None, - Converters = - { - new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }, - new StringEnumConverter(new CamelCaseNamingStrategy()) - }, - ContractResolver = new NullToEmptyObjectResolver() - }; + new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }, + new StringEnumConverter(new CamelCaseNamingStrategy()) + }, + ContractResolver = new NullToEmptyObjectResolver() + }; - public ChatQuotes(Core core) : base(core) { } + public ChatQuotes(Core core) : base(core) { } - public override void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - } + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); } } \ No newline at end of file diff --git a/modules/ChatQuotes/Json/NullToEmptyObjectResolver.cs b/modules/ChatQuotes/Json/NullToEmptyObjectResolver.cs index 75fe112..4a06d91 100644 --- a/modules/ChatQuotes/Json/NullToEmptyObjectResolver.cs +++ b/modules/ChatQuotes/Json/NullToEmptyObjectResolver.cs @@ -1,18 +1,17 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace West.TelegramBot.ChatQuotes.Json +namespace West.TelegramBot.ChatQuotes.Json; + +class NullToEmptyObjectResolver : DefaultContractResolver { - class NullToEmptyObjectResolver : DefaultContractResolver + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { - protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) - { - return type.GetProperties() - .Select(p => { - var jp = base.CreateProperty(p, memberSerialization); - jp.ValueProvider = new NullToEmptyObjectValueProvider(p); - return jp; - }).ToList(); - } + return type.GetProperties() + .Select(p => { + var jp = base.CreateProperty(p, memberSerialization); + jp.ValueProvider = new NullToEmptyObjectValueProvider(p); + return jp; + }).ToList(); } -} +} \ No newline at end of file diff --git a/modules/CodeWatcher/CodeWatcher.cs b/modules/CodeWatcher/CodeWatcher.cs index 0b51462..58ca82b 100644 --- a/modules/CodeWatcher/CodeWatcher.cs +++ b/modules/CodeWatcher/CodeWatcher.cs @@ -4,20 +4,19 @@ using Microsoft.Extensions.DependencyInjection; using West.TelegramBot.CodeWatcher.Options; using West.TelegramBot.CodeWatcher.Service; -namespace West.TelegramBot.CodeWatcher -{ - public class CodeWatcher : Module - { - public CodeWatcher(Core core) : base(core) {} +namespace West.TelegramBot.CodeWatcher; - public override void ConfigureServices(IServiceCollection services) - { - services.AddOption(); +public class CodeWatcher : Module +{ + public CodeWatcher(Core core) : base(core) {} + + public override void ConfigureServices(IServiceCollection services) + { + services.AddOption(); - services.AddHttpClient(c => - { - c.BaseAddress = new Uri(Core.Configuration["HastebinUrl"]!); - }); - } + services.AddHttpClient(c => + { + c.BaseAddress = new Uri(Core.Configuration["HastebinUrl"]!); + }); } } \ No newline at end of file diff --git a/modules/CombotAntiSpam/UserJoinHandler.cs b/modules/CombotAntiSpam/UserJoinHandler.cs index 0bec76a..feac54b 100644 --- a/modules/CombotAntiSpam/UserJoinHandler.cs +++ b/modules/CombotAntiSpam/UserJoinHandler.cs @@ -6,7 +6,6 @@ using BotFramework.Enums; using Kruzya.TelegramBot.CombotAntiSpam.Combot; using Kruzya.TelegramBot.Core.Extensions; using Telegram.Bot; -using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; namespace Kruzya.TelegramBot.CombotAntiSpam; diff --git a/modules/Destiny2.WhereIsXur/Provider/IXurPlaceProvider.cs b/modules/Destiny2.WhereIsXur/Provider/IXurPlaceProvider.cs index 681af6f..b9a101d 100644 --- a/modules/Destiny2.WhereIsXur/Provider/IXurPlaceProvider.cs +++ b/modules/Destiny2.WhereIsXur/Provider/IXurPlaceProvider.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; -namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider +namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider; + +public interface IXurPlaceProvider { - public interface IXurPlaceProvider - { - public Task GetPlaceAsync(); - public string GetPlace(); - } + public Task GetPlaceAsync(); + public string GetPlace(); } \ No newline at end of file diff --git a/modules/Destiny2.WhereIsXur/Provider/XurWiki.cs b/modules/Destiny2.WhereIsXur/Provider/XurWiki.cs index f9a207c..6f32b08 100644 --- a/modules/Destiny2.WhereIsXur/Provider/XurWiki.cs +++ b/modules/Destiny2.WhereIsXur/Provider/XurWiki.cs @@ -2,27 +2,26 @@ using System.Threading.Tasks; using Fizzler.Systems.HtmlAgilityPack; using HtmlAgilityPack; -namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider +namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider; + +public class XurWiki : IXurPlaceProvider { - public class XurWiki : IXurPlaceProvider + public async Task GetPlaceAsync() { - public async Task GetPlaceAsync() + var web = new HtmlWeb(); + var document = await web.LoadFromWebAsync("https://xur.wiki"); + + var place = document.DocumentNode.QuerySelector(".location_name"); + if (place != null) { - var web = new HtmlWeb(); - var document = await web.LoadFromWebAsync("https://xur.wiki"); - - var place = document.DocumentNode.QuerySelector(".location_name"); - if (place != null) - { - return place.InnerText.Trim(); - } - - return ""; + return place.InnerText.Trim(); } - public string GetPlace() - { - return GetPlaceAsync().GetAwaiter().GetResult(); - } + return ""; + } + + public string GetPlace() + { + return GetPlaceAsync().GetAwaiter().GetResult(); } } \ No newline at end of file diff --git a/modules/Destiny2.WhereIsXur/RequestHandler.cs b/modules/Destiny2.WhereIsXur/RequestHandler.cs index a3252e5..acce169 100644 --- a/modules/Destiny2.WhereIsXur/RequestHandler.cs +++ b/modules/Destiny2.WhereIsXur/RequestHandler.cs @@ -5,33 +5,32 @@ using Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider; using Telegram.Bot; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.Destiny2.WhereIsXur +namespace Kruzya.TelegramBot.Destiny2.WhereIsXur; + +public class RequestHandler : BotEventHandler { - public class RequestHandler : BotEventHandler + protected readonly IXurPlaceProvider _placeProvider; + + public RequestHandler(IXurPlaceProvider xurPlaceProvider) { - protected readonly IXurPlaceProvider _placeProvider; - - public RequestHandler(IXurPlaceProvider xurPlaceProvider) - { - _placeProvider = xurPlaceProvider; - } + _placeProvider = xurPlaceProvider; + } - [Command("d2_xur")] - public async Task WhereIsXur() + [Command("d2_xur")] + public async Task WhereIsXur() + { + var place = await _placeProvider.GetPlaceAsync(); + if (string.IsNullOrWhiteSpace(place)) { - var place = await _placeProvider.GetPlaceAsync(); - if (string.IsNullOrWhiteSpace(place)) - { - await Response("unknown"); - return; - } - - await Response(place); + await Response("unknown"); + return; } - protected async Task Response(string place) - { - await Bot.SendTextMessageAsync(Chat, $"Xûr place is {place}", parseMode: ParseMode.Html); - } + await Response(place); + } + + protected async Task Response(string place) + { + await Bot.SendTextMessageAsync(Chat, $"Xûr place is {place}", parseMode: ParseMode.Html); } } \ No newline at end of file diff --git a/modules/Destiny2.WhereIsXur/WhereIsXur.cs b/modules/Destiny2.WhereIsXur/WhereIsXur.cs index bd176c0..bd4fcf4 100644 --- a/modules/Destiny2.WhereIsXur/WhereIsXur.cs +++ b/modules/Destiny2.WhereIsXur/WhereIsXur.cs @@ -2,17 +2,16 @@ using Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider; using Microsoft.Extensions.DependencyInjection; -namespace Kruzya.TelegramBot.Destiny2.WhereIsXur -{ - public class WhereIsXur : Module - { - public WhereIsXur(Core.Core core) : base(core) - { - } +namespace Kruzya.TelegramBot.Destiny2.WhereIsXur; - public override void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - } +public class WhereIsXur : Module +{ + public WhereIsXur(Core.Core core) : base(core) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); } } \ No newline at end of file diff --git a/modules/Entertainment/Entertainment.cs b/modules/Entertainment/Entertainment.cs index d184b4b..4df9b42 100644 --- a/modules/Entertainment/Entertainment.cs +++ b/modules/Entertainment/Entertainment.cs @@ -6,15 +6,14 @@ using Kruzya.TelegramBot.Core; using Microsoft.Extensions.DependencyInjection; -namespace West.Entertainment -{ - public class Entertainment : Module - { - public Entertainment(Core core) : base(core) { } +namespace West.Entertainment; - public override void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - } +public class Entertainment : Module +{ + public Entertainment(Core core) : base(core) { } + + public override void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); } } \ No newline at end of file diff --git a/modules/Entertainment/Handler/Common.cs b/modules/Entertainment/Handler/Common.cs index 8fb02f5..8d8cd50 100644 --- a/modules/Entertainment/Handler/Common.cs +++ b/modules/Entertainment/Handler/Common.cs @@ -13,52 +13,51 @@ using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types.Enums; -namespace West.Entertainment.Handler +namespace West.Entertainment.Handler; + +public class Common : BotEventHandler { - public class Common : BotEventHandler - { - private readonly UserService _userService; - private readonly ILogger _logger; - private readonly CoreContext _db; + private readonly UserService _userService; + private readonly ILogger _logger; + private readonly CoreContext _db; - public Common(CoreContext db, ILogger logger, UserService userService) - { - _db = db; - _logger = logger; - _userService = userService; - } + public Common(CoreContext db, ILogger logger, UserService userService) + { + _db = db; + _logger = logger; + _userService = userService; + } - [InChat(InChat.Public)] - [Command("cooldowns", CommandParseMode.Both)] - public async Task HandleCooldowns() + [InChat(InChat.Public)] + [Command("cooldowns", CommandParseMode.Both)] + public async Task HandleCooldowns() + { + if (!_userService.IsUserSuper(From)) { - if (!_userService.IsUserSuper(From)) - { - _logger.LogWarning("{User} attempted to use super-user command /cooldowns in {Chat}", - From.ToLog(), - Chat.ToLog() - ); - return; - } + _logger.LogWarning("{User} attempted to use super-user command /cooldowns in {Chat}", + From.ToLog(), + Chat.ToLog() + ); + return; + } - var cds = new[] { "python", "javascript" }; + var cds = new[] { "python", "javascript" }; - var msg = new HtmlString().Bold("Кулдауны:").Br(); - foreach (var cd in cds) + var msg = new HtmlString().Bold("Кулдауны:").Br(); + foreach (var cd in cds) + { + var opt = await _db.UserValues.FindOrCreateOption(Chat.Id, $"{cd}NextUse"); + var val = opt.GetValue(); + if (val == default) { - var opt = await _db.UserValues.FindOrCreateOption(Chat.Id, $"{cd}NextUse"); - var val = opt.GetValue(); - if (val == default) - { - continue; - } - - msg.Text($"{cd}: ") - .Code(DateTime.Now > val ? "Ready" : (val - DateTime.Now).ToString()) - .Br(); + continue; } - await Bot.SendTextMessageAsync(Chat.Id, msg.ToString(), parseMode: ParseMode.Html); + msg.Text($"{cd}: ") + .Code(DateTime.Now > val ? "Ready" : (val - DateTime.Now).ToString()) + .Br(); } + + await Bot.SendTextMessageAsync(Chat.Id, msg.ToString(), parseMode: ParseMode.Html); } } \ No newline at end of file diff --git a/modules/Entertainment/Handler/Fun/AbstractAnimationReply.cs b/modules/Entertainment/Handler/Fun/AbstractAnimationReply.cs index 32d5ba8..44d0b97 100644 --- a/modules/Entertainment/Handler/Fun/AbstractAnimationReply.cs +++ b/modules/Entertainment/Handler/Fun/AbstractAnimationReply.cs @@ -11,98 +11,97 @@ using Microsoft.Extensions.Logging; using Telegram.Bot; using Telegram.Bot.Types; -namespace West.Entertainment.Handler.Fun +namespace West.Entertainment.Handler.Fun; + +public abstract class AbstractAnimationReply : BotEventHandler { - public abstract class AbstractAnimationReply : BotEventHandler + private readonly CoreContext _db; + + private readonly ILogger _logger; + private readonly ThreadSafeRandom _rng; + + protected virtual TimeSpan Cooldown => TimeSpan.FromMinutes(_rng.Next(10, 1337)); + + protected Message? Message => RawUpdate.Message; + + private User? RepliedUser => Message?.ReplyToMessage?.From; + + protected abstract Regex AnimationMatch { - private readonly CoreContext _db; - - private readonly ILogger _logger; - private readonly ThreadSafeRandom _rng; - - protected virtual TimeSpan Cooldown => TimeSpan.FromMinutes(_rng.Next(10, 1337)); - - protected Message? Message => RawUpdate.Message; - - private User? RepliedUser => Message?.ReplyToMessage?.From; - - protected abstract Regex AnimationMatch - { - get; - } - - protected AbstractAnimationReply(CoreContext db, ILogger logger, ThreadSafeRandom rng) - { - _db = db; - _logger = logger; - _rng = rng; - } - - protected abstract string GetAnimationName(); - - protected virtual bool CanHandle() => true; - - protected async Task HandleSetAnimation() - { - var animation = Message?.ReplyToMessage?.Animation; - var isAdmin = await Bot.IsUserAdminAsync(Chat, From); - - _logger.LogDebug("IsAdmin: {IsAdmin}. FileId: {FileId}", isAdmin, animation?.FileId); - if (!isAdmin || animation == null) return; - - var animationFileOption = await GetAnimationFileOption(); - animationFileOption.SetValue(animation.FileId); - _db.AddOrUpdate(animationFileOption); - - await Bot.SendTextMessageAsync(Chat.Id, "Animation has been set.", replyToMessageId: Message!.ReplyToMessage!.MessageId); - } - - protected virtual async Task HandleAnimation() - { - var text = Message!.Text!.ToLower(); - if (!AnimationMatch.IsMatch(text.ToLower()) || text.StartsWith("/set") || !CanHandle()) return false; - - var animationFileId = (await GetAnimationFileOption()).GetValue(string.Empty); - var nextUseOption = await GetNextUseOption(); - - _logger.LogDebug("Animation: {Text}, ToLower: {ToLowerText}", - Message!.Text, Message!.Text!.ToLower()); - _logger.LogDebug("AnimationFileId: {FileId}", animationFileId); - - if (string.IsNullOrEmpty(animationFileId)) - { - _logger.LogDebug("AnimationFileId is empty. Ignoring."); - return false; - } - - var nextUseDt = nextUseOption.GetValue(); - if (nextUseDt > DateTime.Now) - { - _logger.LogInformation("{Name} is on cooldown for chat \"{ChatTitle}\". Will be available at {Time}", - GetAnimationName(), Chat.Title, nextUseDt); - - return false; - } - - await Bot.SendAnimationAsync(Chat.Id, new InputFileId(animationFileId), - replyToMessageId: GetMessageIdToReply() ?? Message?.MessageId); - - nextUseOption.SetValue(DateTime.Now + Cooldown); - _db.AddOrUpdate(nextUseOption); - - return true; - } - - private async Task GetAnimationFileOption() - { - return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}File"); - } - - private async Task GetNextUseOption() - { - return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}NextUse"); - } - - protected virtual int? GetMessageIdToReply() => Message?.MessageId; + get; } + + protected AbstractAnimationReply(CoreContext db, ILogger logger, ThreadSafeRandom rng) + { + _db = db; + _logger = logger; + _rng = rng; + } + + protected abstract string GetAnimationName(); + + protected virtual bool CanHandle() => true; + + protected async Task HandleSetAnimation() + { + var animation = Message?.ReplyToMessage?.Animation; + var isAdmin = await Bot.IsUserAdminAsync(Chat, From); + + _logger.LogDebug("IsAdmin: {IsAdmin}. FileId: {FileId}", isAdmin, animation?.FileId); + if (!isAdmin || animation == null) return; + + var animationFileOption = await GetAnimationFileOption(); + animationFileOption.SetValue(animation.FileId); + _db.AddOrUpdate(animationFileOption); + + await Bot.SendTextMessageAsync(Chat.Id, "Animation has been set.", replyToMessageId: Message!.ReplyToMessage!.MessageId); + } + + protected virtual async Task HandleAnimation() + { + var text = Message!.Text!.ToLower(); + if (!AnimationMatch.IsMatch(text.ToLower()) || text.StartsWith("/set") || !CanHandle()) return false; + + var animationFileId = (await GetAnimationFileOption()).GetValue(string.Empty); + var nextUseOption = await GetNextUseOption(); + + _logger.LogDebug("Animation: {Text}, ToLower: {ToLowerText}", + Message!.Text, Message!.Text!.ToLower()); + _logger.LogDebug("AnimationFileId: {FileId}", animationFileId); + + if (string.IsNullOrEmpty(animationFileId)) + { + _logger.LogDebug("AnimationFileId is empty. Ignoring."); + return false; + } + + var nextUseDt = nextUseOption.GetValue(); + if (nextUseDt > DateTime.Now) + { + _logger.LogInformation("{Name} is on cooldown for chat \"{ChatTitle}\". Will be available at {Time}", + GetAnimationName(), Chat.Title, nextUseDt); + + return false; + } + + await Bot.SendAnimationAsync(Chat.Id, new InputFileId(animationFileId), + replyToMessageId: GetMessageIdToReply() ?? Message?.MessageId); + + nextUseOption.SetValue(DateTime.Now + Cooldown); + _db.AddOrUpdate(nextUseOption); + + return true; + } + + private async Task GetAnimationFileOption() + { + return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}File"); + } + + private async Task GetNextUseOption() + { + return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}NextUse"); + } + + protected virtual int? GetMessageIdToReply() => Message?.MessageId; } \ No newline at end of file diff --git a/modules/Entertainment/Handler/Fun/Bayan.cs b/modules/Entertainment/Handler/Fun/Bayan.cs index 08f0101..671dbf2 100644 --- a/modules/Entertainment/Handler/Fun/Bayan.cs +++ b/modules/Entertainment/Handler/Fun/Bayan.cs @@ -10,42 +10,41 @@ using Kruzya.TelegramBot.Core.Data; using Microsoft.Extensions.Logging; using Telegram.Bot; -namespace West.Entertainment.Handler.Fun +namespace West.Entertainment.Handler.Fun; + +public class Bayan : AbstractAnimationReply { - public class Bayan : AbstractAnimationReply + protected override TimeSpan Cooldown => TimeSpan.Zero; + + public Bayan(CoreContext db, ILogger logger, ThreadSafeRandom rng) : base(db, logger, rng) { } + + protected override Regex AnimationMatch + => new("^баян$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + protected override string GetAnimationName() + => "bayan"; + + [InChat(InChat.Public)] + [Command("setbayan", CommandParseMode.Both)] + public async Task HandleSetBayan() + => await HandleSetAnimation(); + + [InChat(InChat.Public)] + [Message(MessageFlag.HasText)] + public async Task HandleBayan() { - protected override TimeSpan Cooldown => TimeSpan.Zero; - - public Bayan(CoreContext db, ILogger logger, ThreadSafeRandom rng) : base(db, logger, rng) { } - - protected override Regex AnimationMatch - => new("^баян$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - protected override string GetAnimationName() - => "bayan"; - - [InChat(InChat.Public)] - [Command("setbayan", CommandParseMode.Both)] - public async Task HandleSetBayan() - => await HandleSetAnimation(); - - [InChat(InChat.Public)] - [Message(MessageFlag.HasText)] - public async Task HandleBayan() + if (await HandleAnimation()) { - if (await HandleAnimation()) - { - await Bot.DeleteMessageAsync(Chat.Id, Message!.MessageId); - } + await Bot.DeleteMessageAsync(Chat.Id, Message!.MessageId); } - - protected override int? GetMessageIdToReply() - { - return Message!.ReplyToMessage!.MessageId; - } - - protected override bool CanHandle() - => Message?.ReplyToMessage != null; - } + + protected override int? GetMessageIdToReply() + { + return Message!.ReplyToMessage!.MessageId; + } + + protected override bool CanHandle() + => Message?.ReplyToMessage != null; + } \ No newline at end of file diff --git a/modules/Entertainment/Handler/Fun/JavaScript.cs b/modules/Entertainment/Handler/Fun/JavaScript.cs index 14d5064..1119ff3 100644 --- a/modules/Entertainment/Handler/Fun/JavaScript.cs +++ b/modules/Entertainment/Handler/Fun/JavaScript.cs @@ -6,28 +6,27 @@ using Kruzya.TelegramBot.Core; using Kruzya.TelegramBot.Core.Data; using Microsoft.Extensions.Logging; -namespace West.Entertainment.Handler.Fun -{ - public class JavaScript : AbstractAnimationReply - { - public JavaScript(CoreContext db, ILogger logger, ThreadSafeRandom rng) : base(db, logger, rng) { } +namespace West.Entertainment.Handler.Fun; - protected override Regex AnimationMatch => new( - @"((? logger, ThreadSafeRandom rng) : base(db, logger, rng) { } + + protected override Regex AnimationMatch => new( + @"((? "javascript"; + protected override string GetAnimationName() => "javascript"; - [InChat(InChat.Public)] - [Command("setjs", CommandParseMode.Both)] - public async Task HandleSetJs() - => await HandleSetAnimation(); + [InChat(InChat.Public)] + [Command("setjs", CommandParseMode.Both)] + public async Task HandleSetJs() + => await HandleSetAnimation(); - [InChat(InChat.Public)] - [Message(MessageFlag.HasText)] - public async Task HandleJs() - { - await HandleAnimation(); - } + [InChat(InChat.Public)] + [Message(MessageFlag.HasText)] + public async Task HandleJs() + { + await HandleAnimation(); } } \ No newline at end of file diff --git a/modules/Entertainment/Handler/Fun/Python.cs b/modules/Entertainment/Handler/Fun/Python.cs index 75cb0a0..7b9b02e 100644 --- a/modules/Entertainment/Handler/Fun/Python.cs +++ b/modules/Entertainment/Handler/Fun/Python.cs @@ -8,29 +8,28 @@ using Kruzya.TelegramBot.Core; using Kruzya.TelegramBot.Core.Data; using Microsoft.Extensions.Logging; -namespace West.Entertainment.Handler.Fun +namespace West.Entertainment.Handler.Fun; + +public class Python : AbstractAnimationReply { - public class Python : AbstractAnimationReply + protected override Regex AnimationMatch => + new("питон|пайтон|python|путхон|питухон|петухон", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + protected override string GetAnimationName() + => "python"; + + [InChat(InChat.Public)] + [Command("setpython", CommandParseMode.Both)] + public async Task HandleSetPython() + => await HandleSetAnimation(); + + [InChat(InChat.Public)] + [Message(MessageFlag.HasText)] + public async Task HandlePython() { - protected override Regex AnimationMatch => - new("питон|пайтон|python|путхон|питухон|петухон", - RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - - protected override string GetAnimationName() - => "python"; - - [InChat(InChat.Public)] - [Command("setpython", CommandParseMode.Both)] - public async Task HandleSetPython() - => await HandleSetAnimation(); - - [InChat(InChat.Public)] - [Message(MessageFlag.HasText)] - public async Task HandlePython() - { - await HandleAnimation(); - } - - public Python(CoreContext db, ILogger logger, ThreadSafeRandom rng) : base(db, logger, rng) { } + await HandleAnimation(); } + + public Python(CoreContext db, ILogger logger, ThreadSafeRandom rng) : base(db, logger, rng) { } } \ No newline at end of file diff --git a/modules/RichSiteSummary/Data/Feed.cs b/modules/RichSiteSummary/Data/Feed.cs index c5cb296..8ffbd11 100644 --- a/modules/RichSiteSummary/Data/Feed.cs +++ b/modules/RichSiteSummary/Data/Feed.cs @@ -8,83 +8,82 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -namespace Kruzya.TelegramBot.RichSiteSummary.Data +namespace Kruzya.TelegramBot.RichSiteSummary.Data; + +/// +/// Represents a feed in database. +/// +public class Feed : RepresentableEntity { - /// - /// Represents a feed in database. - /// - public class Feed : RepresentableEntity + #region RepresentableEntity + protected override string ViewableText { - #region RepresentableEntity - protected override string ViewableText - { - get => Title; - } - - protected override string ViewableUrl - { - get => HomePage; - } - #endregion - - /// - /// The unique feed id. - /// - [Key] - public Guid FeedId { get; set; } - - /// - /// The feed URL. - /// - [Required] - public string Url { get; set; } - - /// - /// The home page for this feed. - /// - [Required] - public string HomePage { get; set; } - - /// - /// Feed title. - /// - [MaxLength(256)] - [Required] - public string Title { get; set; } - - /// - /// DateTime when feed fetched successfully in last time. - /// - [Required] - public DateTime UpdatedAt { get; set; } - - /// - /// This flag indicates, bot should monitor changes in post date, or not. - /// - [Required] - [DefaultValue(false)] - public bool WatchPostDate { get; set; } - - [Required] - [DefaultValue(false)] - public bool IsPrivate { get; set; } - - public Feed() - { - FeedId = new Guid(); - UpdatedAt = DateTime.Now; - } - - /// - /// All posts from this feed. - /// - public virtual ICollection Posts { get; set; } - - /// - /// All exists subscriber for this feed. - /// - public virtual ICollection Subscribers { get; set; } - - public override string ToString() => Title; + get => Title; } + + protected override string ViewableUrl + { + get => HomePage; + } + #endregion + + /// + /// The unique feed id. + /// + [Key] + public Guid FeedId { get; set; } + + /// + /// The feed URL. + /// + [Required] + public string Url { get; set; } + + /// + /// The home page for this feed. + /// + [Required] + public string HomePage { get; set; } + + /// + /// Feed title. + /// + [MaxLength(256)] + [Required] + public string Title { get; set; } + + /// + /// DateTime when feed fetched successfully in last time. + /// + [Required] + public DateTime UpdatedAt { get; set; } + + /// + /// This flag indicates, bot should monitor changes in post date, or not. + /// + [Required] + [DefaultValue(false)] + public bool WatchPostDate { get; set; } + + [Required] + [DefaultValue(false)] + public bool IsPrivate { get; set; } + + public Feed() + { + FeedId = new Guid(); + UpdatedAt = DateTime.Now; + } + + /// + /// All posts from this feed. + /// + public virtual ICollection Posts { get; set; } + + /// + /// All exists subscriber for this feed. + /// + public virtual ICollection Subscribers { get; set; } + + public override string ToString() => Title; } \ No newline at end of file diff --git a/modules/RichSiteSummary/Data/Post.cs b/modules/RichSiteSummary/Data/Post.cs index 5589072..8657e96 100644 --- a/modules/RichSiteSummary/Data/Post.cs +++ b/modules/RichSiteSummary/Data/Post.cs @@ -9,99 +9,98 @@ using System.Text; using Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.RichSiteSummary.Data +namespace Kruzya.TelegramBot.RichSiteSummary.Data; + +/// +/// Represents a post from feed in database. +/// +public class Post : RepresentableEntity { - /// - /// Represents a post from feed in database. - /// - public class Post : RepresentableEntity + #region RepresentableEntity + protected override ParseMode DefaultRepresentation { - #region RepresentableEntity - protected override ParseMode DefaultRepresentation + get => ParseMode.Html; + } + + protected override string ViewableText + { + get => Title; + } + + protected override string ViewableUrl + { + get => UtmUtility.ApplyParameters(new Uri(new(Feed.Url), Url), new UtmParameters() { - get => ParseMode.Html; - } + Source = "telegram", + Type = "rss_post", + Campaign = "subscriber", + Content = PostId.ToString() + }); + } - protected override string ViewableText + #endregion + + /// + /// The unique post id. + /// + [Key] + public Guid PostId { get; set; } + + /// + /// The feed unique identifier from where this post is fetched. + /// + public Guid FeedId { get; set; } + + /// + /// The feed from where this post is fetched. + /// + [Required] + public virtual Feed Feed { get; set; } + + /// + /// The post title. + /// + [MaxLength(256)] + [Required] + public string Title { get; set; } + + /// + /// The URI where this post is located in web. + /// + [Required] + [MaxLength(256)] + public string Url { get; set; } + + /// + /// DateTime when post is created in RSS feed. + /// + [Required] + public DateTime PostedAt { get; set; } + + /// + /// DateTime when post is fetched/updated in database. + /// + [Required] + public DateTime ReceivedAt { get; set; } + + public Post() + { + PostId = new Guid(); + ReceivedAt = DateTime.Now; + } + + public string MessageText + { + get { - get => Title; - } - - protected override string ViewableUrl - { - get => UtmUtility.ApplyParameters(new Uri(new(Feed.Url), Url), new UtmParameters() - { - Source = "telegram", - Type = "rss_post", - Campaign = "subscriber", - Content = PostId.ToString() - }); - } - - #endregion - - /// - /// The unique post id. - /// - [Key] - public Guid PostId { get; set; } - - /// - /// The feed unique identifier from where this post is fetched. - /// - public Guid FeedId { get; set; } - - /// - /// The feed from where this post is fetched. - /// - [Required] - public virtual Feed Feed { get; set; } - - /// - /// The post title. - /// - [MaxLength(256)] - [Required] - public string Title { get; set; } - - /// - /// The URI where this post is located in web. - /// - [Required] - [MaxLength(256)] - public string Url { get; set; } - - /// - /// DateTime when post is created in RSS feed. - /// - [Required] - public DateTime PostedAt { get; set; } - - /// - /// DateTime when post is fetched/updated in database. - /// - [Required] - public DateTime ReceivedAt { get; set; } - - public Post() - { - PostId = new Guid(); - ReceivedAt = DateTime.Now; - } - - public string MessageText - { - get - { - var message = new StringBuilder(); - message.Append($"{Representation("🗞")} New on {Feed.Representation(ParseMode.Html)}\n"); - message.Append("\n"); - message.Append(Title); - message.Append("\n"); - message.Append(Representation("Open in browser")); + var message = new StringBuilder(); + message.Append($"{Representation("🗞")} New on {Feed.Representation(ParseMode.Html)}\n"); + message.Append("\n"); + message.Append(Title); + message.Append("\n"); + message.Append(Representation("Open in browser")); - return message.ToString(); - } + return message.ToString(); } } -} +} \ No newline at end of file diff --git a/modules/RichSiteSummary/Data/RepresentableEntity.cs b/modules/RichSiteSummary/Data/RepresentableEntity.cs index 973159d..b986a31 100644 --- a/modules/RichSiteSummary/Data/RepresentableEntity.cs +++ b/modules/RichSiteSummary/Data/RepresentableEntity.cs @@ -7,55 +7,54 @@ using System; using System.Web; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.RichSiteSummary.Data +namespace Kruzya.TelegramBot.RichSiteSummary.Data; + +/// +/// Implements the basic logic for rendering Database Entities in messages. +/// +public abstract class RepresentableEntity { - /// - /// Implements the basic logic for rendering Database Entities in messages. - /// - public abstract class RepresentableEntity + protected virtual string ViewableText { get; } + protected virtual string ViewableUrl { get; } + protected virtual ParseMode DefaultRepresentation { get; } + + public override string ToString() => Representation(); + + public string Representation(string text) => Representation(DefaultRepresentation, text); + + public string Representation() => Representation(DefaultRepresentation); + + public string Representation(ParseMode parseMode) => Representation(parseMode, ViewableText); + + public string Representation(ParseMode parseMode, string text) { - protected virtual string ViewableText { get; } - protected virtual string ViewableUrl { get; } - protected virtual ParseMode DefaultRepresentation { get; } - - public override string ToString() => Representation(); - - public string Representation(string text) => Representation(DefaultRepresentation, text); - - public string Representation() => Representation(DefaultRepresentation); - - public string Representation(ParseMode parseMode) => Representation(parseMode, ViewableText); - - public string Representation(ParseMode parseMode, string text) + string content = String.Empty; + switch (parseMode) { - string content = String.Empty; - switch (parseMode) - { - case ParseMode.Html: - content = HtmlRepresentation(text); - break; + case ParseMode.Html: + content = HtmlRepresentation(text); + break; - case ParseMode.Markdown: - case ParseMode.MarkdownV2: - throw new NotImplementedException(); - } - - return content; + case ParseMode.Markdown: + case ParseMode.MarkdownV2: + throw new NotImplementedException(); } - #region Representations - - public string RawRepresentation(string text) - => text; - - public string HtmlRepresentation(string text) - { - var escapedText = HttpUtility.HtmlEncode(text); - var escapedUrl = HttpUtility.HtmlAttributeEncode(ViewableUrl); - - return $"{escapedText}"; - } - - #endregion + return content; } + + #region Representations + + public string RawRepresentation(string text) + => text; + + public string HtmlRepresentation(string text) + { + var escapedText = HttpUtility.HtmlEncode(text); + var escapedUrl = HttpUtility.HtmlAttributeEncode(ViewableUrl); + + return $"{escapedText}"; + } + + #endregion } \ No newline at end of file diff --git a/modules/RichSiteSummary/Data/RichSiteSummaryContext.cs b/modules/RichSiteSummary/Data/RichSiteSummaryContext.cs index c837836..1dc651c 100644 --- a/modules/RichSiteSummary/Data/RichSiteSummaryContext.cs +++ b/modules/RichSiteSummary/Data/RichSiteSummaryContext.cs @@ -1,58 +1,57 @@ using Kruzya.TelegramBot.Core.EF; using Microsoft.EntityFrameworkCore; -namespace Kruzya.TelegramBot.RichSiteSummary.Data +namespace Kruzya.TelegramBot.RichSiteSummary.Data; + +public class RichSiteSummaryContext : AutoSaveDbContext { - public class RichSiteSummaryContext : AutoSaveDbContext + /// + /// Repository with all feeds in database. + /// + public DbSet Feeds { get; set; } + + /// + /// Repository with all posts in database. + /// + public DbSet Posts { get; set; } + + /// + /// Repository with all subscriptions in database. + /// + public DbSet Subscriptions { get; set; } + + /// + /// Ensures the database is created and calls parent constructor. + /// + /// + public RichSiteSummaryContext(DbContextOptions options) : base(options) { - /// - /// Repository with all feeds in database. - /// - public DbSet Feeds { get; set; } - - /// - /// Repository with all posts in database. - /// - public DbSet Posts { get; set; } - - /// - /// Repository with all subscriptions in database. - /// - public DbSet Subscriptions { get; set; } + Database.EnsureCreated(); + } - /// - /// Ensures the database is created and calls parent constructor. - /// - /// - public RichSiteSummaryContext(DbContextOptions options) : base(options) - { - Database.EnsureCreated(); - } - - /// - /// Setup the additional unique keys. - /// - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - // Setup unique keys for Feed entity. - modelBuilder.Entity().HasIndex(feed => feed.Url).IsUnique(); - modelBuilder.Entity().HasIndex(feed => feed.HomePage).IsUnique(); + /// + /// Setup the additional unique keys. + /// + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Setup unique keys for Feed entity. + modelBuilder.Entity().HasIndex(feed => feed.Url).IsUnique(); + modelBuilder.Entity().HasIndex(feed => feed.HomePage).IsUnique(); - // Setup unique keys for Post entity. - modelBuilder.Entity() - .HasOne(p => p.Feed) - .WithMany(f => f.Posts) - .HasForeignKey(p => p.FeedId); + // Setup unique keys for Post entity. + modelBuilder.Entity() + .HasOne(p => p.Feed) + .WithMany(f => f.Posts) + .HasForeignKey(p => p.FeedId); - modelBuilder.Entity().HasIndex(post => - new {post.FeedId, post.Url}).IsUnique(); + modelBuilder.Entity().HasIndex(post => + new {post.FeedId, post.Url}).IsUnique(); - // Setup foreign keys for Subscriber entity. - modelBuilder.Entity() - .HasOne(s => s.Feed) - .WithMany(f => f.Subscribers) - .HasForeignKey(s => s.FeedId); - } + // Setup foreign keys for Subscriber entity. + modelBuilder.Entity() + .HasOne(s => s.Feed) + .WithMany(f => f.Subscribers) + .HasForeignKey(s => s.FeedId); } } \ No newline at end of file diff --git a/modules/RichSiteSummary/Data/Subscriber.cs b/modules/RichSiteSummary/Data/Subscriber.cs index 9db6bdd..b88ac0b 100644 --- a/modules/RichSiteSummary/Data/Subscriber.cs +++ b/modules/RichSiteSummary/Data/Subscriber.cs @@ -6,51 +6,50 @@ using System; using System.ComponentModel.DataAnnotations; -namespace Kruzya.TelegramBot.RichSiteSummary.Data +namespace Kruzya.TelegramBot.RichSiteSummary.Data; + +/// +/// Represents a subscription in database. +/// +public class Subscriber { /// - /// Represents a subscription in database. + /// The unique Subscription identifier. /// - public class Subscriber + [Key] + public Guid SubscriptionId { get; set; } + + /// + /// The Telegram subscriber id. + /// + [Required] + public Int64 SubscriberId { get; set; } + + /// + /// Unique feed identifier related with this Subscription. Identifies what user is read. + /// + public Guid FeedId { get; set; } + + /// + /// Feed related with this Subscription. Identifies what user is read. + /// + [Required] + public virtual Feed Feed { get; set; } + + /// + /// When user is subscribed on this feed. + /// + [Required] + public DateTime SubscribedAt { get; set; } + + public Subscriber() { - /// - /// The unique Subscription identifier. - /// - [Key] - public Guid SubscriptionId { get; set; } - - /// - /// The Telegram subscriber id. - /// - [Required] - public Int64 SubscriberId { get; set; } - - /// - /// Unique feed identifier related with this Subscription. Identifies what user is read. - /// - public Guid FeedId { get; set; } - - /// - /// Feed related with this Subscription. Identifies what user is read. - /// - [Required] - public virtual Feed Feed { get; set; } + SubscriptionId = new Guid(); + SubscribedAt = DateTime.Now; + } - /// - /// When user is subscribed on this feed. - /// - [Required] - public DateTime SubscribedAt { get; set; } - - public Subscriber() - { - SubscriptionId = new Guid(); - SubscribedAt = DateTime.Now; - } - - public Subscriber(Feed feed) : base() - { - Feed = feed; - } + public Subscriber(Feed feed) : base() + { + Feed = feed; } } \ No newline at end of file diff --git a/modules/RichSiteSummary/Handler/AbstractHandler.cs b/modules/RichSiteSummary/Handler/AbstractHandler.cs index 71ee2f7..9477203 100644 --- a/modules/RichSiteSummary/Handler/AbstractHandler.cs +++ b/modules/RichSiteSummary/Handler/AbstractHandler.cs @@ -1,15 +1,14 @@ using BotFramework; using Kruzya.TelegramBot.RichSiteSummary.Data; -namespace Kruzya.TelegramBot.RichSiteSummary.Handler -{ - public abstract class AbstractHandler : BotEventHandler - { - protected readonly RichSiteSummaryContext _dbContext; +namespace Kruzya.TelegramBot.RichSiteSummary.Handler; - protected AbstractHandler(RichSiteSummaryContext dbContext) - { - _dbContext = dbContext; - } +public abstract class AbstractHandler : BotEventHandler +{ + protected readonly RichSiteSummaryContext _dbContext; + + protected AbstractHandler(RichSiteSummaryContext dbContext) + { + _dbContext = dbContext; } } \ No newline at end of file diff --git a/modules/RichSiteSummary/Handler/FeedList.cs b/modules/RichSiteSummary/Handler/FeedList.cs index 70dc9f6..5edd191 100644 --- a/modules/RichSiteSummary/Handler/FeedList.cs +++ b/modules/RichSiteSummary/Handler/FeedList.cs @@ -13,299 +13,298 @@ using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.ReplyMarkups; -namespace Kruzya.TelegramBot.RichSiteSummary.Handler +namespace Kruzya.TelegramBot.RichSiteSummary.Handler; + +public class FeedList : AbstractHandler { - public class FeedList : AbstractHandler + public FeedList(RichSiteSummaryContext dbContext) : base(dbContext) { - public FeedList(RichSiteSummaryContext dbContext) : base(dbContext) - { - } + } - [ParametrizedCommand("rss_show")] - public async Task ShowFeed(Feed feed) => await ShowFeed(feed, Chat, From); + [ParametrizedCommand("rss_show")] + public async Task ShowFeed(Feed feed) => await ShowFeed(feed, Chat, From); - public async Task ShowFeed(Guid feedId, Chat chat, User from) + public async Task ShowFeed(Guid feedId, Chat chat, User from) + { + var feed = await _dbContext.FindAsync(feedId); + if (feed == null) { - var feed = await _dbContext.FindAsync(feedId); - if (feed == null) - { - return; - } - - await ShowFeed(feed, chat, from); + return; } - public async Task ShowFeed(Feed feed, Chat chat, User from, Message message = null) + await ShowFeed(feed, chat, from); + } + + public async Task ShowFeed(Feed feed, Chat chat, User from, Message message = null) + { + var textBuilder = new StringBuilder(); + textBuilder.Append(feed.Representation(ParseMode.Html)); + textBuilder.Append("\n"); + textBuilder.Append($"Posts at this moment: {feed.Posts.Count}\n"); + textBuilder.Append($"RSS feed: {feed.Url}\n"); + textBuilder.Append($"Home page: {feed.HomePage}\n"); + textBuilder.Append($"Last synchronization: {feed.UpdatedAt}"); + + var text = textBuilder.ToString(); + + var buttons = new List(); + var callbackData = $"feed|{from.Id}|do|{feed.FeedId}"; + if (await _dbContext.Subscriptions.HasSubscription(chat, feed)) { - var textBuilder = new StringBuilder(); - textBuilder.Append(feed.Representation(ParseMode.Html)); - textBuilder.Append("\n"); - textBuilder.Append($"Posts at this moment: {feed.Posts.Count}\n"); - textBuilder.Append($"RSS feed: {feed.Url}\n"); - textBuilder.Append($"Home page: {feed.HomePage}\n"); - textBuilder.Append($"Last synchronization: {feed.UpdatedAt}"); - - var text = textBuilder.ToString(); - - var buttons = new List(); - var callbackData = $"feed|{from.Id}|do|{feed.FeedId}"; - if (await _dbContext.Subscriptions.HasSubscription(chat, feed)) + buttons.Add(new InlineKeyboardButton("Unsubscribe") { - buttons.Add(new InlineKeyboardButton("Unsubscribe") - { - CallbackData = callbackData, - }); - } - else - { - buttons.Add(new InlineKeyboardButton("Subscribe") - { - CallbackData = callbackData, - }); - } - buttons.Add(new InlineKeyboardButton("Last 10 posts") - { - CallbackData = $"feed|{from.Id}|last|{feed.FeedId}", + CallbackData = callbackData, }); - - var inlineButtons = new InlineKeyboardMarkup(buttons); - if (message == null) - { - await Bot.SendTextMessageAsync(new ChatId(chat.Id), text, 0, ParseMode.Html, null, true, true, false, 0, replyMarkup: inlineButtons); - } - else - { - await Bot.EditMessageTextAsync(chat, message.MessageId, text, ParseMode.Html, null, true, replyMarkup: inlineButtons); - } } + else + { + buttons.Add(new InlineKeyboardButton("Subscribe") + { + CallbackData = callbackData, + }); + } + buttons.Add(new InlineKeyboardButton("Last 10 posts") + { + CallbackData = $"feed|{from.Id}|last|{feed.FeedId}", + }); + + var inlineButtons = new InlineKeyboardMarkup(buttons); + if (message == null) + { + await Bot.SendTextMessageAsync(new ChatId(chat.Id), text, 0, ParseMode.Html, null, true, true, false, 0, replyMarkup: inlineButtons); + } + else + { + await Bot.EditMessageTextAsync(chat, message.MessageId, text, ParseMode.Html, null, true, replyMarkup: inlineButtons); + } + } - [ParametrizedCommand("rss_list")] - public async Task List() => await GenerateMenu(); + [ParametrizedCommand("rss_list")] + public async Task List() => await GenerateMenu(); - [ParametrizedCommand("rss_search")] - public async Task Search(string query) + [ParametrizedCommand("rss_search")] + public async Task Search(string query) + { + if (!(await GenerateMenu(query))) { - if (!(await GenerateMenu(query))) - { - await Bot.SendTextMessageAsync(Chat, $"No one feed match by search pattern ({query}).", 0, ParseMode.Html, null, true); - } + await Bot.SendTextMessageAsync(Chat, $"No one feed match by search pattern ({query}).", 0, ParseMode.Html, null, true); } + } - [Command("rss_subscriptions")] - public async Task Subscriptions() + [Command("rss_subscriptions")] + public async Task Subscriptions() + { + if (!(await GenerateMenu(":sub"))) { - if (!(await GenerateMenu(":sub"))) - { - await Bot.SendTextMessageAsync(Chat, - "There are no one subscription. Add new with /feed_list or /feed_search."); - } + await Bot.SendTextMessageAsync(Chat, + "There are no one subscription. Add new with /feed_list or /feed_search."); } + } - #region Message generator + #region Message generator - protected async Task GenerateMenu() => - await GenerateMenu(string.Empty); + protected async Task GenerateMenu() => + await GenerateMenu(string.Empty); - protected async Task GenerateMenu(string query) => - await GenerateMenu(query, 0); + protected async Task GenerateMenu(string query) => + await GenerateMenu(query, 0); - protected async Task GenerateMenu(string query, UInt16 page, Message editableMessage = null) + protected async Task GenerateMenu(string query, UInt16 page, Message editableMessage = null) + { + var chat = editableMessage != null ? (editableMessage.Chat) : Chat; + var elementsOnPage = 9; + var startIndex = page * elementsOnPage; + var dbQuery = _dbContext.Feeds.AsQueryable() + .Where(f => !f.IsPrivate); + + if (!string.IsNullOrWhiteSpace(query)) { - var chat = editableMessage != null ? (editableMessage.Chat) : Chat; - var elementsOnPage = 9; - var startIndex = page * elementsOnPage; - var dbQuery = _dbContext.Feeds.AsQueryable() - .Where(f => !f.IsPrivate); - - if (!string.IsNullOrWhiteSpace(query)) + var searchQuery = query.ToLower(); + if (query.EndsWith(":sub")) { - var searchQuery = query.ToLower(); - if (query.EndsWith(":sub")) - { - var chatList = await _dbContext.Subscriptions.AllSubscriptions(chat).Select(chat => chat.Feed.FeedId).ToArrayAsync(); - dbQuery = dbQuery.Where(sub => chatList.Contains(sub.FeedId)); + var chatList = await _dbContext.Subscriptions.AllSubscriptions(chat).Select(chat => chat.Feed.FeedId).ToArrayAsync(); + dbQuery = dbQuery.Where(sub => chatList.Contains(sub.FeedId)); - searchQuery = searchQuery.Remove(searchQuery.Length - 4, 4); - } + searchQuery = searchQuery.Remove(searchQuery.Length - 4, 4); + } - dbQuery = dbQuery.Where(feed => feed.Title.ToLower().Contains(searchQuery) || feed.Url.ToLower().Contains(searchQuery) || feed.HomePage.ToLower().Contains(searchQuery)); - } + dbQuery = dbQuery.Where(feed => feed.Title.ToLower().Contains(searchQuery) || feed.Url.ToLower().Contains(searchQuery) || feed.HomePage.ToLower().Contains(searchQuery)); + } - var totalCount = await dbQuery.CountAsync(); - if (totalCount == 0) - { - return false; - } - - if (totalCount == 1) - { - await ShowFeed((await dbQuery.FirstOrDefaultAsync()), Chat, From); - return true; - } - - var result = await dbQuery.Take(elementsOnPage * (page + 1)).Skip(startIndex) - .ToArrayAsync(); - - var backButton = (page > 0); - var nextButton = totalCount >= (startIndex + elementsOnPage); - - var buttons = new List>(); - List row; - foreach (var feed in result) - { - row = new List(); - row.Add(new InlineKeyboardButton(feed.Title) - { - CallbackData = $"feed|{From.Id}|show|{feed.FeedId.ToString()}", - }); - - buttons.Add(row); - } - - if (backButton || nextButton) - { - row = new List(); - if (backButton) - { - row.Add(new InlineKeyboardButton("⬅️") - { - CallbackData = $"feed|{From.Id}|page|{query}|{page - 1}", - }); - } - - row.Add(new InlineKeyboardButton($"{page + 1}/{(totalCount / elementsOnPage) + 1}") - { - CallbackData = $"feed|{From.Id}|donothing", - }); - - if (nextButton) - { - row.Add(new InlineKeyboardButton("➡️") - { - CallbackData = $"feed|{From.Id}|page|{query}|{page + 1}", - }); - } - - buttons.Add(row); - } - - if (editableMessage == null) - { - await Bot.SendTextMessageAsync(Chat, "Select the feed for viewing details", 0, null, - null, true, true, false, 0, replyMarkup: new InlineKeyboardMarkup(buttons)); - } - else - { - await Bot.EditMessageTextAsync(editableMessage.Chat, editableMessage.MessageId, "Select the feed for viewing details", null, null, false, - new InlineKeyboardMarkup(buttons)); - } + var totalCount = await dbQuery.CountAsync(); + if (totalCount == 0) + { + return false; + } + if (totalCount == 1) + { + await ShowFeed((await dbQuery.FirstOrDefaultAsync()), Chat, From); return true; } - - #endregion - - #region Message handler - - [Update(UpdateFlag.CallbackQuery)] - public async Task HandleAction() - { - if (!RawUpdate.CallbackQuery.Data.StartsWith("feed|")) - { - return; - } - - var data = RawUpdate.CallbackQuery.Data.Split('|'); - var fromId = Int64.Parse(data[1]); - if (fromId != From.Id) - { - await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id, - "You can't perform this action: command called by another user.", true); - return; - } - - // Check action. - switch (data[2]) - { - case "page": - await GenerateMenu(data[3], UInt16.Parse(data[4]), RawUpdate.CallbackQuery.Message); - break; - - case "show": - await ShowFeed(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From); - break; - - case "do": - if (RawUpdate.CallbackQuery.From.Id != RawUpdate.CallbackQuery.Message.Chat.Id) - { - var userStatus = (await Bot.GetChatMemberAsync(RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From.Id)).Status; - var allowedUserStatuses = new ChatMemberStatus[] - {ChatMemberStatus.Administrator, ChatMemberStatus.Creator}; - - if (!allowedUserStatuses.Contains(userStatus)) - { - await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id, - "You can't perform this action: you don't have administrator permissions.", true); - return; - } - } - - await ChangeFeedSubscription(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From, RawUpdate.CallbackQuery.Message); - break; - - case "last": - var feed = await _dbContext.FindAsync(Guid.Parse(data[3])); - var posts = feed.Posts.OrderByDescending(p => p.ReceivedAt).Take(10).ToList(); - - var messageTextBuilder = new StringBuilder(); - messageTextBuilder.Append($"Last 10 posts from {feed.Representation(ParseMode.Html)}\n"); - messageTextBuilder.Append("\n"); - - var idx = 1; - foreach (var post in posts) - { - messageTextBuilder.Append($"{idx}. {post}\n"); - idx++; - } - - await Bot.SendTextMessageAsync(RawUpdate.CallbackQuery.Message.Chat, messageTextBuilder.ToString(), - 0, ParseMode.Html, null, true); - break; - } - await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id); - } + var result = await dbQuery.Take(elementsOnPage * (page + 1)).Skip(startIndex) + .ToArrayAsync(); + + var backButton = (page > 0); + var nextButton = totalCount >= (startIndex + elementsOnPage); - protected async Task ChangeFeedSubscription(Guid feedId, Chat chat, User from, Message message) + var buttons = new List>(); + List row; + foreach (var feed in result) { - var feed = await _dbContext.FindAsync(feedId); - if (feed == null) + row = new List(); + row.Add(new InlineKeyboardButton(feed.Title) { - return; - } + CallbackData = $"feed|{From.Id}|show|{feed.FeedId.ToString()}", + }); - var subscriptionState = await _dbContext.Subscriptions.HasSubscription(chat, feed); - if (subscriptionState) - { - var subId = await _dbContext.Subscriptions - .Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id) - .Select(subscriber => subscriber.SubscriptionId).FirstAsync(); - - _dbContext.Remove(_dbContext.Find(subId)); - } - else - { - var subscription = _dbContext.Subscriptions.Create(); - subscription.Feed = feed; - subscription.SubscriberId = chat.Id; - - _dbContext.Add(subscription); - } - - await _dbContext.SaveChangesAsync(); - await ShowFeed(feed, chat, from, message); + buttons.Add(row); } - - #endregion + + if (backButton || nextButton) + { + row = new List(); + if (backButton) + { + row.Add(new InlineKeyboardButton("⬅️") + { + CallbackData = $"feed|{From.Id}|page|{query}|{page - 1}", + }); + } + + row.Add(new InlineKeyboardButton($"{page + 1}/{(totalCount / elementsOnPage) + 1}") + { + CallbackData = $"feed|{From.Id}|donothing", + }); + + if (nextButton) + { + row.Add(new InlineKeyboardButton("➡️") + { + CallbackData = $"feed|{From.Id}|page|{query}|{page + 1}", + }); + } + + buttons.Add(row); + } + + if (editableMessage == null) + { + await Bot.SendTextMessageAsync(Chat, "Select the feed for viewing details", 0, null, + null, true, true, false, 0, replyMarkup: new InlineKeyboardMarkup(buttons)); + } + else + { + await Bot.EditMessageTextAsync(editableMessage.Chat, editableMessage.MessageId, "Select the feed for viewing details", null, null, false, + new InlineKeyboardMarkup(buttons)); + } + + return true; } + + #endregion + + #region Message handler + + [Update(UpdateFlag.CallbackQuery)] + public async Task HandleAction() + { + if (!RawUpdate.CallbackQuery.Data.StartsWith("feed|")) + { + return; + } + + var data = RawUpdate.CallbackQuery.Data.Split('|'); + var fromId = Int64.Parse(data[1]); + if (fromId != From.Id) + { + await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id, + "You can't perform this action: command called by another user.", true); + return; + } + + // Check action. + switch (data[2]) + { + case "page": + await GenerateMenu(data[3], UInt16.Parse(data[4]), RawUpdate.CallbackQuery.Message); + break; + + case "show": + await ShowFeed(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From); + break; + + case "do": + if (RawUpdate.CallbackQuery.From.Id != RawUpdate.CallbackQuery.Message.Chat.Id) + { + var userStatus = (await Bot.GetChatMemberAsync(RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From.Id)).Status; + var allowedUserStatuses = new ChatMemberStatus[] + {ChatMemberStatus.Administrator, ChatMemberStatus.Creator}; + + if (!allowedUserStatuses.Contains(userStatus)) + { + await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id, + "You can't perform this action: you don't have administrator permissions.", true); + return; + } + } + + await ChangeFeedSubscription(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From, RawUpdate.CallbackQuery.Message); + break; + + case "last": + var feed = await _dbContext.FindAsync(Guid.Parse(data[3])); + var posts = feed.Posts.OrderByDescending(p => p.ReceivedAt).Take(10).ToList(); + + var messageTextBuilder = new StringBuilder(); + messageTextBuilder.Append($"Last 10 posts from {feed.Representation(ParseMode.Html)}\n"); + messageTextBuilder.Append("\n"); + + var idx = 1; + foreach (var post in posts) + { + messageTextBuilder.Append($"{idx}. {post}\n"); + idx++; + } + + await Bot.SendTextMessageAsync(RawUpdate.CallbackQuery.Message.Chat, messageTextBuilder.ToString(), + 0, ParseMode.Html, null, true); + break; + } + + await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id); + } + + protected async Task ChangeFeedSubscription(Guid feedId, Chat chat, User from, Message message) + { + var feed = await _dbContext.FindAsync(feedId); + if (feed == null) + { + return; + } + + var subscriptionState = await _dbContext.Subscriptions.HasSubscription(chat, feed); + if (subscriptionState) + { + var subId = await _dbContext.Subscriptions + .Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id) + .Select(subscriber => subscriber.SubscriptionId).FirstAsync(); + + _dbContext.Remove(_dbContext.Find(subId)); + } + else + { + var subscription = _dbContext.Subscriptions.Create(); + subscription.Feed = feed; + subscription.SubscriberId = chat.Id; + + _dbContext.Add(subscription); + } + + await _dbContext.SaveChangesAsync(); + await ShowFeed(feed, chat, from, message); + } + + #endregion } \ No newline at end of file diff --git a/modules/RichSiteSummary/Handler/StatsHandler.cs b/modules/RichSiteSummary/Handler/StatsHandler.cs index 4fd7183..80d0f05 100644 --- a/modules/RichSiteSummary/Handler/StatsHandler.cs +++ b/modules/RichSiteSummary/Handler/StatsHandler.cs @@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore; using Telegram.Bot; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.RichSiteSummary.Handler -{ - public class StatsHandler : AbstractHandler - { - public StatsHandler(RichSiteSummaryContext dbContext) : base(dbContext) - { - } - - [Command("rss_stats")] - public async Task Execute() - { - var feedCount = await _dbContext.Feeds.CountAsync(); - var postsCount = await _dbContext.Posts.CountAsync(); - var subscriptionsCount = await _dbContext.Subscriptions.CountAsync(); - var uniqueSubscribers = - await _dbContext.Subscriptions.Select(sub => sub.SubscriberId).Distinct().CountAsync(); - - var textMessage = new StringBuilder(); - textMessage.Append("ℹ️ In database on this moment:\n"); - textMessage.Append($"- registered {feedCount} feeds\n"); - textMessage.Append($"- saved {postsCount} posts for prevent re-sending\n"); - textMessage.Append( - $"- exists {subscriptionsCount} subscriptions on feeds (unique subscribers: {uniqueSubscribers})"); +namespace Kruzya.TelegramBot.RichSiteSummary.Handler; - await Bot.SendTextMessageAsync(Chat, textMessage.ToString(), parseMode: ParseMode.Html); - } +public class StatsHandler : AbstractHandler +{ + public StatsHandler(RichSiteSummaryContext dbContext) : base(dbContext) + { + } + + [Command("rss_stats")] + public async Task Execute() + { + var feedCount = await _dbContext.Feeds.CountAsync(); + var postsCount = await _dbContext.Posts.CountAsync(); + var subscriptionsCount = await _dbContext.Subscriptions.CountAsync(); + var uniqueSubscribers = + await _dbContext.Subscriptions.Select(sub => sub.SubscriberId).Distinct().CountAsync(); + + var textMessage = new StringBuilder(); + textMessage.Append("ℹ️ In database on this moment:\n"); + textMessage.Append($"- registered {feedCount} feeds\n"); + textMessage.Append($"- saved {postsCount} posts for prevent re-sending\n"); + textMessage.Append( + $"- exists {subscriptionsCount} subscriptions on feeds (unique subscribers: {uniqueSubscribers})"); + + await Bot.SendTextMessageAsync(Chat, textMessage.ToString(), parseMode: ParseMode.Html); } } \ No newline at end of file diff --git a/modules/RichSiteSummary/RichSiteSummary.cs b/modules/RichSiteSummary/RichSiteSummary.cs index 282e25f..fb9911c 100644 --- a/modules/RichSiteSummary/RichSiteSummary.cs +++ b/modules/RichSiteSummary/RichSiteSummary.cs @@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace Kruzya.TelegramBot.RichSiteSummary +namespace Kruzya.TelegramBot.RichSiteSummary; + +public class RichSiteSummary : Module { - public class RichSiteSummary : Module + public RichSiteSummary(Core.Core core) : base(core) { - public RichSiteSummary(Core.Core core) : base(core) - { - } + } - public override void ConfigureServices(IServiceCollection services) - { - var connectionString = Configuration.GetConnectionString("RichSiteSummary"); - services.AddDbContext(options => - options.UseLazyLoadingProxies() - .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); + public override void ConfigureServices(IServiceCollection services) + { + var connectionString = Configuration.GetConnectionString("RichSiteSummary"); + services.AddDbContext(options => + options.UseLazyLoadingProxies() + .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); - services.AddTelegramBotParameterParser>() - .AddTelegramBotParameterParser>() - .AddTelegramBotParameterParser>(); + services.AddTelegramBotParameterParser>() + .AddTelegramBotParameterParser>() + .AddTelegramBotParameterParser>(); - services.AddQueue() - .AddQueue(); + services.AddQueue() + .AddQueue(); - services.AddHostedService() - .AddHostedService() - .AddHostedService(); - } + services.AddHostedService() + .AddHostedService() + .AddHostedService(); } } \ No newline at end of file diff --git a/modules/RichSiteSummary/RichSiteSummaryRepositoryExtension.cs b/modules/RichSiteSummary/RichSiteSummaryRepositoryExtension.cs index ea1d539..95cf763 100644 --- a/modules/RichSiteSummary/RichSiteSummaryRepositoryExtension.cs +++ b/modules/RichSiteSummary/RichSiteSummaryRepositoryExtension.cs @@ -5,68 +5,67 @@ using Kruzya.TelegramBot.RichSiteSummary.Data; using Microsoft.EntityFrameworkCore; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.RichSiteSummary +namespace Kruzya.TelegramBot.RichSiteSummary; + +internal static class RichSiteSummaryRepositoryExtension { - internal static class RichSiteSummaryRepositoryExtension + #region Subscriber + + /// + /// Searchs the , who reads a . + /// + /// + /// + /// The array with all subscribers. + public static async Task ByFeed(this DbSet repository, Feed feed) { - #region Subscriber - - /// - /// Searchs the , who reads a . - /// - /// - /// - /// The array with all subscribers. - public static async Task ByFeed(this DbSet repository, Feed feed) - { - return await repository.Where(subscriber => subscriber.Feed == feed) - .ToArrayAsync(); - } - - public static IQueryable AllSubscriptions(this DbSet repository, Chat chat) - { - return repository.Where(entity => entity.SubscriberId == chat.Id); - } - - public static async Task HasSubscription(this DbSet repository, Chat chat, Feed feed) - { - return (await repository.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id) - .CountAsync()) != 0; - } - - #endregion - - #region Feed - - /// - /// Searchs the for fetching. - /// - /// - /// The period (in seconds) from last fetching. - /// Max results count - /// - public static async Task ForFetching(this DbSet repository, UInt16 period, UInt16 count) => - await repository.Where(feed => feed.UpdatedAt < (DateTime.Now - TimeSpan.FromSeconds(period))) - .OrderBy(feed => feed.UpdatedAt) - .Take(count) - .ToArrayAsync(); - - /// - /// Searchs the for fetching. - /// - /// - /// The period (in seconds) from last fetching. - /// - public static async Task ForFetching(this DbSet repository, UInt16 period) => - await repository.ForFetching(period, 25); - - #endregion - - #region Post - - public static async Task ByFeedAndUrl(this DbSet repository, string url, Feed feed = null) => - await repository.FirstOrDefaultAsync(post => post.Url == url && (feed == null || post.Feed == feed)); - - #endregion + return await repository.Where(subscriber => subscriber.Feed == feed) + .ToArrayAsync(); } + + public static IQueryable AllSubscriptions(this DbSet repository, Chat chat) + { + return repository.Where(entity => entity.SubscriberId == chat.Id); + } + + public static async Task HasSubscription(this DbSet repository, Chat chat, Feed feed) + { + return (await repository.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id) + .CountAsync()) != 0; + } + + #endregion + + #region Feed + + /// + /// Searchs the for fetching. + /// + /// + /// The period (in seconds) from last fetching. + /// Max results count + /// + public static async Task ForFetching(this DbSet repository, UInt16 period, UInt16 count) => + await repository.Where(feed => feed.UpdatedAt < (DateTime.Now - TimeSpan.FromSeconds(period))) + .OrderBy(feed => feed.UpdatedAt) + .Take(count) + .ToArrayAsync(); + + /// + /// Searchs the for fetching. + /// + /// + /// The period (in seconds) from last fetching. + /// + public static async Task ForFetching(this DbSet repository, UInt16 period) => + await repository.ForFetching(period, 25); + + #endregion + + #region Post + + public static async Task ByFeedAndUrl(this DbSet repository, string url, Feed feed = null) => + await repository.FirstOrDefaultAsync(post => post.Url == url && (feed == null || post.Feed == feed)); + + #endregion } \ No newline at end of file diff --git a/modules/RichSiteSummary/Service/MessageSender.cs b/modules/RichSiteSummary/Service/MessageSender.cs index 6c7d82f..8e1cd2c 100644 --- a/modules/RichSiteSummary/Service/MessageSender.cs +++ b/modules/RichSiteSummary/Service/MessageSender.cs @@ -9,81 +9,80 @@ using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.RichSiteSummary.Service +namespace Kruzya.TelegramBot.RichSiteSummary.Service; + +public class MessageSender : AbstractTimedHostedService { - public class MessageSender : AbstractTimedHostedService + protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1); + + protected readonly ConcurrentQueue Queue; + protected readonly ConcurrentQueue UnsubscribeQueue; + + public MessageSender(ILogger logger, IBotInstance bot, ConcurrentQueue userMessageQueue, ConcurrentQueue userUnsubscribeQueue) : base(logger, bot) { - protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1); - - protected readonly ConcurrentQueue Queue; - protected readonly ConcurrentQueue UnsubscribeQueue; - - public MessageSender(ILogger logger, IBotInstance bot, ConcurrentQueue userMessageQueue, ConcurrentQueue userUnsubscribeQueue) : base(logger, bot) + UnsubscribeQueue = userUnsubscribeQueue; + Queue = userMessageQueue; + } + + protected override async Task OnRun() + { + if (Queue.IsEmpty || !Queue.TryDequeue(out var message)) { - UnsubscribeQueue = userUnsubscribeQueue; - Queue = userMessageQueue; + return; } - protected override async Task OnRun() + _logger.LogDebug("Message for {ChatId} dequeued.", message.ChatId.Identifier); + try { - if (Queue.IsEmpty || !Queue.TryDequeue(out var message)) + if (string.IsNullOrWhiteSpace(message.ImageUrl)) { + await _bot.BotClient.SendTextMessageAsync(message.ChatId, message.Text, 0, message.ParseMode, null, + message.DisableWebPagePreview); + } + + else + { + await _bot.BotClient.SendPhotoAsync(message.ChatId, new InputFileUrl(message.ImageUrl), 0, message.Text, + message.ParseMode, null, false, message.DisableWebPagePreview); + } + } + catch (ApiRequestException e) + { + _logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message); + if (!e.Message.Contains("bot was blocked by user")) + { + ReEnqueue(message, e, message.ChatId); // looks like a network issue return; } - _logger.LogDebug("Message for {ChatId} dequeued.", message.ChatId.Identifier); - try + // User added bot to blacklist. + // Unsubscribe him. + UnsubscribeQueue.Enqueue(new UserUnsubscribe { - if (string.IsNullOrWhiteSpace(message.ImageUrl)) - { - await _bot.BotClient.SendTextMessageAsync(message.ChatId, message.Text, 0, message.ParseMode, null, - message.DisableWebPagePreview); - } - - else - { - await _bot.BotClient.SendPhotoAsync(message.ChatId, new InputFileUrl(message.ImageUrl), 0, message.Text, - message.ParseMode, null, false, message.DisableWebPagePreview); - } - } - catch (ApiRequestException e) - { - _logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message); - if (!e.Message.Contains("bot was blocked by user")) - { - ReEnqueue(message, e, message.ChatId); // looks like a network issue - return; - } - - // User added bot to blacklist. - // Unsubscribe him. - UnsubscribeQueue.Enqueue(new UserUnsubscribe - { - ChatId = message.ChatId - }); - } - catch (Exception e) - { - _logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message); - ReEnqueue(message, e); - } + ChatId = message.ChatId + }); } - - private void ReEnqueue(UserMessage message, Exception e = null, ChatId chatId = null) + catch (Exception e) { - if (e != null) - { - var eMessage = new StringBuilder(); - eMessage.Append(e.Message); - if (chatId != null) - { - eMessage.Append($" ({chatId.Identifier})"); - } - - _logger.LogError(eMessage.ToString()); - } - - Queue.Enqueue(message); + _logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message); + ReEnqueue(message, e); } } + + private void ReEnqueue(UserMessage message, Exception e = null, ChatId chatId = null) + { + if (e != null) + { + var eMessage = new StringBuilder(); + eMessage.Append(e.Message); + if (chatId != null) + { + eMessage.Append($" ({chatId.Identifier})"); + } + + _logger.LogError(eMessage.ToString()); + } + + Queue.Enqueue(message); + } } \ No newline at end of file diff --git a/modules/RichSiteSummary/Service/RssFetch.cs b/modules/RichSiteSummary/Service/RssFetch.cs index cd1832f..7e3118a 100644 --- a/modules/RichSiteSummary/Service/RssFetch.cs +++ b/modules/RichSiteSummary/Service/RssFetch.cs @@ -20,242 +20,241 @@ using Feed = Kruzya.TelegramBot.RichSiteSummary.Data.Feed; using CodeHollow.FeedReader.Feeds; -namespace Kruzya.TelegramBot.RichSiteSummary.Service +namespace Kruzya.TelegramBot.RichSiteSummary.Service; + +/// +/// Performs a RSS feed parsing. +/// Grabs the all feeds from database. +/// +/// TODO: move entity grabbing to another service. +/// +public class RssFetch : IHostedService, IDisposable { - /// - /// Performs a RSS feed parsing. - /// Grabs the all feeds from database. - /// - /// TODO: move entity grabbing to another service. - /// - public class RssFetch : IHostedService, IDisposable - { - private Timer _timer; + private Timer _timer; - private readonly ILogger _logger; - private readonly IServiceScopeFactory _scopeFactory; - private readonly ConcurrentQueue _queue; + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private readonly ConcurrentQueue _queue; - private static TimeSpan TimerPeriod => TimeSpan.FromSeconds(45); + private static TimeSpan TimerPeriod => TimeSpan.FromSeconds(45); - public RssFetch(ILogger logger, ConcurrentQueue queue, IServiceScopeFactory scopeFactory) + public RssFetch(ILogger logger, ConcurrentQueue queue, IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + _logger = logger; + _queue = queue; + } + + #region IHostedService + /// + /// Initializes the RSS fetcher timer. + /// + /// + /// + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("RSS fetcher service is starting."); + _timer = new Timer(DoFetch, null, TimeSpan.Zero, TimerPeriod); + + return Task.CompletedTask; + } + + /// + /// Stops the RSS fetcher timer. + /// + /// + /// + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("RSS fetcher service is stopping."); + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + #endregion + #region IDisposable + /// + /// Disposes the timer. + /// + public void Dispose() + { + _timer?.Dispose(); + } + #endregion + + /// + /// Performs the job of fetching RSS data. + /// + /// + private async void DoFetch(object state) + { + _timer.Change(Timeout.Infinite, 0); + _logger.LogDebug("RSS fetcher service is triggered."); + + try { - _scopeFactory = scopeFactory; - _logger = logger; - _queue = queue; - } + using var scope = _scopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var period = scope.ServiceProvider.GetRequiredService() + .GetValue("rssFetchPeriod"); - #region IHostedService - /// - /// Initializes the RSS fetcher timer. - /// - /// - /// - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("RSS fetcher service is starting."); - _timer = new Timer(DoFetch, null, TimeSpan.Zero, TimerPeriod); + var feeds = await dbContext.Feeds.ForFetching(period); + _logger.LogDebug("Received {count} feeds for fetching", new {count = feeds.Length}); - return Task.CompletedTask; - } - - /// - /// Stops the RSS fetcher timer. - /// - /// - /// - public Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("RSS fetcher service is stopping."); - _timer?.Change(Timeout.Infinite, 0); - - return Task.CompletedTask; - } - #endregion - #region IDisposable - /// - /// Disposes the timer. - /// - public void Dispose() - { - _timer?.Dispose(); - } - #endregion - - /// - /// Performs the job of fetching RSS data. - /// - /// - private async void DoFetch(object state) - { - _timer.Change(Timeout.Infinite, 0); - _logger.LogDebug("RSS fetcher service is triggered."); - - try + foreach (var feed in feeds) { - using var scope = _scopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var period = scope.ServiceProvider.GetRequiredService() - .GetValue("rssFetchPeriod"); + await ProcessFeed(feed, dbContext); - var feeds = await dbContext.Feeds.ForFetching(period); - _logger.LogDebug("Received {count} feeds for fetching", new {count = feeds.Length}); - - foreach (var feed in feeds) - { - await ProcessFeed(feed, dbContext); - - feed.UpdatedAt = DateTime.Now; - dbContext.Update(feed); - } - - await dbContext.SaveChangesAsync(); - } - finally - { - _timer.Change(TimerPeriod, TimerPeriod); + feed.UpdatedAt = DateTime.Now; + dbContext.Update(feed); } + + await dbContext.SaveChangesAsync(); } - - #region Feeds - - private async Task ProcessFeed(Feed feed, RichSiteSummaryContext dbContext) + finally { - var parsedFeed = await FetchFeed(feed); - if (parsedFeed == null) - { - return; - } + _timer.Change(TimerPeriod, TimerPeriod); + } + } + + #region Feeds + + private async Task ProcessFeed(Feed feed, RichSiteSummaryContext dbContext) + { + var parsedFeed = await FetchFeed(feed); + if (parsedFeed == null) + { + return; + } - Post feedPost; - var newPosts = new List(); - var postsImages = new Dictionary(); - foreach (var post in parsedFeed.Items) + Post feedPost; + var newPosts = new List(); + var postsImages = new Dictionary(); + foreach (var post in parsedFeed.Items) + { + feedPost = await dbContext.Posts.ByFeedAndUrl(post.Link, feed); + if (feedPost != null) { - feedPost = await dbContext.Posts.ByFeedAndUrl(post.Link, feed); - if (feedPost != null) - { - // skip. This post already exists. - continue; - } - - feedPost = dbContext.Posts.Create(); - feedPost.Feed = feed; - feedPost.Title = post.Title; - feedPost.Url = post.Link; - feedPost.PostedAt = post.PublishingDate.GetValueOrDefault(DateTime.Now); - - postsImages.Add(feedPost, await FetchImageUrl(post)); - newPosts.Add(feedPost); + // skip. This post already exists. + continue; } - if (newPosts.Count > 0) - { - var subscribers = await dbContext.Subscriptions.ByFeed(feed); - foreach (var post in newPosts) - { - var text = post.MessageText; - foreach (var subscriber in subscribers) - { - var message = new UserMessage - { - ChatId = new ChatId(subscriber.SubscriberId), DisableWebPagePreview = true, - ParseMode = ParseMode.Html, Text = text, ImageUrl = postsImages.GetValueOrDefault(post) - }; + feedPost = dbContext.Posts.Create(); + feedPost.Feed = feed; + feedPost.Title = post.Title; + feedPost.Url = post.Link; + feedPost.PostedAt = post.PublishingDate.GetValueOrDefault(DateTime.Now); - _queue.Enqueue(message); - _logger.LogDebug("Queued message for {SubscriberId}", subscriber.SubscriberId); - } + postsImages.Add(feedPost, await FetchImageUrl(post)); + newPosts.Add(feedPost); + } + + if (newPosts.Count > 0) + { + var subscribers = await dbContext.Subscriptions.ByFeed(feed); + foreach (var post in newPosts) + { + var text = post.MessageText; + foreach (var subscriber in subscribers) + { + var message = new UserMessage + { + ChatId = new ChatId(subscriber.SubscriberId), DisableWebPagePreview = true, + ParseMode = ParseMode.Html, Text = text, ImageUrl = postsImages.GetValueOrDefault(post) + }; + + _queue.Enqueue(message); + _logger.LogDebug("Queued message for {SubscriberId}", subscriber.SubscriberId); } } } + } - private async Task FetchFeed(Feed feed) + private async Task FetchFeed(Feed feed) + { + try + { + return await FeedReader.ReadAsync(feed.Url); + } + catch (Exception e) + { + _logger.LogError("Feed {feed} can't be fetched: {error}", feed, e.Message); + return null; + } + } + + #endregion + + #region Image parsing + + private async Task FetchImageUrl(FeedItem item) + { + var feedItem = item.SpecificItem; + if (feedItem is Rss20FeedItem rss20FeedItem) + { + var enclosure = rss20FeedItem.Enclosure; + if (enclosure != null && IsValidImage(enclosure.MediaType)) + { + return enclosure.Url; + } + } + + return await FetchImageUrl(item.Description); + } + + private async Task FetchImageUrl(string description) + { + if (string.IsNullOrWhiteSpace(description)) + { + return ""; + } + + using var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true }; + using var httpClient = new HttpClient(httpClientHandler); + + var imgRegEx = new Regex(@"]+>"); + var srcRegEx = new Regex("src=\"([^\"]+)\""); + var matches = imgRegEx.Matches(description); + + var img = ""; + foreach (Match match in matches) { try { - return await FeedReader.ReadAsync(feed.Url); + var srcData = srcRegEx.Match(match.Value); + var content = srcData.Groups[1].Value; + + var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, content)); + if (response.StatusCode == HttpStatusCode.OK && + IsValidImage(response.Content.Headers.ContentType.MediaType)) + { + img = content; + break; + } } catch (Exception e) { - _logger.LogError("Feed {feed} can't be fetched: {error}", feed, e.Message); - return null; + _logger.LogError("Caused error when fetching image for RSS post: {error}", e.Message); } } - #endregion - - #region Image parsing - - private async Task FetchImageUrl(FeedItem item) - { - var feedItem = item.SpecificItem; - if (feedItem is Rss20FeedItem rss20FeedItem) - { - var enclosure = rss20FeedItem.Enclosure; - if (enclosure != null && IsValidImage(enclosure.MediaType)) - { - return enclosure.Url; - } - } - - return await FetchImageUrl(item.Description); - } - - private async Task FetchImageUrl(string description) - { - if (string.IsNullOrWhiteSpace(description)) - { - return ""; - } - - using var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true }; - using var httpClient = new HttpClient(httpClientHandler); - - var imgRegEx = new Regex(@"]+>"); - var srcRegEx = new Regex("src=\"([^\"]+)\""); - var matches = imgRegEx.Matches(description); - - var img = ""; - foreach (Match match in matches) - { - try - { - var srcData = srcRegEx.Match(match.Value); - var content = srcData.Groups[1].Value; - - var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, content)); - if (response.StatusCode == HttpStatusCode.OK && - IsValidImage(response.Content.Headers.ContentType.MediaType)) - { - img = content; - break; - } - } - catch (Exception e) - { - _logger.LogError("Caused error when fetching image for RSS post: {error}", e.Message); - } - } - - return img; - } - - /// - /// Checks if image is valid for Telegram Bot API. - /// - /// Content-type for received content - /// True, if Telegram maybe can "use" this image, false if not. - private static bool IsValidImage(string contentType) - { - return new[] - { - "image/jpeg", - "image/bmp", - "image/png" - }.Contains(contentType); - } - - #endregion + return img; } + + /// + /// Checks if image is valid for Telegram Bot API. + /// + /// Content-type for received content + /// True, if Telegram maybe can "use" this image, false if not. + private static bool IsValidImage(string contentType) + { + return new[] + { + "image/jpeg", + "image/bmp", + "image/png" + }.Contains(contentType); + } + + #endregion } \ No newline at end of file diff --git a/modules/RichSiteSummary/Service/Unsubscriber.cs b/modules/RichSiteSummary/Service/Unsubscriber.cs index 620d521..8ea127f 100644 --- a/modules/RichSiteSummary/Service/Unsubscriber.cs +++ b/modules/RichSiteSummary/Service/Unsubscriber.cs @@ -9,41 +9,40 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.RichSiteSummary.Service +namespace Kruzya.TelegramBot.RichSiteSummary.Service; + +public class Unsubscriber : AbstractTimedHostedService { - public class Unsubscriber : AbstractTimedHostedService + protected readonly ConcurrentQueue Queue; + protected readonly IServiceScopeFactory ScopeFactory; + + protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1); + + public Unsubscriber(ILogger logger, IBotInstance bot, ConcurrentQueue queue, IServiceScopeFactory scopeFactory) : base(logger, bot) { - protected readonly ConcurrentQueue Queue; - protected readonly IServiceScopeFactory ScopeFactory; + ScopeFactory = scopeFactory; + Queue = queue; + } - protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1); - - public Unsubscriber(ILogger logger, IBotInstance bot, ConcurrentQueue queue, IServiceScopeFactory scopeFactory) : base(logger, bot) + protected override async Task OnRun() + { + if (Queue.IsEmpty || !Queue.TryDequeue(out var user)) { - ScopeFactory = scopeFactory; - Queue = queue; + return; } - protected override async Task OnRun() - { - if (Queue.IsEmpty || !Queue.TryDequeue(out var user)) - { - return; - } + await UnsubscribeUser(user.ChatId); + } - await UnsubscribeUser(user.ChatId); - } + private async Task UnsubscribeUser(ChatId chatId) + { + using var scope = ScopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); - private async Task UnsubscribeUser(ChatId chatId) - { - using var scope = ScopeFactory.CreateScope(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - - var subscriptions = dbContext.Subscriptions; - subscriptions.RemoveRange( - subscriptions.Where(s => s.SubscriberId == chatId.Identifier) - ); - await dbContext.SaveChangesAsync(); - } + var subscriptions = dbContext.Subscriptions; + subscriptions.RemoveRange( + subscriptions.Where(s => s.SubscriberId == chatId.Identifier) + ); + await dbContext.SaveChangesAsync(); } } \ No newline at end of file diff --git a/modules/RichSiteSummary/UrchinTrackingModule/UtmParameters.cs b/modules/RichSiteSummary/UrchinTrackingModule/UtmParameters.cs index 2b64722..b1a448b 100644 --- a/modules/RichSiteSummary/UrchinTrackingModule/UtmParameters.cs +++ b/modules/RichSiteSummary/UrchinTrackingModule/UtmParameters.cs @@ -1,11 +1,10 @@ -namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule +namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule; + +public struct UtmParameters { - public struct UtmParameters - { - public string Source; - public string Type; - public string Campaign; - public string Term; - public string Content; - } + public string Source; + public string Type; + public string Campaign; + public string Term; + public string Content; } \ No newline at end of file diff --git a/modules/RichSiteSummary/UrchinTrackingModule/UtmUtility.cs b/modules/RichSiteSummary/UrchinTrackingModule/UtmUtility.cs index a945c55..e715fe5 100644 --- a/modules/RichSiteSummary/UrchinTrackingModule/UtmUtility.cs +++ b/modules/RichSiteSummary/UrchinTrackingModule/UtmUtility.cs @@ -3,49 +3,48 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Web; -namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule +namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule; + +public class UtmUtility { - public class UtmUtility + public static string ApplyParameters(string url, UtmParameters parameters) + => ApplyParameters(new Uri(url), parameters); + + public static string ApplyParameters(Uri uri, UtmParameters parameters) { - public static string ApplyParameters(string url, UtmParameters parameters) - => ApplyParameters(new Uri(url), parameters); + var query = HttpUtility.ParseQueryString(uri.Query); - public static string ApplyParameters(Uri uri, UtmParameters parameters) + SetIfNotNull(query, "utm_source", parameters.Source); + SetIfNotNull(query, "utm_medium", parameters.Type); + SetIfNotNull(query, "utm_campaign", parameters.Campaign); + SetIfNotNull(query, "utm_term", parameters.Term); + SetIfNotNull(query, "utm_content", parameters.Content); + + var uriBuilder = new UriBuilder(uri); + uriBuilder.Query = BuildQueryString(query); + return uriBuilder.ToString(); + } + + protected static string BuildQueryString(NameValueCollection query) + { + var queryParameters = new List(); + + foreach (var keyName in query.AllKeys) { - var query = HttpUtility.ParseQueryString(uri.Query); - - SetIfNotNull(query, "utm_source", parameters.Source); - SetIfNotNull(query, "utm_medium", parameters.Type); - SetIfNotNull(query, "utm_campaign", parameters.Campaign); - SetIfNotNull(query, "utm_term", parameters.Term); - SetIfNotNull(query, "utm_content", parameters.Content); - - var uriBuilder = new UriBuilder(uri); - uriBuilder.Query = BuildQueryString(query); - return uriBuilder.ToString(); + var value = query[keyName]; + queryParameters.Add($"{HttpUtility.UrlEncode(keyName)}={HttpUtility.UrlEncode(value)}"); } - protected static string BuildQueryString(NameValueCollection query) - { - var queryParameters = new List(); - - foreach (var keyName in query.AllKeys) - { - var value = query[keyName]; - queryParameters.Add($"{HttpUtility.UrlEncode(keyName)}={HttpUtility.UrlEncode(value)}"); - } - - return String.Join('&', queryParameters); - } + return String.Join('&', queryParameters); + } - protected static void SetIfNotNull(NameValueCollection queryParameteters, string key, string value) + protected static void SetIfNotNull(NameValueCollection queryParameteters, string key, string value) + { + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) - { - return; - } - - queryParameteters.Set(key, value); + return; } + + queryParameteters.Set(key, value); } } \ No newline at end of file diff --git a/modules/RichSiteSummary/UserMessage.cs b/modules/RichSiteSummary/UserMessage.cs index f6a5399..2e01064 100644 --- a/modules/RichSiteSummary/UserMessage.cs +++ b/modules/RichSiteSummary/UserMessage.cs @@ -1,14 +1,13 @@ using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.RichSiteSummary +namespace Kruzya.TelegramBot.RichSiteSummary; + +public struct UserMessage { - public struct UserMessage - { - public ChatId ChatId; - public string Text; - public ParseMode ParseMode; - public bool DisableWebPagePreview; - public string ImageUrl; - } + public ChatId ChatId; + public string Text; + public ParseMode ParseMode; + public bool DisableWebPagePreview; + public string ImageUrl; } \ No newline at end of file diff --git a/modules/RichSiteSummary/UserUnsubscribe.cs b/modules/RichSiteSummary/UserUnsubscribe.cs index b8b5ca1..d45627c 100644 --- a/modules/RichSiteSummary/UserUnsubscribe.cs +++ b/modules/RichSiteSummary/UserUnsubscribe.cs @@ -1,9 +1,8 @@ using Telegram.Bot.Types; -namespace Kruzya.TelegramBot.RichSiteSummary +namespace Kruzya.TelegramBot.RichSiteSummary; + +public struct UserUnsubscribe { - public struct UserUnsubscribe - { - public ChatId ChatId; - } + public ChatId ChatId; } \ No newline at end of file diff --git a/modules/UrlLimitations/MessageExtension.cs b/modules/UrlLimitations/MessageExtension.cs index 1306321..36fec19 100644 --- a/modules/UrlLimitations/MessageExtension.cs +++ b/modules/UrlLimitations/MessageExtension.cs @@ -5,46 +5,46 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.UrlLimitations +namespace Kruzya.TelegramBot.UrlLimitations; + +public static class MessageExtension { - public static class MessageExtension + public static async Task HasUnknownMention(this Message message, ITelegramBotClient bot) { - public static async Task HasUnknownMention(this Message message, ITelegramBotClient bot) + foreach (var mention in message.Entities.Where(e => + e.Type == MessageEntityType.Mention || e.Type == MessageEntityType.TextMention)) { - foreach (var mention in message.Entities.Where(e => - e.Type == MessageEntityType.Mention || e.Type == MessageEntityType.TextMention)) + long userId = 0; + if (mention.Type == MessageEntityType.TextMention) { - long userId = 0; - if (mention.Type == MessageEntityType.TextMention) + userId = mention.User.Id; + } + else + { + try { - userId = mention.User.Id; - } - else - { - try - { - var chat = await bot.GetChatAsync( - new ChatId(message.Text.Substring(mention.Offset, mention.Length))); + var chat = await bot.GetChatAsync( + new ChatId(message.Text.Substring(mention.Offset, mention.Length))); - if (chat != null) + if (chat != null) + { + if (chat.Type != ChatType.Private) { - if (chat.Type != ChatType.Private) - { - return true; - } - - userId = Convert.ToInt64(chat.Id); + return true; } - } - catch (Exception) - { - return true; + + userId = Convert.ToInt64(chat.Id); } } + catch (Exception) + { + return true; + } + } - var status = (await bot.GetChatMemberAsync(message.Chat, userId)).Status; + var status = (await bot.GetChatMemberAsync(message.Chat, userId)).Status; - if ((new ChatMemberStatus[] + if ((new ChatMemberStatus[] { ChatMemberStatus.Administrator, ChatMemberStatus.Creator, ChatMemberStatus.Member, ChatMemberStatus.Restricted @@ -52,12 +52,11 @@ namespace Kruzya.TelegramBot.UrlLimitations // i'm not sure about "Restricted" // maybe leaved members with some restrictions also can be "Restricted". }).Any(e => e == status) == false) - { - return true; - } + { + return true; } - - return false; } + + return false; } } \ No newline at end of file diff --git a/modules/UrlLimitations/MessageHandler.cs b/modules/UrlLimitations/MessageHandler.cs index 98a2703..a893784 100644 --- a/modules/UrlLimitations/MessageHandler.cs +++ b/modules/UrlLimitations/MessageHandler.cs @@ -11,49 +11,48 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace Kruzya.TelegramBot.UrlLimitations -{ - public class MessageHandler : BotEventHandler - { - protected ILogger Logger; - protected readonly CoreContext dbCtx; +namespace Kruzya.TelegramBot.UrlLimitations; - public MessageHandler(ILogger logger, CoreContext coreContext) +public class MessageHandler : BotEventHandler +{ + protected ILogger Logger; + protected readonly CoreContext dbCtx; + + public MessageHandler(ILogger logger, CoreContext coreContext) + { + Logger = logger; + dbCtx = coreContext; + } + + [InChat(InChat.Public)] + [Message(MessageFlag.HasText)] + public async Task OnMessageReceived() + { + var option = await dbCtx.UserValues.FindOption(Chat.Id, From.Id, "urlLimitations.messagesAfterJoin"); + if (option == null) { - Logger = logger; - dbCtx = coreContext; + return; } - [InChat(InChat.Public)] - [Message(MessageFlag.HasText)] - public async Task OnMessageReceived() + var messageCount = option.GetValue(); + if (messageCount > 10) { - var option = await dbCtx.UserValues.FindOption(Chat.Id, From.Id, "urlLimitations.messagesAfterJoin"); - if (option == null) - { - return; - } + dbCtx.UserValues.Remove(option); + return; + } - var messageCount = option.GetValue(); - if (messageCount > 10) - { - dbCtx.UserValues.Remove(option); - return; - } - - option.SetValue(messageCount + 1); - var message = RawUpdate.Message; - if (message.Entities.Count(e => e.Type == MessageEntityType.Url || e.Type == MessageEntityType.TextLink) > - 0 || await message.HasUnknownMention(Bot)) - { - var targetChat = new ChatId(Chat.Id); - Task.WaitAll( - Bot.BanChatMemberAsync(new ChatId(Chat.Id), From.Id, DateTime.Now.AddDays(1)), - Bot.DeleteMessageAsync(targetChat, message.MessageId), - Bot.SendTextMessageAsync(targetChat, + option.SetValue(messageCount + 1); + var message = RawUpdate.Message; + if (message.Entities.Count(e => e.Type == MessageEntityType.Url || e.Type == MessageEntityType.TextLink) > + 0 || await message.HasUnknownMention(Bot)) + { + var targetChat = new ChatId(Chat.Id); + Task.WaitAll( + Bot.BanChatMemberAsync(new ChatId(Chat.Id), From.Id, DateTime.Now.AddDays(1)), + Bot.DeleteMessageAsync(targetChat, message.MessageId), + Bot.SendTextMessageAsync(targetChat, $"Member {From.ToHtml(true)} has been banned.\nReason:
Spam suspicion
", parseMode: ParseMode.Html) - ); - } + ); } } } \ No newline at end of file diff --git a/modules/UrlLimitations/UrlLimitations.cs b/modules/UrlLimitations/UrlLimitations.cs index b916538..63d6b3e 100644 --- a/modules/UrlLimitations/UrlLimitations.cs +++ b/modules/UrlLimitations/UrlLimitations.cs @@ -1,11 +1,10 @@ using Kruzya.TelegramBot.Core; -namespace Kruzya.TelegramBot.UrlLimitations +namespace Kruzya.TelegramBot.UrlLimitations; + +public class UrlLimitations : Module { - public class UrlLimitations : Module + public UrlLimitations(Core.Core core) : base(core) { - public UrlLimitations(Core.Core core) : base(core) - { - } } } \ No newline at end of file diff --git a/modules/UrlLimitations/UserJoinHandler.cs b/modules/UrlLimitations/UserJoinHandler.cs index 317a892..765c53c 100644 --- a/modules/UrlLimitations/UserJoinHandler.cs +++ b/modules/UrlLimitations/UserJoinHandler.cs @@ -6,24 +6,23 @@ using BotFramework.Enums; using Kruzya.TelegramBot.Core.Data; using Kruzya.TelegramBot.Core.Extensions; -namespace Kruzya.TelegramBot.UrlLimitations -{ - public class UserJoinHandler : BotEventHandler - { - protected readonly CoreContext dbCtx; - - public UserJoinHandler(CoreContext coreContext) - { - dbCtx = coreContext; - } +namespace Kruzya.TelegramBot.UrlLimitations; - [InChat(InChat.Public)] - [Message(MessageFlag.HasNewChatMembers)] - public async Task OnChatMembersAdded() - { - foreach (var member in RawUpdate.Message.NewChatMembers) - await dbCtx.UserValues.SetOptionValue(Chat.Id, member.Id, "urlLimitations.messagesAfterJoin", - 0); - } +public class UserJoinHandler : BotEventHandler +{ + protected readonly CoreContext dbCtx; + + public UserJoinHandler(CoreContext coreContext) + { + dbCtx = coreContext; + } + + [InChat(InChat.Public)] + [Message(MessageFlag.HasNewChatMembers)] + public async Task OnChatMembersAdded() + { + foreach (var member in RawUpdate.Message.NewChatMembers) + await dbCtx.UserValues.SetOptionValue(Chat.Id, member.Id, "urlLimitations.messagesAfterJoin", + 0); } } \ No newline at end of file diff --git a/modules/UserGroupTag/Handler.cs b/modules/UserGroupTag/Handler.cs index fb829b8..fc436db 100644 --- a/modules/UserGroupTag/Handler.cs +++ b/modules/UserGroupTag/Handler.cs @@ -14,94 +14,93 @@ using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace UserGroupTag +namespace UserGroupTag; + +public class Handler : BotEventHandler { - public class Handler : BotEventHandler + private readonly CoreContext _db; + private readonly ICacheStorage _userCache; + + public Handler(CoreContext db, ICacheStorage userCache) { - private readonly CoreContext _db; - private readonly ICacheStorage _userCache; + _db = db; + _userCache = userCache; + } - public Handler(CoreContext db, ICacheStorage userCache) + [InChat(InChat.Public)] + [ParametrizedCommand("add_group", CommandParseMode.Both)] + public async Task HandleAddGroup(string groupName) + { + // var groupName = group.Text; + var repliedMessage = RawUpdate.Message!.ReplyToMessage; + if (repliedMessage == null || repliedMessage.From!.IsBot || !(await Bot.CanPunishMember(Chat, From)) || string.IsNullOrEmpty(groupName)) { - _db = db; - _userCache = userCache; + return; } - [InChat(InChat.Public)] - [ParametrizedCommand("add_group", CommandParseMode.Both)] - public async Task HandleAddGroup(string groupName) + var from = repliedMessage.From; + var fromId = from.Id; + var chatId = Chat!.Id; + + var userGroupOption = await _db.UserValues.FindOrCreateOption(Chat.Id, "userGroups"); + var tagDictionary = userGroupOption.GetValue(new Dictionary>()); + + var memberList = tagDictionary.GetValueOrDefault(groupName, new List()); + if (memberList.Contains(fromId)) { - // var groupName = group.Text; - var repliedMessage = RawUpdate.Message!.ReplyToMessage; - if (repliedMessage == null || repliedMessage.From!.IsBot || !(await Bot.CanPunishMember(Chat, From)) || string.IsNullOrEmpty(groupName)) - { - return; - } - - var from = repliedMessage.From; - var fromId = from.Id; - var chatId = Chat!.Id; - - var userGroupOption = await _db.UserValues.FindOrCreateOption(Chat.Id, "userGroups"); - var tagDictionary = userGroupOption.GetValue(new Dictionary>()); - - var memberList = tagDictionary.GetValueOrDefault(groupName, new List()); - if (memberList.Contains(fromId)) - { - await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} уже находится в группе {groupName}", - parseMode: ParseMode.Html); - return; - } - memberList.Add(repliedMessage.From.Id); - tagDictionary[groupName] = memberList; - - userGroupOption.SetValue(tagDictionary); - _db.AddOrUpdate(userGroupOption); - - _userCache.SetValue(repliedMessage.From.Id, repliedMessage.From, Int32.MaxValue); - await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} добавлен в группу {groupName}", + await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} уже находится в группе {groupName}", parseMode: ParseMode.Html); + return; } + memberList.Add(repliedMessage.From.Id); + tagDictionary[groupName] = memberList; + + userGroupOption.SetValue(tagDictionary); + _db.AddOrUpdate(userGroupOption); - [InChat(InChat.Public)] - [ParametrizedCommand("tag_group", CommandParseMode.Both)] - public async Task HandleTagGroup(string groupName) + _userCache.SetValue(repliedMessage.From.Id, repliedMessage.From, Int32.MaxValue); + await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} добавлен в группу {groupName}", + parseMode: ParseMode.Html); + } + + [InChat(InChat.Public)] + [ParametrizedCommand("tag_group", CommandParseMode.Both)] + public async Task HandleTagGroup(string groupName) + { + if (string.IsNullOrEmpty(groupName)) { - if (string.IsNullOrEmpty(groupName)) - { - return; - } - - var userGroupOption = await _db.UserValues.FindOption(Chat.Id, "userGroups"); - - var userList = userGroupOption?.GetValue>>()?[groupName]; - if (userList == null || userList.Count == 0) - { - return; - } - - var msg = new HtmlString() - .Text("Пользователь ") - .UserMention(From) - .Text($" призывает группу пользователей {groupName}: "); - foreach (var userId in userList) - { - msg.UserMention(await GetUser(userId, Chat)).Text(", "); - } - - await Bot.SendTextMessageAsync(Chat.Id, msg.ToString().TrimEnd(',', ' '), parseMode: ParseMode.Html); + return; } - protected async Task GetUser(long userId, Chat chat) + var userGroupOption = await _db.UserValues.FindOption(Chat.Id, "userGroups"); + + var userList = userGroupOption?.GetValue>>()?[groupName]; + if (userList == null || userList.Count == 0) { - User user; - if (!_userCache.TryGetValue(userId, out user)) - { - user = (await Bot.GetChatMemberAsync(Chat.Id, userId)).User; - _userCache.SetValue(userId, user, Int32.MaxValue); - } - - return user; + return; } + + var msg = new HtmlString() + .Text("Пользователь ") + .UserMention(From) + .Text($" призывает группу пользователей {groupName}: "); + foreach (var userId in userList) + { + msg.UserMention(await GetUser(userId, Chat)).Text(", "); + } + + await Bot.SendTextMessageAsync(Chat.Id, msg.ToString().TrimEnd(',', ' '), parseMode: ParseMode.Html); + } + + protected async Task GetUser(long userId, Chat chat) + { + User user; + if (!_userCache.TryGetValue(userId, out user)) + { + user = (await Bot.GetChatMemberAsync(Chat.Id, userId)).User; + _userCache.SetValue(userId, user, Int32.MaxValue); + } + + return user; } } \ No newline at end of file diff --git a/modules/UserGroupTag/UserGroupTag.cs b/modules/UserGroupTag/UserGroupTag.cs index e7a7c04..419f4cc 100644 --- a/modules/UserGroupTag/UserGroupTag.cs +++ b/modules/UserGroupTag/UserGroupTag.cs @@ -1,9 +1,8 @@ using Kruzya.TelegramBot.Core; -namespace UserGroupTag +namespace UserGroupTag; + +public class UserGroupTag : Module { - public class UserGroupTag : Module - { - public UserGroupTag(Core core) : base(core) { } - } + public UserGroupTag(Core core) : base(core) { } } \ No newline at end of file