XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

STHApplication

【MAUI】寄当家——安卓端交易管理系统

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using JiDangJia.Contracts;

namespace JiDangJia.Messaging;

public sealed record ChatSyncResult(int Messages,int Downloaded,int Unavailable,bool MoreMessages,bool Legacy=false);

public sealed class ChatSyncClient(LocalChatStore local,HttpClient http,string token,string deviceId,Func<bool> current)
{
    public const int MaxMediaBytes=128*1024*1024;
    private static readonly JsonSerializerOptions Json=new(JsonSerializerDefaults.Web);
    private void Guard(){if(!current())throw new OperationCanceledException("聊天账户已切换");}
    private HttpRequestMessage Request(string path,object? body=null)
    {
        Guard();var request=new HttpRequestMessage(body is null?HttpMethod.Get:HttpMethod.Post,local.Server+"/api/community"+path);
        request.Headers.Authorization=new("Bearer",token);request.Headers.Add("X-Device-Id",deviceId);if(body is not null)request.Content=JsonContent.Create(body);return request;
    }
    private static void Success(HttpResponseMessage response)
    {
        if(response.StatusCode==HttpStatusCode.Unauthorized)throw new HttpRequestException("登录已过期,请重新登录后继续同步",null,response.StatusCode);
        if(!response.IsSuccessStatusCode)throw new HttpRequestException("暂时无法同步私信,本机记录仍可查看",null,response.StatusCode);
    }
    public async Task<ChatSyncResult> Synchronize(CancellationToken ct=default,int maxPages=20,int maxMedia=10)
    {
        Guard();int received=0,downloaded=0,unavailable=0;bool more=false,legacy=false;
        foreach(var read in local.PendingReads())
        {
            using var request=Request("/chat/read",read);using var response=await http.SendAsync(request,ct);Guard();
            // An offline read may refer to a message already purged from the server.
            if(response.IsSuccessStatusCode||response.StatusCode is HttpStatusCode.BadRequest or HttpStatusCode.NotFound)local.ConfirmRead(read);else Success(response);
        }
        for(int page=0;page<Math.Clamp(maxPages,1,100);page++)
        {
            long cursor=local.Cursor;using var request=Request("/chat/sync?after="+cursor);using var response=await http.SendAsync(request,ct);Guard();
            if(response.StatusCode==HttpStatusCode.NotFound){(received,more)=await LegacySync(maxPages,ct);legacy=true;break;}
            Success(response);
            var batch=await response.Content.ReadFromJsonAsync<ChatSyncPage>(Json,ct)??throw new IOException("私信同步结果无效");Guard();
            if(batch.HasMore&&batch.Cursor<=cursor)throw new IOException("私信同步没有取得进展");
            local.Merge(batch);received+=batch.Items.Count;more=batch.HasMore;if(!more)break;
        }
        if(!legacy)
        {
            using var profilesRequest=Request("/chat/conversations");using var profilesResponse=await http.SendAsync(profilesRequest,ct);Guard();Success(profilesResponse);
            var profiles=await profilesResponse.Content.ReadFromJsonAsync<List<Conversation>>(Json,ct)??[];Guard();local.UpdateProfiles(profiles);
        }
        // Download all received/sent attachments, even if their conversation was never opened.
        foreach(var media in local.PendingMedia(maxMedia))
        {
            ct.ThrowIfCancellationRequested();Guard();if(local.ReadMedia(media.Id) is not null)continue;
            try
            {
                using var request=Request("/media/"+media.Id);using var response=await http.SendAsync(request,HttpCompletionOption.ResponseHeadersRead,ct);Guard();
                if(response.StatusCode is HttpStatusCode.Gone or HttpStatusCode.NotFound){local.MediaUnavailable(media.Id);unavailable++;continue;}
                Success(response);if(response.Content.Headers.ContentLength>MaxMediaBytes)throw new IOException("私信附件过大");
                await using var stream=await response.Content.ReadAsStreamAsync(ct);using var data=new MemoryStream();var buffer=new byte[65536];int read;
                while((read=await stream.ReadAsync(buffer,ct))>0){if(data.Length+read>MaxMediaBytes)throw new IOException("私信附件过大");await data.WriteAsync(buffer.AsMemory(0,read),ct);}
                Guard();local.SaveMedia(media.Id,data.ToArray());downloaded++;
            }
            catch(HttpRequestException ex)when(ex.StatusCode!=HttpStatusCode.Unauthorized){Guard();local.RetryMediaLater(media.Id);}
            catch(IOException){Guard();local.RetryMediaLater(media.Id);}
        }
        return new ChatSyncResult(received,downloaded,unavailable,more,legacy);
    }
    private async Task<(int Messages,bool More)> LegacySync(int maxPages,CancellationToken ct)
    {
        using var request=Request("/chat/conversations");using var response=await http.SendAsync(request,ct);Guard();Success(response);
        var conversations=await response.Content.ReadFromJsonAsync<List<Conversation>>(Json,ct)??[];Guard();local.UpdateProfiles(conversations);if(conversations.Count==0)return (0,false);
        int first=conversations.FindIndex(c=>c.PeerId==local.LegacyNextPeer);if(first<0)first=0;
        int remaining=Math.Clamp(maxPages,1,100),received=0;bool more=false;
        for(int i=0;i<conversations.Count&&remaining>0;i++)
        {
            var peer=conversations[(first+i)%conversations.Count];var position=local.LegacyPosition(peer.PeerId);bool newerMore;
            do
            {
                using var pageRequest=Request("/chat/messages?peerId="+Uri.EscapeDataString(peer.PeerId)+"&after="+position.After);
                using var pageResponse=await http.SendAsync(pageRequest,ct);Guard();Success(pageResponse);
                var batch=await pageResponse.Content.ReadFromJsonAsync<MessagePage>(Json,ct)??throw new IOException("旧版私信数据无效");Guard();
                newerMore=position.After>0&&batch.HasMore;
                if(newerMore&&(batch.Items.Count==0||batch.Items.Max(m=>m.Sequence)<=position.After))throw new IOException("旧版消息同步没有取得进展");
                local.SaveLegacy(batch,peer,false);received+=batch.Items.Count;remaining--;position=local.LegacyPosition(peer.PeerId);
            }while(newerMore&&remaining>0);
            while(!position.Complete&&position.Before>0&&remaining>0)
            {
                using var olderRequest=Request("/chat/messages?peerId="+Uri.EscapeDataString(peer.PeerId)+"&before="+position.Before);
                using var olderResponse=await http.SendAsync(olderRequest,ct);Guard();Success(olderResponse);
                var batch=await olderResponse.Content.ReadFromJsonAsync<MessagePage>(Json,ct)??throw new IOException("旧版私信数据无效");Guard();
                if(batch.HasMore&&(batch.Items.Count==0||batch.Items.Min(m=>m.Sequence)>=position.Before))throw new IOException("旧版历史同步没有取得进展");
                local.SaveLegacy(batch,peer,true);received+=batch.Items.Count;remaining--;position=local.LegacyPosition(peer.PeerId);
            }
            more|=newerMore||!position.Complete;local.LegacyNextPeer=conversations[(first+i+1)%conversations.Count].PeerId;
            if(remaining==0&&i+1<conversations.Count)more=true;
        }
        return (received,more);
    }
}