This commit is contained in:
@@ -64,9 +64,20 @@ public sealed class RemoteAgentHostedService(
|
||||
await client.HeartbeatAsync(CreateHeartbeat(remote, reporting, snapshot, null), stoppingToken);
|
||||
await ReplayUnreportedResultsAsync(client, ledger, reporting, stoppingToken);
|
||||
await client.FlushEventsAsync(eventQueue, reporting, stoppingToken);
|
||||
foreach (var account in reporting.Accounts.Where(account => account.Enabled))
|
||||
var pollAccountIds = reporting.Accounts
|
||||
.Where(account => account.Enabled)
|
||||
.Select(account => account.AccountId)
|
||||
.ToList();
|
||||
if (snapshot.ActiveAccountId is { Length: > 0 } activeAccountId &&
|
||||
!pollAccountIds.Contains(activeAccountId, StringComparer.Ordinal))
|
||||
{
|
||||
var tasks = await client.PollTasksAsync(account.AccountId, stoppingToken, waitSeconds: 5);
|
||||
// Poll the verified active account even before a Reporting whitelist is configured.
|
||||
// The task then returns an explicit authorization result instead of staying Pending.
|
||||
pollAccountIds.Add(activeAccountId);
|
||||
}
|
||||
foreach (var accountId in pollAccountIds)
|
||||
{
|
||||
var tasks = await client.PollTasksAsync(accountId, stoppingToken, waitSeconds: 5);
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
await ProcessTaskAsync(client, remote, reporting, ledger, task, stoppingToken);
|
||||
@@ -254,11 +265,13 @@ public sealed class RemoteAgentHostedService(
|
||||
var payload = ReadPayload<ReadSessionsPayload>(task.Payload);
|
||||
ValidatePage(payload.Limit, payload.Offset);
|
||||
var scopes = AuthorizedChatScopes(reporting, task.AccountId);
|
||||
RequireScopes(scopes);
|
||||
var sessions = (await backend.SessionsAsync(task.AccountId, cancellationToken))
|
||||
.Where(session => scopes.Any(scope => scope.ChatId == session.AutomationId))
|
||||
RequireScopes(reporting, task.AccountId, scopes);
|
||||
var sessions = await backend.SessionsAsync(task.AccountId, cancellationToken);
|
||||
var contacts = await ReadContactsForScopesAsync(task.AccountId, scopes, cancellationToken);
|
||||
var visibleSessions = sessions
|
||||
.Where(session => scopes.Any(scope => SessionMatchesScope(session, scope, contacts)))
|
||||
.ToArray();
|
||||
var page = sessions.ToPage(payload.Limit, payload.Offset);
|
||||
var page = visibleSessions.ToPage(payload.Limit, payload.Offset);
|
||||
return new RemoteReadExecution(JsonSerializer.SerializeToElement(page, RemoteJson.Options), scopes);
|
||||
}
|
||||
case "read-contacts":
|
||||
@@ -267,7 +280,7 @@ public sealed class RemoteAgentHostedService(
|
||||
ValidatePage(payload.Limit, payload.Offset);
|
||||
var chatType = payload.GroupsOnly ? ReportingChatType.Group : ReportingChatType.Private;
|
||||
var scopes = AuthorizedChatScopes(reporting, task.AccountId, chatType);
|
||||
RequireScopes(scopes);
|
||||
RequireScopes(reporting, task.AccountId, scopes);
|
||||
var all = new List<ContactInfo>();
|
||||
for (var offset = 0; ;)
|
||||
{
|
||||
@@ -288,17 +301,31 @@ public sealed class RemoteAgentHostedService(
|
||||
{
|
||||
var payload = ReadPayload<ReadMessagesPayload>(task.Payload);
|
||||
ValidatePage(payload.Limit, payload.Offset);
|
||||
var scopes = AuthorizedChatScopes(reporting, task.AccountId)
|
||||
.Where(scope => string.Equals(scope.ChatId, payload.ChatId, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (scopes.Length != 1)
|
||||
throw new ServiceException("ChatNotAuthorized", 403, "The requested chat is not enabled in the local whitelist.");
|
||||
var sessions = (await backend.SessionsAsync(task.AccountId, cancellationToken))
|
||||
var scopes = AuthorizedChatScopes(reporting, task.AccountId);
|
||||
RequireScopes(reporting, task.AccountId, scopes);
|
||||
var sessions = await backend.SessionsAsync(task.AccountId, cancellationToken);
|
||||
var contacts = await ReadContactsForScopesAsync(task.AccountId, scopes, cancellationToken);
|
||||
var candidateSessions = sessions
|
||||
.Where(session => string.Equals(session.AutomationId, payload.ChatId, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (sessions.Length != 1)
|
||||
var scope = scopes.FirstOrDefault(allowed => string.Equals(allowed.ChatId, payload.ChatId, StringComparison.Ordinal));
|
||||
if (scope is null)
|
||||
{
|
||||
var mapped = sessions
|
||||
.Where(session => scopes.Any(allowed => SessionMatchesScope(session, allowed, contacts)))
|
||||
.Where(session => string.Equals(session.AutomationId, payload.ChatId, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
if (mapped.Length == 1)
|
||||
{
|
||||
candidateSessions = mapped;
|
||||
scope = scopes.First(allowed => SessionMatchesScope(mapped[0], allowed, contacts));
|
||||
}
|
||||
}
|
||||
if (scope is null)
|
||||
throw new ServiceException("ChatNotAuthorized", 403, "The requested chat is not enabled in the local whitelist.");
|
||||
if (candidateSessions.Length != 1)
|
||||
throw new ServiceException("ChatIdentityUnconfirmed", 409, "The requested chat identity is not uniquely visible.");
|
||||
var messages = await backend.MessagesAsync(task.AccountId, sessions[0].Name, payload.IncludeContent, cancellationToken);
|
||||
var messages = await backend.MessagesAsync(task.AccountId, candidateSessions[0].AutomationId, payload.IncludeContent, cancellationToken);
|
||||
var page = messages.ToPage(payload.Limit, payload.Offset);
|
||||
return new RemoteReadExecution(JsonSerializer.SerializeToElement(page, RemoteJson.Options), scopes);
|
||||
}
|
||||
@@ -307,6 +334,44 @@ public sealed class RemoteAgentHostedService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<ContactInfo>> ReadContactsForScopesAsync(
|
||||
string accountId, IReadOnlyList<RemoteReportingScope> scopes, CancellationToken cancellationToken)
|
||||
{
|
||||
var contacts = new Dictionary<string, ContactInfo>(StringComparer.Ordinal);
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
for (var offset = 0; ;)
|
||||
{
|
||||
var page = await backend.ContactsAsync(
|
||||
accountId,
|
||||
contains: scope.ChatId,
|
||||
groupsOnly: scope.ChatType == ReportingChatType.Group,
|
||||
200,
|
||||
offset,
|
||||
cancellationToken);
|
||||
foreach (var contact in page.Items.Where(contact => string.Equals(contact.Id, scope.ChatId, StringComparison.Ordinal)))
|
||||
contacts[contact.Id] = contact;
|
||||
if (!page.HasMore) break;
|
||||
var next = page.NextOffset ?? offset + page.Items.Count;
|
||||
if (next <= offset)
|
||||
throw new ServiceException("InvalidPage", 500, "The node returned a non-advancing contact page.");
|
||||
offset = next;
|
||||
}
|
||||
}
|
||||
return contacts.Values.ToArray();
|
||||
}
|
||||
|
||||
private static bool SessionMatchesScope(SessionInfo session, RemoteReportingScope scope, IReadOnlyList<ContactInfo> contacts)
|
||||
{
|
||||
if (string.Equals(scope.ChatId, session.AutomationId, StringComparison.Ordinal) ||
|
||||
string.Equals(scope.ChatId, session.Name, StringComparison.Ordinal))
|
||||
return true;
|
||||
var matches = contacts.Where(contact => string.Equals(contact.Id, scope.ChatId, StringComparison.Ordinal)).ToArray();
|
||||
return matches.Length == 1 && matches[0] is { } contact &&
|
||||
(string.Equals(contact.DisplayName, session.Name, StringComparison.Ordinal) ||
|
||||
string.Equals(contact.Remark, session.Name, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RemoteReportingScope> AuthorizedChatScopes(
|
||||
ReportingConfig reporting, string accountId, ReportingChatType? chatType = null) =>
|
||||
(reporting.FindAccount(accountId)?.AllowedChats ?? [])
|
||||
@@ -316,8 +381,13 @@ public sealed class RemoteAgentHostedService(
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
private static void RequireScopes(IReadOnlyList<RemoteReportingScope> scopes)
|
||||
private static void RequireScopes(ReportingConfig reporting, string accountId, IReadOnlyList<RemoteReportingScope> scopes)
|
||||
{
|
||||
if (!reporting.Enabled)
|
||||
throw new ServiceException("ReportingDisabled", 403, "Reporting is disabled on the node.");
|
||||
var account = reporting.FindAccount(accountId);
|
||||
if (account is null || !account.Enabled)
|
||||
throw new ServiceException("AccountNotAuthorized", 403, "The target account is not enabled in the local Reporting configuration.");
|
||||
if (scopes.Count == 0)
|
||||
throw new ServiceException("ChatNotAuthorized", 403, "The requested data scope is not enabled in the local whitelist.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user