Skip to content

Commit fa38f6d

Browse files
committed
Refactor BotApp and introduce TelegramBotHostedService
Significantly refactored the BotApp class to remove direct dependencies on the Telegram.Bot client, now utilizing a BotControllerMethodsContainer for managing controller methods. Introduced the TelegramBotHostedService to handle the bot's lifecycle and update processing. Updated the BotBuilder class to reflect these changes, including renaming the service addition method for clarity. Overall, these modifications enhance modularity, maintainability, and separation of concerns within the codebase.
1 parent 6d0ac30 commit fa38f6d

File tree

5 files changed

+293
-189
lines changed

5 files changed

+293
-189
lines changed

Sources/TelegramBot.ConsoleTest/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class Program
1111
public static void Main(string[] args)
1212
{
1313
BotBuilder builder = new BotBuilder(args)
14-
.Setup(x => x.ReceiveUpdates = false)
14+
.Setup(x => x.ReceiveUpdates = true)
1515
.UseApiKey(x => x.FromConfiguration());
1616

1717
builder.Services

Sources/TelegramBot/BotApp.cs

Lines changed: 10 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
using System;
22
using System.Linq;
3-
using Telegram.Bot;
43
using System.Threading;
54
using System.Reflection;
6-
using Telegram.Bot.Types;
7-
using TelegramBot.Handlers;
85
using TelegramBot.Services;
9-
using TelegramBot.Builders;
10-
using TelegramBot.Attributes;
116
using System.Threading.Tasks;
12-
using TelegramBot.Extensions;
7+
using TelegramBot.Containers;
138
using TelegramBot.Controllers;
14-
using TelegramBot.Abstractions;
159
using System.Collections.Generic;
1610
using Microsoft.Extensions.Hosting;
1711
using Microsoft.Extensions.Logging;
@@ -26,10 +20,7 @@ public class BotApp : IBot
2620
{
2721
private bool _disposed = false;
2822
private readonly ILogger<BotApp> _logger;
29-
private readonly TelegramBotClient _client;
3023
private readonly ServiceProvider _serviceProvider;
31-
private readonly BotConfiguration _botConfiguration;
32-
private IReadOnlyCollection<MethodInfo> _controllerMethods;
3324
private readonly CancellationTokenSource _cancellationTokenSource;
3425

3526
/// <summary>
@@ -40,15 +31,10 @@ public class BotApp : IBot
4031
/// <summary>
4132
/// Creates a new instance of <see cref="BotApp"/>.
4233
/// </summary>
43-
/// <param name="client">Telegram bot client.</param>
4434
/// <param name="serviceProvider">Service provider.</param>
45-
/// <param name="botConfiguration">Bot configuration.</param>
46-
public BotApp(TelegramBotClient client, ServiceProvider serviceProvider, BotConfiguration botConfiguration)
35+
public BotApp(ServiceProvider serviceProvider)
4736
{
48-
_client = client;
4937
_serviceProvider = serviceProvider;
50-
_botConfiguration = botConfiguration;
51-
_controllerMethods = new List<MethodInfo>();
5238
_cancellationTokenSource = new CancellationTokenSource();
5339
_logger = serviceProvider.GetRequiredService<ILogger<BotApp>>();
5440
}
@@ -68,9 +54,15 @@ public IBot MapControllers()
6854
result.Add(type);
6955
}
7056
}
71-
_controllerMethods = result
57+
var controllerMethods = result
7258
.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
7359
.ToList();
60+
BotControllerMethodsContainer container = _serviceProvider.GetService<BotControllerMethodsContainer>()
61+
?? throw new InvalidOperationException("Bot controller methods container is not registered.");
62+
foreach (var method in controllerMethods)
63+
{
64+
container.AddMethod(method);
65+
}
7466
return this;
7567
}
7668

@@ -104,28 +96,6 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
10496
var mergedToken = MergeTokens(cancellationToken);
10597
AppDomain.CurrentDomain.ProcessExit += new EventHandler(OnProcessExit);
10698
CheckDisposed();
107-
try
108-
{
109-
var botUser = _client.GetMe().Result;
110-
if (_botConfiguration.ReceiveUpdates)
111-
{
112-
_client.StartReceiving(UpdateHandler, ErrorHandler, cancellationToken: mergedToken);
113-
_logger.LogInformation("Bot '{botUser}' started - receiving updates.", botUser.Username);
114-
}
115-
else
116-
{
117-
_logger.LogInformation("Bot '{botUser}' started - not receiving updates.", botUser.Username);
118-
}
119-
}
120-
catch (Exception ex)
121-
{
122-
if (ex is AggregateException aggregateException)
123-
{
124-
ex = aggregateException.InnerException;
125-
}
126-
_logger.LogError(ex, "Error occurred while starting the bot. Probably the bot token is invalid or the network is not available.");
127-
throw ex;
128-
}
12999

130100
var hostApplicationLifetime = _serviceProvider.GetService<IHostApplicationLifetime>() as HostApplicationLifetime
131101
?? throw new InvalidOperationException("Host application lifetime is not registered.");
@@ -135,21 +105,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
135105
foreach (var hostedService in hostedServices)
136106
{
137107
await hostedService.StartAsync(mergedToken);
138-
_logger.LogInformation("Started '{hostedService}'.", hostedService.GetType().Name);
139-
}
140-
141-
var commandRegistrationBuilders = _serviceProvider.GetServices<CommandRegistrationBuilder>();
142-
if (commandRegistrationBuilders != null && commandRegistrationBuilders.Any())
143-
{
144-
foreach (var builder in commandRegistrationBuilders)
145-
{
146-
var commands = builder.Build();
147-
await _client.SetMyCommands(commands,
148-
languageCode: builder.Language,
149-
cancellationToken: mergedToken);
150-
_logger.LogInformation("Registered {count} commands for language '{language}'.",
151-
commands.Count(), builder.Language);
152-
}
108+
_logger.LogInformation("Started hosted service: '{hostedService}'.", hostedService.GetType().Name);
153109
}
154110
}
155111

@@ -208,134 +164,6 @@ private CancellationToken MergeTokens(CancellationToken token)
208164
return CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, token).Token;
209165
}
210166

211-
private Task ErrorHandler(ITelegramBotClient client, Exception exception, CancellationToken token)
212-
{
213-
CheckDisposed();
214-
_logger.LogError(exception, "Error occurred while receiving updates.");
215-
return Task.CompletedTask;
216-
}
217-
218-
private async Task UpdateHandler(ITelegramBotClient client, Update update, CancellationToken token)
219-
{
220-
CheckDisposed();
221-
if (update.Message != null && !string.IsNullOrWhiteSpace(update.Message.Text) && !update.Message.Text.StartsWith('/'))
222-
{
223-
_logger.LogInformation("Received text message: {Text}.", update.Message.Text);
224-
var handler = new TextMessageHandler(_controllerMethods, update);
225-
await HandleRequestAsync(handler, update);
226-
}
227-
else if (update.Message != null && !string.IsNullOrWhiteSpace(update.Message.Text) && update.Message.Text.StartsWith('/'))
228-
{
229-
_logger.LogInformation("Received text command: {Text}.", update.Message.Text);
230-
var handler = new TextCommandHandler(_controllerMethods, update);
231-
await HandleRequestAsync(handler, update);
232-
}
233-
else if (update.CallbackQuery != null && update.CallbackQuery.Data != null)
234-
{
235-
_logger.LogInformation("Received inline query: {Data}.", update.CallbackQuery.Data);
236-
var handler = new InlineQueryHandler(_controllerMethods, update);
237-
await HandleRequestAsync(handler, update);
238-
}
239-
else
240-
{
241-
_logger.LogWarning("Unsupported update type: {UpdateType}.", update.Type);
242-
}
243-
}
244-
245-
private async Task HandleRequestAsync(ITelegramUpdateHandler handler, Update update)
246-
{
247-
CheckDisposed();
248-
bool hasUser = update.TryGetUser(out User user);
249-
if (!hasUser)
250-
{
251-
return;
252-
}
253-
var args = handler.GetArguments();
254-
MethodInfo? method = handler.GetMethodInfo();
255-
if (method == null)
256-
{
257-
_logger.LogWarning("Method not found for message: {Text}.", update.Message?.Text);
258-
return;
259-
}
260-
bool isAuthorized = await AuthorizeAsync(method, user);
261-
if (!isAuthorized)
262-
{
263-
return;
264-
}
265-
CheckMethodMatching(method, args);
266-
BotControllerBase controller = (BotControllerBase)ActivatorUtilities.CreateInstance(_serviceProvider, method.DeclaringType!);
267-
controller.Update = update;
268-
controller.User = user;
269-
controller.Client = _client;
270-
if (_serviceProvider.GetService<IKeyValueProvider>() is IKeyValueProvider keyValueProvider)
271-
{
272-
controller.KeyValueProvider = keyValueProvider;
273-
}
274-
var result = method.Invoke(controller, args);
275-
await ExecuteResultAsync(result, user.Id);
276-
await DisposeAsync(controller);
277-
await DisposeAsync(result);
278-
}
279-
280-
private void CheckMethodMatching(MethodInfo method, object[]? args)
281-
{
282-
if (method.ReturnType != typeof(Task<IActionResult>) && method.ReturnType != typeof(IActionResult))
283-
{
284-
throw new InvalidOperationException("Invalid return type: " + method.ReturnType.Name);
285-
}
286-
if (args != null && method.GetParameters().Length != args?.Length)
287-
{
288-
throw new InvalidOperationException("Invalid arguments count: " + args?.Length);
289-
}
290-
}
291-
292-
private async Task<bool> AuthorizeAsync(MethodInfo method, User user)
293-
{
294-
if (method.GetCustomAttribute<AuthorizeAttribute>() != null
295-
|| method.DeclaringType?.GetCustomAttribute<AuthorizeAttribute>() != null)
296-
{
297-
if (_serviceProvider.GetService<IBotAuthorizationHandler>() is IBotAuthorizationHandler authorizationHandler)
298-
{
299-
if (!authorizationHandler.Authorize(user))
300-
{
301-
await authorizationHandler
302-
.HandleUnauthorized(user)
303-
.ExecuteResultAsync(new ActionContext(_client, user.Id));
304-
return false;
305-
}
306-
}
307-
}
308-
return true;
309-
}
310-
311-
private async Task DisposeAsync(object obj)
312-
{
313-
if (obj is IAsyncDisposable asyncDisposable)
314-
{
315-
await asyncDisposable.DisposeAsync();
316-
}
317-
else if (obj is IDisposable disposable)
318-
{
319-
disposable.Dispose();
320-
}
321-
}
322-
323-
private async Task ExecuteResultAsync(object result, long userId)
324-
{
325-
if (result is Task<IActionResult> taskResult)
326-
{
327-
await (await taskResult).ExecuteResultAsync(new ActionContext(_client, userId));
328-
}
329-
else if (result is IActionResult actionResult)
330-
{
331-
await actionResult.ExecuteResultAsync(new ActionContext(_client, userId));
332-
}
333-
else
334-
{
335-
throw new InvalidOperationException("Invalid result type: " + result.GetType().Name);
336-
}
337-
}
338-
339167
private void CheckDisposed()
340168
{
341169
if (_disposed)

Sources/TelegramBot/Builders/BotBuilder.cs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Extensions.Hosting;
99
using Microsoft.Extensions.Configuration;
1010
using Microsoft.Extensions.DependencyInjection;
11+
using TelegramBot.Containers;
1112

1213
namespace TelegramBot.Builders
1314
{
@@ -24,7 +25,7 @@ public class BotBuilder
2425
/// <summary>
2526
/// A collection of configuration providers for the application to compose. This is useful for adding new configuration sources and providers.
2627
/// </summary>
27-
public ConfigurationManager Configuration { get; }
28+
public IConfigurationManager Configuration { get; }
2829

2930
/// <summary>
3031
/// A collection of logging providers for the application to compose. This is useful for adding new logging providers.
@@ -38,9 +39,9 @@ public class BotBuilder
3839
public BotBuilder() : this(Array.Empty<string>()) { }
3940

4041
/// <summary>
41-
/// Initializes a new instance of the <see cref="BotBuilder"/> class with preconfigured defaults.
42+
/// Initializes a new instance of the <see cref="BotBuilder"/> class with preconfigured defaults and command line arguments.
4243
/// </summary>
43-
/// <param name="args">The command line arguments.</param>
44+
/// <param name="args">Command line arguments.</param>
4445
/// <returns>The <see cref="BotBuilder"/>.</returns>
4546
public BotBuilder(params string[] args)
4647
{
@@ -58,7 +59,34 @@ public BotBuilder(params string[] args)
5859
Logging = new TelegramLoggerBuilder(Services);
5960
Services.AddSingleton(Configuration);
6061
Services.AddSingleton<IConfiguration>(Configuration);
61-
Services.AddSingleton<IConfigurationRoot>(Configuration);
62+
}
63+
64+
/// <summary>
65+
/// Initializes a new instance of the <see cref="BotBuilder"/> class with preconfigured defaults.
66+
/// </summary>
67+
/// <param name="hostBuilder">The host application builder.</param>
68+
/// <returns>The <see cref="BotBuilder"/>.</returns>
69+
public BotBuilder(IHostApplicationBuilder hostBuilder) : this()
70+
{
71+
if (hostBuilder == null)
72+
{
73+
throw new ArgumentNullException(nameof(hostBuilder), "Host application builder cannot be null.");
74+
}
75+
if (hostBuilder.Logging == null)
76+
{
77+
throw new ArgumentNullException(nameof(hostBuilder.Logging), "Host application builder logging cannot be null.");
78+
}
79+
if (hostBuilder.Services == null)
80+
{
81+
throw new ArgumentNullException(nameof(hostBuilder.Services), "Host application builder services cannot be null.");
82+
}
83+
if (hostBuilder.Configuration == null)
84+
{
85+
throw new ArgumentNullException(nameof(hostBuilder.Configuration), "Host application builder configuration cannot be null.");
86+
}
87+
Logging = hostBuilder.Logging;
88+
Services = hostBuilder.Services;
89+
Configuration = hostBuilder.Configuration;
6290
}
6391

6492
private readonly BotConfiguration _botConfiguration = new BotConfiguration();
@@ -119,7 +147,7 @@ public BotBuilder UseApiKey(Action<TelegramApiKeyBuilder> setupApiKey)
119147
/// </summary>
120148
/// <param name="services">The services to use.</param>
121149
/// <returns>This instance of <see cref="BotBuilder"/>.</returns>
122-
public BotBuilder UseServices(IServiceCollection services)
150+
public BotBuilder AddServices(IServiceCollection services)
123151
{
124152
foreach (var service in services)
125153
{
@@ -158,7 +186,10 @@ public IBot Build()
158186
Services.AddSingleton<IKeyValueProvider, InMemoryKeyValueProvider>();
159187
}
160188
Services.AddSingleton<IHostApplicationLifetime, HostApplicationLifetime>();
161-
return new BotApp(client, Services.BuildServiceProvider(), _botConfiguration);
189+
Services.AddHostedService<TelegramBotHostedService>();
190+
Services.AddSingleton(_botConfiguration);
191+
Services.AddSingleton<BotControllerMethodsContainer>();
192+
return new BotApp(Services.BuildServiceProvider());
162193
}
163194
}
164195
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Collections.Generic;
4+
5+
namespace TelegramBot.Containers
6+
{
7+
internal class BotControllerMethodsContainer
8+
{
9+
internal IReadOnlyCollection<MethodInfo> Methods => _methods.AsReadOnly();
10+
private readonly List<MethodInfo> _methods = new List<MethodInfo>();
11+
12+
internal void AddMethod(MethodInfo method)
13+
{
14+
if (method == null)
15+
{
16+
throw new ArgumentNullException(nameof(method), "Method cannot be null.");
17+
}
18+
if (!_methods.Contains(method))
19+
{
20+
_methods.Add(method);
21+
}
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)