Ctrl K
ComunidadGuías…

Ferrite.Core/RequestChain/MTProtoRequestProcessor.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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<Task<byte[]?>>> _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<ValueTask<TLBytes?>> 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<byte[]?> AnswerOnce(TLExecutionContext ctx, Func<ValueTask<TLBytes?>> 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<Task<byte[]?>>(
            () => 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<byte[]?> InvokeAsync(Func<ValueTask<TLBytes?>> 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
        };
    }
}