An open API service indexing awesome lists of open source software.

https://github.com/win7user10/laraue.telegram.net

Create backend for telegram bots
https://github.com/win7user10/laraue.telegram.net

csharp csharp-library telegram telegram-bot

Last synced: 4 months ago
JSON representation

Create backend for telegram bots

Awesome Lists containing this project

README

          

# Laraue.Telegram.NET

This library contains infrastructure code to write testable telegram bots for one to one conversations.
The library use https://github.com/TelegramBots/Telegram.Bot package inside to communicate with Telegram.

## Laraue.Telegram.NET.Core

[![latest version](https://img.shields.io/nuget/v/Laraue.Telegram.NET.Core)](https://www.nuget.org/packages/Laraue.Telegram.NET.Core)
[![latest version](https://img.shields.io/nuget/dt/Laraue.Telegram.NET.Core)](https://www.nuget.org/packages/Laraue.Telegram.NET.Core)

The basic idea of the library is to register all possible telegram routes in classes
inherited from _TelegramController_.

```csharp
public class MenuController : TelegramController
{
private readonly IMenuService _service;

public SettingsController(IMenuService service)
{
_service = service;
}

[TelegramMessageRoute("/start")] // When the user will send the '/start' message
public Task ShowMenuAsync(TelegramRequestContext requestContext)
{
return _service.HandleStartAsync(requestContext.Update.Message!); // execute this code
}
```

To process callback queries can be used attribute _TelegramCallbackRouteAttribute_,
any request type can be handled by implementing the attribute inherited from _TelegramBaseRouteAttribute_.

To start using this and other controllers, the library should be added with the following way:

A. Using webhooks
```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions();
builder.Services.Configure(builder.Configuration.GetSection("Telegram"));

builder
.Services
.AddTelegramCore(new TelegramBotClientOptions("5118263652:AAHiPDQ8kVcbs2WZWG4Z..."))
.AddInMemoryUpdatesQueue();

var app = builder.Build();
app.MapTelegramRequests("/api/telegram");
app.Run();
```

B. Using long pooling
```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions();
builder.Services.Configure(builder.Configuration.GetSection("Telegram"));

builder
.Services
.AddTelegramCore(new TelegramBotClientOptions("5118263652:AAHiPDQ8kVcbs2WZWG4Z..."))
.AddInMemoryUpdatesQueue();

var app = builder.Build();
app.Run();
```

The library takes the decision what way to use based on the property `WebhookUrl` in the app settings.
When the setting is set, the application considers Webhook mode is used, otherwise long pooling mode
is active.

The token in the TelegramBotClientOptions can be taken via @BotFather bot in the Telegram.
_MapTelegramRequests_ method setup the address which will listen callbacks from the telegram.
This address should be set also on the telegram side by calling the next url:

**Note**: The AddInMemoryUpdatesQueue call adds to the container InMemory queue to store telegram updates.
It is not recommended due to possible updates loss. The better way is to use
`AddEfCoreUpdatesQueue` from the package `Laraue.Telegram.NET.UpdatesQueue.EFCore`
to store all updates queue in the database or implementing custom `IUpdatesQueue`.
```
https://api.telegram.org/bot5118263652:AAHiPDQ8kVcbs2WZWG4Z.../setWebhook?url=https://your.host/api/telegram
```

## Laraue.Telegram.NET.Authentication

[![latest version](https://img.shields.io/nuget/v/Laraue.Telegram.NET.Authentication)](https://www.nuget.org/packages/Laraue.Telegram.NET.Authentication)
[![latest version](https://img.shields.io/nuget/dt/Laraue.Telegram.NET.Authentication)](https://www.nuget.org/packages/Laraue.Telegram.NET.Authentication)

As soon as each request is usually associated with the specific user, it is convenient to
have information about system user in the request instead of manual finding
user by telegram id when it is required.
This library helps to integrate ASP.NET.Identity with the telegram request context.
To use it the model of user can be defined (example for the user with id of _Guid_ type)
```csharp
public class User : TelegramIdentityUser
{}

```
Register Auth functionality in the container. Here is the using of ```User``` model with _Guid_ as identifier.
```csharp
services.AddTelegramCore(new TelegramBotClientOptions(builder.Configuration["Telegram:Token"]!))
.AddTelegramAuthentication()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders()
```

After that in each telegram controller method can be retrieved ```TelegramRequestContext``` which contains information
about the user made the request.

```csharp
public class UserController : TelegramController
{
[TelegramMessageRoute("/me")]
public void PrintMyId(TelegramRequestContext requestContext)
{
Console.WriteLine(requestContext.UserId); // user id in our storage
Console.WriteLine(requestContext.Update.GetUserId()); // telegram user id from the update
}
```

To prevent writing generic type for each request, new context class can be created and added to the container.
```csharp
public sealed class RequestContext : TelegramRequestContext
{}
```

```csharp
services.AddTelegramAuthentication()
```

Now the class ```RequestContext``` can be injected without defining request generic type in the each request.

```csharp
public class UserController : TelegramController
{
[TelegramMessageRoute("/also-me")]
public void PrintMyId(RequestContext requestContext)
{
Console.WriteLine(requestContext.UserId);
}
```

**Note** - ```AddTelegramAuthentication()``` returns ```Microsoft.AspNetCore.Identity.IdentityBuilder```,
use it to configure identity options.

The logic of retrieving user id field is next: from the received telegram message telegram identifier of the user that send the message takes.
Then this identifier maps to the identifier in the database. Then this identifier sets to the request context.

### Middleware
The package has the opportunity to extend request pipeline by adding custom middlewares. The example is the next
```csharp
public class LogExceptionsMiddleware : ITelegramMiddleware
{
private readonly ITelegramMiddleware _next;
private readonly TelegramRequestContext _telegramRequestContext;

public LogExceptionsMiddleware(
ITelegramMiddleware next,
TelegramRequestContext telegramRequestContext)
{
_next = next;
_telegramRequestContext = telegramRequestContext;
}

public async Task InvokeAsync(CancellationToken ct = default)
{
try
{
return await _next.InvokeAsync(ct);
}
catch (BadTelegramRequestException ex)
{
_logger.LogError(ex, "Error occured");
}

return null;
}
}
```
Adding middleware to the request pipeline
```csharp
services.AddTelegramMiddleware();
```
**Note** - middlewares executes in the order they were added.

## Laraue.Telegram.NET.Interceptors

[![latest version](https://img.shields.io/nuget/v/Laraue.Telegram.NET.Interceptors)](https://www.nuget.org/packages/Laraue.Telegram.NET.Interceptors)
[![latest version](https://img.shields.io/nuget/dt/Laraue.Telegram.NET.Interceptors)](https://www.nuget.org/packages/Laraue.Telegram.NET.Interceptors)

The main case of the library is sometimes something from the user should be asked,
and next his answer should be considered as answer to this question.
This library allow to create such functionality. To use it should be implemented how to store Type that should be used to answer
for question.
```csharp
public class InterceptorState : BaseInterceptorState
{
protected override Task TryGetStringIdentifierFromStorageAsync(string userId)
{
throw new NotImplementedException();
}

protected override Task SetStringIdentifierToStorageAsync(string userId, string id)
{
throw new NotImplementedException();
}

public override Task ResetAsync(string userId)
{
throw new NotImplementedException();
}
}
```
Add the functionality to container. Interceptors are depending on _Laraue.Telegram.NET.Core_
and _Laraue.Telegram.NET.Authentication_ packages and should be registered in such order.
```csharp
services.AddTelegramCore(new TelegramBotClientOptions(builder.Configuration["Telegram:Token"]!))
.AddTelegramRequestEfCoreInterceptors()
.AddTelegramAuthentication()
```
**Note** - there is already implemented IInterceptorState with storing state in the DB via EFCore in the package
_Laraue.Telegram.NET.Interceptors.EFCore_.
```csharp
services.AddTelegramRequestEfCoreInterceptors(assemblies})
```

After interceptors state storage is setup, the interceptor can be implemented
```csharp
public class UpdateAgeInterceptor : BaseRequestInterceptor
{
private readonly IUserRepository _repository;

public UpdateAgeResponseInterceptor(IUserRepository repository)
{
_repository = repository;
}

protected override Task ValidateAsync(
TelegramRequestContext requestContext,
InterceptResult interceptResult,
UpdateAgeContext interceptorContext)
{
if (uint.TryParse(update.Message!.Text, out var age))
{
interceptResult.SetModel(age);
}
else
{
interceptResult.SetError("Age should be a positive number");
}

return Task.CompletedTask;
}

protected abstract Task ExecuteRouteAsync(
TelegramRequestContext requestContext,
uint model,
UpdateAgeContext interceptorContext)
{
return _repository.UpdateUserAgeAsync(interceptorContext.UserId, model);
}
}

```
There is a short example of interceptor using
```csharp
public class TestController : TelegramController
{
private readonly IInterceptorState _questionState;
private readonly ITelegramBotClient _telegramClient;

public TestController(IInterceptorState questionState, ITelegramBotClient telegramClient)
{
_questionStorage = questionState;
_telegramClient = telegramClient;
}

[TelegramMessageRoute("/start")]
public async Task HandleStart(TelegramRequestContext context)
{
await _client.SendTextMessageAsync(
context.Update.GetUserId(),
"What is your age?");

await _questionState.SetAsync(context.UserId, new UpdateAgeInterceptor
{
UserId = context.UserId
});
}
}
```

## Laraue.Telegram.NET.Testing
[![latest version](https://img.shields.io/nuget/v/Laraue.Telegram.NET.Testing)](https://www.nuget.org/packages/Laraue.Telegram.NET.Testing)
[![latest version](https://img.shields.io/nuget/dt/Laraue.Telegram.NET.Testing)](https://www.nuget.org/packages/Laraue.Telegram.NET.Testing)

The package allows to write integration tests for host built with Telegram.NET library.

### Usage
Create the class of Test Host implementing `TelegramTestHost` or `TelegramTestHost` from the package.
```csharp
public class AppTelegramTestHost(IServiceCollection serviceCollection)
: TelegramTestHost(serviceCollection)
{
protected override void BeforeFirstRequest()
{
// Do something before test server starts, migration run for example
// When tests use real DB here can be it clearing
}

protected override void Dispose(bool disposing)
{
// Do something when the host is no more used
}
}
```
My own recomendation for the next step is to create the base test class that have a method to run the host.
`XUnit` example:
```csharp
public class IntegrationTest
{
protected static AppTelegramTestHost GetTelegramTestHost()
{
var builder = WebApplication.CreateBuilder();

builder.Configuration.AddJsonFile("appsettings.json");
builder.AddApplicationServices();

return new AppTelegramTestHost(appServices);
}
}
```
Test infrastructure is ready. The test example:
```csharp
public class StartControllerTests : IntegrationTest
{
[Fact]
public async Task Start_ShouldSendWelcome_Always()
{
// Run the host from the base class
using var telegramTestHost = GetTelegramTestHost();

// Emulate new message in chat
await telegramTestHost.SendUpdateAsync(new Update
{
Message = new Message
{
Text = "/start",
From = DefaultUser, // Can be determined in base class to avoid repeating
}
});

// Ensure only one message was sent back to Telegram
var request = telegramTestHost
.Requests()
.Single();

// Check the message text
request.CheckMessage("Hello, user");

// Check the buttons in the message
request.CheckButtonsSequentially(buttons =>
buttons
.HasButtonsRow(
new ButtonAssert("Menu", "menu")));
}
}
```
More complex asserts can be made with the services requested from the Test Host container:
```csharp
using var telegramTestHost = GetTelegramTestHost();
using var scope = telegramTestHost.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService();

Assert.Single(dbContext.Users);
```