Support for connecting to Telegram via on-demand HTTP requests instead of permanent TCP connection: client.HttpMode (experimental)

This commit is contained in:
Wizou 2024-09-30 02:15:10 +02:00
parent 4f9accdfc8
commit a19db86c1d
2 changed files with 59 additions and 25 deletions

View file

@ -187,10 +187,10 @@ namespace WTelegram
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param> /// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
/// <param name="schedule_date">UTC timestamp when the message should be sent</param> /// <param name="schedule_date">UTC timestamp when the message should be sent</param>
/// <param name="videoUrlAsFile">Any <see cref="InputMediaDocumentExternal"/> URL pointing to a video should be considered as non-streamable</param> /// <param name="videoUrlAsFile">Any <see cref="InputMediaDocumentExternal"/> URL pointing to a video should be considered as non-streamable</param>
/// <returns>The last of the media group messages, confirmed by Telegram</returns> /// <returns>The media group messages as received by Telegram</returns>
/// <remarks> /// <remarks>
/// * The caption/entities are set on the last media<br/> /// * The caption/entities are set on the first media<br/>
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported by downloading the file from the web via HttpClient and sending it to Telegram. /// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported natively for bot accounts, and for user accounts by downloading the file from the web via HttpClient and sending it to Telegram.
/// WTelegramClient proxy settings don't apply to HttpClient<br/> /// WTelegramClient proxy settings don't apply to HttpClient<br/>
/// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes /// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
/// </remarks> /// </remarks>

View file

@ -6,6 +6,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Sockets; using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
@ -28,8 +29,6 @@ namespace WTelegram
/// <summary>This event will be called when unsollicited updates/messages are sent by Telegram servers</summary> /// <summary>This event will be called when unsollicited updates/messages are sent by Telegram servers</summary>
/// <remarks>Make your handler <see langword="async"/>, or return <see cref="Task.CompletedTask"/> or <see langword="null"/><br/>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30">Examples/Program_ReactorError.cs</see> for how to use this<br/>or <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21">Examples/Program_ListenUpdate.cs</see> using the UpdateManager class instead</remarks> /// <remarks>Make your handler <see langword="async"/>, or return <see cref="Task.CompletedTask"/> or <see langword="null"/><br/>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30">Examples/Program_ReactorError.cs</see> for how to use this<br/>or <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21">Examples/Program_ListenUpdate.cs</see> using the UpdateManager class instead</remarks>
public event Func<UpdatesBase, Task> OnUpdates; public event Func<UpdatesBase, Task> OnUpdates;
[Obsolete("This event was renamed OnUpdates (plural). You may also want to consider using our new UpdateManager class instead (see FAQ)")]
public event Func<UpdatesBase, Task> OnUpdate { add { OnUpdates += value; } remove { OnUpdates -= value; } }
/// <summary>This event is called for other types of notifications (login states, reactor errors, ...)</summary> /// <summary>This event is called for other types of notifications (login states, reactor errors, ...)</summary>
public event Func<IObject, Task> OnOther; public event Func<IObject, Task> OnOther;
/// <summary>Use this handler to intercept Updates that resulted from your own API calls</summary> /// <summary>Use this handler to intercept Updates that resulted from your own API calls</summary>
@ -67,6 +66,7 @@ namespace WTelegram
private Session.DCSession _dcSession; private Session.DCSession _dcSession;
private TcpClient _tcpClient; private TcpClient _tcpClient;
private Stream _networkStream; private Stream _networkStream;
private HttpClient _httpClient;
private IObject _lastSentMsg; private IObject _lastSentMsg;
private long _lastRecvMsgId; private long _lastRecvMsgId;
private readonly List<long> _msgsToAck = []; private readonly List<long> _msgsToAck = [];
@ -199,6 +199,10 @@ namespace WTelegram
public void DisableUpdates(bool disable = true) => _dcSession.DisableUpdates(disable); public void DisableUpdates(bool disable = true) => _dcSession.DisableUpdates(disable);
/// <summary>Enable connecting to Telegram via on-demand HTTP requests instead of permanent TCP connection</summary>
/// <param name="httpClient">HttpClient to use. Leave <see langword="null"/> for a default one</param>
public void HttpMode(HttpClient httpClient = null) => _httpClient = httpClient ?? new();
/// <summary>Disconnect from Telegram <i>(shouldn't be needed in normal usage)</i></summary> /// <summary>Disconnect from Telegram <i>(shouldn't be needed in normal usage)</i></summary>
/// <param name="resetUser">Forget about logged-in user</param> /// <param name="resetUser">Forget about logged-in user</param>
/// <param name="resetSessions">Disconnect secondary sessions with other DCs</param> /// <param name="resetSessions">Disconnect secondary sessions with other DCs</param>
@ -513,16 +517,16 @@ namespace WTelegram
return null; return null;
} }
} }
static string TransportError(int error_code) => error_code switch
{
404 => "Auth key not found",
429 => "Transport flood",
444 => "Invalid DC",
_ => Enum.GetName(typeof(HttpStatusCode), error_code) ?? "Transport error"
};
} }
static string TransportError(int error_code) => error_code switch
{
404 => "Auth key not found",
429 => "Transport flood",
444 => "Invalid DC",
_ => Enum.GetName(typeof(HttpStatusCode), error_code) ?? "Transport error"
};
internal void CheckSalt() internal void CheckSalt()
{ {
lock (_session) lock (_session)
@ -871,6 +875,8 @@ namespace WTelegram
throw new Exception("Library was not compiled with OBFUSCATION symbol"); throw new Exception("Library was not compiled with OBFUSCATION symbol");
#endif #endif
} }
if (_httpClient != null)
_reactorTask = Task.CompletedTask;
else else
{ {
endpoint = _dcSession?.EndPoint ?? GetDefaultEndpoint(out int defaultDc); endpoint = _dcSession?.EndPoint ?? GetDefaultEndpoint(out int defaultDc);
@ -930,16 +936,19 @@ namespace WTelegram
_networkStream = _tcpClient.GetStream(); _networkStream = _tcpClient.GetStream();
} }
byte protocolId = (byte)(_paddedMode ? 0xDD : 0xEE);
#if OBFUSCATION
(_sendCtr, _recvCtr, preamble) = InitObfuscation(secret, protocolId, dcId);
#else
preamble = new byte[] { protocolId, protocolId, protocolId, protocolId };
#endif
await _networkStream.WriteAsync(preamble, 0, preamble.Length, _cts.Token);
_dcSession.Salts?.Remove(DateTime.MaxValue); _dcSession.Salts?.Remove(DateTime.MaxValue);
_reactorTask = Reactor(_networkStream, _cts); if (_networkStream != null)
{
byte protocolId = (byte)(_paddedMode ? 0xDD : 0xEE);
#if OBFUSCATION
(_sendCtr, _recvCtr, preamble) = InitObfuscation(secret, protocolId, dcId);
#else
preamble = new byte[] { protocolId, protocolId, protocolId, protocolId };
#endif
await _networkStream.WriteAsync(preamble, 0, preamble.Length, _cts.Token);
_reactorTask = Reactor(_networkStream, _cts);
}
_sendSemaphore.Release(); _sendSemaphore.Release();
try try
@ -947,7 +956,7 @@ namespace WTelegram
if (_dcSession.AuthKeyID == 0) if (_dcSession.AuthKeyID == 0)
await CreateAuthorizationKey(this, _dcSession); await CreateAuthorizationKey(this, _dcSession);
var keepAliveTask = KeepAlive(_cts.Token); if (_networkStream != null) _ = KeepAlive(_cts.Token);
if (quickResume && _dcSession.Layer == Layer.Version && _dcSession.DataCenter != null && _session.MainDC != 0) if (quickResume && _dcSession.Layer == Layer.Version && _dcSession.DataCenter != null && _session.MainDC != 0)
TLConfig = new Config { this_dc = _session.MainDC, dc_options = _session.DcOptions }; TLConfig = new Config { this_dc = _session.MainDC, dc_options = _session.DcOptions };
else else
@ -1467,10 +1476,11 @@ namespace WTelegram
int frameLength = (int)memStream.Length; int frameLength = (int)memStream.Length;
BinaryPrimitives.WriteInt32LittleEndian(buffer, frameLength - 4); // patch payload_len with correct value BinaryPrimitives.WriteInt32LittleEndian(buffer, frameLength - 4); // patch payload_len with correct value
#if OBFUSCATION #if OBFUSCATION
_sendCtr.EncryptDecrypt(buffer, frameLength); _sendCtr?.EncryptDecrypt(buffer, frameLength);
#endif #endif
await _networkStream.WriteAsync(buffer, 0, frameLength); var sending = SendFrame(buffer, frameLength);
_lastSentMsg = msg; _lastSentMsg = msg;
await sending;
} }
finally finally
{ {
@ -1478,6 +1488,24 @@ namespace WTelegram
} }
} }
private async Task SendFrame(byte[] buffer, int frameLength)
{
if (_httpClient == null)
await _networkStream.WriteAsync(buffer, 0, frameLength);
else
{
var endpoint = _dcSession?.EndPoint ?? GetDefaultEndpoint(out _);
var content = new ByteArrayContent(buffer, 4, frameLength - 4);
var response = await _httpClient.PostAsync($"http://{endpoint}/api", content);
if (response.StatusCode != HttpStatusCode.OK)
throw new RpcException((int)response.StatusCode, TransportError((int)response.StatusCode));
var data = await response.Content.ReadAsByteArrayAsync();
var obj = ReadFrame(data, data.Length);
if (obj != null)
await HandleMessageAsync(obj);
}
}
internal async Task<T> InvokeBare<T>(IMethod<T> request) internal async Task<T> InvokeBare<T>(IMethod<T> request)
{ {
if (_bareRpc != null) throw new WTException("A bare request is already undergoing"); if (_bareRpc != null) throw new WTException("A bare request is already undergoing");
@ -1501,6 +1529,12 @@ namespace WTelegram
retry: retry:
var rpc = new Rpc { type = typeof(T) }; var rpc = new Rpc { type = typeof(T) };
await SendAsync(query, true, rpc); await SendAsync(query, true, rpc);
if (_httpClient != null && !rpc.Task.IsCompleted)
{
await SendAsync(new HttpWait { max_delay = 30, wait_after = 10, max_wait = 1000 * PingInterval }, true);
if (!rpc.Task.IsCompleted) rpc.tcs.TrySetException(new RpcException(417, "Missing RPC response via HTTP"));
}
var result = await rpc.Task; var result = await rpc.Task;
switch (result) switch (result)
{ {