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

XFEExtension

【DLL】XFE各类拓展是一个C#的DLL库,旨在优化C#代码中常用语句的使用,并提供更简洁的访问方式,同时提供Xunit测试框架,快速搭建服务器/客户端,免费ChatGPTAPI接口,免费通讯服务器,XFE下载器,新增格式等

公开
关注 0 Fork 0 Star 0
UTF-8
using XFEExtension.NetCore.DelegateExtension;
using XFEExtension.NetCore.Exceptions;
using System.Text;
using XFEExtension.NetCore.FormatExtension;

namespace XFEExtension.NetCore.CyberComm.XCCNetWork;

/// <summary>
/// XCC消息接收器
/// </summary>
public class XCCMessageReceiveHelper
{
    private readonly Dictionary<(string GroupId, string MessageId), XCCFile> _xCCFileDictionary = [];
    private readonly Dictionary<string, List<XCCMessage>> _xCCMessageDictionary = [];
    private bool _loaded;
    /// <summary>
    /// 自动保存到本地
    /// </summary>
    public bool AutoSaveInLocal { get; set; }
    /// <summary>
    /// 保存的根目录
    /// </summary>
    public string SavePathRoot { get; set; }
    /// <summary>单个文件的最大字节数。</summary>
    public long MaxFileBytes { get; set; } = 64 * 1024 * 1024;
    /// <summary>单条文本消息的最大 UTF-8 字节数。</summary>
    public int MaxTextMessageBytes { get; set; } = 1024 * 1024;
    /// <summary>
    /// 接收到文件事件
    /// </summary>
    public event MessageReceivedHandler<XCCFile>? FileReceived;
    /// <summary>
    /// 接收到文本事件
    /// </summary>
    public event MessageReceivedHandler<XCCMessage>? TextReceived;
    /// <summary>
    /// 错误发生事件
    /// </summary>
    public event XFEEventHandler<XFECyberCommException>? ExceptionOccurred;
    /// <summary>
    /// 接收到实时音频字节流事件
    /// </summary>
    public event XFEEventHandler<byte[]>? AudioBufferReceived;
    /// <summary>
    /// 从设置的根目录加载
    /// </summary>
    /// <returns></returns>
    public async Task Load()
    {
        await Task.Run(() =>
        {
            if (Directory.Exists(SavePathRoot))
            {
                foreach (var groupIdFullPath in Directory.EnumerateDirectories(SavePathRoot))
                {
                    var groupId = Path.GetFileName(groupIdFullPath);
                    var messagePath = Path.Combine(groupIdFullPath, "XFEMessage", "XFEMessage.xfe");
                    if (File.Exists(messagePath))
                    {
                        var xCCMessageList = new List<XCCMessage>();
                        foreach (var entry in new XFEMultiDictionary(File.ReadAllText(messagePath)))
                        {
                            var xCCMessage = XCCMessage.ConvertToXCCMessage(entry.Content, groupId);
                            xCCMessageList.Add(xCCMessage);
                            if (xCCMessage.MessageType == XCCTextMessageType.Text)
                                TextReceived?.Invoke(true, xCCMessage);
                            else
                                FileReceived?.Invoke(true, LoadFile(xCCMessage)!);
                        }
                        _xCCMessageDictionary.Add(groupId, xCCMessageList);
                    }
                }
            }
            else
            {
                Directory.CreateDirectory(SavePathRoot);
            }
        });
        _loaded = true;
    }
    /// <summary>
    /// 从设置的根目录的指定群组加载
    /// </summary>
    /// <param name="groupId">群组ID</param>
    /// <returns></returns>
    public async Task LoadGroup(string groupId)
    {
        await Task.Run(() =>
        {
            var messagePath = Path.Combine(GetSafeGroupDirectory(groupId), "XFEMessage", "XFEMessage.xfe");
            if (File.Exists(messagePath))
            {
                var xCCMessageList = new List<XCCMessage>();
                foreach (var entry in new XFEMultiDictionary(File.ReadAllText(messagePath)))
                {
                    var xCCMessage = XCCMessage.ConvertToXCCMessage(entry.Content, groupId);
                    xCCMessageList.Add(xCCMessage);
                    if (xCCMessage.MessageType == XCCTextMessageType.Text)
                        TextReceived?.Invoke(true, xCCMessage);
                    else
                        FileReceived?.Invoke(true, LoadFile(xCCMessage)!);
                }
                _xCCMessageDictionary.Add(groupId, xCCMessageList);
            }
        });
        _loaded = true;
    }
    /// <summary>
    /// 清理无用文件
    /// </summary>
    /// <returns></returns>
    public async Task ClearUselessFile()
    {
        if (!_loaded)
            throw new XFEExtensionException("不能在加载完成前调用清理");
        await Task.Run(() =>
        {
            foreach (var groupId in _xCCMessageDictionary.Keys)
            {
                foreach (var filePath in Directory.EnumerateFiles(GetSafeGroupDirectory(groupId), "*.xfe", SearchOption.TopDirectoryOnly))
                {
                    var messageId = Path.GetFileNameWithoutExtension(filePath);
                    if (!_xCCMessageDictionary.TryGetValue(groupId, out var value) || value.Find(x => x.MessageId == messageId) is null)
                    {
                        File.Delete(filePath);
                    }
                }
            }
        });
    }
    private XCCFile? LoadFile(XCCMessage xCCMessage)
    {
        var filePath = GetSafeFilePath(xCCMessage.GroupId, xCCMessage.MessageId);
        byte[]? fileBuffer = null;
        if (File.Exists(filePath) && new FileInfo(filePath).Length <= MaxFileBytes)
            fileBuffer = File.ReadAllBytes(filePath);
        XCCFile xCCFile;
        if (_xCCFileDictionary.TryGetValue((xCCMessage.GroupId, xCCMessage.MessageId), out var value))
        {
            if (!value.Loaded && fileBuffer is not null)
                value.LoadFile(fileBuffer);
            return value;
        }
        switch (xCCMessage.MessageType)
        {
            case XCCTextMessageType.Image:
                xCCFile = new XCCFile(xCCMessage.GroupId, xCCMessage.MessageId, XCCFileType.Image, xCCMessage.Sender, xCCMessage.SendTime, fileBuffer);
                break;
            case XCCTextMessageType.Audio:
                xCCFile = new XCCFile(xCCMessage.GroupId, xCCMessage.MessageId, XCCFileType.Audio, xCCMessage.Sender, xCCMessage.SendTime, fileBuffer);
                break;
            case XCCTextMessageType.Video:
                xCCFile = new XCCFile(xCCMessage.GroupId, xCCMessage.MessageId, XCCFileType.Video, xCCMessage.Sender, xCCMessage.SendTime, fileBuffer);
                break;
            default:
                return null;
        }
        _xCCFileDictionary.Add((xCCMessage.GroupId, xCCMessage.MessageId), xCCFile);
        return xCCFile;
    }
    /// <summary>
    /// 获取文件
    /// </summary>
    /// <param name="messageId">消息ID</param>
    /// <returns></returns>
    public XCCFile? GetFile(string messageId)
    {
        return _xCCFileDictionary.FirstOrDefault(pair => pair.Key.MessageId == messageId).Value;
    }
    /// <summary>按群组和消息 ID 获取文件,避免跨群 ID 冲突。</summary>
    public XCCFile? GetFile(string groupId, string messageId) =>
        _xCCFileDictionary.TryGetValue((groupId, messageId), out var value) ? value : null;
    /// <summary>
    /// 添加文件
    /// </summary>
    /// <param name="xCCFile">XCC文件实例</param>
    public void AddFile(XCCFile xCCFile)
    {
        _xCCFileDictionary.Add((xCCFile.GroupId, xCCFile.MessageId), xCCFile);
        if (AutoSaveInLocal && xCCFile.FileBuffer is not null)
            SaveFile(xCCFile);
    }
    /// <summary>
    /// 保存文件
    /// </summary>
    /// <param name="xCCFile">XCC文件实例</param>
    public void SaveFile(XCCFile xCCFile)
    {
        if (xCCFile.FileBuffer is null) throw new ArgumentException("文件内容不能为空", nameof(xCCFile));
        if (xCCFile.FileBuffer.LongLength > MaxFileBytes) throw new XFEExtensionException("文件超过允许的大小");
        var filePath = GetSafeFilePath(xCCFile.GroupId, xCCFile.MessageId);
        Directory.CreateDirectory(Path.GetDirectoryName(filePath)!);
        AtomicWrite(filePath, xCCFile.FileBuffer);
    }
    /// <summary>
    /// 保存群组消息
    /// </summary>
    /// <param name="groupId"></param>
    public void SaveMessage(string groupId)
    {
        var filePath = Path.Combine(GetSafeGroupDirectory(groupId), "XFEMessage");
        if (!Directory.Exists(filePath))
        {
            Directory.CreateDirectory(filePath);
        }
        var storageDictionary = new XFEDictionary();
        foreach (var xCCMessage in _xCCMessageDictionary[groupId])
        {
            storageDictionary.Add(xCCMessage.MessageId, xCCMessage.ToString());
        }
        AtomicWrite(Path.Combine(filePath, "XFEMessage.xfe"), System.Text.Encoding.UTF8.GetBytes(storageDictionary.ToString()));
    }
    private void ReceiveFilePlaceHolder(XCCTextMessageReceivedEventArgs e, XCCFileType fileType)
    {
        var xCCFile = new XCCFile(e.GroupId, e.MessageId!, fileType, e.Sender!, e.SendTime);
        _xCCFileDictionary.TryAdd((e.GroupId, e.MessageId!), xCCFile);
        FileReceived?.Invoke(e.IsHistory, xCCFile);
        if (AutoSaveInLocal)
            SaveMessage(e.GroupId);
    }
    private void ReceiveTextMessage(object? sender, XCCTextMessageReceivedEventArgs e)
    {
        if (Encoding.UTF8.GetByteCount(e.TextMessage ?? string.Empty) > MaxTextMessageBytes)
        {
            ExceptionOccurred?.Invoke(new XFECyberCommException("接收到的 XCC 文本消息超过允许的大小"));
            return;
        }
        var message = new XCCMessage(e.MessageId!, e.MessageType, e.TextMessage ?? string.Empty, e.Sender!, e.SendTime, e.GroupId);
        if (_xCCMessageDictionary.TryGetValue(e.GroupId, out var value))
        {
            if (value.Find(x => x.MessageId == e.MessageId) is null)
            {
                value.Add(message);
            }
            else
            {
                return;
            }
        }
        else
        {
            _xCCMessageDictionary.Add(e.GroupId, [message]);
        }
        switch (e.MessageType)
        {
            case XCCTextMessageType.Text:
                TextReceived?.Invoke(e.IsHistory, message);
                if (AutoSaveInLocal)
                    SaveMessage(e.GroupId);
                break;
            case XCCTextMessageType.Image:
                ReceiveFilePlaceHolder(e, XCCFileType.Image);
                break;
            case XCCTextMessageType.Audio:
                ReceiveFilePlaceHolder(e, XCCFileType.Audio);
                break;
            case XCCTextMessageType.Video:
                ReceiveFilePlaceHolder(e, XCCFileType.Video);
                break;
        }
    }
    private void ReceiveBinaryMessage(object? sender, XCCBinaryMessageReceivedEventArgs e)
    {
        if (e.MessageType == XCCBinaryMessageType.AudioBuffer)
        {
            AudioBufferReceived?.Invoke(e.BinaryMessage);
            return;
        }
        if (e.BinaryMessage.LongLength > MaxFileBytes)
        {
            ExceptionOccurred?.Invoke(new XFECyberCommException("接收到的 XCC 文件超过允许的大小"));
            return;
        }
        if (_xCCFileDictionary.TryGetValue((e.GroupId, e.MessageId!), out var value))
        {
            if (!value.Loaded)
            {
                value.LoadFile(e.BinaryMessage);
                if (AutoSaveInLocal)
                    SaveFile(value);
            }
        }
        else
        {
            var fileType = XCCFileType.Image;
            switch (e.MessageType)
            {
                case XCCBinaryMessageType.Image:
                    fileType = XCCFileType.Image;
                    break;
                case XCCBinaryMessageType.Audio:
                    fileType = XCCFileType.Audio;
                    break;
                case XCCBinaryMessageType.AudioBuffer:
                    break;
                case XCCBinaryMessageType.Video:
                    fileType = XCCFileType.Video;
                    break;
            }
            var xCCFile = new XCCFile(e.GroupId, e.MessageId!, fileType, e.Sender!, e.SendTime, e.BinaryMessage);
            _xCCFileDictionary.Add((e.GroupId, e.MessageId!), xCCFile);
            if (!e.IsHistory)
                FileReceived?.Invoke(e.IsHistory, xCCFile);
        }
    }
    private void XCCNetWork_ExceptionMessageReceived(object? sender, XCCExceptionMessageReceivedEventArgs e)
    {
        ExceptionOccurred?.Invoke(e.Exception);
    }
    /// <summary>
    /// XCC消息接收器
    /// </summary>
    /// <param name="savePathRoot">保存根目录</param>
    /// <param name="autoSaveInLocal">自动保存</param>
    public XCCMessageReceiveHelper(string savePathRoot, bool autoSaveInLocal = true)
    {
        AutoSaveInLocal = autoSaveInLocal;
        SavePathRoot = Path.GetFullPath(savePathRoot);
    }
    /// <summary>
    /// XCC消息接收器
    /// </summary>
    /// <param name="savePathRoot">保存根目录</param>
    /// <param name="xCCNetWork">XCC网络通讯实例</param>
    /// <param name="autoSaveInLocal">自动保存</param>
    public XCCMessageReceiveHelper(string savePathRoot, XCCNetWork xCCNetWork, bool autoSaveInLocal = true)
    {
        AutoSaveInLocal = autoSaveInLocal;
        SavePathRoot = Path.GetFullPath(savePathRoot);
        xCCNetWork.TextMessageReceived += ReceiveTextMessage;
        xCCNetWork.BinaryMessageReceived += ReceiveBinaryMessage;
        xCCNetWork.ExceptionMessageReceived += XCCNetWork_ExceptionMessageReceived;
    }

    private string GetSafeGroupDirectory(string groupId)
    {
        ValidatePathSegment(groupId, nameof(groupId));
        var root = Path.GetFullPath(SavePathRoot);
        var path = Path.GetFullPath(Path.Combine(root, groupId));
        if (!path.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
            throw new XFEExtensionException("群组路径超出保存根目录");
        return path;
    }

    private string GetSafeFilePath(string groupId, string messageId)
    {
        ValidatePathSegment(messageId, nameof(messageId));
        return Path.Combine(GetSafeGroupDirectory(groupId), $"{messageId}.xfe");
    }

    private static void ValidatePathSegment(string value, string parameterName)
    {
        if (string.IsNullOrWhiteSpace(value) || value is "." or ".." ||
            value.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || value.Contains(Path.DirectorySeparatorChar) || value.Contains(Path.AltDirectorySeparatorChar))
            throw new ArgumentException("标识不能包含路径字符", parameterName);
    }

    private static void AtomicWrite(string path, byte[] content)
    {
        var temporaryPath = $"{path}.{Guid.NewGuid():N}.tmp";
        try
        {
            File.WriteAllBytes(temporaryPath, content);
            File.Move(temporaryPath, path, true);
        }
        finally
        {
            if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
        }
    }
}