diff --git a/Directory.Packages.props b/Directory.Packages.props
index 52b8793..dc25262 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,16 +1,23 @@
-
- net8.0
- enable
- enable
- 8.0.0
-
-
-
-
-
-
-
-
-
-
+
+ net8.0
+ enable
+ enable
+ 8.0.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/Nocr.TelegramListener.AppServices.csproj b/src/Nocr.TelegramListener.AppServices/Nocr.TelegramListener.AppServices.csproj
index ae0057e..96f2b81 100644
--- a/src/Nocr.TelegramListener.AppServices/Nocr.TelegramListener.AppServices.csproj
+++ b/src/Nocr.TelegramListener.AppServices/Nocr.TelegramListener.AppServices.csproj
@@ -2,10 +2,12 @@
+
+
-
+
diff --git a/src/Nocr.TelegramListener.AppServices/ServiceCollectionExtensions.cs b/src/Nocr.TelegramListener.AppServices/ServiceCollectionExtensions.cs
index e72941d..330e802 100644
--- a/src/Nocr.TelegramListener.AppServices/ServiceCollectionExtensions.cs
+++ b/src/Nocr.TelegramListener.AppServices/ServiceCollectionExtensions.cs
@@ -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(configuration.GetSection(nameof(WTelegramClientOptions)));
+ services.AddHostedService();
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddSingleton();
+ services.AddSingleton();
return services;
}
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/IMessageHandler.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/IMessageHandler.cs
new file mode 100644
index 0000000..bad3987
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/IMessageHandler.cs
@@ -0,0 +1,8 @@
+using TL;
+
+namespace Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
+
+public interface IMessageHandler
+{
+ Task HandleMessage(MessageBase messageBase, bool edit = false);
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/IUpdateHandler.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/IUpdateHandler.cs
new file mode 100644
index 0000000..573ba85
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/IUpdateHandler.cs
@@ -0,0 +1,8 @@
+using TL;
+
+namespace Nocr.TelegramListener.AppServices.UpdateListeners.Handlers;
+
+public interface IUpdateHandler
+{
+ Task HandleUpdate(UpdatesBase updates, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/MessageHandler.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/MessageHandler.cs
new file mode 100644
index 0000000..7023b30
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/MessageHandler.cs
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/UpdateHandler.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/UpdateHandler.cs
new file mode 100644
index 0000000..159eea2
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/Handlers/UpdateHandler.cs
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/ITelegramClientContainer.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/ITelegramClientContainer.cs
new file mode 100644
index 0000000..5e61279
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/ITelegramClientContainer.cs
@@ -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();
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramClientContainer.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramClientContainer.cs
new file mode 100644
index 0000000..fd8bca7
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramClientContainer.cs
@@ -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 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();
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramObjectExtensions.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramObjectExtensions.cs
new file mode 100644
index 0000000..659a0c1
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramObjectExtensions.cs
@@ -0,0 +1,16 @@
+using TL;
+
+namespace Nocr.TelegramListener.AppServices.UpdateListeners;
+
+public static class TelegramObjectExtensions
+{
+ public static string User(this IDictionary dictionary, long id) =>
+ dictionary.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
+
+ public static string Chat(this IDictionary dictionary, long id) =>
+ dictionary.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
+
+ public static string Peer(this Peer peer, IDictionary users, IDictionary 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}";
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramRegistry.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramRegistry.cs
new file mode 100644
index 0000000..13d271e
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/TelegramRegistry.cs
@@ -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 Users = new();
+ public ConcurrentDictionary 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();
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/UpdateListenerBackgroundService.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/UpdateListenerBackgroundService.cs
new file mode 100644
index 0000000..d1e3c2a
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/UpdateListenerBackgroundService.cs
@@ -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,
+ 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();
+ await updateHandler.HandleUpdate(updates);
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.AppServices/UpdateListeners/WTelegramClientOptions.cs b/src/Nocr.TelegramListener.AppServices/UpdateListeners/WTelegramClientOptions.cs
new file mode 100644
index 0000000..dad08a5
--- /dev/null
+++ b/src/Nocr.TelegramListener.AppServices/UpdateListeners/WTelegramClientOptions.cs
@@ -0,0 +1,19 @@
+namespace Nocr.TelegramListener.AppServices.UpdateListeners;
+
+///
+/// Options for WTelegramClient
+///
+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; }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.Core/BackgroundServices/IRepeatableBackgroundServiceHandler.cs b/src/Nocr.TelegramListener.Core/BackgroundServices/IRepeatableBackgroundServiceHandler.cs
new file mode 100644
index 0000000..e66dd15
--- /dev/null
+++ b/src/Nocr.TelegramListener.Core/BackgroundServices/IRepeatableBackgroundServiceHandler.cs
@@ -0,0 +1,6 @@
+namespace Nocr.TelegramListener.Core.BackgroundServices;
+
+public interface IRepeatableBackgroundServiceHandler
+{
+ Task Handle(CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.Core/BackgroundServices/RepeatableBackgroundService.cs b/src/Nocr.TelegramListener.Core/BackgroundServices/RepeatableBackgroundService.cs
new file mode 100644
index 0000000..3210e7d
--- /dev/null
+++ b/src/Nocr.TelegramListener.Core/BackgroundServices/RepeatableBackgroundService.cs
@@ -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 : 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();
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.Core/BackgroundServices/RepeatableServiceOptions.cs b/src/Nocr.TelegramListener.Core/BackgroundServices/RepeatableServiceOptions.cs
new file mode 100644
index 0000000..2b6fb38
--- /dev/null
+++ b/src/Nocr.TelegramListener.Core/BackgroundServices/RepeatableServiceOptions.cs
@@ -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);
+}
\ No newline at end of file
diff --git a/src/Nocr.TelegramListener.Core/Nocr.TelegramListener.Core.csproj b/src/Nocr.TelegramListener.Core/Nocr.TelegramListener.Core.csproj
index 2ef1a36..c621883 100644
--- a/src/Nocr.TelegramListener.Core/Nocr.TelegramListener.Core.csproj
+++ b/src/Nocr.TelegramListener.Core/Nocr.TelegramListener.Core.csproj
@@ -1 +1,8 @@
-
+
+
+
+
+
+
+
+
diff --git a/src/Nocr.TelegramListener.Host/Infrastructure/Startup.cs b/src/Nocr.TelegramListener.Host/Infrastructure/Startup.cs
index a945626..075211f 100644
--- a/src/Nocr.TelegramListener.Host/Infrastructure/Startup.cs
+++ b/src/Nocr.TelegramListener.Host/Infrastructure/Startup.cs
@@ -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();
- services.AddAppServices();
+ services.AddAppServices(Configuration);
}
public void Configure(IApplicationBuilder app)