Add infrastructure for update handling
This commit is contained in:
parent
b17f3dbd0a
commit
f2b4864237
@ -1,16 +1,23 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<MicrosoftVersion>8.0.0</MicrosoftVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Serilog">
|
||||
<PackageVersion Include="Serilog" Version="3.1.1"/>
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1"/>
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Microsoft">
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<MicrosoftVersion>8.0.0</MicrosoftVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Telegram">
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageVersion Include="WTelegramClient" Version="3.7.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Serilog">
|
||||
<PackageVersion Include="Serilog" Version="3.1.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Microsoft">
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftVersion)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -2,10 +2,12 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
|
||||
<PackageReference Include="WTelegramClient" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Nocr.TelegramListener.Async.Api.Contracts\Nocr.TelegramListener.Async.Api.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Nocr.TelegramListener.Async.Api.Contracts\Nocr.TelegramListener.Async.Api.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Nocr.TelegramListener.Core\Nocr.TelegramListener.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -1,15 +1,25 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
using Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services)
|
||||
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
if (services == null)
|
||||
if (services == null)
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
|
||||
// Add registrations here
|
||||
services.Configure<WTelegramClientOptions>(configuration.GetSection(nameof(WTelegramClientOptions)));
|
||||
services.AddHostedService<UpdateListenerBackgroundService>();
|
||||
|
||||
services.AddScoped<IUpdateHandler, UpdateHandler>();
|
||||
services.AddScoped<IMessageHandler, MessageHandler>();
|
||||
services.AddSingleton<ITelegramClientContainer, TelegramClientContainer>();
|
||||
services.AddSingleton<TelegramRegistry>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
using TL;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
|
||||
|
||||
public interface IMessageHandler
|
||||
{
|
||||
Task HandleMessage(MessageBase messageBase, bool edit = false);
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using TL;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
|
||||
|
||||
public interface IUpdateHandler
|
||||
{
|
||||
Task HandleUpdate(UpdatesBase updates, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
using TL;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
|
||||
|
||||
public sealed class MessageHandler : IMessageHandler
|
||||
{
|
||||
private readonly TelegramRegistry _telegramRegistry;
|
||||
|
||||
public MessageHandler(TelegramRegistry telegramRegistry)
|
||||
{
|
||||
_telegramRegistry = telegramRegistry ?? throw new ArgumentNullException(nameof(telegramRegistry));
|
||||
}
|
||||
|
||||
public Task HandleMessage(MessageBase messageBase, bool edit = false)
|
||||
{
|
||||
if (edit) Console.Write("(Edit): ");
|
||||
switch (messageBase)
|
||||
{
|
||||
case Message m:
|
||||
Console.WriteLine(
|
||||
$"{m.from_id.Peer(_telegramRegistry.Users, _telegramRegistry.Chats) ?? m.post_author} in {m.peer_id.Peer(_telegramRegistry.Users, _telegramRegistry.Chats)}> {m.message}");
|
||||
break;
|
||||
case MessageService ms:
|
||||
Console.WriteLine(
|
||||
$"{ms.from_id.Peer(_telegramRegistry.Users, _telegramRegistry.Chats)} in {ms.peer_id.Peer(_telegramRegistry.Users, _telegramRegistry.Chats)} [{ms.action.GetType().Name[13..]}]");
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
using TL;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
|
||||
|
||||
public sealed class UpdateHandler : IUpdateHandler
|
||||
{
|
||||
private readonly ITelegramClientContainer _clientContainer;
|
||||
private readonly TelegramRegistry _telegramRegistry;
|
||||
private readonly IMessageHandler _messageHandler;
|
||||
|
||||
public UpdateHandler(ITelegramClientContainer clientContainer, TelegramRegistry telegramRegistry,
|
||||
IMessageHandler messageHandler)
|
||||
{
|
||||
_clientContainer = clientContainer ?? throw new ArgumentNullException(nameof(clientContainer));
|
||||
_telegramRegistry = telegramRegistry ?? throw new ArgumentNullException(nameof(telegramRegistry));
|
||||
_messageHandler = messageHandler ?? throw new ArgumentNullException(nameof(messageHandler));
|
||||
}
|
||||
|
||||
public async Task HandleUpdate(UpdatesBase updates, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var (client, users, chats) =
|
||||
(_clientContainer.Client, _telegramRegistry.Users, _telegramRegistry.Chats);
|
||||
|
||||
updates.CollectUsersChats(users, chats);
|
||||
if (updates is UpdateShortMessage usm && !users.ContainsKey(usm.user_id))
|
||||
(await client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0)).CollectUsersChats(
|
||||
users, chats);
|
||||
else if (updates is UpdateShortChatMessage uscm &&
|
||||
(!users.ContainsKey(uscm.from_id) ||
|
||||
!chats.ContainsKey(uscm.chat_id)))
|
||||
(await client.Updates_GetDifference(uscm.pts - uscm.pts_count, uscm.date, 0)).CollectUsersChats(
|
||||
users,
|
||||
chats);
|
||||
foreach (var update in updates.UpdateList)
|
||||
switch (update)
|
||||
{
|
||||
case UpdateNewMessage unm:
|
||||
await _messageHandler.HandleMessage(unm.message);
|
||||
break;
|
||||
case UpdateEditMessage uem:
|
||||
await _messageHandler.HandleMessage(uem.message, true);
|
||||
break;
|
||||
// Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
|
||||
case UpdateDeleteChannelMessages udcm:
|
||||
Console.WriteLine(
|
||||
$"{udcm.messages.Length} message(s) deleted in {chats.Chat(udcm.channel_id)}");
|
||||
break;
|
||||
case UpdateDeleteMessages udm:
|
||||
Console.WriteLine($"{udm.messages.Length} message(s) deleted");
|
||||
break;
|
||||
case UpdateUserTyping uut:
|
||||
Console.WriteLine($"{users.User(uut.user_id)} is {uut.action}");
|
||||
break;
|
||||
case UpdateChatUserTyping ucut:
|
||||
Console.WriteLine(
|
||||
$"{ucut.from_id.Peer(users, chats)} is {ucut.action} in {chats.Chat(ucut.chat_id)}");
|
||||
break;
|
||||
case UpdateChannelUserTyping ucut2:
|
||||
Console.WriteLine(
|
||||
$"{ucut2.from_id.Peer(users, chats)} is {ucut2.action} in {chats.Chat(ucut2.channel_id)}");
|
||||
break;
|
||||
case UpdateChatParticipants { participants: ChatParticipants cp }:
|
||||
Console.WriteLine(
|
||||
$"{cp.participants.Length} participants in {chats.Chat(cp.chat_id)}");
|
||||
break;
|
||||
case UpdateUserStatus uus:
|
||||
Console.WriteLine(
|
||||
$"{users.User(uus.user_id)} is now {uus.status.GetType().Name[10..]}");
|
||||
break;
|
||||
case UpdateUserName uun:
|
||||
Console.WriteLine(
|
||||
$"{users.User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}");
|
||||
break;
|
||||
case UpdateUser uu:
|
||||
Console.WriteLine($"{users.User(uu.user_id)} has changed infos/photo");
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine(update.GetType().Name);
|
||||
break; // there are much more update types than the above example cases
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
using WTelegram;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
|
||||
public interface ITelegramClientContainer
|
||||
{
|
||||
Client Client { get; }
|
||||
|
||||
public bool Initialized { get; }
|
||||
|
||||
public void Initialize();
|
||||
|
||||
public void Reset();
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using WTelegram;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
|
||||
public sealed class TelegramClientContainer : ITelegramClientContainer, IDisposable
|
||||
{
|
||||
private Client? _client;
|
||||
private readonly WTelegramClientOptions _options;
|
||||
|
||||
public Client Client => _client ?? throw new InvalidOperationException("Client not initialized yet");
|
||||
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public TelegramClientContainer(IOptions<WTelegramClientOptions> options)
|
||||
{
|
||||
_options = options.Value ?? throw new ArgumentNullException(nameof(options));
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (Initialized)
|
||||
return;
|
||||
|
||||
_client = new Client(ConfigureWTelegramClient);
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Initialized = false;
|
||||
Dispose();
|
||||
_client = null;
|
||||
}
|
||||
|
||||
private string ConfigureWTelegramClient(string what)
|
||||
{
|
||||
switch (what)
|
||||
{
|
||||
case "api_id": return _options.ApiId;
|
||||
case "api_hash": return _options.ApiHash;
|
||||
case "phone_number": return _options.PhoneNumber;
|
||||
case "verification_code":
|
||||
Console.Write("Code: ");
|
||||
return Console.ReadLine();
|
||||
//case "first_name": return "Dmitry"; // if sign-up is required
|
||||
//case "last_name": return "Charushnikov"; // if sign-up is required
|
||||
//case "password": return ""; // if user has enabled 2FA
|
||||
default: return null; // let WTelegramClient decide the default config
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_client?.Dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
using TL;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
|
||||
public static class TelegramObjectExtensions
|
||||
{
|
||||
public static string User(this IDictionary<long, User> dictionary, long id) =>
|
||||
dictionary.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
|
||||
|
||||
public static string Chat(this IDictionary<long, ChatBase> dictionary, long id) =>
|
||||
dictionary.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
|
||||
|
||||
public static string Peer(this Peer peer, IDictionary<long, User> users, IDictionary<long, ChatBase> chats) => peer is null ? null
|
||||
: peer is PeerUser user ? users.User(user.user_id)
|
||||
: peer is PeerChat or PeerChannel ? chats.Chat(peer.ID) : $"Peer {peer.ID}";
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
using System.Collections.Concurrent;
|
||||
using TL;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
|
||||
public sealed class TelegramRegistry
|
||||
{
|
||||
public User My { get; private set; }
|
||||
public ConcurrentDictionary<long, User> Users = new();
|
||||
public ConcurrentDictionary<long, ChatBase> Chats = new();
|
||||
|
||||
public void SetMy(User my)
|
||||
{
|
||||
if (my == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(my));
|
||||
}
|
||||
|
||||
if (My == null)
|
||||
{
|
||||
My = my;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("My already set");
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
My = null;
|
||||
Users.Clear();
|
||||
Chats.Clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
|
||||
using TL;
|
||||
using TL.Methods;
|
||||
using WTelegram;
|
||||
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
|
||||
public sealed class UpdateListenerBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ITelegramClientContainer _telegramClientContainer;
|
||||
private readonly TelegramRegistry _telegramRegistry;
|
||||
private readonly WTelegramClientOptions _wTelegramClientOptions;
|
||||
|
||||
public UpdateListenerBackgroundService(IServiceProvider serviceProvider,
|
||||
IOptions<WTelegramClientOptions> wTelegramClientOptions,
|
||||
ITelegramClientContainer telegramClientContainer,
|
||||
TelegramRegistry telegramRegistry)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_telegramClientContainer = telegramClientContainer ?? throw new ArgumentNullException(nameof(telegramClientContainer));
|
||||
_telegramRegistry = telegramRegistry ?? throw new ArgumentNullException(nameof(telegramRegistry));
|
||||
_wTelegramClientOptions = wTelegramClientOptions.Value ??
|
||||
throw new ArgumentNullException(nameof(wTelegramClientOptions));
|
||||
}
|
||||
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
Console.WriteLine(
|
||||
"The program will display updates received for the logged-in user. Press any key to terminate");
|
||||
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
|
||||
Client? client = null;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (client == null)
|
||||
{
|
||||
_telegramRegistry.Clear();
|
||||
_telegramClientContainer.Reset();
|
||||
_telegramClientContainer.Initialize();
|
||||
|
||||
client = _telegramClientContainer.Client;
|
||||
client.OnUpdate += HandleUpdates;
|
||||
var my = await client.LoginUserIfNeeded();
|
||||
_telegramRegistry.SetMy(my);
|
||||
_telegramRegistry.Users[my.id] = my;
|
||||
|
||||
// Note: on login, Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged
|
||||
Console.WriteLine(
|
||||
$"We are logged-in as {_telegramRegistry.My.username ?? _telegramRegistry.My.first_name + " " + _telegramRegistry.My.last_name} (id {_telegramRegistry.My.id})");
|
||||
// We collect all infos about the users/chats so that updates can be printed with their names
|
||||
var dialogs = await client.Messages_GetAllDialogs(); // dialogs = groups/channels/users
|
||||
dialogs.CollectUsersChats(_telegramRegistry.Users, _telegramRegistry.Chats);
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUpdates(UpdatesBase updates)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var updateHandler = scope.ServiceProvider.GetRequiredService<IUpdateHandler>();
|
||||
await updateHandler.HandleUpdate(updates);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
namespace Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
|
||||
/// <summary>
|
||||
/// Options for WTelegramClient
|
||||
/// </summary>
|
||||
public sealed class WTelegramClientOptions
|
||||
{
|
||||
public string ApiId { get; set; }
|
||||
|
||||
public string ApiHash { get; set; }
|
||||
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace Nocr.TelegramListener.Core.BackgroundServices;
|
||||
|
||||
public interface IRepeatableBackgroundServiceHandler
|
||||
{
|
||||
Task Handle(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Nocr.TelegramListener.Core.BackgroundServices;
|
||||
|
||||
public abstract class RepeatableBackgroundService<THandler, TOptions> : BackgroundService
|
||||
where THandler : IRepeatableBackgroundServiceHandler
|
||||
where TOptions : RepeatableServiceOptions
|
||||
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly TOptions _options;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
protected RepeatableBackgroundService(ILogger logger, TOptions options, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var handler = scope.ServiceProvider.GetRequiredService<THandler>();
|
||||
await handler.Handle(stoppingToken);
|
||||
|
||||
await Task.Delay(_options.Interval, stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogCritical(ex, "Failed to process...");
|
||||
await Task.Delay(_options.ExceptionInterval, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Nocr.TelegramListener.Core.BackgroundServices;
|
||||
|
||||
public abstract class RepeatableServiceOptions
|
||||
{
|
||||
public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(60);
|
||||
|
||||
public TimeSpan ExceptionInterval { get; set; } = TimeSpan.FromSeconds(300);
|
||||
}
|
||||
@ -1 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk" />
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Nocr.TelegramListener.AppServices;
|
||||
using Nocr.TelegramListener.AppServices.UpdateListeners;
|
||||
using Nocr.TelegramListener.Core.Dates;
|
||||
|
||||
namespace Nocr.TelegramListener.Host.Infrastructure;
|
||||
@ -16,7 +17,7 @@ public class Startup
|
||||
{
|
||||
services.AddSingleton<ICurrentDateProvider, DefaultCurrentDateProvider>();
|
||||
|
||||
services.AddAppServices();
|
||||
services.AddAppServices(Configuration);
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user