Use file-scoped namespaces

This commit is contained in:
Andriy
2023-07-29 15:14:52 +03:00
parent a24332370d
commit 8ab067a027
58 changed files with 2222 additions and 2280 deletions
+6 -7
View File
@@ -1,9 +1,8 @@
namespace Kruzya.TelegramBot.Core.Cache
namespace Kruzya.TelegramBot.Core.Cache;
public interface ICacheStorage<TKey, TValue>
{
public interface ICacheStorage<TKey, TValue>
{
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);
}
+88 -89
View File
@@ -2,103 +2,102 @@
using System.Collections.Generic;
using System.Threading;
namespace Kruzya.TelegramBot.Core.Cache
namespace Kruzya.TelegramBot.Core.Cache;
public class MemoryCache<TKey, TValue> : ICacheStorage<TKey, TValue>
{
public class MemoryCache<TKey, TValue> : ICacheStorage<TKey, TValue>
/// <summary>
/// Represents a cache entry in internal dictionary.
/// </summary>
/// <typeparam name="TVal"></typeparam>
internal struct CacheEntry<TVal>
{
/// <summary>
/// Represents a cache entry in internal dictionary.
/// </summary>
/// <typeparam name="TVal"></typeparam>
internal struct CacheEntry<TVal>
{
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;
}
}
/// <summary>
///
/// </summary>
private readonly Dictionary<TKey, CacheEntry<TValue>> _dict = new();
/// <summary>
/// The TTL for all entries if not set.
/// </summary>
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<TValue> 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<TValue>()
{
Created = DateTime.Now,
ExpiresAt = timeToLive,
Value = value
});
return (Created.AddSeconds(ExpiresAt)) < DateTime.Now;
}
}
/// <summary>
///
/// </summary>
private readonly Dictionary<TKey, CacheEntry<TValue>> _dict = new();
/// <summary>
/// The TTL for all entries if not set.
/// </summary>
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<TValue> 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<TValue>()
{
Created = DateTime.Now,
ExpiresAt = timeToLive,
Value = value
});
}
}
+171 -172
View File
@@ -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
{
/// <summary>
/// The Core API version.
/// </summary>
public UInt32 ApiVersion => 1;
/// <summary>
/// The Core API version.
/// </summary>
public UInt32 ApiVersion => 1;
/// <summary>
/// The array that contains all modules instances.
/// </summary>
public Module[] Modules => _modules.ToArray();
/// <summary>
/// The array that contains all modules instances.
/// </summary>
public Module[] Modules => _modules.ToArray();
/// <summary>
/// The current bot configuration.
/// </summary>
public IConfiguration Configuration => _configuration;
/// <summary>
/// The current bot configuration.
/// </summary>
public IConfiguration Configuration => _configuration;
#region Private properties
#region Private properties
/// <summary>
/// The array that contains all modules instances.
/// </summary>
private List<Module> _modules = new List<Module>();
/// <summary>
/// The array that contains all modules instances.
/// </summary>
private List<Module> _modules = new List<Module>();
/// <summary>
/// The current bot configuration.
/// </summary>
private IConfiguration _configuration;
/// <summary>
/// The current bot configuration.
/// </summary>
private IConfiguration _configuration;
/// <summary>
/// The all directories for loading our module assemblies.
/// </summary>
private List<string> _directories
/// <summary>
/// The all directories for loading our module assemblies.
/// </summary>
private List<string> _directories
{
get
{
get
// For start, load all assemblies in required directory.
// And register in internal temporary array.
var modulesDirectory = new List<string>(new string[]
{
// For start, load all assemblies in required directory.
// And register in internal temporary array.
var modulesDirectory = new List<string>(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;
}
}
/// <summary>
/// List of filenames (without extension) what should be not loaded.
/// </summary>
private List<string> _ignoredAssemblyFilenames => _configuration
.GetSection("IgnoredModuleFilenames").GetChildren()
.Select(q => q.Value).ToList();
/// <summary>
/// List of filenames (without extension) what should be not loaded.
/// </summary>
private List<string> _ignoredAssemblyFilenames => _configuration
.GetSection("IgnoredModuleFilenames").GetChildren()
.Select(q => q.Value).ToList();
#endregion
#endregion
public Core(IConfiguration configuration)
public Core(IConfiguration configuration)
{
_configuration = configuration;
HookAppDomain();
Start();
}
/// <summary>
/// Initializes all required submodules.
/// </summary>
private void Start()
{
var types = new List<Type>();
foreach (var directory in _directories)
{
_configuration = configuration;
HookAppDomain();
Start();
}
/// <summary>
/// Initializes all required submodules.
/// </summary>
private void Start()
{
var types = new List<Type>();
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);
}
}
/// <summary>
/// Loads the assembly and appends all Module types to list.
/// </summary>
/// <param name="path">The full path to assembly file</param>
/// <returns><see cref="IEnumerable{T}"/> if success, null otherwise.</returns>
private IEnumerable<Type>? 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)));
}
/// <summary>
/// Ensures if module is loaded and started. Note that call can change module event chain.
/// </summary>
/// <typeparam name="T">The Module type.</typeparam>
/// <returns>Module instance.</returns>
public T EnsureLoaded<T>()
where T : Module
{
return (T) EnsureLoaded(typeof(T));
}
/// <summary>
/// Ensures if module is loaded and started. Note that call can change module event chain.
/// </summary>
/// <param name="moduleType">The Module type.</param>
/// <returns>Module instance.</returns>
/// <exception cref="Exception">The given type is not extends the Module class</exception>
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<string>();
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);
}
}
}
/// <summary>
/// Loads the assembly and appends all Module types to list.
/// </summary>
/// <param name="path">The full path to assembly file</param>
/// <returns><see cref="IEnumerable{T}"/> if success, null otherwise.</returns>
private IEnumerable<Type>? 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)));
}
/// <summary>
/// Ensures if module is loaded and started. Note that call can change module event chain.
/// </summary>
/// <typeparam name="T">The Module type.</typeparam>
/// <returns>Module instance.</returns>
public T EnsureLoaded<T>()
where T : Module
{
return (T) EnsureLoaded(typeof(T));
}
/// <summary>
/// Ensures if module is loaded and started. Note that call can change module event chain.
/// </summary>
/// <param name="moduleType">The Module type.</param>
/// <returns>Module instance.</returns>
/// <exception cref="Exception">The given type is not extends the Module class</exception>
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<string>();
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;
};
}
}
+18 -19
View File
@@ -1,24 +1,23 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Kruzya.TelegramBot.Core.Data
{
public class BotUser
{
/// <summary>
/// The unique bot user identifier.
/// </summary>
[Key]
public Guid BotUserId { get; set; }
/// <summary>
/// The chat identifier where this user is related.
/// </summary>
public long ChatId { get; set; }
namespace Kruzya.TelegramBot.Core.Data;
/// <summary>
/// The Telegram User identifier.
/// </summary>
public long UserId { get; set; }
}
public class BotUser
{
/// <summary>
/// The unique bot user identifier.
/// </summary>
[Key]
public Guid BotUserId { get; set; }
/// <summary>
/// The chat identifier where this user is related.
/// </summary>
public long ChatId { get; set; }
/// <summary>
/// The Telegram User identifier.
/// </summary>
public long UserId { get; set; }
}
+26 -27
View File
@@ -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>(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>(T defVal = default)
try
{
try
{
return JsonSerializer.Deserialize<T>(new ReadOnlySpan<byte>(Value));
}
catch (Exception)
{
return defVal;
}
return JsonSerializer.Deserialize<T>(new ReadOnlySpan<byte>(Value));
}
public void SetValue<T>(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>(T value)
{
Value = JsonSerializer.SerializeToUtf8Bytes(value, new JsonSerializerOptions { WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
ValueType = value.GetType().FullName;
}
}
+17 -18
View File
@@ -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
/// <summary>
/// The all known users.
/// </summary>
public DbSet<BotUser> Users { get; set; }
/// <summary>
/// The all knows user options and values.
/// </summary>
public DbSet<BotUserValue> UserValues { get; set; }
public DbSet<ChatOptionValue> ChatOptionValues { get; set; }
public CoreContext(DbContextOptions<CoreContext> ctx) : base(ctx)
{
/// <summary>
/// The all known users.
/// </summary>
public DbSet<BotUser> Users { get; set; }
/// <summary>
/// The all knows user options and values.
/// </summary>
public DbSet<BotUserValue> UserValues { get; set; }
public DbSet<ChatOptionValue> ChatOptionValues { get; set; }
public CoreContext(DbContextOptions<CoreContext> ctx) : base(ctx)
{
Database.Migrate();
}
Database.Migrate();
}
}
+28 -29
View File
@@ -2,40 +2,39 @@
using BotFramework;
using Microsoft.EntityFrameworkCore;
namespace Kruzya.TelegramBot.Core
namespace Kruzya.TelegramBot.Core;
/// <summary>
/// Resolves the GUIDs from database to entities.
/// </summary>
/// <typeparam name="TResolvedEntity">Entity type</typeparam>
/// <typeparam name="TAppContext">Database context where entity should be finded</typeparam>
public class DbResolverParameter<TResolvedEntity, TAppContext> : IParameterParser<TResolvedEntity>
where TAppContext : DbContext
where TResolvedEntity : class
{
/// <summary>
/// Resolves the GUIDs from database to entities.
/// </summary>
/// <typeparam name="TResolvedEntity">Entity type</typeparam>
/// <typeparam name="TAppContext">Database context where entity should be finded</typeparam>
public class DbResolverParameter<TResolvedEntity, TAppContext> : IParameterParser<TResolvedEntity>
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<TResolvedEntity>(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<TResolvedEntity>(id);
return (result != null);
}
return false;
}
return false;
}
}
+30 -31
View File
@@ -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<bool> CanPunishMember(this ITelegramBotClient bot, Chat chat, User user, User victim = null)
{
public static async Task<bool> 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<bool> IsUserAdminAsync(this ITelegramBotClient bot, Chat chat, User user)
return canUse;
}
public static async Task<bool> 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<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat)
{
return await CanDeleteMessagesAsync(bot, chat, await bot.GetMeAsync());
}
return isAdmin;
}
public static async Task<bool> 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<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat)
{
return await CanDeleteMessagesAsync(bot, chat, await bot.GetMeAsync());
}
public static async Task<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat, User user)
{
return await bot.GetChatMemberAsync(chat.Id, user.Id) is ChatMemberOwner
or ChatMemberAdministrator {CanRestrictMembers: true};
}
}
+13 -14
View File
@@ -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<T> GetJsonAsync<T>(this HttpClient client, Uri requestUri)
{
var response = await client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<T>(response);
}
namespace Kruzya.TelegramBot.Core.Extensions;
public static async Task<T> GetJsonAsync<T>(this HttpClient client, string requestUri)
{
var response = await client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<T>(response);
}
public static class HttpClientExtension
{
public static async Task<T> GetJsonAsync<T>(this HttpClient client, Uri requestUri)
{
var response = await client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<T>(response);
}
public static async Task<T> GetJsonAsync<T>(this HttpClient client, string requestUri)
{
var response = await client.GetStringAsync(requestUri);
return JsonConvert.DeserializeObject<T>(response);
}
}
+12 -13
View File
@@ -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);
}
}
+9 -10
View File
@@ -1,15 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace Kruzya.TelegramBot.Core.Extensions
{
public static partial class RepositoryExtension
{
public static TEntity Create<TEntity>(this DbSet<TEntity> 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<TEntity>(this DbSet<TEntity> repository) where TEntity : class, new()
{
var entity = new TEntity();
repository.Add(entity);
return entity;
}
}
+11 -12
View File
@@ -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);
}
}
+20 -21
View File
@@ -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($"<a href=\"tg://user?id={user.Id}\">");
var text = new StringBuilder();
text.Append($"<a href=\"tg://user?id={user.Id}\">");
if (byUsername && !string.IsNullOrWhiteSpace(user.Username))
{
text.Append(user.Username);
}
else
{
text.Append($"{user.FirstName} {user.LastName}".Trim().HtmlEncode());
}
return text.Append("</a>").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("</a>").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"})";
}
}
+40 -41
View File
@@ -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<Message> 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<Message> Reply(string text)
{
return await Bot.SendTextMessageAsync(Chat.Id, text,
replyToMessageId: Message?.MessageId, parseMode: ParseMode.Html);
}
protected async Task<bool> CanPunish()
{
return RepliedUser != null && await Bot.CanPunishMember(Chat, From, RepliedUser);
}
protected async Task<bool> 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<BotUserValue> GetWarnOption(User user)
{
return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "warnCount");
}
protected async Task<BotUserValue> 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);
}
}
+49 -50
View File
@@ -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<bool> 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<bool> 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
});
}
}
+27 -28
View File
@@ -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)
{
}
/// <summary>
/// Ensures if module is loaded and started. Note that call can change module event chain.
/// </summary>
/// <typeparam name="T">The Module type.</typeparam>
/// <returns>Module instance.</returns>
protected T EnsureLoaded<T>() 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)
{
}
/// <summary>
/// Ensures if module is loaded and started. Note that call can change module event chain.
/// </summary>
/// <typeparam name="T">The Module type.</typeparam>
/// <returns>Module instance.</returns>
protected T EnsureLoaded<T>() where T : Module
{
return (T) Core.EnsureLoaded(typeof(T));
}
}
+13 -14
View File
@@ -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<Startup>(); });
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<Startup>(); });
}
+56 -57
View File
@@ -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
/// <summary>
/// Disposes the timer.
/// </summary>
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
/// <summary>
/// Disposes the timer.
/// </summary>
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);
}
}
+9 -10
View File
@@ -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<double> GetUserReputationAsync(Chat chat, User user);
public Task<double> SetUserReputationAsync(Chat chat, User user, double value);
public Task<double> GetReputationDiffAsync(Chat chat, User sender, User receiver);
public Task<double> IncrementReputationAsync(Chat chat, User user, double diff);
public Task<IEnumerable<IReputationEntity>> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0);
}
}
public Task<double> GetUserReputationAsync(Chat chat, User user);
public Task<double> SetUserReputationAsync(Chat chat, User user, double value);
public Task<double> GetReputationDiffAsync(Chat chat, User sender, User receiver);
public Task<double> IncrementReputationAsync(Chat chat, User user, double diff);
public Task<IEnumerable<IReputationEntity>> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0);
}
+62 -63
View File
@@ -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<long> _superUsers;
private readonly Dictionary<long, string> _statusMap;
private readonly ICacheStorage<long, User> _userCache;
private readonly ITelegramBotClient _bot;
private readonly ILogger<UserService> _logger;
public UserService(
IConfiguration configuration,
IBotInstance bot,
ICacheStorage<long, User> userCache,
ILogger<UserService> logger)
{
private readonly List<long> _superUsers;
private readonly Dictionary<long, string> _statusMap;
private readonly ICacheStorage<long, User> _userCache;
private readonly ITelegramBotClient _bot;
private readonly ILogger<UserService> _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<long, User> userCache,
ILogger<UserService> logger)
_userCache = userCache;
_bot = bot.BotClient;
_logger = logger;
}
public async Task<User> 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<User> 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<User> GetUserByChat(long chatId, long userId)
{
return (await _bot.GetChatMemberAsync(chatId, userId)).User;
}
private async Task<User> 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;
}
}
+50 -51
View File
@@ -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>(_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<CoreContext>(ctx =>
ctx.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
// Cache for users.
services.AddSingleton<ICacheStorage<long, User>, MemoryCache<long, User>>();
services.AddSingleton<UserService>();
services.AddSingleton<IAntiFlood, MemoryAntiFloodService>();
services.AddSingleton<ThreadSafeRandom>();
services.AddSingleton<ConcurrentBag<DeleteRequest>>();
services.AddHostedService<DeleteService>();
services.AddScoped<IOptionProvider, DbOptionProvider>();
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>(_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<CoreContext>(ctx =>
ctx.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
// Cache for users.
services.AddSingleton<ICacheStorage<long, User>, MemoryCache<long, User>>();
services.AddSingleton<UserService>();
services.AddSingleton<IAntiFlood, MemoryAntiFloodService>();
services.AddSingleton<ThreadSafeRandom>();
services.AddSingleton<ConcurrentBag<DeleteRequest>>();
services.AddHostedService<DeleteService>();
services.AddScoped<IOptionProvider, DbOptionProvider>();
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);
}
}