feat: expand merged chat records directly from read-only databases
Add db merged with exact account/chat/local-ID selection and shard ambiguity checks. Preserve Int64 composite message types and parse bounded, DTD-free embedded record XML, including duplicate messages and source metadata. Revalidate cached keys against current page-1 HMAC; replace the SQL write probe with sqlite3_db_readonly. Core tests: 129 passed. Windows Session 0 database validation: 5/5; all 16 text/sender/source-ID/timestamp entries matched independent XML parsing. Read-only UI baseline: 3/3; zero sends.
This commit is contained in:
@@ -545,6 +545,30 @@ try
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
case "db" when args[1] == "merged":
|
||||
{
|
||||
var accountId = GetRequiredOption(args, "--account");
|
||||
var chatId = GetRequiredOption(args, "--chat");
|
||||
if (!long.TryParse(GetRequiredOption(args, "--local-id"), System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var localId) || localId <= 0)
|
||||
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "--local-id must be a positive 64-bit integer.");
|
||||
var accounts = await DatabaseKeyStore.LoadAsync(GetOption(args, "--key-file"), cancellationToken);
|
||||
var account = accounts.SingleOrDefault(item => string.Equals(item.AccountRootFingerprint, accountId, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new WxAgentException(WxAgentErrorCode.DatabaseKeyNotFound, "Account fingerprint was not found in the key store.");
|
||||
var result = await WechatMessageDbReader.ReadMergedAsync(account.AccountRootPath, account.Databases, chatId, localId,
|
||||
cancellationToken, GetOption(args, "--database"));
|
||||
var includeContent = HasOption(args, "--include-content");
|
||||
WriteJson(new
|
||||
{
|
||||
account = account.AccountRootFingerprint,
|
||||
database = result.DatabaseRelativePath,
|
||||
chatId = includeContent ? chatId : MaskIdentifier(chatId),
|
||||
recordId = $"{account.AccountRootFingerprint}:{result.DatabaseRelativePath}:{localId}",
|
||||
parent = new { result.Parent.LocalId, result.Parent.ServerId, result.Parent.Type, result.Parent.Timestamp },
|
||||
record = ToMergedOutput(result.Record, includeContent)
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
case "db" when args[1] == "messages":
|
||||
{
|
||||
var accountId = GetRequiredOption(args, "--account");
|
||||
@@ -793,6 +817,9 @@ static int ValidateCommandLine(string[] values)
|
||||
case "query":
|
||||
ValidateOptions(values, 2, ["--account", "--database", "--key-file", "--timeout"], []);
|
||||
return 30;
|
||||
case "merged":
|
||||
ValidateOptions(values, 2, ["--account", "--chat", "--local-id", "--database", "--key-file", "--timeout"], ["--include-content"]);
|
||||
return 60;
|
||||
case "messages":
|
||||
ValidateOptions(values, 2, ["--account", "--chat", "--key-file", "--limit", "--timeout"], ["--include-content"]);
|
||||
return 60;
|
||||
@@ -846,6 +873,32 @@ static string? GetOption(string[] values, string name)
|
||||
static string GetRequiredOption(string[] values, string name) =>
|
||||
GetOption(values, name) ?? throw new WxAgentException(WxAgentErrorCode.InvalidArgument, $"Missing required option {name}.");
|
||||
|
||||
static object ToMergedOutput(WechatMergedChat record, bool includeContent) => new
|
||||
{
|
||||
title = includeContent ? record.Title : null,
|
||||
description = includeContent ? record.Description : null,
|
||||
count = record.Messages.Count,
|
||||
messages = record.Messages.Select(message => new
|
||||
{
|
||||
path = message.Path,
|
||||
dataId = message.DataId,
|
||||
dataType = message.DataType,
|
||||
senderId = MaskIdentifier(message.SenderHash ?? message.SenderName),
|
||||
senderName = includeContent ? message.SenderName : null,
|
||||
sourceLocalId = message.SourceLocalId,
|
||||
sourceServerId = message.SourceServerId,
|
||||
timestamp = message.Timestamp,
|
||||
displayTime = message.DisplayTime,
|
||||
text = includeContent ? message.Text : null,
|
||||
length = message.Text?.Length,
|
||||
contentAvailable = message.Text is not null,
|
||||
title = includeContent ? message.Title : null,
|
||||
format = message.Format,
|
||||
sizeBytes = message.SizeBytes,
|
||||
nestedRecord = message.NestedRecord is null ? null : ToMergedOutput(message.NestedRecord, includeContent)
|
||||
})
|
||||
};
|
||||
|
||||
static string? MaskIdentifier(string? identifier) =>
|
||||
string.IsNullOrEmpty(identifier)
|
||||
? identifier
|
||||
@@ -950,6 +1003,7 @@ WxAgent.Host commands:
|
||||
db status [--key-file <path>] [--timeout 30]
|
||||
db query --account <fingerprint> --database <relative-path> [--key-file <path>] [--timeout 30]
|
||||
db messages --account <fingerprint> --chat <wxid> [--limit 50] [--include-content] [--key-file <path>] [--timeout 60]
|
||||
db merged --account <fingerprint> --chat <wxid> --local-id <id> [--database <relative-path>] [--include-content] [--key-file <path>] [--timeout 60]
|
||||
db contacts --account <fingerprint> [--contains <text>] [--include-content] [--key-file <path>] [--timeout 60]
|
||||
|
||||
Chat commands default to File Transfer Assistant; send/monitor and listener-smoke accept --session.
|
||||
|
||||
Reference in New Issue
Block a user