using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace WxAgent.Core; public sealed record WechatMomentSnapshot( string Author, string Text, string Fingerprint, string? Time = null, int ImageCount = 0, bool IsLiked = false); public sealed record WechatMessageContent( ChatMessageType Type, string Text, Uri? Url = null, QuotedMessageSnapshot? Quote = null); public sealed record WechatNoteContentPart(string Kind, string Value); public sealed record WechatNoteContent(IReadOnlyList Parts) { public string ToMarkdown() => string.Join( Environment.NewLine + Environment.NewLine, Parts.Select(part => part.Kind == "file" ? $"[{Path.GetFileName(part.Value)}]({part.Value})" : part.Value)); } public enum WechatMomentPrivacy { Public, TagWhitelist, TagBlacklist } public sealed record WechatMomentPublishOptions( WechatMomentPrivacy Privacy = WechatMomentPrivacy.Public, IReadOnlyList? Tags = null) { public void Validate() { if (!Enum.IsDefined(Privacy)) throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Unknown moment privacy mode."); if (Privacy != WechatMomentPrivacy.Public) WechatOperationPolicy.ValidateNames(Tags ?? [], nameof(Tags)); } } public static partial class WechatMessageContentParser { [GeneratedRegex(@"https?://[^\s<>\]\[\))]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] private static partial Regex UrlRegex(); public static Uri? GetUrl(ChatMessageSnapshot message) { ArgumentNullException.ThrowIfNull(message); var match = UrlRegex().Match(message.Text); return match.Success && Uri.TryCreate(match.Value.TrimEnd('.', ',', '。', ','), UriKind.Absolute, out var uri) ? uri : null; } public static WechatMessageContent GetContent(ChatMessageSnapshot message) { ArgumentNullException.ThrowIfNull(message); return new WechatMessageContent(message.Type, message.Text, GetUrl(message), message.Quote); } public static string ToMarkdown(ChatMessageSnapshot message) { ArgumentNullException.ThrowIfNull(message); var content = GetContent(message); var body = content.Type switch { ChatMessageType.Image => $"![图片]({content.Url?.AbsoluteUri ?? string.Empty})", ChatMessageType.Video => $"[视频]({content.Url?.AbsoluteUri ?? string.Empty})", ChatMessageType.File => $"[文件]({content.Url?.AbsoluteUri ?? string.Empty})", ChatMessageType.Voice => $"> 语音:{content.Text}", ChatMessageType.Link when content.Url is not null => $"[{LinkTitle(content.Text, content.Url)}]({content.Url.AbsoluteUri})", _ => content.Text }; if (content.Quote is null) return body; var quote = string.Join(Environment.NewLine, content.Quote.Text.Split('\n').Select(line => $"> {line}")); return $"{quote}{Environment.NewLine}{Environment.NewLine}{body}"; } public static string MomentFingerprint(string author, string text, string? time) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes($"{author}\n{text}\n{time}")); return Convert.ToHexString(bytes).ToLowerInvariant(); } private static string LinkTitle(string text, Uri url) { var title = UrlRegex().Replace(text, string.Empty, 1).Trim(); return string.IsNullOrWhiteSpace(title) ? url.Host : title; } }