using System.Security.Cryptography;
using System.Text;
using ZstdSharp;
namespace WxAgent.Core;
/// Database-backed message with stable identity, sender and timestamps.
public sealed record DbMessage(
long LocalId,
long ServerId,
string ChatId,
string? SenderWxId,
string? SenderName,
string? SenderAvatarUrl,
long Type,
string Content,
DateTimeOffset Timestamp,
bool? IsSelf);
public static class WechatDbMessage
{
// WeChat 4.x stores message_content as hex; rows with WCDB_CT_message_content=1 are zstd-compressed (magic 28 b5 2f fd).
public static string TableNameFor(string chatId) =>
"Msg_" + Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(chatId))).ToLowerInvariant();
public static string DecodeContent(string hexContent, bool compressed)
{
if (string.IsNullOrEmpty(hexContent)) return string.Empty;
if (hexContent.Length > WechatMergedChatParser.MaxXmlCharacters * 2)
throw new InvalidDataException("Database message content exceeds the decoding limit.");
byte[] bytes;
try
{
bytes = Convert.FromHexString(hexContent);
}
catch (FormatException)
{
return string.Empty;
}
if (compressed && bytes.Length >= 4 && bytes[0] == 0x28 && bytes[1] == 0xb5 && bytes[2] == 0x2f && bytes[3] == 0xfd)
{
try
{
using var source = new MemoryStream(bytes);
using var decompressor = new DecompressionStream(source);
using var reader = new StreamReader(decompressor, Encoding.UTF8);
var decoded = new StringBuilder();
var buffer = new char[4096];
int read;
while ((read = reader.Read(buffer, 0, buffer.Length)) != 0)
{
if (decoded.Length + read > WechatMergedChatParser.MaxXmlCharacters)
throw new InvalidDataException("Decompressed message exceeds the decoding limit.");
decoded.Append(buffer, 0, read);
}
return decoded.ToString();
}
catch (Exception exception) when (exception is not (OutOfMemoryException or InvalidDataException))
{
return string.Empty;
}
}
return compressed ? string.Empty : Encoding.UTF8.GetString(bytes);
}
/// The account root directory name is <wxid>_<random>; the reference project treats the wxid prefix as self.
public static bool? IsSelf(string? senderWxId, string? accountRootDirectoryName)
{
if (string.IsNullOrEmpty(senderWxId) || string.IsNullOrEmpty(accountRootDirectoryName)) return null;
return accountRootDirectoryName.StartsWith(senderWxId + "_", StringComparison.Ordinal);
}
}