mirror of
https://github.com/Bubuni-Team/telegram-bot.git
synced 2026-07-31 00:29:24 +03:00
Use file-scoped namespaces
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"})";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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>(); });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user