Skip to content

Commit

Permalink
Refactor Telegram Bridge and add support of picture attachments and c…
Browse files Browse the repository at this point in the history
…aptions
  • Loading branch information
fembina committed Nov 14, 2024
1 parent 2a08621 commit 5212652
Show file tree
Hide file tree
Showing 46 changed files with 400 additions and 344 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Talkie.Bridges.Telegram.Clients;

public interface ITelegramBotApiClient : IDisposable
public interface ITelegramClient : IDisposable
{
internal Task<TResult> SendAsync<TResult, TRequest>(string methodName, TRequest request,
CancellationToken cancellationToken = default);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
using System.Text.Json;
using Talkie.Bridges.Telegram.Configurations;
using Talkie.Bridges.Telegram.Models;
using Talkie.Bridges.Telegram.Responses;
using Talkie.Bridges.Telegram.Serialization;
using ClientHttpVersion = System.Net.HttpVersion;

namespace Talkie.Bridges.Telegram.Clients;

public sealed class TelegramBotApiClient : ITelegramBotApiClient
public sealed class TelegramClient : ITelegramClient
{
private const string Gzip = "gzip";

Expand All @@ -20,24 +20,19 @@ public sealed class TelegramBotApiClient : ITelegramBotApiClient

private readonly HttpClient _client;

private readonly ServerConfiguration _serverConfiguration;

private readonly ClientConfiguration _clientConfiguration;
private readonly TelegramConfiguration _configuration;

private readonly CancellationTokenSource _globalCancellationTokenSource = new();

public TelegramBotApiClient(ServerConfiguration serverConfiguration,
ClientConfiguration? clientConfiguration = null)
public TelegramClient(TelegramConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(serverConfiguration);

_serverConfiguration = serverConfiguration;
_clientConfiguration = clientConfiguration ?? new ClientConfiguration();
configuration.ThrowIfInvalid();

_client = BuildHttpClient(serverConfiguration, _clientConfiguration);
_configuration = configuration;
_client = BuildHttpClient(configuration);
}

async Task<TResult> ITelegramBotApiClient.SendAsync<TResult, TRequest>(string methodName, TRequest request,
async Task<TResult> ITelegramClient.SendAsync<TResult, TRequest>(string methodName, TRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
Expand All @@ -47,11 +42,11 @@ async Task<TResult> ITelegramBotApiClient.SendAsync<TResult, TRequest>(string me
.CreateLinkedTokenSource(_globalCancellationTokenSource.Token, cancellationToken);

return await SendRepeatableRequestAsync<TResult, TRequest>(methodName, request, scopedCancellationTokenSource.Token)
?? throw new TelegramBotApiRequestException(this, methodName,
?? throw new TelegramRequestException(this, methodName,
description: "Result is null");
}

async Task<TResult> ITelegramBotApiClient.SendAsync<TResult>(string methodName,
async Task<TResult> ITelegramClient.SendAsync<TResult>(string methodName,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(methodName);
Expand All @@ -66,7 +61,7 @@ async Task<TResult> ITelegramBotApiClient.SendAsync<TResult>(string methodName,
return result;
}

Task ITelegramBotApiClient.SendAsync<TRequest>(string methodName, TRequest request,
Task ITelegramClient.SendAsync<TRequest>(string methodName, TRequest request,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
Expand All @@ -78,7 +73,7 @@ Task ITelegramBotApiClient.SendAsync<TRequest>(string methodName, TRequest reque
return SendRepeatableRequestAsync<object, TRequest>(methodName, request, scopedCancellationTokenSource.Token);
}

Task ITelegramBotApiClient.SendAsync(string methodName,
Task ITelegramClient.SendAsync(string methodName,
CancellationToken cancellationToken)
{
using var scopedCancellationTokenSource = CancellationTokenSource
Expand All @@ -97,7 +92,7 @@ public async Task<Stream> DownloadAsync(string file, CancellationToken cancellat
{
return await DownloadCoreAsync(file, cancellationToken);
}
catch (TelegramBotApiRequestException exception)
catch (TelegramRequestException exception)
{
if (exception.StatusCode is null or not HttpStatusCode.TooManyRequests)
{
Expand All @@ -116,9 +111,11 @@ private async Task<Stream> DownloadCoreAsync(string file,

try
{
var serverConfiguration = _configuration.ServerConfiguration;

var httpRequest = new HttpRequestMessage(HttpMethod.Get, file)
{
RequestUri = new Uri($"https://{_serverConfiguration.Domain}/file/bot{_serverConfiguration.Token}/{file}")
RequestUri = new Uri($"https://{serverConfiguration.Domain}/file/bot{serverConfiguration.Token}/{file}")
};

Console.WriteLine(httpRequest.RequestUri);
Expand All @@ -132,19 +129,19 @@ private async Task<Stream> DownloadCoreAsync(string file,

var jsonResponse = await httpResponse.Content.ReadAsStringAsync(cancellationToken);

if (JsonSerializer.Deserialize(jsonResponse, typeof(Response), ModelsJsonSerializerContext.Default)
is not Response response)
if (JsonSerializer.Deserialize(jsonResponse, typeof(TelegramResponse), ModelsJsonSerializerContext.Default)
is not TelegramResponse response)
{
throw new TelegramBotApiRequestException(this, "download",
throw new TelegramRequestException(this, "download",
description: "Failed to deserialize response");
}

throw new TelegramBotApiRequestException(this, "download",
throw new TelegramRequestException(this, "download",
statusCode: (HttpStatusCode?)response.ErrorCode,
description: response.Description,
parameters: response.Parameters);
}
catch (TelegramBotApiRequestException)
catch (TelegramRequestException)
{
throw;
}
Expand All @@ -154,7 +151,7 @@ private async Task<Stream> DownloadCoreAsync(string file,
}
catch (Exception exception)
{
throw new TelegramBotApiRequestException(this, "download",
throw new TelegramRequestException(this, "download",
description: "Unknown error occurred while sending request",
innerException: exception);
}
Expand All @@ -169,7 +166,7 @@ private async Task<Stream> DownloadCoreAsync(string file,
{
return await SendRequestAsync<TResult, TRequest>(method, request, cancellationToken);
}
catch (TelegramBotApiRequestException exception)
catch (TelegramRequestException exception)
{
if (exception.StatusCode is null or not HttpStatusCode.TooManyRequests)
{
Expand All @@ -194,7 +191,7 @@ private async Task<Stream> DownloadCoreAsync(string file,

return await ParseHttpResponseAsync<TResult>(method, httpResponse, cancellationToken);
}
catch (TelegramBotApiRequestException)
catch (TelegramRequestException)
{
throw;
}
Expand All @@ -204,7 +201,7 @@ private async Task<Stream> DownloadCoreAsync(string file,
}
catch (Exception exception)
{
throw new TelegramBotApiRequestException(this, method,
throw new TelegramRequestException(this, method,
description: "Unknown error occurred while sending request",
innerException: exception);
}
Expand All @@ -220,16 +217,16 @@ private async Task<Stream> DownloadCoreAsync(string file,

var responseJson = await httpResponse.Content.ReadAsStringAsync(cancellationToken);

if (JsonSerializer.Deserialize(responseJson, typeof(Response<TResult>), ModelsJsonSerializerContext.Default)
is not Response<TResult> response)
if (JsonSerializer.Deserialize(responseJson, typeof(TelegramResponse<TResult>), ModelsJsonSerializerContext.Default)
is not TelegramResponse<TResult> response)
{
throw new TelegramBotApiRequestException(this, method,
throw new TelegramRequestException(this, method,
description: "Failed to deserialize response");
}

if (response.Ok is false || httpResponse.IsSuccessStatusCode is false)
{
throw new TelegramBotApiRequestException(this, method,
throw new TelegramRequestException(this, method,
statusCode: (HttpStatusCode?)response.ErrorCode,
description: response.Description,
parameters: response.Parameters);
Expand All @@ -247,7 +244,7 @@ private async Task<HttpRequestMessage> BuildHttpRequestAsync<TRequest>(string me

var requestJson = JsonSerializer.Serialize(request, typeof(TRequest), ModelsJsonSerializerContext.Default);

if (_clientConfiguration.UseGzipCompression)
if (_configuration.ClientConfiguration.UseGzipCompression)
{
await AddGzipJsonContentAsync(httpRequest, requestJson, cancellationToken);
}
Expand Down Expand Up @@ -282,19 +279,19 @@ private static async Task AddGzipJsonContentAsync(HttpRequestMessage httpRequest
httpRequest.Content.Headers.ContentEncoding.Add(Gzip);
}

private async Task WaitRetryDelayAsync(TelegramBotApiRequestException exception,
private async Task WaitRetryDelayAsync(TelegramRequestException exception,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

if (exception.Parameters.TryGetValue(TelegramBotApiRequestException.ParameterNames.RetryAfter, out var delay)
if (exception.Parameters.TryGetValue(TelegramRequestException.ParameterNames.RetryAfter, out var delay)
&& delay.TryGetNumber(out var delaySeconds))
{
await Task.Delay(TimeSpan.FromSeconds(delaySeconds), cancellationToken);
}
else
{
await Task.Delay(_serverConfiguration.DefaultRetryDelay, cancellationToken);
await Task.Delay(_configuration.ServerConfiguration.DefaultRetryDelay, cancellationToken);
}
}

Expand All @@ -306,8 +303,11 @@ public void Dispose()
_client.Dispose();
}

private static HttpClient BuildHttpClient(ServerConfiguration serverConfiguration, ClientConfiguration clientConfiguration)
private static HttpClient BuildHttpClient(TelegramConfiguration configuration)
{
var clientConfiguration = configuration.ClientConfiguration;
var serverConfiguration = configuration.ServerConfiguration;

var client = new HttpClient(new SocketsHttpHandler
{
EnableMultipleHttp2Connections = true,
Expand All @@ -326,9 +326,9 @@ private static HttpClient BuildHttpClient(ServerConfiguration serverConfiguratio
{
DefaultRequestVersion = clientConfiguration.ProtocolVersion switch
{
HttpProtocol.Version10 => ClientHttpVersion.Version10,
HttpProtocol.Version11 => ClientHttpVersion.Version11,
HttpProtocol.Version20 => ClientHttpVersion.Version20,
HttpProtocol.Version10 => HttpVersion.Version10,
HttpProtocol.Version11 => HttpVersion.Version11,
HttpProtocol.Version20 => HttpVersion.Version20,
_ => throw new ArgumentOutOfRangeException(nameof(clientConfiguration.ProtocolVersion))
},
BaseAddress = new Uri($"https://{serverConfiguration.Domain}/bot{serverConfiguration.Token}/")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Talkie.Bridges.Telegram.Models;
using Talkie.Bridges.Telegram.Requests;

namespace Talkie.Bridges.Telegram.Clients;

public static partial class TelegramClientExtensions
{
public static Task<TelegramUpdate[]> GetUpdatesAsync(this ITelegramClient client, TelegramGetUpdatesRequest getUpdates,
CancellationToken cancellationToken = default)
{
return client.SendAsync<TelegramUpdate[], TelegramGetUpdatesRequest>("getUpdates", getUpdates, cancellationToken);
}

public static Task<TelegramUser> GetMeAsync(this ITelegramClient client,
CancellationToken cancellationToken = default)
{
return client.SendAsync<TelegramUser>("getMe", cancellationToken);
}

public static Task<TelegramMessage> SendMessageAsync(this ITelegramClient client, TelegramSendMessageRequest request,
CancellationToken cancellationToken = default)
{
return client.SendAsync<TelegramMessage, TelegramSendMessageRequest>("sendMessage", request, cancellationToken);
}

public static Task<bool> DeleteMessageAsync(this ITelegramClient client, TelegramDeleteMessageRequest request,
CancellationToken cancellationToken = default)
{
return client.SendAsync<bool, TelegramDeleteMessageRequest>("deleteMessage", request, cancellationToken);
}

public static Task<TelegramMessage> EditMessageTextAsync(this ITelegramClient client, TelegramEditMessageTextRequest request,
CancellationToken cancellationToken = default)
{
return client.SendAsync<TelegramMessage, TelegramEditMessageTextRequest>("editMessageText", request, cancellationToken);
}

public static Task<TelegramFile> GetFileAsync(this ITelegramClient client, TelegramGetFileRequest request,
CancellationToken cancellationToken = default)
{
return client.SendAsync<TelegramFile, TelegramGetFileRequest>("getFile", request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Talkie.Bridges.Telegram.Clients;

public sealed partial class TelegramBotApiRequestException
public sealed partial class TelegramRequestException
{
public static class ParameterNames
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

namespace Talkie.Bridges.Telegram.Clients;

public sealed partial class TelegramBotApiRequestException : Exception
public sealed partial class TelegramRequestException : Exception
{
public TelegramBotApiRequestException(ITelegramBotApiClient client, string methodName,
public TelegramRequestException(ITelegramClient client, string methodName,
HttpStatusCode? statusCode = null,
string? description = null,
IReadOnlyDictionary<string, TextOrNumber>? parameters = null,
IReadOnlyDictionary<string, TextOrNumberValue>? parameters = null,
Exception? innerException = null) : base(null, innerException)
{
ArgumentNullException.ThrowIfNull(client);
Expand All @@ -19,19 +19,19 @@ public TelegramBotApiRequestException(ITelegramBotApiClient client, string metho
Client = client;
MethodName = methodName;
StatusCode = statusCode;
Parameters = parameters ?? FrozenDictionary<string, TextOrNumber>.Empty;
Parameters = parameters ?? FrozenDictionary<string, TextOrNumberValue>.Empty;
Description = description;
}

public ITelegramBotApiClient Client { get; }
public ITelegramClient Client { get; }

public HttpStatusCode? StatusCode { get; }

public string MethodName { get; }

public string? Description { get; }

public IReadOnlyDictionary<string, TextOrNumber> Parameters { get; }
public IReadOnlyDictionary<string, TextOrNumberValue> Parameters { get; }

public override string HelpLink => $"https://core.telegram.org/bots/api#{MethodName}";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Talkie.Bridges.Telegram.Models;

namespace Talkie.Bridges.Telegram.Configurations;

public sealed record ClientConfiguration
public sealed record TelegramClientConfiguration
{
public HttpProtocol ProtocolVersion { get; init; } = HttpProtocol.Version20;

Expand Down
Loading

0 comments on commit 5212652

Please sign in to comment.