feat: add remote control plane and whitelist reads
Build web service image / build (push) Successful in 1m53s
Build web service image / build (push) Successful in 1m53s
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using WxAgent.Core;
|
||||
|
||||
var baseUrl = Required("--base-url");
|
||||
var nodeId = Required("--node-id");
|
||||
var nodeToken = Required("--node-token");
|
||||
var webUser = Required("--web-user");
|
||||
var webPassword = Required("--web-password");
|
||||
var accountId = Get("--account-id") ?? "account-a";
|
||||
var allowedChatId = Get("--allowed-chat-id") ?? "allowed-chat";
|
||||
var blockedChatId = Get("--blocked-chat-id") ?? "blocked-chat";
|
||||
|
||||
using var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") };
|
||||
var webToken = await LoginAsync(http, webUser, webPassword);
|
||||
var config = new ReportingConfig
|
||||
{
|
||||
Enabled = true,
|
||||
ConfigVersion = 1,
|
||||
Accounts = [new AccountReportingConfig
|
||||
{
|
||||
AccountId = accountId,
|
||||
Enabled = true,
|
||||
AllowedChats = [new AllowedChat { Type = ReportingChatType.Group, ChatId = allowedChatId, Enabled = true, IdentityVerified = true }]
|
||||
}]
|
||||
};
|
||||
using var node = new RemoteControlClient(new RemoteAgentOptions
|
||||
{
|
||||
AuthAddress = baseUrl,
|
||||
Token = nodeToken,
|
||||
NodeId = nodeId,
|
||||
ActiveAccountId = accountId
|
||||
}, http);
|
||||
await node.RegisterAsync(new RemoteNodeRegistration(nodeId, "integration", RemoteProtocol.Version,
|
||||
["heartbeat", "poll-tasks", "send-text", "report-message"], config.ConfigVersion,
|
||||
[new RemoteAccountSummary(accountId, true, true, 1, 0)]));
|
||||
await node.HeartbeatAsync(new RemoteHeartbeat(nodeId, "integration", RemoteProtocol.Version, RemoteNodeStatus.Online,
|
||||
true, true, false, accountId, 0, config.ConfigVersion, "integration-heartbeat"));
|
||||
|
||||
var payload = JsonDocument.Parse("{\"target_id\":\"target-chat\",\"text\":\"integration task\",\"confirmed\":true}").RootElement.Clone();
|
||||
var taskResponse = await WebPostAsync<RemoteTaskSubmissionResponse>(http, "/v1/tasks", webToken, new RemoteTaskSubmission(
|
||||
nodeId, accountId, "send-text", "integration-task-1", payload));
|
||||
var task = (await node.PollTasksAsync(accountId)).Single(item => item.TaskId == taskResponse.TaskId);
|
||||
var accepted = await node.AcknowledgeTaskAsync(task);
|
||||
var started = await node.StartTaskAsync(accepted);
|
||||
await node.SendTaskResultAsync(new RemoteTaskResult(started.TaskId, accountId, started.LeaseGeneration,
|
||||
RemoteTaskStatus.Succeeded, null, null, true, null, "integration-result"), config);
|
||||
|
||||
var readResponse = await WebPostAsync<RemoteTaskSubmissionResponse>(http, "/v1/reads/sessions", webToken,
|
||||
new { node_id = nodeId, account_id = accountId, idempotency_key = "integration-read-1", limit = 20, offset = 0 });
|
||||
var readTask = (await node.PollTasksAsync(accountId)).Single(item => item.TaskId == readResponse.TaskId);
|
||||
var readAccepted = await node.AcknowledgeTaskAsync(readTask);
|
||||
var readStarted = await node.StartTaskAsync(readAccepted);
|
||||
using var readContentDocument = JsonDocument.Parse("{\"items\":[{\"automationId\":\"allowed-chat\"}],\"limit\":20,\"offset\":0,\"hasMore\":false}");
|
||||
await node.SendTaskResultAsync(new RemoteTaskResult(readStarted.TaskId, accountId, readStarted.LeaseGeneration,
|
||||
RemoteTaskStatus.Succeeded, null, null, false, readContentDocument.RootElement.Clone(), "integration-read-result"), config,
|
||||
chatScopes: [new RemoteReportingScope(allowedChatId, ReportingChatType.Group)]);
|
||||
var storedRead = await WebGetAsync<JsonElement>(http, "/v1/tasks/" + readResponse.TaskId, webToken);
|
||||
if (!storedRead.TryGetProperty("result", out var readResult)
|
||||
|| !readResult.TryGetProperty("content", out var readContent)
|
||||
|| readContent.ValueKind != JsonValueKind.Object)
|
||||
throw new InvalidOperationException("Remote read result content was not retained.");
|
||||
|
||||
var denied = await node.SubmitEventAsync(config, new RemoteMessageEvent(nodeId, accountId, blockedChatId,
|
||||
ReportingChatType.Private, 1, "message", DateTimeOffset.UtcNow, "blocked-content", config.ConfigVersion,
|
||||
config.ConfigVersion, "integration-denied"));
|
||||
var acceptedEvent = await node.SubmitEventAsync(config, new RemoteMessageEvent(nodeId, accountId, allowedChatId,
|
||||
ReportingChatType.Group, 1, "message", DateTimeOffset.UtcNow, "allowed-content", config.ConfigVersion,
|
||||
config.ConfigVersion, "integration-allowed"));
|
||||
var events = await WebGetAsync<EventList>(http, "/v1/events?limit=20", webToken);
|
||||
if (denied.Accepted || !acceptedEvent.Accepted || events.Events.Count != 1 || events.Events[0].ChatId != allowedChatId)
|
||||
throw new InvalidOperationException("Remote whitelist integration assertion failed.");
|
||||
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
status = "ok",
|
||||
node = nodeId,
|
||||
task = taskResponse.TaskId,
|
||||
taskStatus = RemoteTaskStatus.Succeeded.ToString(),
|
||||
readTask = readResponse.TaskId,
|
||||
readContentRetained = true,
|
||||
deniedEventAccepted = denied.Accepted,
|
||||
acceptedEvent = acceptedEvent.EventId,
|
||||
storedEventCount = events.Events.Count,
|
||||
privacy = "blocked event content was not sent"
|
||||
}, new JsonSerializerOptions { WriteIndented = true }));
|
||||
|
||||
async Task<T> WebPostAsync<T>(HttpClient client, string path, string token, object value)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, path);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(value, RemoteJson.Options), Encoding.UTF8, "application/json");
|
||||
using var response = await client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode) throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {content}");
|
||||
return JsonSerializer.Deserialize<T>(content, RemoteJson.Options) ?? throw new InvalidOperationException("Empty response.");
|
||||
}
|
||||
|
||||
async Task<T> WebGetAsync<T>(HttpClient client, string path, string token)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, path);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
using var response = await client.SendAsync(request);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
if (!response.IsSuccessStatusCode) throw new InvalidOperationException($"HTTP {(int)response.StatusCode}: {content}");
|
||||
return JsonSerializer.Deserialize<T>(content, RemoteJson.Options) ?? throw new InvalidOperationException("Empty response.");
|
||||
}
|
||||
|
||||
async Task<string> LoginAsync(HttpClient client, string username, string password)
|
||||
{
|
||||
var result = await WebPostAsync<LoginResponse>(client, "/v1/auth/login", "", new { username, password });
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
string Required(string name) => Get(name) ?? throw new ArgumentException($"Missing {name}.");
|
||||
string? Get(string name)
|
||||
{
|
||||
var index = Array.IndexOf(args, name);
|
||||
return index >= 0 && index + 1 < args.Length ? args[index + 1] : null;
|
||||
}
|
||||
|
||||
sealed record LoginResponse(
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("access_token")] string AccessToken,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("token_type")] string TokenType,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("expires_in")] int ExpiresIn,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("correlation_id")] string CorrelationId);
|
||||
sealed record EventList([property: System.Text.Json.Serialization.JsonPropertyName("events")] IReadOnlyList<EventView> Events);
|
||||
sealed record EventView(
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("event_id")] string EventId,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("node_id")] string NodeId,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("account_id")] string AccountId,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("chat_id")] string ChatId,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("chat_type")] string ChatType,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("event_seq")] long EventSeq,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("event_type")] string EventType,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("occurred_at")] DateTimeOffset OccurredAt,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("content")] string? Content,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("config_version")] long ConfigVersion,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("authorization_version")] long AuthorizationVersion,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("correlation_id")] string CorrelationId,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("authorized")] bool Authorized,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("content_hash")] string ContentHash,
|
||||
[property: System.Text.Json.Serialization.JsonPropertyName("received_at")] DateTimeOffset ReceivedAt);
|
||||
Reference in New Issue
Block a user