diff --git a/Lagrange.Core/BotContext.cs b/Lagrange.Core/BotContext.cs index f702ff6f..c938e2c6 100644 --- a/Lagrange.Core/BotContext.cs +++ b/Lagrange.Core/BotContext.cs @@ -23,6 +23,7 @@ internal BotContext(BotConfig config, BotKeystore keystore, BotAppInfo appInfo) SocketContext = new SocketContext(this); EventContext = new EventContext(this); HighwayContext = new HighwayContext(this); + FlashTransferContext = new FlashTransferContext(this); } public BotConfig Config { get; } @@ -41,6 +42,7 @@ internal BotContext(BotConfig config, BotKeystore keystore, BotAppInfo appInfo) internal EventContext EventContext { get; } internal HighwayContext HighwayContext { get; } + internal FlashTransferContext FlashTransferContext { get; } #region Shortcut Methods diff --git a/Lagrange.Core/Internal/Context/FlashTransferContext.cs b/Lagrange.Core/Internal/Context/FlashTransferContext.cs new file mode 100644 index 00000000..d5124819 --- /dev/null +++ b/Lagrange.Core/Internal/Context/FlashTransferContext.cs @@ -0,0 +1,113 @@ +using System.Security.Cryptography; +using Lagrange.Core.Internal.Packets.Service; +using Lagrange.Core.Utility; +using Lagrange.Core.Utility.Cryptography; +using Lagrange.Core.Utility.Extension; + +namespace Lagrange.Core.Internal.Context; + +public class FlashTransferContext +{ + private const string Tag = nameof(FlashTransferContext); + private readonly BotContext _botContext; + private readonly HttpClient _client; + private readonly string? _url; + private const uint ChunkSize = 1024 * 1024; + + internal FlashTransferContext(BotContext botContext) + { + _botContext = botContext; + _client = new HttpClient(); + _client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip"); + _url = "https://multimedia.qfile.qq.com/sliceupload"; + } + + public async Task UploadFile(string uKey, Stream bodyStream) + { + var sha1StateVs = new FlashTransferSha1StateV { State = [] }; + var chunkCount = (uint)((bodyStream.Length + ChunkSize - 1) / ChunkSize); + + var sha1Stream = new Sha1Stream(); + for (uint i = 0; i < chunkCount; i++) + { + if (i != chunkCount - 1) + { + var accLength = (int)((i + 1) * ChunkSize); + var accBuffer = new byte[accLength]; + + bodyStream.Position = 0; + await bodyStream.ReadExactlyAsync(accBuffer, 0, accLength); + + var accSpan = accBuffer.AsSpan(); + var digest = new byte[20]; + sha1Stream.Update(accSpan); + sha1Stream.Hash(digest, false); + sha1Stream.Reset(); + sha1StateVs.State.Add(digest.ToArray()); + } + else + { + bodyStream.Position = 0; + sha1StateVs.State.Add(bodyStream.Sha1()); + } + } + + for (uint i = 0; i < chunkCount; i++) + { + var chunkStart = (long)(i * ChunkSize); + var chunkLength = (int)Math.Min(ChunkSize, bodyStream.Length - chunkStart); + + bodyStream.Position = chunkStart; + var uploadBuffer = new byte[chunkLength]; + await bodyStream.ReadExactlyAsync(uploadBuffer, 0, chunkLength); + + var success = await UploadChunk(uKey, (uint)chunkStart, sha1StateVs, uploadBuffer); + if (!success) return false; + } + + return true; + } + + private async Task UploadChunk(string uKey, uint start, FlashTransferSha1StateV chunkSha1S, byte[] body) + { + var req = new FlashTransferUploadReq + { + FieId1 = 0, + AppId = 1407, + FileId3 = 2, + Body = new FlashTransferUploadBody + { + FieId1 = [], + UKey = uKey, + Start = start, + End = (uint)(start + body.Length - 1), + Sha1 = SHA1.HashData(body), + Sha1StateV = chunkSha1S, + Body = body + } + }; + var payload = ProtoHelper.Serialize(req).ToArray(); + var request = new HttpRequestMessage(HttpMethod.Post, _url) + { + Headers = + { + { "Accept", "*/*" }, + { "Expect", "100-continue" }, + { "Connection", "Keep-Alive" } + }, + Content = new ByteArrayContent(payload) + }; + var response = await _client.SendAsync(request); + var responseBytes = await response.Content.ReadAsByteArrayAsync(); + var resp = ProtoHelper.Deserialize(responseBytes); + + if (resp.Status != "success") + { + _botContext.LogError(Tag, + $"FlashTransfer Upload chunk {start} failed: {resp.Status}"); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs new file mode 100644 index 00000000..f44213e1 --- /dev/null +++ b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadReq.cs @@ -0,0 +1,31 @@ +using Lagrange.Proto; + +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 'required' 修饰符或声明为可以为 null。 +namespace Lagrange.Core.Internal.Packets.Service; + +[ProtoPackable] +internal partial class FlashTransferUploadReq +{ + [ProtoMember(1)] public uint FieId1 { get; set; } // 0 + [ProtoMember(2)] public uint AppId { get; set; } // 1407 + [ProtoMember(3)] public uint FileId3 { get; set; } // 0 + [ProtoMember(107)] public FlashTransferUploadBody Body { get; set; } +} + +[ProtoPackable] +internal partial class FlashTransferUploadBody +{ + [ProtoMember(1)] public byte[] FieId1 { get; set; } // Empty + [ProtoMember(2)] public string UKey { get; set; } + [ProtoMember(3)] public uint Start { get; set; } // Start + [ProtoMember(4)] public uint End { get; set; } // Start + Size - 1 + [ProtoMember(5)] public byte[] Sha1 { get; set; } + [ProtoMember(6)] public FlashTransferSha1StateV Sha1StateV { get; set; } + [ProtoMember(7)] public byte[] Body { get; set; } +} + +[ProtoPackable] +internal partial class FlashTransferSha1StateV +{ + [ProtoMember(1)] public List State { get; set; } +} \ No newline at end of file diff --git a/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs new file mode 100644 index 00000000..f576555f --- /dev/null +++ b/Lagrange.Core/Internal/Packets/Service/FlashTransferUploadResp.cs @@ -0,0 +1,10 @@ +using Lagrange.Proto; +#pragma warning disable CS8618 // 在退出构造函数时,不可为 null 的字段必须包含非 null 值。请考虑添加 'required' 修饰符或声明为可以为 null。 + +namespace Lagrange.Core.Internal.Packets.Service; + +[ProtoPackable] +internal partial class FlashTransferUploadResp +{ + [ProtoMember(5)] public string Status { get; set; } +} \ No newline at end of file diff --git a/Lagrange.Core/Message/Entities/ImageEntity.cs b/Lagrange.Core/Message/Entities/ImageEntity.cs index 3af59c02..9d830634 100644 --- a/Lagrange.Core/Message/Entities/ImageEntity.cs +++ b/Lagrange.Core/Message/Entities/ImageEntity.cs @@ -1,4 +1,6 @@ using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Lagrange.Core.Internal.Events.Message; using Lagrange.Core.Internal.Packets.Message; using Lagrange.Core.Internal.Packets.Service; @@ -43,7 +45,15 @@ public override async Task Preprocess(BotContext context, BotMessage message) if (result.Ext != null) { - await context.HighwayContext.UploadFile(Stream.Value, message.IsGroup() ? 1004 : 1003, ProtoHelper.Serialize(result.Ext)); + // Aot 和 MacOS 下使用 FlashTransfer 上传 + if (RuntimeFeature.IsDynamicCodeCompiled && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + await context.HighwayContext.UploadFile(Stream.Value, message.IsGroup() ? 1004 : 1003, ProtoHelper.Serialize(result.Ext)); + } + else + { + await context.FlashTransferContext.UploadFile(result.Ext.UKey, Stream.Value); + } } } finally diff --git a/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs b/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs index 7711754c..2bcd7fbe 100644 --- a/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs +++ b/Lagrange.Core/Utility/Cryptography/Sha1Stream.cs @@ -30,7 +30,7 @@ public Sha1Stream() // Initialize SHA1 context Reset(); } - private void Reset() + public void Reset() { _state[0] = 0x67452301; _state[1] = 0xEFCDAB89;