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 bool TryGetValue(TKey key, out TValue value);
|
public TValue GetOr(TKey key, TValue value);
|
||||||
public void SetValue(TKey key, TValue value, int timeToLive);
|
|
||||||
public TValue GetOr(TKey key, TValue value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+89
-90
@@ -2,103 +2,102 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
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>
|
public TVal Value;
|
||||||
/// Represents a cache entry in internal dictionary.
|
public DateTime Created;
|
||||||
/// </summary>
|
public int ExpiresAt;
|
||||||
/// <typeparam name="TVal"></typeparam>
|
|
||||||
internal struct CacheEntry<TVal>
|
public bool IsExpired()
|
||||||
{
|
{
|
||||||
public TVal Value;
|
if (ExpiresAt == Timeout.Infinite)
|
||||||
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 MemoryCache(int defaultTtl)
|
|
||||||
{
|
|
||||||
_defaultTtl = defaultTtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetValue(TKey key, out TValue value)
|
|
||||||
{
|
|
||||||
value = default;
|
|
||||||
if (!_dict.ContainsKey(key))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheEntry<TValue> temp;
|
return (Created.AddSeconds(ExpiresAt)) < DateTime.Now;
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+177
-178
@@ -7,201 +7,200 @@ using System.Linq;
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Extensions.Configuration;
|
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 array that contains all modules instances.
|
||||||
|
/// </summary>
|
||||||
|
public Module[] Modules => _modules.ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current bot configuration.
|
||||||
|
/// </summary>
|
||||||
|
public IConfiguration Configuration => _configuration;
|
||||||
|
|
||||||
|
#region Private properties
|
||||||
|
|
||||||
|
/// <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 all directories for loading our module assemblies.
|
||||||
|
/// </summary>
|
||||||
|
private List<string> _directories
|
||||||
{
|
{
|
||||||
/// <summary>
|
get
|
||||||
/// 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 current bot configuration.
|
|
||||||
/// </summary>
|
|
||||||
public IConfiguration Configuration => _configuration;
|
|
||||||
|
|
||||||
#region Private properties
|
|
||||||
|
|
||||||
/// <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 all directories for loading our module assemblies.
|
|
||||||
/// </summary>
|
|
||||||
private List<string> _directories
|
|
||||||
{
|
{
|
||||||
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.
|
Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "modules"),
|
||||||
// And register in internal temporary array.
|
Path.Join(Environment.CurrentDirectory, "modules")
|
||||||
var modulesDirectory = new List<string>(new string[]
|
});
|
||||||
{
|
|
||||||
Path.Join(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "modules"),
|
|
||||||
Path.Join(Environment.CurrentDirectory, "modules")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also lookup load directories from config.
|
// Also lookup load directories from config.
|
||||||
modulesDirectory.AddRange(_configuration.GetSection("ModuleDirectories").GetChildren().Select(q => q.Value)
|
modulesDirectory.AddRange(_configuration.GetSection("ModuleDirectories").GetChildren().Select(q => q.Value)
|
||||||
.ToList());
|
.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();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories))
|
||||||
/// 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
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(directory))
|
var assemblyTypes = LoadAssembly(file);
|
||||||
|
if (assemblyTypes == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in Directory.EnumerateFiles(directory, "*.dll", SearchOption.AllDirectories))
|
types.AddRange(assemblyTypes);
|
||||||
{
|
|
||||||
var assemblyTypes = LoadAssembly(file);
|
|
||||||
if (assemblyTypes == null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
types.AddRange(assemblyTypes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instantiate them.
|
|
||||||
foreach (var module in types)
|
|
||||||
{
|
|
||||||
EnsureLoaded(module);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Instantiate them.
|
||||||
/// Loads the assembly and appends all Module types to list.
|
foreach (var module in types)
|
||||||
/// </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();
|
EnsureLoaded(module);
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+16
-17
@@ -1,24 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Core.Data
|
namespace Kruzya.TelegramBot.Core.Data;
|
||||||
|
|
||||||
|
public class BotUser
|
||||||
{
|
{
|
||||||
public class BotUser
|
/// <summary>
|
||||||
{
|
/// The unique bot user identifier.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// The unique bot user identifier.
|
[Key]
|
||||||
/// </summary>
|
public Guid BotUserId { get; set; }
|
||||||
[Key]
|
|
||||||
public Guid BotUserId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The chat identifier where this user is related.
|
/// The chat identifier where this user is related.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ChatId { get; set; }
|
public long ChatId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Telegram User identifier.
|
/// The Telegram User identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long UserId { get; set; }
|
public long UserId { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+26
-27
@@ -4,38 +4,37 @@ using System.ComponentModel.DataAnnotations.Schema;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
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)]
|
try
|
||||||
[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
|
return JsonSerializer.Deserialize<T>(new ReadOnlySpan<byte>(Value));
|
||||||
{
|
|
||||||
return JsonSerializer.Deserialize<T>(new ReadOnlySpan<byte>(Value));
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return defVal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
public void SetValue<T>(T value)
|
|
||||||
{
|
{
|
||||||
Value = JsonSerializer.SerializeToUtf8Bytes(value, new JsonSerializerOptions { WriteIndented = false,
|
return defVal;
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull });
|
|
||||||
ValueType = value.GetType().FullName;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 Kruzya.TelegramBot.Core.EF;
|
||||||
using Microsoft.EntityFrameworkCore;
|
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>
|
Database.Migrate();
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-29
@@ -2,40 +2,39 @@
|
|||||||
using BotFramework;
|
using BotFramework;
|
||||||
using Microsoft.EntityFrameworkCore;
|
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>
|
private TAppContext _dbContext;
|
||||||
/// Resolves the GUIDs from database to entities.
|
|
||||||
/// </summary>
|
public DbResolverParameter(TAppContext dbContext)
|
||||||
/// <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;
|
_dbContext = dbContext;
|
||||||
|
}
|
||||||
|
|
||||||
public DbResolverParameter(TAppContext dbContext)
|
public TResolvedEntity DefaultInstance()
|
||||||
|
{
|
||||||
|
return default(TResolvedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValue(string text, out TResolvedEntity result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
Guid id;
|
||||||
|
if (Guid.TryParse(text, out id))
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
result = _dbContext.Find<TResolvedEntity>(id);
|
||||||
|
return (result != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TResolvedEntity DefaultInstance()
|
return false;
|
||||||
{
|
|
||||||
return default(TResolvedEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,45 +2,44 @@
|
|||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types;
|
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);
|
var victimMember = await bot.GetChatMemberAsync(chat, victim.Id);
|
||||||
|
canUse = victimMember is not ChatMemberOwner && victimMember is not ChatMemberAdministrator && !victim.IsBot;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
isAdmin = callerAdmin.CanRestrictMembers;
|
||||||
var isAdmin = callerMember is ChatMemberOwner;
|
|
||||||
|
|
||||||
if (callerMember is ChatMemberAdministrator callerAdmin)
|
|
||||||
{
|
|
||||||
isAdmin = callerAdmin.CanRestrictMembers;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isAdmin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat)
|
return isAdmin;
|
||||||
{
|
}
|
||||||
return await CanDeleteMessagesAsync(bot, chat, await bot.GetMeAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat, User user)
|
public static async Task<bool> CanDeleteMessagesAsync(this ITelegramBotClient bot, Chat chat)
|
||||||
{
|
{
|
||||||
return await bot.GetChatMemberAsync(chat.Id, user.Id) is ChatMemberOwner
|
return await CanDeleteMessagesAsync(bot, chat, await bot.GetMeAsync());
|
||||||
or ChatMemberAdministrator {CanRestrictMembers: true};
|
}
|
||||||
}
|
|
||||||
|
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 System.Threading.Tasks;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Core.Extensions
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<T> GetJsonAsync<T>(this HttpClient client, string requestUri)
|
public static class HttpClientExtension
|
||||||
{
|
{
|
||||||
var response = await client.GetStringAsync(requestUri);
|
public static async Task<T> GetJsonAsync<T>(this HttpClient client, Uri requestUri)
|
||||||
return JsonConvert.DeserializeObject<T>(response);
|
{
|
||||||
}
|
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.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Core.Extensions
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Configure(this Module[] modules, IApplicationBuilder app, IWebHostEnvironment env)
|
internal static class ModuleArrayExtension
|
||||||
{
|
{
|
||||||
foreach (var module in modules) module.Configure(app, env);
|
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;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Core.Extensions
|
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);
|
|
||||||
|
|
||||||
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;
|
using System.Web;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Core.Extensions
|
namespace Kruzya.TelegramBot.Core.Extensions;
|
||||||
{
|
|
||||||
public static class StringExtension
|
|
||||||
{
|
|
||||||
public static string HtmlEncode(this string content)
|
|
||||||
{
|
|
||||||
return HttpUtility.HtmlEncode(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string HtmlDecode(this string content)
|
public static class StringExtension
|
||||||
{
|
{
|
||||||
return HttpUtility.HtmlDecode(content);
|
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 System.Text;
|
||||||
using Telegram.Bot.Types;
|
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}\">");
|
||||||
|
|
||||||
|
if (byUsername && !string.IsNullOrWhiteSpace(user.Username))
|
||||||
{
|
{
|
||||||
var text = new StringBuilder();
|
text.Append(user.Username);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
public static string ToLog(this User user)
|
|
||||||
{
|
{
|
||||||
var name = $"{user.FirstName} {user.LastName}".Trim();
|
text.Append($"{user.FirstName} {user.LastName}".Trim().HtmlEncode());
|
||||||
|
|
||||||
return $"{name} (ID: {user.Id}, Username: {user.Username ?? "N/A"})";
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
using Telegram.Bot.Types.Enums;
|
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 async Task<bool> CanPunish()
|
||||||
|
{
|
||||||
protected CommonHandler(CoreContext db)
|
return RepliedUser != null && await Bot.CanPunishMember(Chat, From, RepliedUser);
|
||||||
{
|
}
|
||||||
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 BanMember(long userId, DateTime banEnd)
|
protected async Task BanMember(long userId, DateTime banEnd)
|
||||||
{
|
{
|
||||||
await Bot.RestrictChatMemberAsync(
|
await Bot.RestrictChatMemberAsync(
|
||||||
Chat.Id,
|
Chat.Id,
|
||||||
userId,
|
userId,
|
||||||
new ChatPermissions
|
new ChatPermissions
|
||||||
{
|
{
|
||||||
CanSendMessages = false
|
CanSendMessages = false
|
||||||
},
|
},
|
||||||
banEnd
|
banEnd
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<BotUserValue> GetWarnOption(User user)
|
protected async Task<BotUserValue> GetWarnOption(User user)
|
||||||
{
|
{
|
||||||
return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "warnCount");
|
return await Db.UserValues.FindOrCreateOption(Chat.Id, user.Id, "warnCount");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task AnswerQuery(string id, string? message = null)
|
protected async Task AnswerQuery(string id, string? message = null)
|
||||||
{
|
{
|
||||||
await Bot.AnswerCallbackQueryAsync(id, message);
|
await Bot.AnswerCallbackQueryAsync(id, message);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,71 +6,70 @@ using Kruzya.TelegramBot.Core.Data;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Telegram.Bot.Types;
|
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)]
|
protected override bool CanHandle(HandlerParams param)
|
||||||
public sealed class HandleEverythingAttribute : HandlerAttribute
|
|
||||||
{
|
{
|
||||||
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task VerifyChatPair(Message message)
|
protected readonly CoreContext databaseContext;
|
||||||
{
|
|
||||||
await VerifyChatPair(message.Chat, message.From);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task VerifyChatPair(Chat chat, User user)
|
public GeneralHandler(CoreContext dbCtx)
|
||||||
{
|
{
|
||||||
await VerifyChatPair(chat.Id, user.Id);
|
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 (message == null)
|
||||||
if (hasEntry)
|
|
||||||
{
|
{
|
||||||
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()
|
return;
|
||||||
{
|
|
||||||
ChatId = chat,
|
|
||||||
UserId = user
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await MakeChatPair(chat, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MakeChatPair(long chat, long user)
|
||||||
|
{
|
||||||
|
await databaseContext.Users.AddAsync(new BotUser()
|
||||||
|
{
|
||||||
|
ChatId = chat,
|
||||||
|
UserId = user
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+23
-24
@@ -3,34 +3,33 @@ using Microsoft.AspNetCore.Hosting;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
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;
|
Core = core;
|
||||||
protected IConfiguration Configuration => Core.Configuration;
|
}
|
||||||
|
|
||||||
public Module(Core core)
|
public virtual void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
Core = core;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void ConfigureServices(IServiceCollection services)
|
public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
/// <summary>
|
/// <returns>Module instance.</returns>
|
||||||
/// Ensures if module is loaded and started. Note that call can change module event chain.
|
protected T EnsureLoaded<T>() where T : Module
|
||||||
/// </summary>
|
{
|
||||||
/// <typeparam name="T">The Module type.</typeparam>
|
return (T) Core.EnsureLoaded(typeof(T));
|
||||||
/// <returns>Module instance.</returns>
|
|
||||||
protected T EnsureLoaded<T>() where T : Module
|
|
||||||
{
|
|
||||||
return (T) Core.EnsureLoaded(typeof(T));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-13
@@ -2,19 +2,18 @@ using System.Globalization;
|
|||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Core
|
namespace Kruzya.TelegramBot.Core;
|
||||||
{
|
|
||||||
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) =>
|
public class Program
|
||||||
Host.CreateDefaultBuilder(args)
|
{
|
||||||
.UseSystemd()
|
public static void Main(string[] args)
|
||||||
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
|
{
|
||||||
|
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.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly ILogger _logger;
|
#region IHostedService
|
||||||
protected readonly IBotInstance _bot;
|
|
||||||
|
|
||||||
protected virtual TimeSpan TimerPeriod => TimeSpan.FromSeconds(45);
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Message sender service is starting.");
|
||||||
|
_timer = new Timer(Run, null, TimeSpan.Zero, TimerPeriod);
|
||||||
|
|
||||||
protected AbstractTimedHostedService(ILogger logger, IBotInstance bot)
|
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
|
||||||
{
|
{
|
||||||
_bot = bot;
|
await OnRun();
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
#region IHostedService
|
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Message sender service is starting.");
|
var timerPeriod = TimerPeriod;
|
||||||
_timer = new Timer(Run, null, TimeSpan.Zero, TimerPeriod);
|
_timer?.Change(timerPeriod, 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual async Task OnRun()
|
||||||
|
{
|
||||||
|
await Task.Delay(500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,13 @@ using System.Threading.Tasks;
|
|||||||
using Kruzya.TelegramBot.Core.Data;
|
using Kruzya.TelegramBot.Core.Data;
|
||||||
using Telegram.Bot.Types;
|
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> GetUserReputationAsync(Chat chat, User user);
|
public Task<double> GetReputationDiffAsync(Chat chat, User sender, User receiver);
|
||||||
public Task<double> SetUserReputationAsync(Chat chat, User user, double value);
|
public Task<double> IncrementReputationAsync(Chat chat, User user, double diff);
|
||||||
public Task<double> GetReputationDiffAsync(Chat chat, User sender, User receiver);
|
public Task<IEnumerable<IReputationEntity>> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0);
|
||||||
public Task<double> IncrementReputationAsync(Chat chat, User user, double diff);
|
|
||||||
public Task<IEnumerable<IReputationEntity>> GetChatRatingAsync(Chat chat, int limit = 10, int offset = 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+61
-62
@@ -11,81 +11,80 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types;
|
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;
|
_superUsers = configuration.GetSection("SuperUsers")
|
||||||
private readonly Dictionary<long, string> _statusMap;
|
.GetChildren()
|
||||||
private readonly ICacheStorage<long, User> _userCache;
|
.Select(q => Convert.ToInt64(q.Value))
|
||||||
private readonly ITelegramBotClient _bot;
|
.ToList();
|
||||||
private readonly ILogger<UserService> _logger;
|
|
||||||
|
|
||||||
|
_statusMap = configuration.GetSection("CustomMemberStatus")
|
||||||
|
.GetChildren()
|
||||||
|
.ToDictionary(x => Convert.ToInt64(x.Key), x => x.Value);
|
||||||
|
|
||||||
public UserService(
|
_userCache = userCache;
|
||||||
IConfiguration configuration,
|
_bot = bot.BotClient;
|
||||||
IBotInstance bot,
|
_logger = logger;
|
||||||
ICacheStorage<long, User> userCache,
|
}
|
||||||
ILogger<UserService> 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")
|
if (bypassCache)
|
||||||
.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)
|
_logger.LogDebug("Bypassing cache for {UserId}.", userId);
|
||||||
{
|
}
|
||||||
_logger.LogDebug("Bypassing cache for {UserId}.", userId);
|
else
|
||||||
}
|
{
|
||||||
else
|
_logger.LogDebug("User {UserId} is not found in cache. Fetching from Telegram API.", userId);
|
||||||
{
|
|
||||||
_logger.LogDebug("User {UserId} is not found in cache. Fetching from Telegram API.", userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
user = await GetUserByChat(chatId, userId);
|
|
||||||
CacheUser(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
user = await GetUserByChat(chatId, userId);
|
||||||
|
CacheUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CacheUser(User user)
|
return 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)
|
private void CacheUser(User user)
|
||||||
{
|
{
|
||||||
return (await _bot.GetChatMemberAsync(chatId, userId)).User;
|
// TODO: config entry for TTL
|
||||||
}
|
_userCache.SetValue(user.Id, user, Convert.ToInt32(TimeSpan.FromHours(1).TotalSeconds));
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsUserSuper(User user)
|
private async Task<User> GetUserByChat(long chatId, long userId)
|
||||||
{
|
{
|
||||||
return IsUserSuper(user.Id);
|
return (await _bot.GetChatMemberAsync(chatId, userId)).User;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsUserSuper(long userId)
|
public bool IsUserSuper(User user)
|
||||||
{
|
{
|
||||||
return _superUsers.Contains(userId);
|
return IsUserSuper(user.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? GetUserCustomStatus(long userId)
|
public bool IsUserSuper(long userId)
|
||||||
{
|
{
|
||||||
return _statusMap.ContainsKey(userId) ? _statusMap[userId] : null;
|
return _superUsers.Contains(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? GetUserCustomStatus(long userId)
|
||||||
|
{
|
||||||
|
return _statusMap.ContainsKey(userId) ? _statusMap[userId] : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+38
-39
@@ -14,57 +14,56 @@ using System.Collections.Concurrent;
|
|||||||
using Kruzya.TelegramBot.Core.Options;
|
using Kruzya.TelegramBot.Core.Options;
|
||||||
using Telegram.Bot.Types;
|
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;
|
// TODO: add Core to DI.
|
||||||
|
_core = new Core(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
public Startup(IConfiguration 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
|
||||||
// TODO: add Core to DI.
|
public void ConfigureServices(IServiceCollection services)
|
||||||
_core = new Core(configuration);
|
{
|
||||||
}
|
services.AddTelegramBot();
|
||||||
|
services.AddLogging(builder => builder.AddConsole());
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// Add all modules to DI with Core instance.
|
||||||
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
|
services.AddSingleton<Core>(_core);
|
||||||
public void ConfigureServices(IServiceCollection services)
|
foreach (var module in _core.Modules) services.AddSingleton(module.GetType(), module);
|
||||||
{
|
|
||||||
services.AddTelegramBot();
|
|
||||||
services.AddLogging(builder => builder.AddConsole());
|
|
||||||
|
|
||||||
// Add all modules to DI with Core instance.
|
// Add database context to DI.
|
||||||
services.AddSingleton<Core>(_core);
|
var connectionString = _core.Configuration.GetConnectionString("DefaultConnection");
|
||||||
foreach (var module in _core.Modules) services.AddSingleton(module.GetType(), module);
|
services.AddDbContext<CoreContext>(ctx =>
|
||||||
|
ctx.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
||||||
|
|
||||||
// Add database context to DI.
|
// Cache for users.
|
||||||
var connectionString = _core.Configuration.GetConnectionString("DefaultConnection");
|
services.AddSingleton<ICacheStorage<long, User>, MemoryCache<long, User>>();
|
||||||
services.AddDbContext<CoreContext>(ctx =>
|
|
||||||
ctx.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
|
||||||
|
|
||||||
// Cache for users.
|
services.AddSingleton<UserService>();
|
||||||
services.AddSingleton<ICacheStorage<long, User>, MemoryCache<long, User>>();
|
services.AddSingleton<IAntiFlood, MemoryAntiFloodService>();
|
||||||
|
|
||||||
services.AddSingleton<UserService>();
|
services.AddSingleton<ThreadSafeRandom>();
|
||||||
services.AddSingleton<IAntiFlood, MemoryAntiFloodService>();
|
|
||||||
|
|
||||||
services.AddSingleton<ThreadSafeRandom>();
|
|
||||||
|
|
||||||
|
|
||||||
services.AddSingleton<ConcurrentBag<DeleteRequest>>();
|
services.AddSingleton<ConcurrentBag<DeleteRequest>>();
|
||||||
services.AddHostedService<DeleteService>();
|
services.AddHostedService<DeleteService>();
|
||||||
|
|
||||||
services.AddScoped<IOptionProvider, DbOptionProvider>();
|
services.AddScoped<IOptionProvider, DbOptionProvider>();
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
|
|
||||||
// Trigger module handlers.
|
// Trigger module handlers.
|
||||||
_core.Modules.ConfigureServices(services);
|
_core.Modules.ConfigureServices(services);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
_core.Modules.Configure(app, env);
|
_core.Modules.Configure(app, env);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,27 +6,26 @@ using Newtonsoft.Json.Converters;
|
|||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
using West.TelegramBot.ChatQuotes.Json;
|
using West.TelegramBot.ChatQuotes.Json;
|
||||||
|
|
||||||
namespace West.TelegramBot.ChatQuotes
|
namespace West.TelegramBot.ChatQuotes;
|
||||||
|
|
||||||
|
public class ChatQuotes : Module
|
||||||
{
|
{
|
||||||
public class ChatQuotes : Module
|
public static readonly JsonSerializerSettings SerializerSettings = new()
|
||||||
{
|
{
|
||||||
public static readonly JsonSerializerSettings SerializerSettings = new()
|
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
||||||
|
DateParseHandling = DateParseHandling.None,
|
||||||
|
Converters =
|
||||||
{
|
{
|
||||||
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
|
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal },
|
||||||
DateParseHandling = DateParseHandling.None,
|
new StringEnumConverter(new CamelCaseNamingStrategy())
|
||||||
Converters =
|
},
|
||||||
{
|
ContractResolver = new NullToEmptyObjectResolver()
|
||||||
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal },
|
};
|
||||||
new StringEnumConverter(new CamelCaseNamingStrategy())
|
|
||||||
},
|
|
||||||
ContractResolver = new NullToEmptyObjectResolver()
|
|
||||||
};
|
|
||||||
|
|
||||||
public ChatQuotes(Core core) : base(core) { }
|
public ChatQuotes(Core core) : base(core) { }
|
||||||
|
|
||||||
public override void ConfigureServices(IServiceCollection services)
|
public override void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<QuoteGeneratorService>();
|
services.AddSingleton<QuoteGeneratorService>();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
namespace West.TelegramBot.ChatQuotes.Json
|
namespace West.TelegramBot.ChatQuotes.Json;
|
||||||
|
|
||||||
|
class NullToEmptyObjectResolver : DefaultContractResolver
|
||||||
{
|
{
|
||||||
class NullToEmptyObjectResolver : DefaultContractResolver
|
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
|
||||||
{
|
{
|
||||||
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
|
return type.GetProperties()
|
||||||
{
|
.Select(p => {
|
||||||
return type.GetProperties()
|
var jp = base.CreateProperty(p, memberSerialization);
|
||||||
.Select(p => {
|
jp.ValueProvider = new NullToEmptyObjectValueProvider(p);
|
||||||
var jp = base.CreateProperty(p, memberSerialization);
|
return jp;
|
||||||
jp.ValueProvider = new NullToEmptyObjectValueProvider(p);
|
}).ToList();
|
||||||
return jp;
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,20 +4,19 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using West.TelegramBot.CodeWatcher.Options;
|
using West.TelegramBot.CodeWatcher.Options;
|
||||||
using West.TelegramBot.CodeWatcher.Service;
|
using West.TelegramBot.CodeWatcher.Service;
|
||||||
|
|
||||||
namespace West.TelegramBot.CodeWatcher
|
namespace West.TelegramBot.CodeWatcher;
|
||||||
|
|
||||||
|
public class CodeWatcher : Module
|
||||||
{
|
{
|
||||||
public class CodeWatcher : Module
|
public CodeWatcher(Core core) : base(core) {}
|
||||||
|
|
||||||
|
public override void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
public CodeWatcher(Core core) : base(core) {}
|
services.AddOption<CodeWatcherMode>();
|
||||||
|
|
||||||
public override void ConfigureServices(IServiceCollection services)
|
services.AddHttpClient<IHasteService, HasteService>(c =>
|
||||||
{
|
{
|
||||||
services.AddOption<CodeWatcherMode>();
|
c.BaseAddress = new Uri(Core.Configuration["HastebinUrl"]!);
|
||||||
|
});
|
||||||
services.AddHttpClient<IHasteService, HasteService>(c =>
|
|
||||||
{
|
|
||||||
c.BaseAddress = new Uri(Core.Configuration["HastebinUrl"]!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ using BotFramework.Enums;
|
|||||||
using Kruzya.TelegramBot.CombotAntiSpam.Combot;
|
using Kruzya.TelegramBot.CombotAntiSpam.Combot;
|
||||||
using Kruzya.TelegramBot.Core.Extensions;
|
using Kruzya.TelegramBot.Core.Extensions;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types;
|
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.CombotAntiSpam;
|
namespace Kruzya.TelegramBot.CombotAntiSpam;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider
|
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider;
|
||||||
|
|
||||||
|
public interface IXurPlaceProvider
|
||||||
{
|
{
|
||||||
public interface IXurPlaceProvider
|
public Task<string> GetPlaceAsync();
|
||||||
{
|
public string GetPlace();
|
||||||
public Task<string> GetPlaceAsync();
|
|
||||||
public string GetPlace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -2,27 +2,26 @@ using System.Threading.Tasks;
|
|||||||
using Fizzler.Systems.HtmlAgilityPack;
|
using Fizzler.Systems.HtmlAgilityPack;
|
||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider
|
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider;
|
||||||
|
|
||||||
|
public class XurWiki : IXurPlaceProvider
|
||||||
{
|
{
|
||||||
public class XurWiki : IXurPlaceProvider
|
public async Task<string> GetPlaceAsync()
|
||||||
{
|
{
|
||||||
public async Task<string> GetPlaceAsync()
|
var web = new HtmlWeb();
|
||||||
|
var document = await web.LoadFromWebAsync("https://xur.wiki");
|
||||||
|
|
||||||
|
var place = document.DocumentNode.QuerySelector(".location_name");
|
||||||
|
if (place != null)
|
||||||
{
|
{
|
||||||
var web = new HtmlWeb();
|
return place.InnerText.Trim();
|
||||||
var document = await web.LoadFromWebAsync("https://xur.wiki");
|
|
||||||
|
|
||||||
var place = document.DocumentNode.QuerySelector(".location_name");
|
|
||||||
if (place != null)
|
|
||||||
{
|
|
||||||
return place.InnerText.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPlace()
|
return "";
|
||||||
{
|
}
|
||||||
return GetPlaceAsync().GetAwaiter().GetResult();
|
|
||||||
}
|
public string GetPlace()
|
||||||
|
{
|
||||||
|
return GetPlaceAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,33 +5,32 @@ using Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider;
|
|||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur
|
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur;
|
||||||
|
|
||||||
|
public class RequestHandler : BotEventHandler
|
||||||
{
|
{
|
||||||
public class RequestHandler : BotEventHandler
|
protected readonly IXurPlaceProvider _placeProvider;
|
||||||
|
|
||||||
|
public RequestHandler(IXurPlaceProvider xurPlaceProvider)
|
||||||
{
|
{
|
||||||
protected readonly IXurPlaceProvider _placeProvider;
|
_placeProvider = xurPlaceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
public RequestHandler(IXurPlaceProvider xurPlaceProvider)
|
[Command("d2_xur")]
|
||||||
|
public async Task WhereIsXur()
|
||||||
|
{
|
||||||
|
var place = await _placeProvider.GetPlaceAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(place))
|
||||||
{
|
{
|
||||||
_placeProvider = xurPlaceProvider;
|
await Response("unknown");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("d2_xur")]
|
await Response(place);
|
||||||
public async Task WhereIsXur()
|
}
|
||||||
{
|
|
||||||
var place = await _placeProvider.GetPlaceAsync();
|
|
||||||
if (string.IsNullOrWhiteSpace(place))
|
|
||||||
{
|
|
||||||
await Response("unknown");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Response(place);
|
protected async Task Response(string place)
|
||||||
}
|
{
|
||||||
|
await Bot.SendTextMessageAsync(Chat, $"<b>Xûr</b> place is <code>{place}</code>", parseMode: ParseMode.Html);
|
||||||
protected async Task Response(string place)
|
|
||||||
{
|
|
||||||
await Bot.SendTextMessageAsync(Chat, $"<b>Xûr</b> place is <code>{place}</code>", parseMode: ParseMode.Html);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,17 +2,16 @@
|
|||||||
using Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider;
|
using Kruzya.TelegramBot.Destiny2.WhereIsXur.Provider;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur
|
namespace Kruzya.TelegramBot.Destiny2.WhereIsXur;
|
||||||
{
|
|
||||||
public class WhereIsXur : Module
|
|
||||||
{
|
|
||||||
public WhereIsXur(Core.Core core) : base(core)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ConfigureServices(IServiceCollection services)
|
public class WhereIsXur : Module
|
||||||
{
|
{
|
||||||
services.AddSingleton<IXurPlaceProvider, XurWiki>();
|
public WhereIsXur(Core.Core core) : base(core)
|
||||||
}
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IXurPlaceProvider, XurWiki>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,14 @@ using Kruzya.TelegramBot.Core;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
|
||||||
namespace West.Entertainment
|
namespace West.Entertainment;
|
||||||
{
|
|
||||||
public class Entertainment : Module
|
|
||||||
{
|
|
||||||
public Entertainment(Core core) : base(core) { }
|
|
||||||
|
|
||||||
public override void ConfigureServices(IServiceCollection services)
|
public class Entertainment : Module
|
||||||
{
|
{
|
||||||
services.AddSingleton<IGuessCache, GuessCache>();
|
public Entertainment(Core core) : base(core) { }
|
||||||
}
|
|
||||||
|
public override void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IGuessCache, GuessCache>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,52 +13,51 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace West.Entertainment.Handler
|
namespace West.Entertainment.Handler;
|
||||||
|
|
||||||
|
public class Common : BotEventHandler
|
||||||
{
|
{
|
||||||
public class Common : BotEventHandler
|
private readonly UserService _userService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly CoreContext _db;
|
||||||
|
|
||||||
|
public Common(CoreContext db, ILogger<Common> logger, UserService userService)
|
||||||
{
|
{
|
||||||
private readonly UserService _userService;
|
_db = db;
|
||||||
private readonly ILogger _logger;
|
_logger = logger;
|
||||||
private readonly CoreContext _db;
|
_userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
public Common(CoreContext db, ILogger<Common> logger, UserService userService)
|
[InChat(InChat.Public)]
|
||||||
|
[Command("cooldowns", CommandParseMode.Both)]
|
||||||
|
public async Task HandleCooldowns()
|
||||||
|
{
|
||||||
|
if (!_userService.IsUserSuper(From))
|
||||||
{
|
{
|
||||||
_db = db;
|
_logger.LogWarning("{User} attempted to use super-user command /cooldowns in {Chat}",
|
||||||
_logger = logger;
|
From.ToLog(),
|
||||||
_userService = userService;
|
Chat.ToLog()
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
var cds = new[] { "python", "javascript" };
|
||||||
[Command("cooldowns", CommandParseMode.Both)]
|
|
||||||
public async Task HandleCooldowns()
|
var msg = new HtmlString().Bold("Кулдауны:").Br();
|
||||||
|
foreach (var cd in cds)
|
||||||
{
|
{
|
||||||
if (!_userService.IsUserSuper(From))
|
var opt = await _db.UserValues.FindOrCreateOption(Chat.Id, $"{cd}NextUse");
|
||||||
|
var val = opt.GetValue<DateTime>();
|
||||||
|
if (val == default)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("{User} attempted to use super-user command /cooldowns in {Chat}",
|
continue;
|
||||||
From.ToLog(),
|
|
||||||
Chat.ToLog()
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cds = new[] { "python", "javascript" };
|
msg.Text($"{cd}: ")
|
||||||
|
.Code(DateTime.Now > val ? "Ready" : (val - DateTime.Now).ToString())
|
||||||
var msg = new HtmlString().Bold("Кулдауны:").Br();
|
.Br();
|
||||||
foreach (var cd in cds)
|
|
||||||
{
|
|
||||||
var opt = await _db.UserValues.FindOrCreateOption(Chat.Id, $"{cd}NextUse");
|
|
||||||
var val = opt.GetValue<DateTime>();
|
|
||||||
if (val == default)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
msg.Text($"{cd}: ")
|
|
||||||
.Code(DateTime.Now > val ? "Ready" : (val - DateTime.Now).ToString())
|
|
||||||
.Br();
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bot.SendTextMessageAsync(Chat.Id, msg.ToString(), parseMode: ParseMode.Html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Bot.SendTextMessageAsync(Chat.Id, msg.ToString(), parseMode: ParseMode.Html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,98 +11,97 @@ using Microsoft.Extensions.Logging;
|
|||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
namespace West.Entertainment.Handler.Fun
|
namespace West.Entertainment.Handler.Fun;
|
||||||
|
|
||||||
|
public abstract class AbstractAnimationReply : BotEventHandler
|
||||||
{
|
{
|
||||||
public abstract class AbstractAnimationReply : BotEventHandler
|
private readonly CoreContext _db;
|
||||||
|
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly ThreadSafeRandom _rng;
|
||||||
|
|
||||||
|
protected virtual TimeSpan Cooldown => TimeSpan.FromMinutes(_rng.Next(10, 1337));
|
||||||
|
|
||||||
|
protected Message? Message => RawUpdate.Message;
|
||||||
|
|
||||||
|
private User? RepliedUser => Message?.ReplyToMessage?.From;
|
||||||
|
|
||||||
|
protected abstract Regex AnimationMatch
|
||||||
{
|
{
|
||||||
private readonly CoreContext _db;
|
get;
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly ThreadSafeRandom _rng;
|
|
||||||
|
|
||||||
protected virtual TimeSpan Cooldown => TimeSpan.FromMinutes(_rng.Next(10, 1337));
|
|
||||||
|
|
||||||
protected Message? Message => RawUpdate.Message;
|
|
||||||
|
|
||||||
private User? RepliedUser => Message?.ReplyToMessage?.From;
|
|
||||||
|
|
||||||
protected abstract Regex AnimationMatch
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected AbstractAnimationReply(CoreContext db, ILogger logger, ThreadSafeRandom rng)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_logger = logger;
|
|
||||||
_rng = rng;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract string GetAnimationName();
|
|
||||||
|
|
||||||
protected virtual bool CanHandle() => true;
|
|
||||||
|
|
||||||
protected async Task HandleSetAnimation()
|
|
||||||
{
|
|
||||||
var animation = Message?.ReplyToMessage?.Animation;
|
|
||||||
var isAdmin = await Bot.IsUserAdminAsync(Chat, From);
|
|
||||||
|
|
||||||
_logger.LogDebug("IsAdmin: {IsAdmin}. FileId: {FileId}", isAdmin, animation?.FileId);
|
|
||||||
if (!isAdmin || animation == null) return;
|
|
||||||
|
|
||||||
var animationFileOption = await GetAnimationFileOption();
|
|
||||||
animationFileOption.SetValue(animation.FileId);
|
|
||||||
_db.AddOrUpdate(animationFileOption);
|
|
||||||
|
|
||||||
await Bot.SendTextMessageAsync(Chat.Id, "Animation has been set.", replyToMessageId: Message!.ReplyToMessage!.MessageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual async Task<bool> HandleAnimation()
|
|
||||||
{
|
|
||||||
var text = Message!.Text!.ToLower();
|
|
||||||
if (!AnimationMatch.IsMatch(text.ToLower()) || text.StartsWith("/set") || !CanHandle()) return false;
|
|
||||||
|
|
||||||
var animationFileId = (await GetAnimationFileOption()).GetValue(string.Empty);
|
|
||||||
var nextUseOption = await GetNextUseOption();
|
|
||||||
|
|
||||||
_logger.LogDebug("Animation: {Text}, ToLower: {ToLowerText}",
|
|
||||||
Message!.Text, Message!.Text!.ToLower());
|
|
||||||
_logger.LogDebug("AnimationFileId: {FileId}", animationFileId);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(animationFileId))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("AnimationFileId is empty. Ignoring.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextUseDt = nextUseOption.GetValue<DateTime>();
|
|
||||||
if (nextUseDt > DateTime.Now)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("{Name} is on cooldown for chat \"{ChatTitle}\". Will be available at {Time}",
|
|
||||||
GetAnimationName(), Chat.Title, nextUseDt);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bot.SendAnimationAsync(Chat.Id, new InputFileId(animationFileId),
|
|
||||||
replyToMessageId: GetMessageIdToReply() ?? Message?.MessageId);
|
|
||||||
|
|
||||||
nextUseOption.SetValue(DateTime.Now + Cooldown);
|
|
||||||
_db.AddOrUpdate(nextUseOption);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<BotUserValue> GetAnimationFileOption()
|
|
||||||
{
|
|
||||||
return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}File");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<BotUserValue> GetNextUseOption()
|
|
||||||
{
|
|
||||||
return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}NextUse");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual int? GetMessageIdToReply() => Message?.MessageId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AbstractAnimationReply(CoreContext db, ILogger logger, ThreadSafeRandom rng)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_logger = logger;
|
||||||
|
_rng = rng;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract string GetAnimationName();
|
||||||
|
|
||||||
|
protected virtual bool CanHandle() => true;
|
||||||
|
|
||||||
|
protected async Task HandleSetAnimation()
|
||||||
|
{
|
||||||
|
var animation = Message?.ReplyToMessage?.Animation;
|
||||||
|
var isAdmin = await Bot.IsUserAdminAsync(Chat, From);
|
||||||
|
|
||||||
|
_logger.LogDebug("IsAdmin: {IsAdmin}. FileId: {FileId}", isAdmin, animation?.FileId);
|
||||||
|
if (!isAdmin || animation == null) return;
|
||||||
|
|
||||||
|
var animationFileOption = await GetAnimationFileOption();
|
||||||
|
animationFileOption.SetValue(animation.FileId);
|
||||||
|
_db.AddOrUpdate(animationFileOption);
|
||||||
|
|
||||||
|
await Bot.SendTextMessageAsync(Chat.Id, "Animation has been set.", replyToMessageId: Message!.ReplyToMessage!.MessageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task<bool> HandleAnimation()
|
||||||
|
{
|
||||||
|
var text = Message!.Text!.ToLower();
|
||||||
|
if (!AnimationMatch.IsMatch(text.ToLower()) || text.StartsWith("/set") || !CanHandle()) return false;
|
||||||
|
|
||||||
|
var animationFileId = (await GetAnimationFileOption()).GetValue(string.Empty);
|
||||||
|
var nextUseOption = await GetNextUseOption();
|
||||||
|
|
||||||
|
_logger.LogDebug("Animation: {Text}, ToLower: {ToLowerText}",
|
||||||
|
Message!.Text, Message!.Text!.ToLower());
|
||||||
|
_logger.LogDebug("AnimationFileId: {FileId}", animationFileId);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(animationFileId))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("AnimationFileId is empty. Ignoring.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextUseDt = nextUseOption.GetValue<DateTime>();
|
||||||
|
if (nextUseDt > DateTime.Now)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("{Name} is on cooldown for chat \"{ChatTitle}\". Will be available at {Time}",
|
||||||
|
GetAnimationName(), Chat.Title, nextUseDt);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bot.SendAnimationAsync(Chat.Id, new InputFileId(animationFileId),
|
||||||
|
replyToMessageId: GetMessageIdToReply() ?? Message?.MessageId);
|
||||||
|
|
||||||
|
nextUseOption.SetValue(DateTime.Now + Cooldown);
|
||||||
|
_db.AddOrUpdate(nextUseOption);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BotUserValue> GetAnimationFileOption()
|
||||||
|
{
|
||||||
|
return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}File");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<BotUserValue> GetNextUseOption()
|
||||||
|
{
|
||||||
|
return await _db.UserValues.FindOrCreateOption(Chat.Id, $"{GetAnimationName()}NextUse");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual int? GetMessageIdToReply() => Message?.MessageId;
|
||||||
}
|
}
|
||||||
@@ -10,42 +10,41 @@ using Kruzya.TelegramBot.Core.Data;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
|
|
||||||
namespace West.Entertainment.Handler.Fun
|
namespace West.Entertainment.Handler.Fun;
|
||||||
|
|
||||||
|
public class Bayan : AbstractAnimationReply
|
||||||
{
|
{
|
||||||
public class Bayan : AbstractAnimationReply
|
protected override TimeSpan Cooldown => TimeSpan.Zero;
|
||||||
|
|
||||||
|
public Bayan(CoreContext db, ILogger<Bayan> logger, ThreadSafeRandom rng) : base(db, logger, rng) { }
|
||||||
|
|
||||||
|
protected override Regex AnimationMatch
|
||||||
|
=> new("^баян$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
protected override string GetAnimationName()
|
||||||
|
=> "bayan";
|
||||||
|
|
||||||
|
[InChat(InChat.Public)]
|
||||||
|
[Command("setbayan", CommandParseMode.Both)]
|
||||||
|
public async Task HandleSetBayan()
|
||||||
|
=> await HandleSetAnimation();
|
||||||
|
|
||||||
|
[InChat(InChat.Public)]
|
||||||
|
[Message(MessageFlag.HasText)]
|
||||||
|
public async Task HandleBayan()
|
||||||
{
|
{
|
||||||
protected override TimeSpan Cooldown => TimeSpan.Zero;
|
if (await HandleAnimation())
|
||||||
|
|
||||||
public Bayan(CoreContext db, ILogger<Bayan> logger, ThreadSafeRandom rng) : base(db, logger, rng) { }
|
|
||||||
|
|
||||||
protected override Regex AnimationMatch
|
|
||||||
=> new("^баян$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
protected override string GetAnimationName()
|
|
||||||
=> "bayan";
|
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
|
||||||
[Command("setbayan", CommandParseMode.Both)]
|
|
||||||
public async Task HandleSetBayan()
|
|
||||||
=> await HandleSetAnimation();
|
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
|
||||||
[Message(MessageFlag.HasText)]
|
|
||||||
public async Task HandleBayan()
|
|
||||||
{
|
{
|
||||||
if (await HandleAnimation())
|
await Bot.DeleteMessageAsync(Chat.Id, Message!.MessageId);
|
||||||
{
|
|
||||||
await Bot.DeleteMessageAsync(Chat.Id, Message!.MessageId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override int? GetMessageIdToReply()
|
|
||||||
{
|
|
||||||
return Message!.ReplyToMessage!.MessageId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool CanHandle()
|
|
||||||
=> Message?.ReplyToMessage != null;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override int? GetMessageIdToReply()
|
||||||
|
{
|
||||||
|
return Message!.ReplyToMessage!.MessageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CanHandle()
|
||||||
|
=> Message?.ReplyToMessage != null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,28 +6,27 @@ using Kruzya.TelegramBot.Core;
|
|||||||
using Kruzya.TelegramBot.Core.Data;
|
using Kruzya.TelegramBot.Core.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace West.Entertainment.Handler.Fun
|
namespace West.Entertainment.Handler.Fun;
|
||||||
{
|
|
||||||
public class JavaScript : AbstractAnimationReply
|
|
||||||
{
|
|
||||||
public JavaScript(CoreContext db, ILogger<JavaScript> logger, ThreadSafeRandom rng) : base(db, logger, rng) { }
|
|
||||||
|
|
||||||
protected override Regex AnimationMatch => new(
|
public class JavaScript : AbstractAnimationReply
|
||||||
@"((?<![\w])(жс|js|v8)(?![\w]))|жаваскрипт|javascript|жопаскрипт|жиес|жиэс|nodejs",
|
{
|
||||||
|
public JavaScript(CoreContext db, ILogger<JavaScript> logger, ThreadSafeRandom rng) : base(db, logger, rng) { }
|
||||||
|
|
||||||
|
protected override Regex AnimationMatch => new(
|
||||||
|
@"((?<![\w])(жс|js|v8)(?![\w]))|жаваскрипт|javascript|жопаскрипт|жиес|жиэс|nodejs",
|
||||||
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
protected override string GetAnimationName() => "javascript";
|
protected override string GetAnimationName() => "javascript";
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
[InChat(InChat.Public)]
|
||||||
[Command("setjs", CommandParseMode.Both)]
|
[Command("setjs", CommandParseMode.Both)]
|
||||||
public async Task HandleSetJs()
|
public async Task HandleSetJs()
|
||||||
=> await HandleSetAnimation();
|
=> await HandleSetAnimation();
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
[InChat(InChat.Public)]
|
||||||
[Message(MessageFlag.HasText)]
|
[Message(MessageFlag.HasText)]
|
||||||
public async Task HandleJs()
|
public async Task HandleJs()
|
||||||
{
|
{
|
||||||
await HandleAnimation();
|
await HandleAnimation();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,29 +8,28 @@ using Kruzya.TelegramBot.Core;
|
|||||||
using Kruzya.TelegramBot.Core.Data;
|
using Kruzya.TelegramBot.Core.Data;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace West.Entertainment.Handler.Fun
|
namespace West.Entertainment.Handler.Fun;
|
||||||
|
|
||||||
|
public class Python : AbstractAnimationReply
|
||||||
{
|
{
|
||||||
public class Python : AbstractAnimationReply
|
protected override Regex AnimationMatch =>
|
||||||
|
new("питон|пайтон|python|путхон|питухон|петухон",
|
||||||
|
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
protected override string GetAnimationName()
|
||||||
|
=> "python";
|
||||||
|
|
||||||
|
[InChat(InChat.Public)]
|
||||||
|
[Command("setpython", CommandParseMode.Both)]
|
||||||
|
public async Task HandleSetPython()
|
||||||
|
=> await HandleSetAnimation();
|
||||||
|
|
||||||
|
[InChat(InChat.Public)]
|
||||||
|
[Message(MessageFlag.HasText)]
|
||||||
|
public async Task HandlePython()
|
||||||
{
|
{
|
||||||
protected override Regex AnimationMatch =>
|
await HandleAnimation();
|
||||||
new("питон|пайтон|python|путхон|питухон|петухон",
|
|
||||||
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
protected override string GetAnimationName()
|
|
||||||
=> "python";
|
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
|
||||||
[Command("setpython", CommandParseMode.Both)]
|
|
||||||
public async Task HandleSetPython()
|
|
||||||
=> await HandleSetAnimation();
|
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
|
||||||
[Message(MessageFlag.HasText)]
|
|
||||||
public async Task HandlePython()
|
|
||||||
{
|
|
||||||
await HandleAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Python(CoreContext db, ILogger<Python> logger, ThreadSafeRandom rng) : base(db, logger, rng) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Python(CoreContext db, ILogger<Python> logger, ThreadSafeRandom rng) : base(db, logger, rng) { }
|
||||||
}
|
}
|
||||||
@@ -8,83 +8,82 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a feed in database.
|
||||||
|
/// </summary>
|
||||||
|
public class Feed : RepresentableEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
#region RepresentableEntity
|
||||||
/// Represents a feed in database.
|
protected override string ViewableText
|
||||||
/// </summary>
|
|
||||||
public class Feed : RepresentableEntity
|
|
||||||
{
|
{
|
||||||
#region RepresentableEntity
|
get => Title;
|
||||||
protected override string ViewableText
|
|
||||||
{
|
|
||||||
get => Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string ViewableUrl
|
|
||||||
{
|
|
||||||
get => HomePage;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The unique feed id.
|
|
||||||
/// </summary>
|
|
||||||
[Key]
|
|
||||||
public Guid FeedId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The feed URL.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The home page for this feed.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string HomePage { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Feed title.
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(256)]
|
|
||||||
[Required]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DateTime when feed fetched successfully in last time.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public DateTime UpdatedAt { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This flag indicates, bot should monitor changes in post date, or not.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
[DefaultValue(false)]
|
|
||||||
public bool WatchPostDate { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[DefaultValue(false)]
|
|
||||||
public bool IsPrivate { get; set; }
|
|
||||||
|
|
||||||
public Feed()
|
|
||||||
{
|
|
||||||
FeedId = new Guid();
|
|
||||||
UpdatedAt = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All posts from this feed.
|
|
||||||
/// </summary>
|
|
||||||
public virtual ICollection<Post> Posts { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All exists subscriber for this feed.
|
|
||||||
/// </summary>
|
|
||||||
public virtual ICollection<Subscriber> Subscribers { get; set; }
|
|
||||||
|
|
||||||
public override string ToString() => Title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override string ViewableUrl
|
||||||
|
{
|
||||||
|
get => HomePage;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The unique feed id.
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
public Guid FeedId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The feed URL.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The home page for this feed.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string HomePage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Feed title.
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(256)]
|
||||||
|
[Required]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DateTime when feed fetched successfully in last time.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This flag indicates, bot should monitor changes in post date, or not.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool WatchPostDate { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[DefaultValue(false)]
|
||||||
|
public bool IsPrivate { get; set; }
|
||||||
|
|
||||||
|
public Feed()
|
||||||
|
{
|
||||||
|
FeedId = new Guid();
|
||||||
|
UpdatedAt = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All posts from this feed.
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<Post> Posts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All exists subscriber for this feed.
|
||||||
|
/// </summary>
|
||||||
|
public virtual ICollection<Subscriber> Subscribers { get; set; }
|
||||||
|
|
||||||
|
public override string ToString() => Title;
|
||||||
}
|
}
|
||||||
@@ -9,99 +9,98 @@ using System.Text;
|
|||||||
using Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
using Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a post from feed in database.
|
||||||
|
/// </summary>
|
||||||
|
public class Post : RepresentableEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
#region RepresentableEntity
|
||||||
/// Represents a post from feed in database.
|
protected override ParseMode DefaultRepresentation
|
||||||
/// </summary>
|
|
||||||
public class Post : RepresentableEntity
|
|
||||||
{
|
{
|
||||||
#region RepresentableEntity
|
get => ParseMode.Html;
|
||||||
protected override ParseMode DefaultRepresentation
|
}
|
||||||
|
|
||||||
|
protected override string ViewableText
|
||||||
|
{
|
||||||
|
get => Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override string ViewableUrl
|
||||||
|
{
|
||||||
|
get => UtmUtility.ApplyParameters(new Uri(new(Feed.Url), Url), new UtmParameters()
|
||||||
{
|
{
|
||||||
get => ParseMode.Html;
|
Source = "telegram",
|
||||||
}
|
Type = "rss_post",
|
||||||
|
Campaign = "subscriber",
|
||||||
|
Content = PostId.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected override string ViewableText
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The unique post id.
|
||||||
|
/// </summary>
|
||||||
|
[Key]
|
||||||
|
public Guid PostId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The feed unique identifier from where this post is fetched.
|
||||||
|
/// </summary>
|
||||||
|
public Guid FeedId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The feed from where this post is fetched.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public virtual Feed Feed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The post title.
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(256)]
|
||||||
|
[Required]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URI where this post is located in web.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DateTime when post is created in RSS feed.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateTime PostedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DateTime when post is fetched/updated in database.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateTime ReceivedAt { get; set; }
|
||||||
|
|
||||||
|
public Post()
|
||||||
|
{
|
||||||
|
PostId = new Guid();
|
||||||
|
ReceivedAt = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string MessageText
|
||||||
|
{
|
||||||
|
get
|
||||||
{
|
{
|
||||||
get => Title;
|
var message = new StringBuilder();
|
||||||
}
|
message.Append($"{Representation("🗞")} New on {Feed.Representation(ParseMode.Html)}\n");
|
||||||
|
message.Append("\n");
|
||||||
|
message.Append(Title);
|
||||||
|
message.Append("\n");
|
||||||
|
message.Append(Representation("Open in browser"));
|
||||||
|
|
||||||
protected override string ViewableUrl
|
return message.ToString();
|
||||||
{
|
|
||||||
get => UtmUtility.ApplyParameters(new Uri(new(Feed.Url), Url), new UtmParameters()
|
|
||||||
{
|
|
||||||
Source = "telegram",
|
|
||||||
Type = "rss_post",
|
|
||||||
Campaign = "subscriber",
|
|
||||||
Content = PostId.ToString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The unique post id.
|
|
||||||
/// </summary>
|
|
||||||
[Key]
|
|
||||||
public Guid PostId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The feed unique identifier from where this post is fetched.
|
|
||||||
/// </summary>
|
|
||||||
public Guid FeedId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The feed from where this post is fetched.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public virtual Feed Feed { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The post title.
|
|
||||||
/// </summary>
|
|
||||||
[MaxLength(256)]
|
|
||||||
[Required]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The URI where this post is located in web.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
[MaxLength(256)]
|
|
||||||
public string Url { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DateTime when post is created in RSS feed.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public DateTime PostedAt { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DateTime when post is fetched/updated in database.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public DateTime ReceivedAt { get; set; }
|
|
||||||
|
|
||||||
public Post()
|
|
||||||
{
|
|
||||||
PostId = new Guid();
|
|
||||||
ReceivedAt = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string MessageText
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
var message = new StringBuilder();
|
|
||||||
message.Append($"{Representation("🗞")} New on {Feed.Representation(ParseMode.Html)}\n");
|
|
||||||
message.Append("\n");
|
|
||||||
message.Append(Title);
|
|
||||||
message.Append("\n");
|
|
||||||
message.Append(Representation("Open in browser"));
|
|
||||||
|
|
||||||
return message.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,55 +7,54 @@ using System;
|
|||||||
using System.Web;
|
using System.Web;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implements the basic logic for rendering Database Entities in messages.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class RepresentableEntity
|
||||||
{
|
{
|
||||||
/// <summary>
|
protected virtual string ViewableText { get; }
|
||||||
/// Implements the basic logic for rendering Database Entities in messages.
|
protected virtual string ViewableUrl { get; }
|
||||||
/// </summary>
|
protected virtual ParseMode DefaultRepresentation { get; }
|
||||||
public abstract class RepresentableEntity
|
|
||||||
|
public override string ToString() => Representation();
|
||||||
|
|
||||||
|
public string Representation(string text) => Representation(DefaultRepresentation, text);
|
||||||
|
|
||||||
|
public string Representation() => Representation(DefaultRepresentation);
|
||||||
|
|
||||||
|
public string Representation(ParseMode parseMode) => Representation(parseMode, ViewableText);
|
||||||
|
|
||||||
|
public string Representation(ParseMode parseMode, string text)
|
||||||
{
|
{
|
||||||
protected virtual string ViewableText { get; }
|
string content = String.Empty;
|
||||||
protected virtual string ViewableUrl { get; }
|
switch (parseMode)
|
||||||
protected virtual ParseMode DefaultRepresentation { get; }
|
|
||||||
|
|
||||||
public override string ToString() => Representation();
|
|
||||||
|
|
||||||
public string Representation(string text) => Representation(DefaultRepresentation, text);
|
|
||||||
|
|
||||||
public string Representation() => Representation(DefaultRepresentation);
|
|
||||||
|
|
||||||
public string Representation(ParseMode parseMode) => Representation(parseMode, ViewableText);
|
|
||||||
|
|
||||||
public string Representation(ParseMode parseMode, string text)
|
|
||||||
{
|
{
|
||||||
string content = String.Empty;
|
case ParseMode.Html:
|
||||||
switch (parseMode)
|
content = HtmlRepresentation(text);
|
||||||
{
|
break;
|
||||||
case ParseMode.Html:
|
|
||||||
content = HtmlRepresentation(text);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ParseMode.Markdown:
|
case ParseMode.Markdown:
|
||||||
case ParseMode.MarkdownV2:
|
case ParseMode.MarkdownV2:
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Representations
|
return content;
|
||||||
|
|
||||||
public string RawRepresentation(string text)
|
|
||||||
=> text;
|
|
||||||
|
|
||||||
public string HtmlRepresentation(string text)
|
|
||||||
{
|
|
||||||
var escapedText = HttpUtility.HtmlEncode(text);
|
|
||||||
var escapedUrl = HttpUtility.HtmlAttributeEncode(ViewableUrl);
|
|
||||||
|
|
||||||
return $"<a href=\"{escapedUrl}\">{escapedText}</a>";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Representations
|
||||||
|
|
||||||
|
public string RawRepresentation(string text)
|
||||||
|
=> text;
|
||||||
|
|
||||||
|
public string HtmlRepresentation(string text)
|
||||||
|
{
|
||||||
|
var escapedText = HttpUtility.HtmlEncode(text);
|
||||||
|
var escapedUrl = HttpUtility.HtmlAttributeEncode(ViewableUrl);
|
||||||
|
|
||||||
|
return $"<a href=\"{escapedUrl}\">{escapedText}</a>";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -1,58 +1,57 @@
|
|||||||
using Kruzya.TelegramBot.Core.EF;
|
using Kruzya.TelegramBot.Core.EF;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||||
|
|
||||||
|
public class RichSiteSummaryContext : AutoSaveDbContext
|
||||||
{
|
{
|
||||||
public class RichSiteSummaryContext : AutoSaveDbContext
|
/// <summary>
|
||||||
|
/// Repository with all feeds in database.
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<Feed> Feeds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository with all posts in database.
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<Post> Posts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repository with all subscriptions in database.
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<Subscriber> Subscriptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures the database is created and calls parent constructor.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
public RichSiteSummaryContext(DbContextOptions<RichSiteSummaryContext> options) : base(options)
|
||||||
{
|
{
|
||||||
/// <summary>
|
Database.EnsureCreated();
|
||||||
/// Repository with all feeds in database.
|
}
|
||||||
/// </summary>
|
|
||||||
public DbSet<Feed> Feeds { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Repository with all posts in database.
|
/// Setup the additional unique keys.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<Post> Posts { get; set; }
|
/// <param name="modelBuilder"></param>
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
// Setup unique keys for Feed entity.
|
||||||
|
modelBuilder.Entity<Feed>().HasIndex(feed => feed.Url).IsUnique();
|
||||||
|
modelBuilder.Entity<Feed>().HasIndex(feed => feed.HomePage).IsUnique();
|
||||||
|
|
||||||
/// <summary>
|
// Setup unique keys for Post entity.
|
||||||
/// Repository with all subscriptions in database.
|
modelBuilder.Entity<Post>()
|
||||||
/// </summary>
|
.HasOne(p => p.Feed)
|
||||||
public DbSet<Subscriber> Subscriptions { get; set; }
|
.WithMany(f => f.Posts)
|
||||||
|
.HasForeignKey(p => p.FeedId);
|
||||||
|
|
||||||
/// <summary>
|
modelBuilder.Entity<Post>().HasIndex(post =>
|
||||||
/// Ensures the database is created and calls parent constructor.
|
new {post.FeedId, post.Url}).IsUnique();
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
public RichSiteSummaryContext(DbContextOptions<RichSiteSummaryContext> options) : base(options)
|
|
||||||
{
|
|
||||||
Database.EnsureCreated();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
// Setup foreign keys for Subscriber entity.
|
||||||
/// Setup the additional unique keys.
|
modelBuilder.Entity<Subscriber>()
|
||||||
/// </summary>
|
.HasOne(s => s.Feed)
|
||||||
/// <param name="modelBuilder"></param>
|
.WithMany(f => f.Subscribers)
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
.HasForeignKey(s => s.FeedId);
|
||||||
{
|
|
||||||
// Setup unique keys for Feed entity.
|
|
||||||
modelBuilder.Entity<Feed>().HasIndex(feed => feed.Url).IsUnique();
|
|
||||||
modelBuilder.Entity<Feed>().HasIndex(feed => feed.HomePage).IsUnique();
|
|
||||||
|
|
||||||
// Setup unique keys for Post entity.
|
|
||||||
modelBuilder.Entity<Post>()
|
|
||||||
.HasOne(p => p.Feed)
|
|
||||||
.WithMany(f => f.Posts)
|
|
||||||
.HasForeignKey(p => p.FeedId);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Post>().HasIndex(post =>
|
|
||||||
new {post.FeedId, post.Url}).IsUnique();
|
|
||||||
|
|
||||||
// Setup foreign keys for Subscriber entity.
|
|
||||||
modelBuilder.Entity<Subscriber>()
|
|
||||||
.HasOne(s => s.Feed)
|
|
||||||
.WithMany(f => f.Subscribers)
|
|
||||||
.HasForeignKey(s => s.FeedId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,51 +6,50 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Data
|
namespace Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a subscription in database.
|
||||||
|
/// </summary>
|
||||||
|
public class Subscriber
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a subscription in database.
|
/// The unique Subscription identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Subscriber
|
[Key]
|
||||||
|
public Guid SubscriptionId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Telegram subscriber id.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public Int64 SubscriberId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unique feed identifier related with this Subscription. Identifies what user is read.
|
||||||
|
/// </summary>
|
||||||
|
public Guid FeedId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Feed related with this Subscription. Identifies what user is read.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public virtual Feed Feed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When user is subscribed on this feed.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public DateTime SubscribedAt { get; set; }
|
||||||
|
|
||||||
|
public Subscriber()
|
||||||
{
|
{
|
||||||
/// <summary>
|
SubscriptionId = new Guid();
|
||||||
/// The unique Subscription identifier.
|
SubscribedAt = DateTime.Now;
|
||||||
/// </summary>
|
}
|
||||||
[Key]
|
|
||||||
public Guid SubscriptionId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
public Subscriber(Feed feed) : base()
|
||||||
/// The Telegram subscriber id.
|
{
|
||||||
/// </summary>
|
Feed = feed;
|
||||||
[Required]
|
|
||||||
public Int64 SubscriberId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unique feed identifier related with this Subscription. Identifies what user is read.
|
|
||||||
/// </summary>
|
|
||||||
public Guid FeedId { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Feed related with this Subscription. Identifies what user is read.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public virtual Feed Feed { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When user is subscribed on this feed.
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public DateTime SubscribedAt { get; set; }
|
|
||||||
|
|
||||||
public Subscriber()
|
|
||||||
{
|
|
||||||
SubscriptionId = new Guid();
|
|
||||||
SubscribedAt = DateTime.Now;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Subscriber(Feed feed) : base()
|
|
||||||
{
|
|
||||||
Feed = feed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
using BotFramework;
|
using BotFramework;
|
||||||
using Kruzya.TelegramBot.RichSiteSummary.Data;
|
using Kruzya.TelegramBot.RichSiteSummary.Data;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler
|
namespace Kruzya.TelegramBot.RichSiteSummary.Handler;
|
||||||
{
|
|
||||||
public abstract class AbstractHandler : BotEventHandler
|
|
||||||
{
|
|
||||||
protected readonly RichSiteSummaryContext _dbContext;
|
|
||||||
|
|
||||||
protected AbstractHandler(RichSiteSummaryContext dbContext)
|
public abstract class AbstractHandler : BotEventHandler
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
protected readonly RichSiteSummaryContext _dbContext;
|
||||||
}
|
|
||||||
|
protected AbstractHandler(RichSiteSummaryContext dbContext)
|
||||||
|
{
|
||||||
|
_dbContext = dbContext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,299 +13,298 @@ using Telegram.Bot.Types;
|
|||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
using Telegram.Bot.Types.ReplyMarkups;
|
using Telegram.Bot.Types.ReplyMarkups;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler
|
namespace Kruzya.TelegramBot.RichSiteSummary.Handler;
|
||||||
|
|
||||||
|
public class FeedList : AbstractHandler
|
||||||
{
|
{
|
||||||
public class FeedList : AbstractHandler
|
public FeedList(RichSiteSummaryContext dbContext) : base(dbContext)
|
||||||
{
|
{
|
||||||
public FeedList(RichSiteSummaryContext dbContext) : base(dbContext)
|
}
|
||||||
|
|
||||||
|
[ParametrizedCommand("rss_show")]
|
||||||
|
public async Task ShowFeed(Feed feed) => await ShowFeed(feed, Chat, From);
|
||||||
|
|
||||||
|
public async Task ShowFeed(Guid feedId, Chat chat, User from)
|
||||||
|
{
|
||||||
|
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
||||||
|
if (feed == null)
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[ParametrizedCommand("rss_show")]
|
await ShowFeed(feed, chat, from);
|
||||||
public async Task ShowFeed(Feed feed) => await ShowFeed(feed, Chat, From);
|
}
|
||||||
|
|
||||||
public async Task ShowFeed(Guid feedId, Chat chat, User from)
|
public async Task ShowFeed(Feed feed, Chat chat, User from, Message message = null)
|
||||||
|
{
|
||||||
|
var textBuilder = new StringBuilder();
|
||||||
|
textBuilder.Append(feed.Representation(ParseMode.Html));
|
||||||
|
textBuilder.Append("\n");
|
||||||
|
textBuilder.Append($"<b>Posts at this moment</b>: {feed.Posts.Count}\n");
|
||||||
|
textBuilder.Append($"<b>RSS feed</b>: <code>{feed.Url}</code>\n");
|
||||||
|
textBuilder.Append($"<b>Home page</b>: <code>{feed.HomePage}</code>\n");
|
||||||
|
textBuilder.Append($"<b>Last synchronization</b>: <code>{feed.UpdatedAt}</code>");
|
||||||
|
|
||||||
|
var text = textBuilder.ToString();
|
||||||
|
|
||||||
|
var buttons = new List<InlineKeyboardButton>();
|
||||||
|
var callbackData = $"feed|{from.Id}|do|{feed.FeedId}";
|
||||||
|
if (await _dbContext.Subscriptions.HasSubscription(chat, feed))
|
||||||
{
|
{
|
||||||
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
buttons.Add(new InlineKeyboardButton("Unsubscribe")
|
||||||
if (feed == null)
|
|
||||||
{
|
{
|
||||||
return;
|
CallbackData = callbackData,
|
||||||
}
|
|
||||||
|
|
||||||
await ShowFeed(feed, chat, from);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task ShowFeed(Feed feed, Chat chat, User from, Message message = null)
|
|
||||||
{
|
|
||||||
var textBuilder = new StringBuilder();
|
|
||||||
textBuilder.Append(feed.Representation(ParseMode.Html));
|
|
||||||
textBuilder.Append("\n");
|
|
||||||
textBuilder.Append($"<b>Posts at this moment</b>: {feed.Posts.Count}\n");
|
|
||||||
textBuilder.Append($"<b>RSS feed</b>: <code>{feed.Url}</code>\n");
|
|
||||||
textBuilder.Append($"<b>Home page</b>: <code>{feed.HomePage}</code>\n");
|
|
||||||
textBuilder.Append($"<b>Last synchronization</b>: <code>{feed.UpdatedAt}</code>");
|
|
||||||
|
|
||||||
var text = textBuilder.ToString();
|
|
||||||
|
|
||||||
var buttons = new List<InlineKeyboardButton>();
|
|
||||||
var callbackData = $"feed|{from.Id}|do|{feed.FeedId}";
|
|
||||||
if (await _dbContext.Subscriptions.HasSubscription(chat, feed))
|
|
||||||
{
|
|
||||||
buttons.Add(new InlineKeyboardButton("Unsubscribe")
|
|
||||||
{
|
|
||||||
CallbackData = callbackData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttons.Add(new InlineKeyboardButton("Subscribe")
|
|
||||||
{
|
|
||||||
CallbackData = callbackData,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
buttons.Add(new InlineKeyboardButton("Last 10 posts")
|
|
||||||
{
|
|
||||||
CallbackData = $"feed|{from.Id}|last|{feed.FeedId}",
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buttons.Add(new InlineKeyboardButton("Subscribe")
|
||||||
|
{
|
||||||
|
CallbackData = callbackData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buttons.Add(new InlineKeyboardButton("Last 10 posts")
|
||||||
|
{
|
||||||
|
CallbackData = $"feed|{from.Id}|last|{feed.FeedId}",
|
||||||
|
});
|
||||||
|
|
||||||
var inlineButtons = new InlineKeyboardMarkup(buttons);
|
var inlineButtons = new InlineKeyboardMarkup(buttons);
|
||||||
if (message == null)
|
if (message == null)
|
||||||
|
{
|
||||||
|
await Bot.SendTextMessageAsync(new ChatId(chat.Id), text, 0, ParseMode.Html, null, true, true, false, 0, replyMarkup: inlineButtons);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Bot.EditMessageTextAsync(chat, message.MessageId, text, ParseMode.Html, null, true, replyMarkup: inlineButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ParametrizedCommand("rss_list")]
|
||||||
|
public async Task List() => await GenerateMenu();
|
||||||
|
|
||||||
|
[ParametrizedCommand("rss_search")]
|
||||||
|
public async Task Search(string query)
|
||||||
|
{
|
||||||
|
if (!(await GenerateMenu(query)))
|
||||||
|
{
|
||||||
|
await Bot.SendTextMessageAsync(Chat, $"No one feed match by search pattern (<code>{query}</code>).", 0, ParseMode.Html, null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command("rss_subscriptions")]
|
||||||
|
public async Task Subscriptions()
|
||||||
|
{
|
||||||
|
if (!(await GenerateMenu(":sub")))
|
||||||
|
{
|
||||||
|
await Bot.SendTextMessageAsync(Chat,
|
||||||
|
"There are no one subscription. Add new with /feed_list or /feed_search.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Message generator
|
||||||
|
|
||||||
|
protected async Task<bool> GenerateMenu() =>
|
||||||
|
await GenerateMenu(string.Empty);
|
||||||
|
|
||||||
|
protected async Task<bool> GenerateMenu(string query) =>
|
||||||
|
await GenerateMenu(query, 0);
|
||||||
|
|
||||||
|
protected async Task<bool> GenerateMenu(string query, UInt16 page, Message editableMessage = null)
|
||||||
|
{
|
||||||
|
var chat = editableMessage != null ? (editableMessage.Chat) : Chat;
|
||||||
|
var elementsOnPage = 9;
|
||||||
|
var startIndex = page * elementsOnPage;
|
||||||
|
var dbQuery = _dbContext.Feeds.AsQueryable()
|
||||||
|
.Where(f => !f.IsPrivate);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query))
|
||||||
|
{
|
||||||
|
var searchQuery = query.ToLower();
|
||||||
|
if (query.EndsWith(":sub"))
|
||||||
{
|
{
|
||||||
await Bot.SendTextMessageAsync(new ChatId(chat.Id), text, 0, ParseMode.Html, null, true, true, false, 0, replyMarkup: inlineButtons);
|
var chatList = await _dbContext.Subscriptions.AllSubscriptions(chat).Select(chat => chat.Feed.FeedId).ToArrayAsync();
|
||||||
}
|
dbQuery = dbQuery.Where(sub => chatList.Contains(sub.FeedId));
|
||||||
else
|
|
||||||
{
|
searchQuery = searchQuery.Remove(searchQuery.Length - 4, 4);
|
||||||
await Bot.EditMessageTextAsync(chat, message.MessageId, text, ParseMode.Html, null, true, replyMarkup: inlineButtons);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbQuery = dbQuery.Where(feed => feed.Title.ToLower().Contains(searchQuery) || feed.Url.ToLower().Contains(searchQuery) || feed.HomePage.ToLower().Contains(searchQuery));
|
||||||
}
|
}
|
||||||
|
|
||||||
[ParametrizedCommand("rss_list")]
|
var totalCount = await dbQuery.CountAsync();
|
||||||
public async Task List() => await GenerateMenu();
|
if (totalCount == 0)
|
||||||
|
|
||||||
[ParametrizedCommand("rss_search")]
|
|
||||||
public async Task Search(string query)
|
|
||||||
{
|
{
|
||||||
if (!(await GenerateMenu(query)))
|
return false;
|
||||||
{
|
|
||||||
await Bot.SendTextMessageAsync(Chat, $"No one feed match by search pattern (<code>{query}</code>).", 0, ParseMode.Html, null, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("rss_subscriptions")]
|
if (totalCount == 1)
|
||||||
public async Task Subscriptions()
|
|
||||||
{
|
{
|
||||||
if (!(await GenerateMenu(":sub")))
|
await ShowFeed((await dbQuery.FirstOrDefaultAsync()), Chat, From);
|
||||||
{
|
|
||||||
await Bot.SendTextMessageAsync(Chat,
|
|
||||||
"There are no one subscription. Add new with /feed_list or /feed_search.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Message generator
|
|
||||||
|
|
||||||
protected async Task<bool> GenerateMenu() =>
|
|
||||||
await GenerateMenu(string.Empty);
|
|
||||||
|
|
||||||
protected async Task<bool> GenerateMenu(string query) =>
|
|
||||||
await GenerateMenu(query, 0);
|
|
||||||
|
|
||||||
protected async Task<bool> GenerateMenu(string query, UInt16 page, Message editableMessage = null)
|
|
||||||
{
|
|
||||||
var chat = editableMessage != null ? (editableMessage.Chat) : Chat;
|
|
||||||
var elementsOnPage = 9;
|
|
||||||
var startIndex = page * elementsOnPage;
|
|
||||||
var dbQuery = _dbContext.Feeds.AsQueryable()
|
|
||||||
.Where(f => !f.IsPrivate);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(query))
|
|
||||||
{
|
|
||||||
var searchQuery = query.ToLower();
|
|
||||||
if (query.EndsWith(":sub"))
|
|
||||||
{
|
|
||||||
var chatList = await _dbContext.Subscriptions.AllSubscriptions(chat).Select(chat => chat.Feed.FeedId).ToArrayAsync();
|
|
||||||
dbQuery = dbQuery.Where(sub => chatList.Contains(sub.FeedId));
|
|
||||||
|
|
||||||
searchQuery = searchQuery.Remove(searchQuery.Length - 4, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
dbQuery = dbQuery.Where(feed => feed.Title.ToLower().Contains(searchQuery) || feed.Url.ToLower().Contains(searchQuery) || feed.HomePage.ToLower().Contains(searchQuery));
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalCount = await dbQuery.CountAsync();
|
|
||||||
if (totalCount == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount == 1)
|
|
||||||
{
|
|
||||||
await ShowFeed((await dbQuery.FirstOrDefaultAsync()), Chat, From);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await dbQuery.Take(elementsOnPage * (page + 1)).Skip(startIndex)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
var backButton = (page > 0);
|
|
||||||
var nextButton = totalCount >= (startIndex + elementsOnPage);
|
|
||||||
|
|
||||||
var buttons = new List<List<InlineKeyboardButton>>();
|
|
||||||
List<InlineKeyboardButton> row;
|
|
||||||
foreach (var feed in result)
|
|
||||||
{
|
|
||||||
row = new List<InlineKeyboardButton>();
|
|
||||||
row.Add(new InlineKeyboardButton(feed.Title)
|
|
||||||
{
|
|
||||||
CallbackData = $"feed|{From.Id}|show|{feed.FeedId.ToString()}",
|
|
||||||
});
|
|
||||||
|
|
||||||
buttons.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backButton || nextButton)
|
|
||||||
{
|
|
||||||
row = new List<InlineKeyboardButton>();
|
|
||||||
if (backButton)
|
|
||||||
{
|
|
||||||
row.Add(new InlineKeyboardButton("⬅️")
|
|
||||||
{
|
|
||||||
CallbackData = $"feed|{From.Id}|page|{query}|{page - 1}",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
row.Add(new InlineKeyboardButton($"{page + 1}/{(totalCount / elementsOnPage) + 1}")
|
|
||||||
{
|
|
||||||
CallbackData = $"feed|{From.Id}|donothing",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (nextButton)
|
|
||||||
{
|
|
||||||
row.Add(new InlineKeyboardButton("➡️")
|
|
||||||
{
|
|
||||||
CallbackData = $"feed|{From.Id}|page|{query}|{page + 1}",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
buttons.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editableMessage == null)
|
|
||||||
{
|
|
||||||
await Bot.SendTextMessageAsync(Chat, "Select the feed for viewing details", 0, null,
|
|
||||||
null, true, true, false, 0, replyMarkup: new InlineKeyboardMarkup(buttons));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Bot.EditMessageTextAsync(editableMessage.Chat, editableMessage.MessageId, "Select the feed for viewing details", null, null, false,
|
|
||||||
new InlineKeyboardMarkup(buttons));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
var result = await dbQuery.Take(elementsOnPage * (page + 1)).Skip(startIndex)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
#region Message handler
|
var backButton = (page > 0);
|
||||||
|
var nextButton = totalCount >= (startIndex + elementsOnPage);
|
||||||
|
|
||||||
[Update(UpdateFlag.CallbackQuery)]
|
var buttons = new List<List<InlineKeyboardButton>>();
|
||||||
public async Task HandleAction()
|
List<InlineKeyboardButton> row;
|
||||||
|
foreach (var feed in result)
|
||||||
{
|
{
|
||||||
if (!RawUpdate.CallbackQuery.Data.StartsWith("feed|"))
|
row = new List<InlineKeyboardButton>();
|
||||||
|
row.Add(new InlineKeyboardButton(feed.Title)
|
||||||
{
|
{
|
||||||
return;
|
CallbackData = $"feed|{From.Id}|show|{feed.FeedId.ToString()}",
|
||||||
}
|
});
|
||||||
|
|
||||||
var data = RawUpdate.CallbackQuery.Data.Split('|');
|
buttons.Add(row);
|
||||||
var fromId = Int64.Parse(data[1]);
|
|
||||||
if (fromId != From.Id)
|
|
||||||
{
|
|
||||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
|
||||||
"You can't perform this action: command called by another user.", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check action.
|
|
||||||
switch (data[2])
|
|
||||||
{
|
|
||||||
case "page":
|
|
||||||
await GenerateMenu(data[3], UInt16.Parse(data[4]), RawUpdate.CallbackQuery.Message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "show":
|
|
||||||
await ShowFeed(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "do":
|
|
||||||
if (RawUpdate.CallbackQuery.From.Id != RawUpdate.CallbackQuery.Message.Chat.Id)
|
|
||||||
{
|
|
||||||
var userStatus = (await Bot.GetChatMemberAsync(RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From.Id)).Status;
|
|
||||||
var allowedUserStatuses = new ChatMemberStatus[]
|
|
||||||
{ChatMemberStatus.Administrator, ChatMemberStatus.Creator};
|
|
||||||
|
|
||||||
if (!allowedUserStatuses.Contains(userStatus))
|
|
||||||
{
|
|
||||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
|
||||||
"You can't perform this action: you don't have administrator permissions.", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ChangeFeedSubscription(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From, RawUpdate.CallbackQuery.Message);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "last":
|
|
||||||
var feed = await _dbContext.FindAsync<Feed>(Guid.Parse(data[3]));
|
|
||||||
var posts = feed.Posts.OrderByDescending(p => p.ReceivedAt).Take(10).ToList();
|
|
||||||
|
|
||||||
var messageTextBuilder = new StringBuilder();
|
|
||||||
messageTextBuilder.Append($"Last 10 posts from {feed.Representation(ParseMode.Html)}\n");
|
|
||||||
messageTextBuilder.Append("\n");
|
|
||||||
|
|
||||||
var idx = 1;
|
|
||||||
foreach (var post in posts)
|
|
||||||
{
|
|
||||||
messageTextBuilder.Append($"{idx}. {post}\n");
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bot.SendTextMessageAsync(RawUpdate.CallbackQuery.Message.Chat, messageTextBuilder.ToString(),
|
|
||||||
0, ParseMode.Html, null, true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ChangeFeedSubscription(Guid feedId, Chat chat, User from, Message message)
|
if (backButton || nextButton)
|
||||||
{
|
{
|
||||||
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
row = new List<InlineKeyboardButton>();
|
||||||
if (feed == null)
|
if (backButton)
|
||||||
{
|
{
|
||||||
return;
|
row.Add(new InlineKeyboardButton("⬅️")
|
||||||
|
{
|
||||||
|
CallbackData = $"feed|{From.Id}|page|{query}|{page - 1}",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var subscriptionState = await _dbContext.Subscriptions.HasSubscription(chat, feed);
|
row.Add(new InlineKeyboardButton($"{page + 1}/{(totalCount / elementsOnPage) + 1}")
|
||||||
if (subscriptionState)
|
|
||||||
{
|
{
|
||||||
var subId = await _dbContext.Subscriptions
|
CallbackData = $"feed|{From.Id}|donothing",
|
||||||
.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
});
|
||||||
.Select(subscriber => subscriber.SubscriptionId).FirstAsync();
|
|
||||||
|
|
||||||
_dbContext.Remove(_dbContext.Find<Subscriber>(subId));
|
if (nextButton)
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
var subscription = _dbContext.Subscriptions.Create();
|
row.Add(new InlineKeyboardButton("➡️")
|
||||||
subscription.Feed = feed;
|
{
|
||||||
subscription.SubscriberId = chat.Id;
|
CallbackData = $"feed|{From.Id}|page|{query}|{page + 1}",
|
||||||
|
});
|
||||||
_dbContext.Add(subscription);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await _dbContext.SaveChangesAsync();
|
buttons.Add(row);
|
||||||
await ShowFeed(feed, chat, from, message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
if (editableMessage == null)
|
||||||
|
{
|
||||||
|
await Bot.SendTextMessageAsync(Chat, "Select the feed for viewing details", 0, null,
|
||||||
|
null, true, true, false, 0, replyMarkup: new InlineKeyboardMarkup(buttons));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Bot.EditMessageTextAsync(editableMessage.Chat, editableMessage.MessageId, "Select the feed for viewing details", null, null, false,
|
||||||
|
new InlineKeyboardMarkup(buttons));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Message handler
|
||||||
|
|
||||||
|
[Update(UpdateFlag.CallbackQuery)]
|
||||||
|
public async Task HandleAction()
|
||||||
|
{
|
||||||
|
if (!RawUpdate.CallbackQuery.Data.StartsWith("feed|"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = RawUpdate.CallbackQuery.Data.Split('|');
|
||||||
|
var fromId = Int64.Parse(data[1]);
|
||||||
|
if (fromId != From.Id)
|
||||||
|
{
|
||||||
|
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
||||||
|
"You can't perform this action: command called by another user.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check action.
|
||||||
|
switch (data[2])
|
||||||
|
{
|
||||||
|
case "page":
|
||||||
|
await GenerateMenu(data[3], UInt16.Parse(data[4]), RawUpdate.CallbackQuery.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "show":
|
||||||
|
await ShowFeed(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "do":
|
||||||
|
if (RawUpdate.CallbackQuery.From.Id != RawUpdate.CallbackQuery.Message.Chat.Id)
|
||||||
|
{
|
||||||
|
var userStatus = (await Bot.GetChatMemberAsync(RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From.Id)).Status;
|
||||||
|
var allowedUserStatuses = new ChatMemberStatus[]
|
||||||
|
{ChatMemberStatus.Administrator, ChatMemberStatus.Creator};
|
||||||
|
|
||||||
|
if (!allowedUserStatuses.Contains(userStatus))
|
||||||
|
{
|
||||||
|
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id,
|
||||||
|
"You can't perform this action: you don't have administrator permissions.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ChangeFeedSubscription(Guid.Parse(data[3]), RawUpdate.CallbackQuery.Message.Chat, RawUpdate.CallbackQuery.From, RawUpdate.CallbackQuery.Message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "last":
|
||||||
|
var feed = await _dbContext.FindAsync<Feed>(Guid.Parse(data[3]));
|
||||||
|
var posts = feed.Posts.OrderByDescending(p => p.ReceivedAt).Take(10).ToList();
|
||||||
|
|
||||||
|
var messageTextBuilder = new StringBuilder();
|
||||||
|
messageTextBuilder.Append($"Last 10 posts from {feed.Representation(ParseMode.Html)}\n");
|
||||||
|
messageTextBuilder.Append("\n");
|
||||||
|
|
||||||
|
var idx = 1;
|
||||||
|
foreach (var post in posts)
|
||||||
|
{
|
||||||
|
messageTextBuilder.Append($"{idx}. {post}\n");
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bot.SendTextMessageAsync(RawUpdate.CallbackQuery.Message.Chat, messageTextBuilder.ToString(),
|
||||||
|
0, ParseMode.Html, null, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bot.AnswerCallbackQueryAsync(RawUpdate.CallbackQuery.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task ChangeFeedSubscription(Guid feedId, Chat chat, User from, Message message)
|
||||||
|
{
|
||||||
|
var feed = await _dbContext.FindAsync<Feed>(feedId);
|
||||||
|
if (feed == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subscriptionState = await _dbContext.Subscriptions.HasSubscription(chat, feed);
|
||||||
|
if (subscriptionState)
|
||||||
|
{
|
||||||
|
var subId = await _dbContext.Subscriptions
|
||||||
|
.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
||||||
|
.Select(subscriber => subscriber.SubscriptionId).FirstAsync();
|
||||||
|
|
||||||
|
_dbContext.Remove(_dbContext.Find<Subscriber>(subId));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var subscription = _dbContext.Subscriptions.Create();
|
||||||
|
subscription.Feed = feed;
|
||||||
|
subscription.SubscriberId = chat.Id;
|
||||||
|
|
||||||
|
_dbContext.Add(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _dbContext.SaveChangesAsync();
|
||||||
|
await ShowFeed(feed, chat, from, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Telegram.Bot;
|
using Telegram.Bot;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Handler
|
namespace Kruzya.TelegramBot.RichSiteSummary.Handler;
|
||||||
|
|
||||||
|
public class StatsHandler : AbstractHandler
|
||||||
{
|
{
|
||||||
public class StatsHandler : AbstractHandler
|
public StatsHandler(RichSiteSummaryContext dbContext) : base(dbContext)
|
||||||
{
|
{
|
||||||
public StatsHandler(RichSiteSummaryContext dbContext) : base(dbContext)
|
}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[Command("rss_stats")]
|
[Command("rss_stats")]
|
||||||
public async Task Execute()
|
public async Task Execute()
|
||||||
{
|
{
|
||||||
var feedCount = await _dbContext.Feeds.CountAsync();
|
var feedCount = await _dbContext.Feeds.CountAsync();
|
||||||
var postsCount = await _dbContext.Posts.CountAsync();
|
var postsCount = await _dbContext.Posts.CountAsync();
|
||||||
var subscriptionsCount = await _dbContext.Subscriptions.CountAsync();
|
var subscriptionsCount = await _dbContext.Subscriptions.CountAsync();
|
||||||
var uniqueSubscribers =
|
var uniqueSubscribers =
|
||||||
await _dbContext.Subscriptions.Select(sub => sub.SubscriberId).Distinct().CountAsync();
|
await _dbContext.Subscriptions.Select(sub => sub.SubscriberId).Distinct().CountAsync();
|
||||||
|
|
||||||
var textMessage = new StringBuilder();
|
var textMessage = new StringBuilder();
|
||||||
textMessage.Append("ℹ️ In database on this moment:\n");
|
textMessage.Append("ℹ️ In database on this moment:\n");
|
||||||
textMessage.Append($"- registered <b>{feedCount} feeds</b>\n");
|
textMessage.Append($"- registered <b>{feedCount} feeds</b>\n");
|
||||||
textMessage.Append($"- saved <b>{postsCount} posts</b> for prevent re-sending\n");
|
textMessage.Append($"- saved <b>{postsCount} posts</b> for prevent re-sending\n");
|
||||||
textMessage.Append(
|
textMessage.Append(
|
||||||
$"- exists <b>{subscriptionsCount} subscriptions on feeds</b> (<b>unique subscribers: {uniqueSubscribers}</b>)");
|
$"- exists <b>{subscriptionsCount} subscriptions on feeds</b> (<b>unique subscribers: {uniqueSubscribers}</b>)");
|
||||||
|
|
||||||
await Bot.SendTextMessageAsync(Chat, textMessage.ToString(), parseMode: ParseMode.Html);
|
await Bot.SendTextMessageAsync(Chat, textMessage.ToString(), parseMode: ParseMode.Html);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,31 +7,30 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||||
|
|
||||||
|
public class RichSiteSummary : Module
|
||||||
{
|
{
|
||||||
public class RichSiteSummary : Module
|
public RichSiteSummary(Core.Core core) : base(core)
|
||||||
{
|
{
|
||||||
public RichSiteSummary(Core.Core core) : base(core)
|
}
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ConfigureServices(IServiceCollection services)
|
public override void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
var connectionString = Configuration.GetConnectionString("RichSiteSummary");
|
var connectionString = Configuration.GetConnectionString("RichSiteSummary");
|
||||||
services.AddDbContext<RichSiteSummaryContext>(options =>
|
services.AddDbContext<RichSiteSummaryContext>(options =>
|
||||||
options.UseLazyLoadingProxies()
|
options.UseLazyLoadingProxies()
|
||||||
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
||||||
|
|
||||||
services.AddTelegramBotParameterParser<Feed, DbResolverParameter<Feed, RichSiteSummaryContext>>()
|
services.AddTelegramBotParameterParser<Feed, DbResolverParameter<Feed, RichSiteSummaryContext>>()
|
||||||
.AddTelegramBotParameterParser<Post, DbResolverParameter<Post, RichSiteSummaryContext>>()
|
.AddTelegramBotParameterParser<Post, DbResolverParameter<Post, RichSiteSummaryContext>>()
|
||||||
.AddTelegramBotParameterParser<Subscriber, DbResolverParameter<Subscriber, RichSiteSummaryContext>>();
|
.AddTelegramBotParameterParser<Subscriber, DbResolverParameter<Subscriber, RichSiteSummaryContext>>();
|
||||||
|
|
||||||
services.AddQueue<UserMessage>()
|
services.AddQueue<UserMessage>()
|
||||||
.AddQueue<UserUnsubscribe>();
|
.AddQueue<UserUnsubscribe>();
|
||||||
|
|
||||||
services.AddHostedService<RssFetch>()
|
services.AddHostedService<RssFetch>()
|
||||||
.AddHostedService<Unsubscriber>()
|
.AddHostedService<Unsubscriber>()
|
||||||
.AddHostedService<MessageSender>();
|
.AddHostedService<MessageSender>();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,68 +5,67 @@ using Kruzya.TelegramBot.RichSiteSummary.Data;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||||
|
|
||||||
|
internal static class RichSiteSummaryRepositoryExtension
|
||||||
{
|
{
|
||||||
internal static class RichSiteSummaryRepositoryExtension
|
#region Subscriber
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searchs the <see cref="Subscriber"/>, who reads a <see cref="Feed"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
/// <param name="feed"></param>
|
||||||
|
/// <returns>The array with all subscribers.</returns>
|
||||||
|
public static async Task<Subscriber[]> ByFeed(this DbSet<Subscriber> repository, Feed feed)
|
||||||
{
|
{
|
||||||
#region Subscriber
|
return await repository.Where(subscriber => subscriber.Feed == feed)
|
||||||
|
.ToArrayAsync();
|
||||||
/// <summary>
|
|
||||||
/// Searchs the <see cref="Subscriber"/>, who reads a <see cref="Feed"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
/// <param name="feed"></param>
|
|
||||||
/// <returns>The array with all subscribers.</returns>
|
|
||||||
public static async Task<Subscriber[]> ByFeed(this DbSet<Subscriber> repository, Feed feed)
|
|
||||||
{
|
|
||||||
return await repository.Where(subscriber => subscriber.Feed == feed)
|
|
||||||
.ToArrayAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IQueryable<Subscriber> AllSubscriptions(this DbSet<Subscriber> repository, Chat chat)
|
|
||||||
{
|
|
||||||
return repository.Where(entity => entity.SubscriberId == chat.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<bool> HasSubscription(this DbSet<Subscriber> repository, Chat chat, Feed feed)
|
|
||||||
{
|
|
||||||
return (await repository.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
|
||||||
.CountAsync()) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Feed
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Searchs the <see cref="Feed"/> for fetching.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
/// <param name="period">The period (in seconds) from last fetching.</param>
|
|
||||||
/// <param name="count">Max results count</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period, UInt16 count) =>
|
|
||||||
await repository.Where(feed => feed.UpdatedAt < (DateTime.Now - TimeSpan.FromSeconds(period)))
|
|
||||||
.OrderBy(feed => feed.UpdatedAt)
|
|
||||||
.Take(count)
|
|
||||||
.ToArrayAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Searchs the <see cref="Feed"/> for fetching.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="repository"></param>
|
|
||||||
/// <param name="period">The period (in seconds) from last fetching.</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period) =>
|
|
||||||
await repository.ForFetching(period, 25);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Post
|
|
||||||
|
|
||||||
public static async Task<Post> ByFeedAndUrl(this DbSet<Post> repository, string url, Feed feed = null) =>
|
|
||||||
await repository.FirstOrDefaultAsync(post => post.Url == url && (feed == null || post.Feed == feed));
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IQueryable<Subscriber> AllSubscriptions(this DbSet<Subscriber> repository, Chat chat)
|
||||||
|
{
|
||||||
|
return repository.Where(entity => entity.SubscriberId == chat.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> HasSubscription(this DbSet<Subscriber> repository, Chat chat, Feed feed)
|
||||||
|
{
|
||||||
|
return (await repository.Where(subscriber => subscriber.Feed == feed && subscriber.SubscriberId == chat.Id)
|
||||||
|
.CountAsync()) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Feed
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searchs the <see cref="Feed"/> for fetching.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
/// <param name="period">The period (in seconds) from last fetching.</param>
|
||||||
|
/// <param name="count">Max results count</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period, UInt16 count) =>
|
||||||
|
await repository.Where(feed => feed.UpdatedAt < (DateTime.Now - TimeSpan.FromSeconds(period)))
|
||||||
|
.OrderBy(feed => feed.UpdatedAt)
|
||||||
|
.Take(count)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searchs the <see cref="Feed"/> for fetching.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repository"></param>
|
||||||
|
/// <param name="period">The period (in seconds) from last fetching.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<Feed[]> ForFetching(this DbSet<Feed> repository, UInt16 period) =>
|
||||||
|
await repository.ForFetching(period, 25);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Post
|
||||||
|
|
||||||
|
public static async Task<Post> ByFeedAndUrl(this DbSet<Post> repository, string url, Feed feed = null) =>
|
||||||
|
await repository.FirstOrDefaultAsync(post => post.Url == url && (feed == null || post.Feed == feed));
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -9,81 +9,80 @@ using Telegram.Bot;
|
|||||||
using Telegram.Bot.Exceptions;
|
using Telegram.Bot.Exceptions;
|
||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service
|
namespace Kruzya.TelegramBot.RichSiteSummary.Service;
|
||||||
|
|
||||||
|
public class MessageSender : AbstractTimedHostedService
|
||||||
{
|
{
|
||||||
public class MessageSender : AbstractTimedHostedService
|
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
protected readonly ConcurrentQueue<UserMessage> Queue;
|
||||||
|
protected readonly ConcurrentQueue<UserUnsubscribe> UnsubscribeQueue;
|
||||||
|
|
||||||
|
public MessageSender(ILogger<MessageSender> logger, IBotInstance bot, ConcurrentQueue<UserMessage> userMessageQueue, ConcurrentQueue<UserUnsubscribe> userUnsubscribeQueue) : base(logger, bot)
|
||||||
{
|
{
|
||||||
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
UnsubscribeQueue = userUnsubscribeQueue;
|
||||||
|
Queue = userMessageQueue;
|
||||||
|
}
|
||||||
|
|
||||||
protected readonly ConcurrentQueue<UserMessage> Queue;
|
protected override async Task OnRun()
|
||||||
protected readonly ConcurrentQueue<UserUnsubscribe> UnsubscribeQueue;
|
{
|
||||||
|
if (Queue.IsEmpty || !Queue.TryDequeue(out var message))
|
||||||
public MessageSender(ILogger<MessageSender> logger, IBotInstance bot, ConcurrentQueue<UserMessage> userMessageQueue, ConcurrentQueue<UserUnsubscribe> userUnsubscribeQueue) : base(logger, bot)
|
|
||||||
{
|
{
|
||||||
UnsubscribeQueue = userUnsubscribeQueue;
|
return;
|
||||||
Queue = userMessageQueue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnRun()
|
_logger.LogDebug("Message for {ChatId} dequeued.", message.ChatId.Identifier);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (Queue.IsEmpty || !Queue.TryDequeue(out var message))
|
if (string.IsNullOrWhiteSpace(message.ImageUrl))
|
||||||
{
|
{
|
||||||
|
await _bot.BotClient.SendTextMessageAsync(message.ChatId, message.Text, 0, message.ParseMode, null,
|
||||||
|
message.DisableWebPagePreview);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _bot.BotClient.SendPhotoAsync(message.ChatId, new InputFileUrl(message.ImageUrl), 0, message.Text,
|
||||||
|
message.ParseMode, null, false, message.DisableWebPagePreview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ApiRequestException e)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
||||||
|
if (!e.Message.Contains("bot was blocked by user"))
|
||||||
|
{
|
||||||
|
ReEnqueue(message, e, message.ChatId); // looks like a network issue
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Message for {ChatId} dequeued.", message.ChatId.Identifier);
|
// User added bot to blacklist.
|
||||||
try
|
// Unsubscribe him.
|
||||||
|
UnsubscribeQueue.Enqueue(new UserUnsubscribe
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(message.ImageUrl))
|
ChatId = message.ChatId
|
||||||
{
|
});
|
||||||
await _bot.BotClient.SendTextMessageAsync(message.ChatId, message.Text, 0, message.ParseMode, null,
|
|
||||||
message.DisableWebPagePreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _bot.BotClient.SendPhotoAsync(message.ChatId, new InputFileUrl(message.ImageUrl), 0, message.Text,
|
|
||||||
message.ParseMode, null, false, message.DisableWebPagePreview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ApiRequestException e)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
|
||||||
if (!e.Message.Contains("bot was blocked by user"))
|
|
||||||
{
|
|
||||||
ReEnqueue(message, e, message.ChatId); // looks like a network issue
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User added bot to blacklist.
|
|
||||||
// Unsubscribe him.
|
|
||||||
UnsubscribeQueue.Enqueue(new UserUnsubscribe
|
|
||||||
{
|
|
||||||
ChatId = message.ChatId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
|
||||||
ReEnqueue(message, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
private void ReEnqueue(UserMessage message, Exception e = null, ChatId chatId = null)
|
|
||||||
{
|
{
|
||||||
if (e != null)
|
_logger.LogDebug("Message for {ChatId} is failed: {error}.", message.ChatId.Identifier, e.Message);
|
||||||
{
|
ReEnqueue(message, e);
|
||||||
var eMessage = new StringBuilder();
|
|
||||||
eMessage.Append(e.Message);
|
|
||||||
if (chatId != null)
|
|
||||||
{
|
|
||||||
eMessage.Append($" ({chatId.Identifier})");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogError(eMessage.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
Queue.Enqueue(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReEnqueue(UserMessage message, Exception e = null, ChatId chatId = null)
|
||||||
|
{
|
||||||
|
if (e != null)
|
||||||
|
{
|
||||||
|
var eMessage = new StringBuilder();
|
||||||
|
eMessage.Append(e.Message);
|
||||||
|
if (chatId != null)
|
||||||
|
{
|
||||||
|
eMessage.Append($" ({chatId.Identifier})");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogError(eMessage.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue.Enqueue(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -20,242 +20,241 @@ using Feed = Kruzya.TelegramBot.RichSiteSummary.Data.Feed;
|
|||||||
|
|
||||||
using CodeHollow.FeedReader.Feeds;
|
using CodeHollow.FeedReader.Feeds;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service
|
namespace Kruzya.TelegramBot.RichSiteSummary.Service;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a RSS feed parsing.
|
||||||
|
/// Grabs the all feeds from database.
|
||||||
|
///
|
||||||
|
/// TODO: move entity grabbing to another service.
|
||||||
|
/// </summary>
|
||||||
|
public class RssFetch : IHostedService, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
private Timer _timer;
|
||||||
/// Performs a RSS feed parsing.
|
|
||||||
/// Grabs the all feeds from database.
|
private readonly ILogger _logger;
|
||||||
///
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
/// TODO: move entity grabbing to another service.
|
private readonly ConcurrentQueue<UserMessage> _queue;
|
||||||
/// </summary>
|
|
||||||
public class RssFetch : IHostedService, IDisposable
|
private static TimeSpan TimerPeriod => TimeSpan.FromSeconds(45);
|
||||||
|
|
||||||
|
public RssFetch(ILogger<RssFetch> logger, ConcurrentQueue<UserMessage> queue, IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
private Timer _timer;
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
_queue = queue;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
#region IHostedService
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
/// <summary>
|
||||||
private readonly ConcurrentQueue<UserMessage> _queue;
|
/// Initializes the RSS fetcher timer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RSS fetcher service is starting.");
|
||||||
|
_timer = new Timer(DoFetch, null, TimeSpan.Zero, TimerPeriod);
|
||||||
|
|
||||||
private static TimeSpan TimerPeriod => TimeSpan.FromSeconds(45);
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public RssFetch(ILogger<RssFetch> logger, ConcurrentQueue<UserMessage> queue, IServiceScopeFactory scopeFactory)
|
/// <summary>
|
||||||
|
/// Stops the RSS fetcher timer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("RSS fetcher 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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs the job of fetching RSS data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state"></param>
|
||||||
|
private async void DoFetch(object state)
|
||||||
|
{
|
||||||
|
_timer.Change(Timeout.Infinite, 0);
|
||||||
|
_logger.LogDebug("RSS fetcher service is triggered.");
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_scopeFactory = scopeFactory;
|
using var scope = _scopeFactory.CreateScope();
|
||||||
_logger = logger;
|
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
||||||
_queue = queue;
|
var period = scope.ServiceProvider.GetRequiredService<IConfiguration>()
|
||||||
}
|
.GetValue<ushort>("rssFetchPeriod");
|
||||||
|
|
||||||
#region IHostedService
|
var feeds = await dbContext.Feeds.ForFetching(period);
|
||||||
/// <summary>
|
_logger.LogDebug("Received {count} feeds for fetching", new {count = feeds.Length});
|
||||||
/// Initializes the RSS fetcher timer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("RSS fetcher service is starting.");
|
|
||||||
_timer = new Timer(DoFetch, null, TimeSpan.Zero, TimerPeriod);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
foreach (var feed in feeds)
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops the RSS fetcher timer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("RSS fetcher 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
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs the job of fetching RSS data.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="state"></param>
|
|
||||||
private async void DoFetch(object state)
|
|
||||||
{
|
|
||||||
_timer.Change(Timeout.Infinite, 0);
|
|
||||||
_logger.LogDebug("RSS fetcher service is triggered.");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
await ProcessFeed(feed, dbContext);
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
|
||||||
var period = scope.ServiceProvider.GetRequiredService<IConfiguration>()
|
|
||||||
.GetValue<ushort>("rssFetchPeriod");
|
|
||||||
|
|
||||||
var feeds = await dbContext.Feeds.ForFetching(period);
|
feed.UpdatedAt = DateTime.Now;
|
||||||
_logger.LogDebug("Received {count} feeds for fetching", new {count = feeds.Length});
|
dbContext.Update(feed);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var feed in feeds)
|
await dbContext.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_timer.Change(TimerPeriod, TimerPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Feeds
|
||||||
|
|
||||||
|
private async Task ProcessFeed(Feed feed, RichSiteSummaryContext dbContext)
|
||||||
|
{
|
||||||
|
var parsedFeed = await FetchFeed(feed);
|
||||||
|
if (parsedFeed == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Post feedPost;
|
||||||
|
var newPosts = new List<Post>();
|
||||||
|
var postsImages = new Dictionary<Post, string>();
|
||||||
|
foreach (var post in parsedFeed.Items)
|
||||||
|
{
|
||||||
|
feedPost = await dbContext.Posts.ByFeedAndUrl(post.Link, feed);
|
||||||
|
if (feedPost != null)
|
||||||
|
{
|
||||||
|
// skip. This post already exists.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
feedPost = dbContext.Posts.Create();
|
||||||
|
feedPost.Feed = feed;
|
||||||
|
feedPost.Title = post.Title;
|
||||||
|
feedPost.Url = post.Link;
|
||||||
|
feedPost.PostedAt = post.PublishingDate.GetValueOrDefault(DateTime.Now);
|
||||||
|
|
||||||
|
postsImages.Add(feedPost, await FetchImageUrl(post));
|
||||||
|
newPosts.Add(feedPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPosts.Count > 0)
|
||||||
|
{
|
||||||
|
var subscribers = await dbContext.Subscriptions.ByFeed(feed);
|
||||||
|
foreach (var post in newPosts)
|
||||||
|
{
|
||||||
|
var text = post.MessageText;
|
||||||
|
foreach (var subscriber in subscribers)
|
||||||
{
|
{
|
||||||
await ProcessFeed(feed, dbContext);
|
var message = new UserMessage
|
||||||
|
|
||||||
feed.UpdatedAt = DateTime.Now;
|
|
||||||
dbContext.Update(feed);
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_timer.Change(TimerPeriod, TimerPeriod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Feeds
|
|
||||||
|
|
||||||
private async Task ProcessFeed(Feed feed, RichSiteSummaryContext dbContext)
|
|
||||||
{
|
|
||||||
var parsedFeed = await FetchFeed(feed);
|
|
||||||
if (parsedFeed == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Post feedPost;
|
|
||||||
var newPosts = new List<Post>();
|
|
||||||
var postsImages = new Dictionary<Post, string>();
|
|
||||||
foreach (var post in parsedFeed.Items)
|
|
||||||
{
|
|
||||||
feedPost = await dbContext.Posts.ByFeedAndUrl(post.Link, feed);
|
|
||||||
if (feedPost != null)
|
|
||||||
{
|
|
||||||
// skip. This post already exists.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
feedPost = dbContext.Posts.Create();
|
|
||||||
feedPost.Feed = feed;
|
|
||||||
feedPost.Title = post.Title;
|
|
||||||
feedPost.Url = post.Link;
|
|
||||||
feedPost.PostedAt = post.PublishingDate.GetValueOrDefault(DateTime.Now);
|
|
||||||
|
|
||||||
postsImages.Add(feedPost, await FetchImageUrl(post));
|
|
||||||
newPosts.Add(feedPost);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPosts.Count > 0)
|
|
||||||
{
|
|
||||||
var subscribers = await dbContext.Subscriptions.ByFeed(feed);
|
|
||||||
foreach (var post in newPosts)
|
|
||||||
{
|
|
||||||
var text = post.MessageText;
|
|
||||||
foreach (var subscriber in subscribers)
|
|
||||||
{
|
{
|
||||||
var message = new UserMessage
|
ChatId = new ChatId(subscriber.SubscriberId), DisableWebPagePreview = true,
|
||||||
{
|
ParseMode = ParseMode.Html, Text = text, ImageUrl = postsImages.GetValueOrDefault(post)
|
||||||
ChatId = new ChatId(subscriber.SubscriberId), DisableWebPagePreview = true,
|
};
|
||||||
ParseMode = ParseMode.Html, Text = text, ImageUrl = postsImages.GetValueOrDefault(post)
|
|
||||||
};
|
|
||||||
|
|
||||||
_queue.Enqueue(message);
|
_queue.Enqueue(message);
|
||||||
_logger.LogDebug("Queued message for {SubscriberId}", subscriber.SubscriberId);
|
_logger.LogDebug("Queued message for {SubscriberId}", subscriber.SubscriberId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<CodeHollow.FeedReader.Feed> FetchFeed(Feed feed)
|
private async Task<CodeHollow.FeedReader.Feed> FetchFeed(Feed feed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await FeedReader.ReadAsync(feed.Url);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError("Feed {feed} can't be fetched: {error}", feed, e.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Image parsing
|
||||||
|
|
||||||
|
private async Task<string> FetchImageUrl(FeedItem item)
|
||||||
|
{
|
||||||
|
var feedItem = item.SpecificItem;
|
||||||
|
if (feedItem is Rss20FeedItem rss20FeedItem)
|
||||||
|
{
|
||||||
|
var enclosure = rss20FeedItem.Enclosure;
|
||||||
|
if (enclosure != null && IsValidImage(enclosure.MediaType))
|
||||||
|
{
|
||||||
|
return enclosure.Url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await FetchImageUrl(item.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> FetchImageUrl(string description)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(description))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
using var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true };
|
||||||
|
using var httpClient = new HttpClient(httpClientHandler);
|
||||||
|
|
||||||
|
var imgRegEx = new Regex(@"<img[^>]+>");
|
||||||
|
var srcRegEx = new Regex("src=\"([^\"]+)\"");
|
||||||
|
var matches = imgRegEx.Matches(description);
|
||||||
|
|
||||||
|
var img = "";
|
||||||
|
foreach (Match match in matches)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await FeedReader.ReadAsync(feed.Url);
|
var srcData = srcRegEx.Match(match.Value);
|
||||||
|
var content = srcData.Groups[1].Value;
|
||||||
|
|
||||||
|
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, content));
|
||||||
|
if (response.StatusCode == HttpStatusCode.OK &&
|
||||||
|
IsValidImage(response.Content.Headers.ContentType.MediaType))
|
||||||
|
{
|
||||||
|
img = content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError("Feed {feed} can't be fetched: {error}", feed, e.Message);
|
_logger.LogError("Caused error when fetching image for RSS post: {error}", e.Message);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
return img;
|
||||||
|
|
||||||
#region Image parsing
|
|
||||||
|
|
||||||
private async Task<string> FetchImageUrl(FeedItem item)
|
|
||||||
{
|
|
||||||
var feedItem = item.SpecificItem;
|
|
||||||
if (feedItem is Rss20FeedItem rss20FeedItem)
|
|
||||||
{
|
|
||||||
var enclosure = rss20FeedItem.Enclosure;
|
|
||||||
if (enclosure != null && IsValidImage(enclosure.MediaType))
|
|
||||||
{
|
|
||||||
return enclosure.Url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await FetchImageUrl(item.Description);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> FetchImageUrl(string description)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(description))
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
using var httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true };
|
|
||||||
using var httpClient = new HttpClient(httpClientHandler);
|
|
||||||
|
|
||||||
var imgRegEx = new Regex(@"<img[^>]+>");
|
|
||||||
var srcRegEx = new Regex("src=\"([^\"]+)\"");
|
|
||||||
var matches = imgRegEx.Matches(description);
|
|
||||||
|
|
||||||
var img = "";
|
|
||||||
foreach (Match match in matches)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var srcData = srcRegEx.Match(match.Value);
|
|
||||||
var content = srcData.Groups[1].Value;
|
|
||||||
|
|
||||||
var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, content));
|
|
||||||
if (response.StatusCode == HttpStatusCode.OK &&
|
|
||||||
IsValidImage(response.Content.Headers.ContentType.MediaType))
|
|
||||||
{
|
|
||||||
img = content;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
_logger.LogError("Caused error when fetching image for RSS post: {error}", e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if image is valid for Telegram Bot API.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="contentType">Content-type for received content</param>
|
|
||||||
/// <returns>True, if Telegram maybe can "use" this image, false if not.</returns>
|
|
||||||
private static bool IsValidImage(string contentType)
|
|
||||||
{
|
|
||||||
return new[]
|
|
||||||
{
|
|
||||||
"image/jpeg",
|
|
||||||
"image/bmp",
|
|
||||||
"image/png"
|
|
||||||
}.Contains(contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if image is valid for Telegram Bot API.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentType">Content-type for received content</param>
|
||||||
|
/// <returns>True, if Telegram maybe can "use" this image, false if not.</returns>
|
||||||
|
private static bool IsValidImage(string contentType)
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
"image/jpeg",
|
||||||
|
"image/bmp",
|
||||||
|
"image/png"
|
||||||
|
}.Contains(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -9,41 +9,40 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.Service
|
namespace Kruzya.TelegramBot.RichSiteSummary.Service;
|
||||||
|
|
||||||
|
public class Unsubscriber : AbstractTimedHostedService
|
||||||
{
|
{
|
||||||
public class Unsubscriber : AbstractTimedHostedService
|
protected readonly ConcurrentQueue<UserUnsubscribe> Queue;
|
||||||
|
protected readonly IServiceScopeFactory ScopeFactory;
|
||||||
|
|
||||||
|
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
||||||
|
|
||||||
|
public Unsubscriber(ILogger<Unsubscriber> logger, IBotInstance bot, ConcurrentQueue<UserUnsubscribe> queue, IServiceScopeFactory scopeFactory) : base(logger, bot)
|
||||||
{
|
{
|
||||||
protected readonly ConcurrentQueue<UserUnsubscribe> Queue;
|
ScopeFactory = scopeFactory;
|
||||||
protected readonly IServiceScopeFactory ScopeFactory;
|
Queue = queue;
|
||||||
|
}
|
||||||
|
|
||||||
protected override TimeSpan TimerPeriod => TimeSpan.FromSeconds(1);
|
protected override async Task OnRun()
|
||||||
|
{
|
||||||
public Unsubscriber(ILogger<Unsubscriber> logger, IBotInstance bot, ConcurrentQueue<UserUnsubscribe> queue, IServiceScopeFactory scopeFactory) : base(logger, bot)
|
if (Queue.IsEmpty || !Queue.TryDequeue(out var user))
|
||||||
{
|
{
|
||||||
ScopeFactory = scopeFactory;
|
return;
|
||||||
Queue = queue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnRun()
|
await UnsubscribeUser(user.ChatId);
|
||||||
{
|
}
|
||||||
if (Queue.IsEmpty || !Queue.TryDequeue(out var user))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await UnsubscribeUser(user.ChatId);
|
private async Task UnsubscribeUser(ChatId chatId)
|
||||||
}
|
{
|
||||||
|
using var scope = ScopeFactory.CreateScope();
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
||||||
|
|
||||||
private async Task UnsubscribeUser(ChatId chatId)
|
var subscriptions = dbContext.Subscriptions;
|
||||||
{
|
subscriptions.RemoveRange(
|
||||||
using var scope = ScopeFactory.CreateScope();
|
subscriptions.Where(s => s.SubscriberId == chatId.Identifier)
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<RichSiteSummaryContext>();
|
);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
var subscriptions = dbContext.Subscriptions;
|
|
||||||
subscriptions.RemoveRange(
|
|
||||||
subscriptions.Where(s => s.SubscriberId == chatId.Identifier)
|
|
||||||
);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule
|
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
||||||
|
|
||||||
|
public struct UtmParameters
|
||||||
{
|
{
|
||||||
public struct UtmParameters
|
public string Source;
|
||||||
{
|
public string Type;
|
||||||
public string Source;
|
public string Campaign;
|
||||||
public string Type;
|
public string Term;
|
||||||
public string Campaign;
|
public string Content;
|
||||||
public string Term;
|
|
||||||
public string Content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,49 +3,48 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule
|
namespace Kruzya.TelegramBot.RichSiteSummary.UrchinTrackingModule;
|
||||||
|
|
||||||
|
public class UtmUtility
|
||||||
{
|
{
|
||||||
public class UtmUtility
|
public static string ApplyParameters(string url, UtmParameters parameters)
|
||||||
|
=> ApplyParameters(new Uri(url), parameters);
|
||||||
|
|
||||||
|
public static string ApplyParameters(Uri uri, UtmParameters parameters)
|
||||||
{
|
{
|
||||||
public static string ApplyParameters(string url, UtmParameters parameters)
|
var query = HttpUtility.ParseQueryString(uri.Query);
|
||||||
=> ApplyParameters(new Uri(url), parameters);
|
|
||||||
|
|
||||||
public static string ApplyParameters(Uri uri, UtmParameters parameters)
|
SetIfNotNull(query, "utm_source", parameters.Source);
|
||||||
|
SetIfNotNull(query, "utm_medium", parameters.Type);
|
||||||
|
SetIfNotNull(query, "utm_campaign", parameters.Campaign);
|
||||||
|
SetIfNotNull(query, "utm_term", parameters.Term);
|
||||||
|
SetIfNotNull(query, "utm_content", parameters.Content);
|
||||||
|
|
||||||
|
var uriBuilder = new UriBuilder(uri);
|
||||||
|
uriBuilder.Query = BuildQueryString(query);
|
||||||
|
return uriBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static string BuildQueryString(NameValueCollection query)
|
||||||
|
{
|
||||||
|
var queryParameters = new List<string>();
|
||||||
|
|
||||||
|
foreach (var keyName in query.AllKeys)
|
||||||
{
|
{
|
||||||
var query = HttpUtility.ParseQueryString(uri.Query);
|
var value = query[keyName];
|
||||||
|
queryParameters.Add($"{HttpUtility.UrlEncode(keyName)}={HttpUtility.UrlEncode(value)}");
|
||||||
SetIfNotNull(query, "utm_source", parameters.Source);
|
|
||||||
SetIfNotNull(query, "utm_medium", parameters.Type);
|
|
||||||
SetIfNotNull(query, "utm_campaign", parameters.Campaign);
|
|
||||||
SetIfNotNull(query, "utm_term", parameters.Term);
|
|
||||||
SetIfNotNull(query, "utm_content", parameters.Content);
|
|
||||||
|
|
||||||
var uriBuilder = new UriBuilder(uri);
|
|
||||||
uriBuilder.Query = BuildQueryString(query);
|
|
||||||
return uriBuilder.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static string BuildQueryString(NameValueCollection query)
|
return String.Join('&', queryParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static void SetIfNotNull(NameValueCollection queryParameteters, string key, string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
{
|
{
|
||||||
var queryParameters = new List<string>();
|
return;
|
||||||
|
|
||||||
foreach (var keyName in query.AllKeys)
|
|
||||||
{
|
|
||||||
var value = query[keyName];
|
|
||||||
queryParameters.Add($"{HttpUtility.UrlEncode(keyName)}={HttpUtility.UrlEncode(value)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return String.Join('&', queryParameters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static void SetIfNotNull(NameValueCollection queryParameteters, string key, string value)
|
queryParameteters.Set(key, value);
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParameteters.Set(key, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||||
|
|
||||||
|
public struct UserMessage
|
||||||
{
|
{
|
||||||
public struct UserMessage
|
public ChatId ChatId;
|
||||||
{
|
public string Text;
|
||||||
public ChatId ChatId;
|
public ParseMode ParseMode;
|
||||||
public string Text;
|
public bool DisableWebPagePreview;
|
||||||
public ParseMode ParseMode;
|
public string ImageUrl;
|
||||||
public bool DisableWebPagePreview;
|
|
||||||
public string ImageUrl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.RichSiteSummary
|
namespace Kruzya.TelegramBot.RichSiteSummary;
|
||||||
|
|
||||||
|
public struct UserUnsubscribe
|
||||||
{
|
{
|
||||||
public struct UserUnsubscribe
|
public ChatId ChatId;
|
||||||
{
|
|
||||||
public ChatId ChatId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -5,46 +5,46 @@ using Telegram.Bot;
|
|||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.UrlLimitations
|
namespace Kruzya.TelegramBot.UrlLimitations;
|
||||||
|
|
||||||
|
public static class MessageExtension
|
||||||
{
|
{
|
||||||
public static class MessageExtension
|
public static async Task<bool> HasUnknownMention(this Message message, ITelegramBotClient bot)
|
||||||
{
|
{
|
||||||
public static async Task<bool> HasUnknownMention(this Message message, ITelegramBotClient bot)
|
foreach (var mention in message.Entities.Where(e =>
|
||||||
|
e.Type == MessageEntityType.Mention || e.Type == MessageEntityType.TextMention))
|
||||||
{
|
{
|
||||||
foreach (var mention in message.Entities.Where(e =>
|
long userId = 0;
|
||||||
e.Type == MessageEntityType.Mention || e.Type == MessageEntityType.TextMention))
|
if (mention.Type == MessageEntityType.TextMention)
|
||||||
{
|
{
|
||||||
long userId = 0;
|
userId = mention.User.Id;
|
||||||
if (mention.Type == MessageEntityType.TextMention)
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
userId = mention.User.Id;
|
var chat = await bot.GetChatAsync(
|
||||||
}
|
new ChatId(message.Text.Substring(mention.Offset, mention.Length)));
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var chat = await bot.GetChatAsync(
|
|
||||||
new ChatId(message.Text.Substring(mention.Offset, mention.Length)));
|
|
||||||
|
|
||||||
if (chat != null)
|
if (chat != null)
|
||||||
|
{
|
||||||
|
if (chat.Type != ChatType.Private)
|
||||||
{
|
{
|
||||||
if (chat.Type != ChatType.Private)
|
return true;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
userId = Convert.ToInt64(chat.Id);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (Exception)
|
userId = Convert.ToInt64(chat.Id);
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var status = (await bot.GetChatMemberAsync(message.Chat, userId)).Status;
|
var status = (await bot.GetChatMemberAsync(message.Chat, userId)).Status;
|
||||||
|
|
||||||
if ((new ChatMemberStatus[]
|
if ((new ChatMemberStatus[]
|
||||||
{
|
{
|
||||||
ChatMemberStatus.Administrator, ChatMemberStatus.Creator, ChatMemberStatus.Member,
|
ChatMemberStatus.Administrator, ChatMemberStatus.Creator, ChatMemberStatus.Member,
|
||||||
ChatMemberStatus.Restricted
|
ChatMemberStatus.Restricted
|
||||||
@@ -52,12 +52,11 @@ namespace Kruzya.TelegramBot.UrlLimitations
|
|||||||
// i'm not sure about "Restricted"
|
// i'm not sure about "Restricted"
|
||||||
// maybe leaved members with some restrictions also can be "Restricted".
|
// maybe leaved members with some restrictions also can be "Restricted".
|
||||||
}).Any(e => e == status) == false)
|
}).Any(e => e == status) == false)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,49 +11,48 @@ using Telegram.Bot;
|
|||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.UrlLimitations
|
namespace Kruzya.TelegramBot.UrlLimitations;
|
||||||
{
|
|
||||||
public class MessageHandler : BotEventHandler
|
|
||||||
{
|
|
||||||
protected ILogger Logger;
|
|
||||||
protected readonly CoreContext dbCtx;
|
|
||||||
|
|
||||||
public MessageHandler(ILogger<MessageHandler> logger, CoreContext coreContext)
|
public class MessageHandler : BotEventHandler
|
||||||
|
{
|
||||||
|
protected ILogger Logger;
|
||||||
|
protected readonly CoreContext dbCtx;
|
||||||
|
|
||||||
|
public MessageHandler(ILogger<MessageHandler> logger, CoreContext coreContext)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
dbCtx = coreContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
[InChat(InChat.Public)]
|
||||||
|
[Message(MessageFlag.HasText)]
|
||||||
|
public async Task OnMessageReceived()
|
||||||
|
{
|
||||||
|
var option = await dbCtx.UserValues.FindOption(Chat.Id, From.Id, "urlLimitations.messagesAfterJoin");
|
||||||
|
if (option == null)
|
||||||
{
|
{
|
||||||
Logger = logger;
|
return;
|
||||||
dbCtx = coreContext;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
var messageCount = option.GetValue<UInt16>();
|
||||||
[Message(MessageFlag.HasText)]
|
if (messageCount > 10)
|
||||||
public async Task OnMessageReceived()
|
|
||||||
{
|
{
|
||||||
var option = await dbCtx.UserValues.FindOption(Chat.Id, From.Id, "urlLimitations.messagesAfterJoin");
|
dbCtx.UserValues.Remove(option);
|
||||||
if (option == null)
|
return;
|
||||||
{
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var messageCount = option.GetValue<UInt16>();
|
option.SetValue(messageCount + 1);
|
||||||
if (messageCount > 10)
|
var message = RawUpdate.Message;
|
||||||
{
|
if (message.Entities.Count(e => e.Type == MessageEntityType.Url || e.Type == MessageEntityType.TextLink) >
|
||||||
dbCtx.UserValues.Remove(option);
|
0 || await message.HasUnknownMention(Bot))
|
||||||
return;
|
{
|
||||||
}
|
var targetChat = new ChatId(Chat.Id);
|
||||||
|
Task.WaitAll(
|
||||||
option.SetValue(messageCount + 1);
|
Bot.BanChatMemberAsync(new ChatId(Chat.Id), From.Id, DateTime.Now.AddDays(1)),
|
||||||
var message = RawUpdate.Message;
|
Bot.DeleteMessageAsync(targetChat, message.MessageId),
|
||||||
if (message.Entities.Count(e => e.Type == MessageEntityType.Url || e.Type == MessageEntityType.TextLink) >
|
Bot.SendTextMessageAsync(targetChat,
|
||||||
0 || await message.HasUnknownMention(Bot))
|
|
||||||
{
|
|
||||||
var targetChat = new ChatId(Chat.Id);
|
|
||||||
Task.WaitAll(
|
|
||||||
Bot.BanChatMemberAsync(new ChatId(Chat.Id), From.Id, DateTime.Now.AddDays(1)),
|
|
||||||
Bot.DeleteMessageAsync(targetChat, message.MessageId),
|
|
||||||
Bot.SendTextMessageAsync(targetChat,
|
|
||||||
$"Member {From.ToHtml(true)} has been banned.\n<b>Reason</b>: <pre>Spam suspicion</pre>", parseMode: ParseMode.Html)
|
$"Member {From.ToHtml(true)} has been banned.\n<b>Reason</b>: <pre>Spam suspicion</pre>", parseMode: ParseMode.Html)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
using Kruzya.TelegramBot.Core;
|
using Kruzya.TelegramBot.Core;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.UrlLimitations
|
namespace Kruzya.TelegramBot.UrlLimitations;
|
||||||
|
|
||||||
|
public class UrlLimitations : Module
|
||||||
{
|
{
|
||||||
public class UrlLimitations : Module
|
public UrlLimitations(Core.Core core) : base(core)
|
||||||
{
|
{
|
||||||
public UrlLimitations(Core.Core core) : base(core)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,24 +6,23 @@ using BotFramework.Enums;
|
|||||||
using Kruzya.TelegramBot.Core.Data;
|
using Kruzya.TelegramBot.Core.Data;
|
||||||
using Kruzya.TelegramBot.Core.Extensions;
|
using Kruzya.TelegramBot.Core.Extensions;
|
||||||
|
|
||||||
namespace Kruzya.TelegramBot.UrlLimitations
|
namespace Kruzya.TelegramBot.UrlLimitations;
|
||||||
|
|
||||||
|
public class UserJoinHandler : BotEventHandler
|
||||||
{
|
{
|
||||||
public class UserJoinHandler : BotEventHandler
|
protected readonly CoreContext dbCtx;
|
||||||
|
|
||||||
|
public UserJoinHandler(CoreContext coreContext)
|
||||||
{
|
{
|
||||||
protected readonly CoreContext dbCtx;
|
dbCtx = coreContext;
|
||||||
|
}
|
||||||
|
|
||||||
public UserJoinHandler(CoreContext coreContext)
|
[InChat(InChat.Public)]
|
||||||
{
|
[Message(MessageFlag.HasNewChatMembers)]
|
||||||
dbCtx = coreContext;
|
public async Task OnChatMembersAdded()
|
||||||
}
|
{
|
||||||
|
foreach (var member in RawUpdate.Message.NewChatMembers)
|
||||||
[InChat(InChat.Public)]
|
await dbCtx.UserValues.SetOptionValue<UInt16>(Chat.Id, member.Id, "urlLimitations.messagesAfterJoin",
|
||||||
[Message(MessageFlag.HasNewChatMembers)]
|
0);
|
||||||
public async Task OnChatMembersAdded()
|
|
||||||
{
|
|
||||||
foreach (var member in RawUpdate.Message.NewChatMembers)
|
|
||||||
await dbCtx.UserValues.SetOptionValue<UInt16>(Chat.Id, member.Id, "urlLimitations.messagesAfterJoin",
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,94 +14,93 @@ using Telegram.Bot;
|
|||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
using Telegram.Bot.Types.Enums;
|
using Telegram.Bot.Types.Enums;
|
||||||
|
|
||||||
namespace UserGroupTag
|
namespace UserGroupTag;
|
||||||
|
|
||||||
|
public class Handler : BotEventHandler
|
||||||
{
|
{
|
||||||
public class Handler : BotEventHandler
|
private readonly CoreContext _db;
|
||||||
|
private readonly ICacheStorage<long, User> _userCache;
|
||||||
|
|
||||||
|
public Handler(CoreContext db, ICacheStorage<long, User> userCache)
|
||||||
{
|
{
|
||||||
private readonly CoreContext _db;
|
_db = db;
|
||||||
private readonly ICacheStorage<long, User> _userCache;
|
_userCache = userCache;
|
||||||
|
}
|
||||||
|
|
||||||
public Handler(CoreContext db, ICacheStorage<long, User> userCache)
|
[InChat(InChat.Public)]
|
||||||
|
[ParametrizedCommand("add_group", CommandParseMode.Both)]
|
||||||
|
public async Task HandleAddGroup(string groupName)
|
||||||
|
{
|
||||||
|
// var groupName = group.Text;
|
||||||
|
var repliedMessage = RawUpdate.Message!.ReplyToMessage;
|
||||||
|
if (repliedMessage == null || repliedMessage.From!.IsBot || !(await Bot.CanPunishMember(Chat, From)) || string.IsNullOrEmpty(groupName))
|
||||||
{
|
{
|
||||||
_db = db;
|
return;
|
||||||
_userCache = userCache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
var from = repliedMessage.From;
|
||||||
[ParametrizedCommand("add_group", CommandParseMode.Both)]
|
var fromId = from.Id;
|
||||||
public async Task HandleAddGroup(string groupName)
|
var chatId = Chat!.Id;
|
||||||
|
|
||||||
|
var userGroupOption = await _db.UserValues.FindOrCreateOption(Chat.Id, "userGroups");
|
||||||
|
var tagDictionary = userGroupOption.GetValue(new Dictionary<string, List<long>>());
|
||||||
|
|
||||||
|
var memberList = tagDictionary.GetValueOrDefault(groupName, new List<long>());
|
||||||
|
if (memberList.Contains(fromId))
|
||||||
{
|
{
|
||||||
// var groupName = group.Text;
|
await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} уже находится в группе {groupName}",
|
||||||
var repliedMessage = RawUpdate.Message!.ReplyToMessage;
|
|
||||||
if (repliedMessage == null || repliedMessage.From!.IsBot || !(await Bot.CanPunishMember(Chat, From)) || string.IsNullOrEmpty(groupName))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var from = repliedMessage.From;
|
|
||||||
var fromId = from.Id;
|
|
||||||
var chatId = Chat!.Id;
|
|
||||||
|
|
||||||
var userGroupOption = await _db.UserValues.FindOrCreateOption(Chat.Id, "userGroups");
|
|
||||||
var tagDictionary = userGroupOption.GetValue(new Dictionary<string, List<long>>());
|
|
||||||
|
|
||||||
var memberList = tagDictionary.GetValueOrDefault(groupName, new List<long>());
|
|
||||||
if (memberList.Contains(fromId))
|
|
||||||
{
|
|
||||||
await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} уже находится в группе {groupName}",
|
|
||||||
parseMode: ParseMode.Html);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
memberList.Add(repliedMessage.From.Id);
|
|
||||||
tagDictionary[groupName] = memberList;
|
|
||||||
|
|
||||||
userGroupOption.SetValue(tagDictionary);
|
|
||||||
_db.AddOrUpdate(userGroupOption);
|
|
||||||
|
|
||||||
_userCache.SetValue(repliedMessage.From.Id, repliedMessage.From, Int32.MaxValue);
|
|
||||||
await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} добавлен в группу {groupName}",
|
|
||||||
parseMode: ParseMode.Html);
|
parseMode: ParseMode.Html);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
memberList.Add(repliedMessage.From.Id);
|
||||||
|
tagDictionary[groupName] = memberList;
|
||||||
|
|
||||||
[InChat(InChat.Public)]
|
userGroupOption.SetValue(tagDictionary);
|
||||||
[ParametrizedCommand("tag_group", CommandParseMode.Both)]
|
_db.AddOrUpdate(userGroupOption);
|
||||||
public async Task HandleTagGroup(string groupName)
|
|
||||||
|
_userCache.SetValue(repliedMessage.From.Id, repliedMessage.From, Int32.MaxValue);
|
||||||
|
await Bot.SendTextMessageAsync(chatId, $"{from.ToHtml()} добавлен в группу {groupName}",
|
||||||
|
parseMode: ParseMode.Html);
|
||||||
|
}
|
||||||
|
|
||||||
|
[InChat(InChat.Public)]
|
||||||
|
[ParametrizedCommand("tag_group", CommandParseMode.Both)]
|
||||||
|
public async Task HandleTagGroup(string groupName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(groupName))
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(groupName))
|
return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var userGroupOption = await _db.UserValues.FindOption(Chat.Id, "userGroups");
|
|
||||||
|
|
||||||
var userList = userGroupOption?.GetValue<Dictionary<string, List<long>>>()?[groupName];
|
|
||||||
if (userList == null || userList.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg = new HtmlString()
|
|
||||||
.Text("Пользователь ")
|
|
||||||
.UserMention(From)
|
|
||||||
.Text($" призывает группу пользователей {groupName}: ");
|
|
||||||
foreach (var userId in userList)
|
|
||||||
{
|
|
||||||
msg.UserMention(await GetUser(userId, Chat)).Text(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Bot.SendTextMessageAsync(Chat.Id, msg.ToString().TrimEnd(',', ' '), parseMode: ParseMode.Html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task<User> GetUser(long userId, Chat chat)
|
var userGroupOption = await _db.UserValues.FindOption(Chat.Id, "userGroups");
|
||||||
|
|
||||||
|
var userList = userGroupOption?.GetValue<Dictionary<string, List<long>>>()?[groupName];
|
||||||
|
if (userList == null || userList.Count == 0)
|
||||||
{
|
{
|
||||||
User user;
|
return;
|
||||||
if (!_userCache.TryGetValue(userId, out user))
|
|
||||||
{
|
|
||||||
user = (await Bot.GetChatMemberAsync(Chat.Id, userId)).User;
|
|
||||||
_userCache.SetValue(userId, user, Int32.MaxValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var msg = new HtmlString()
|
||||||
|
.Text("Пользователь ")
|
||||||
|
.UserMention(From)
|
||||||
|
.Text($" призывает группу пользователей {groupName}: ");
|
||||||
|
foreach (var userId in userList)
|
||||||
|
{
|
||||||
|
msg.UserMention(await GetUser(userId, Chat)).Text(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bot.SendTextMessageAsync(Chat.Id, msg.ToString().TrimEnd(',', ' '), parseMode: ParseMode.Html);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task<User> GetUser(long userId, Chat chat)
|
||||||
|
{
|
||||||
|
User user;
|
||||||
|
if (!_userCache.TryGetValue(userId, out user))
|
||||||
|
{
|
||||||
|
user = (await Bot.GetChatMemberAsync(Chat.Id, userId)).User;
|
||||||
|
_userCache.SetValue(userId, user, Int32.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
using Kruzya.TelegramBot.Core;
|
using Kruzya.TelegramBot.Core;
|
||||||
|
|
||||||
namespace UserGroupTag
|
namespace UserGroupTag;
|
||||||
|
|
||||||
|
public class UserGroupTag : Module
|
||||||
{
|
{
|
||||||
public class UserGroupTag : Module
|
public UserGroupTag(Core core) : base(core) { }
|
||||||
{
|
|
||||||
public UserGroupTag(Core core) : base(core) { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user