Use file-scoped namespaces

This commit is contained in:
Andriy
2023-07-29 15:14:52 +03:00
parent a24332370d
commit 8ab067a027
58 changed files with 2222 additions and 2280 deletions
+6 -7
View File
@@ -1,9 +1,8 @@
namespace Kruzya.TelegramBot.Core.Cache namespace Kruzya.TelegramBot.Core.Cache;
public interface ICacheStorage<TKey, TValue>
{ {
public interface ICacheStorage<TKey, TValue> public bool TryGetValue(TKey key, out TValue value);
{ public void SetValue(TKey key, TValue value, int timeToLive);
public 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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
} }
} }
+30 -31
View File
@@ -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};
} }
} }
+13 -14
View File
@@ -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);
} }
} }
+11 -12
View File
@@ -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);
} }
} }
+9 -10
View File
@@ -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;
} }
} }
+11 -12
View File
@@ -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);
} }
} }
+19 -20
View File
@@ -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"})";
} }
} }
+40 -41
View File
@@ -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);
}
} }
} }
+49 -50
View File
@@ -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
View File
@@ -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
View File
@@ -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>(); });
} }
+55 -56
View File
@@ -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);
}
} }
+8 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
} }
} }
+16 -17
View File
@@ -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();
}
} }
} }
+10 -11
View File
@@ -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();
}
} }
+16 -17
View File
@@ -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();
} }
} }
+20 -21
View File
@@ -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);
}
} }
} }
+10 -11
View File
@@ -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>();
} }
} }
+8 -9
View File
@@ -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>();
} }
} }
+35 -36
View File
@@ -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;
} }
+32 -33
View File
@@ -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;
} }
+17 -18
View File
@@ -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();
}
} }
} }
+21 -22
View File
@@ -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) { }
} }
+75 -76
View File
@@ -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;
} }
+86 -87
View File
@@ -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);
}
} }
} }
+40 -41
View File
@@ -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;
} }
} }
+254 -255
View File
@@ -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
} }
+20 -21
View File
@@ -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);
}
} }
} }
+19 -20
View File
@@ -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);
}
} }
+209 -210
View File
@@ -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
} }
+27 -28
View File
@@ -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);
}
} }
} }
+8 -9
View File
@@ -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;
}
} }
+4 -5
View File
@@ -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;
}
} }
+32 -33
View File
@@ -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;
} }
} }
+35 -36
View File
@@ -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)
); );
}
} }
} }
} }
+4 -5
View File
@@ -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)
{
}
} }
} }
+15 -16
View File
@@ -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);
}
} }
} }
+73 -74
View File
@@ -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;
} }
} }
+4 -5
View File
@@ -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) { }
}
} }