feat: implement M5 moments and rich messages
This commit is contained in:
@@ -11,6 +11,12 @@ public enum ChatMessageType
|
||||
Video,
|
||||
Voice,
|
||||
Link,
|
||||
Location,
|
||||
Emotion,
|
||||
Merge,
|
||||
PersonalCard,
|
||||
Note,
|
||||
Other,
|
||||
Quote,
|
||||
System
|
||||
}
|
||||
@@ -106,6 +112,12 @@ public static class VisibleMessageParser
|
||||
return ChatMessageType.File;
|
||||
}
|
||||
|
||||
if (text is "[位置]" or "位置" || text.StartsWith("位置\n", StringComparison.Ordinal)) return ChatMessageType.Location;
|
||||
if (text is "[动画表情]" or "动画表情" or "表情") return ChatMessageType.Emotion;
|
||||
if (text.Contains("聊天记录", StringComparison.Ordinal)) return ChatMessageType.Merge;
|
||||
if (text is "[名片]" or "个人名片" || text.StartsWith("名片\n", StringComparison.Ordinal)) return ChatMessageType.PersonalCard;
|
||||
if (text is "[笔记]" or "笔记" || text.StartsWith("笔记\n", StringComparison.Ordinal)) return ChatMessageType.Note;
|
||||
|
||||
if (!text.Contains(Path.DirectorySeparatorChar) && !text.Contains(Path.AltDirectorySeparatorChar) &&
|
||||
Path.GetExtension(text) is { Length: > 1 } extension && extension.Length <= 10)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
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<WechatNoteContentPart> 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<string>? 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 => $"",
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user