// SPDX-License-Identifier: AGPL-3.0-or-later // Copyright (C) 2022-2026 Aykut Alparslan KOC using System; using System.Collections.Concurrent; using Ferrite.Core.Execution; using Ferrite.Core.Execution.Functions; using Ferrite.Services.Sessions; using Ferrite.TL; using Ferrite.Utils; namespace Ferrite.Core.RequestChain; public class MTProtoRequestProcessor : ILinkedHandler { private const int AnsweredMessageLimit = 4096; private readonly ISessionService _sessionManager; private readonly IMessagePipe _pipe; private readonly IExecutionEngine _api; private readonly ILogger _log; private readonly ConcurrentDictionary<(long Session, long MessageId), Lazy>> _answers = new(); private readonly ConcurrentQueue<(long, long)> _answered = new(); public MTProtoRequestProcessor(ISessionService sessionManager, IMessagePipe pipe, IExecutionEngine api, ILogger log) { _sessionManager = sessionManager; _pipe = pipe; _api = api; _log = log; } public ILinkedHandler SetNext(ILinkedHandler value) { Next = value; return Next; } public ILinkedHandler? Next { get; set; } public ValueTask Process(object? sender, ITLStreamingObject input, TLExecutionContext ctx) { if (sender is IMTProtoConnection connection) { _ = ProcessStreamingAsync(connection, input, ctx); } return ValueTask.CompletedTask; } private async Task ProcessStreamingAsync(IMTProtoConnection connection, ITLStreamingObject input, TLExecutionContext ctx) { try { await ProcessAndSend(connection, ctx, () => _api.Invoke(input, ctx), ack: true, input.Constructor, input.Constructor); } catch (Exception e) { _log.Error(e, $"😭 => {this} => streaming #{input.Constructor:x} => {e.Message}"); } finally { try { await input.DrainAsync(); } catch (Exception e) { _log.Error(e, $"😭 => {this} => drain #{input.Constructor:x} => {e.Message}"); } } } private static byte[] BuildMsgsAckPayload(long msgId) { var msgIds = new TL.VectorOfLong(); msgIds.Append(msgId); using var ack = TL.mtproto.MsgsAck.Builder().MsgIds(msgIds).Build(); return ack.TLBytes!.Value.AsSpan().ToArray(); } public async ValueTask Process(object? sender, TLBytes input, TLExecutionContext ctx) { if (sender is IMTProtoConnection connection) { if (input.Constructor == Constructors.baseLayer_GetFile || _api.IsFileRequest(input)) { await ProcessFile(connection, input, ctx); } else { await ProcessAndSend(connection, ctx, () => _api.Invoke(input, ctx), RequiresEarlyAck(input.Constructor), input.Constructor, ResolveMethodConstructor(input)); } } else { await ProcessAndForward(input, ctx); } if (Next != null) await Next.Process(sender, input, ctx); else input.Dispose(); } private async Task ProcessAndForward(TLBytes input, TLExecutionContext ctx) { AfterResponse afterResponse = AfterResponse.Begin(); try { if (await _api.Invoke(input, ctx) is { } result) { using (result) { await ForwardToRemoteSession(ctx, result.AsSpan().ToArray(), ResolveMethodConstructor(input)); } } } finally { await CompleteAfterResponse(afterResponse, input.Constructor); } } private async Task ProcessAndSend(IMTProtoConnection connection, TLExecutionContext ctx, Func> invoke, bool ack, int constructor, int methodConstructor) { if (ack) { await Send(connection, ctx, BuildMsgsAckPayload(ctx.MessageId), null); } AfterResponse afterResponse = AfterResponse.Begin(); try { if (await AnswerOnce(ctx, invoke, constructor) is { } data) { await Send(connection, ctx, data, methodConstructor); } } finally { await CompleteAfterResponse(afterResponse, constructor); } } private async ValueTask CompleteAfterResponse(AfterResponse afterResponse, int constructor) { try { await afterResponse.CompleteAsync(); } catch (Exception e) { _log.Error(e, $"😭 => {this} => after response #{constructor:x} => {e.Message}"); } } private Task AnswerOnce(TLExecutionContext ctx, Func> invoke, int constructor) { _log.Debug($"📥 request #{constructor.ToString("x")} session={ctx.SessionId} " + $"msgId={ctx.MessageId} authKey={ctx.AuthKeyId} " + $"permAuthKey={ctx.PermAuthKeyId}"); var identity = (ctx.SessionId, ctx.MessageId); var answer = _answers.GetOrAdd(identity, _ => new Lazy>( () => InvokeAsync(invoke), LazyThreadSafetyMode.ExecutionAndPublication)); if (answer.IsValueCreated) { _log.Debug($"repeated message {ctx.MessageId} in session {ctx.SessionId} " + "is answered from its first execution"); } else { RememberAnswer(identity); } return answer.Value; } private static async Task InvokeAsync(Func> invoke) { if (await invoke() is not { } result) return null; using (result) return result.AsSpan().ToArray(); } private void RememberAnswer((long, long) identity) { _answered.Enqueue(identity); while (_answered.Count > AnsweredMessageLimit && _answered.TryDequeue(out var evicted)) { _answers.TryRemove(evicted, out _); } } private async Task ProcessFile(IMTProtoConnection connection, TLBytes input, TLExecutionContext ctx) { var result = await _api.InvokeFile(input, ctx); if (result.File != null) await connection.SendAsync(result.File); else if (result.Error is { } error) { using (error) using (var rpcResult = RpcResultGenerator.Generate(error, ctx.MessageId)) { await Send(connection, ctx, rpcResult.AsSpan().ToArray(), null); } } } private static bool RequiresEarlyAck(int constructor) { return constructor is Constructors.baseLayer_UploadProfilePhoto; } private static ValueTask Send(IMTProtoConnection connection, TLExecutionContext ctx, byte[] data, int? methodConstructor) { return connection.SendAsync(BuildResponse(ctx, data, methodConstructor)); } private async Task ForwardToRemoteSession(TLExecutionContext ctx, byte[] data, int methodConstructor) { if (await _sessionManager.GetSessionStateAsync(ctx.SessionId) is { } session) { var bytes = MTProtoMessageEnvelope.Serialize( BuildResponse(ctx, data, methodConstructor)); await _pipe.WriteMessageAsync(MessagePipeChannels.ForNode(session.NodeId), bytes); } } private static int ResolveMethodConstructor(TLBytes input) { try { return RequestUnwrapper.MethodConstructor(input); } catch { return input.Constructor; } } private static MTProtoMessage BuildResponse(TLExecutionContext ctx, byte[] data, int? methodConstructor) { return new MTProtoMessage { MessageType = MTProtoMessageType.Encrypted, SessionId = ctx.SessionId, IsResponse = true, IsContentRelated = true, MessageId = ctx.MessageId, Data = data, RequestConstructor = methodConstructor }; } }