Files
JiboExperiments/OpenJibo/src/Jibo.Cloud/dotnet/src/Jibo.Cloud.Application/Services/JiboCloudProtocolService.cs
Jacob Dubin 8c97968d95 refactors
2026-04-26 20:57:08 -05:00

719 lines
27 KiB
C#

using System.Text.Json;
using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Domain.Models;
namespace Jibo.Cloud.Application.Services;
public sealed class JiboCloudProtocolService(ICloudStateStore stateStore)
{
private static readonly string[] AcceptedHosts =
[
"api.jibo.com",
"openjibo.com",
"openjibo.ai",
"localhost"
];
public Task<ProtocolDispatchResult> DispatchAsync(ProtocolEnvelope envelope, CancellationToken cancellationToken = default)
{
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
envelope.Path == "/" &&
string.IsNullOrWhiteSpace(envelope.ServicePrefix))
{
return Task.FromResult(ProtocolDispatchResult.NoContent());
}
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
envelope.Path.Equals("/health", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(ProtocolDispatchResult.Ok(new { ok = true, host = envelope.HostName }));
}
if (envelope.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) &&
envelope.Path.StartsWith("/media/", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleMediaContent(envelope));
}
if (envelope.Method.Equals("PUT", StringComparison.OrdinalIgnoreCase) &&
(envelope.Path.Equals("/upload/asr-binary", StringComparison.OrdinalIgnoreCase) ||
envelope.Path.Equals("/upload/log-events", StringComparison.OrdinalIgnoreCase) ||
envelope.Path.Equals("/upload/log-binary", StringComparison.OrdinalIgnoreCase)))
{
return Task.FromResult(ProtocolDispatchResult.Raw(200, string.Empty));
}
if (!AcceptedHosts.Contains(envelope.HostName, StringComparer.OrdinalIgnoreCase))
{
return Task.FromResult(ProtocolDispatchResult.Ok(new
{
ok = true,
accepted = false,
host = envelope.HostName
}));
}
var servicePrefix = envelope.ServicePrefix ?? string.Empty;
var operation = envelope.Operation ?? string.Empty;
if (servicePrefix.StartsWith("Log_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleLog(operation, envelope));
}
if (servicePrefix.StartsWith("Backup_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleBackup(operation));
}
if (servicePrefix.StartsWith("Account_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleAccount(operation, envelope));
}
if (servicePrefix.StartsWith("Notification_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleNotification(operation, envelope));
}
if (servicePrefix.StartsWith("Loop_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleLoop(operation));
}
if (servicePrefix.Equals("Media_20160725", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleMedia(operation, envelope));
}
if (servicePrefix.StartsWith("Key_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleKey(operation, envelope));
}
if (servicePrefix.StartsWith("Person_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandlePerson(operation));
}
if (servicePrefix.StartsWith("Robot_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleRobot(operation, envelope));
}
if (servicePrefix.StartsWith("Update_", StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(HandleUpdate(operation, envelope));
}
return Task.FromResult(ProtocolDispatchResult.Ok(new
{
ok = true,
host = envelope.HostName,
target = $"{servicePrefix}.{operation}".Trim('.'),
operation,
note = "unknown target default response"
}));
}
private ProtocolDispatchResult HandleAccount(string operation, ProtocolEnvelope envelope)
{
var account = stateStore.GetAccount();
var body = envelope.TryParseBody();
if (operation.Equals("CreateHubToken", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new
{
token = stateStore.IssueHubToken(),
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
});
}
if (operation.Equals("CreateAccessToken", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new
{
token = $"access-{account.AccountId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
expires = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()
});
}
if (operation.Equals("CheckEmail", StringComparison.OrdinalIgnoreCase))
{
var email = ReadString(body, "email") ?? string.Empty;
return ProtocolDispatchResult.Ok(new
{
exists = email.Equals(account.Email, StringComparison.OrdinalIgnoreCase)
});
}
if (operation is "Create" or "Login")
{
return ProtocolDispatchResult.Ok(new
{
id = account.AccountId,
email = ReadString(body, "email") ?? account.Email,
firstName = ReadString(body, "firstName") ?? account.FirstName,
lastName = ReadString(body, "lastName") ?? account.LastName,
gender = "unknown",
birthday = 631152000000L,
phoneNumber = "+10000000000",
photoUrl = string.Empty,
isActive = true,
messagingAllowed = true,
accessKeyId = account.AccessKeyId,
secretAccessKey = account.SecretAccessKey,
roles = Array.Empty<object>(),
facebookConnected = false,
termsAccepted = true
});
}
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
{
var ids = ReadStringArray(body, "ids");
var matches = ids.Count == 0 || ids.Contains(account.AccountId, StringComparer.OrdinalIgnoreCase);
if (!matches)
{
return ProtocolDispatchResult.Ok(Array.Empty<object>());
}
return ProtocolDispatchResult.Ok(new[]
{
new
{
id = account.AccountId,
email = account.Email,
firstName = account.FirstName,
lastName = account.LastName,
accessKeyId = account.AccessKeyId,
secretAccessKey = account.SecretAccessKey
}
});
}
switch (operation)
{
case "Update" or "ResetKeys" or "Remove" or "ActivateByCode" or "ResendActivationCode" or
"ChangePassword" or "SendPasswordReset" or "PasswordResetByCode" or "UpdatePhoto" or "RemovePhoto" or
"VerifyPhoneByCode" or "AcceptTerms" or "FacebookConnect" or "FacebookMobileConnect":
return ProtocolDispatchResult.Ok(new
{
id = account.AccountId,
email = account.Email,
firstName = account.FirstName,
lastName = account.LastName,
accessKeyId = account.AccessKeyId,
secretAccessKey = account.SecretAccessKey
});
case "ChangeEmail" or "SendPhoneVerificationCode":
return ProtocolDispatchResult.Ok(new
{
id = account.AccountId
});
}
if (operation.Equals("GetAccountByAccessToken", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new
{
id = account.AccountId,
accessKeyId = account.AccessKeyId,
secretAccessKey = account.SecretAccessKey,
email = account.Email,
friendlyId = stateStore.GetRobot().RobotId,
payload = ReadObject(body, "payload")
});
}
if (operation.Equals("Search", StringComparison.OrdinalIgnoreCase))
{
var query = (ReadString(body, "query") ?? string.Empty).ToLowerInvariant();
var haystack = $"{account.Email} {account.FirstName} {account.LastName} {account.AccountId}".ToLowerInvariant();
return ProtocolDispatchResult.Ok(query.Length > 0 && haystack.Contains(query)
?
[
new
{
id = account.AccountId,
email = account.Email,
firstName = account.FirstName,
lastName = account.LastName
}
]
: Array.Empty<object>());
}
if (operation.Equals("FacebookPrepareLogin", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new
{
url = "https://example.com/facebook-login",
client_id = "fake-client-id",
scope = "email",
response_type = "token",
state = $"fb-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
redirect_uri = "https://api.jibo.com/facebook/callback"
});
}
if (operation.Equals("ConfirmEmailReset", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new { });
}
return ProtocolDispatchResult.Ok(new
{
id = account.AccountId,
email = account.Email,
firstName = account.FirstName,
lastName = account.LastName
});
}
private ProtocolDispatchResult HandleNotification(string operation, ProtocolEnvelope envelope)
{
if (!operation.Equals("NewRobotToken", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new { ok = true, operation });
}
var body = envelope.TryParseBody();
var deviceId = !string.IsNullOrWhiteSpace(envelope.DeviceId)
? envelope.DeviceId!
: ReadString(body, "deviceId")
?? ReadString(body, "serial_number")
?? ReadString(body, "serialNumber")
?? ReadString(body, "cpuid")
?? ReadString(body, "cpuId")
?? ReadString(body, "robotId")
?? "unknown-device";
stateStore.GetOrCreateDevice(deviceId, envelope.FirmwareVersion, envelope.ApplicationVersion);
return ProtocolDispatchResult.Ok(new
{
token = stateStore.IssueRobotToken(deviceId)
});
}
private ProtocolDispatchResult HandleLoop(string operation)
{
if (operation is not ("List" or "ListLoops"))
{
return ProtocolDispatchResult.Ok(Array.Empty<object>());
}
return ProtocolDispatchResult.Ok(stateStore.GetLoops().Select(loop => new
{
id = loop.LoopId,
name = loop.Name,
owner = loop.OwnerAccountId,
robot = loop.RobotId,
robotFriendlyId = loop.RobotFriendlyId,
members = Array.Empty<object>(),
isSuspended = loop.IsSuspended,
created = loop.CreatedUtc.ToUnixTimeMilliseconds(),
updated = loop.UpdatedUtc.ToUnixTimeMilliseconds()
}).ToArray());
}
private static ProtocolDispatchResult HandleLog(string operation, ProtocolEnvelope envelope)
{
return operation switch
{
"PutEventsAsync" => ProtocolDispatchResult.Ok(new
{
contentEncoding = "gzip",
uploadUrl = "https://api.jibo.com/upload/log-events"
}),
"PutEvents" => ProtocolDispatchResult.Ok(new { }),
"PutBinaryAsync" => ProtocolDispatchResult.Ok(new
{
url = "https://api.jibo.com/log/binary/fake-id",
uploadUrl = "https://api.jibo.com/upload/log-binary"
}),
"PutAsrBinary" => ProtocolDispatchResult.Ok(new
{
bucketName = "openjibo-test",
key = "asr/fake-key",
uploadUrl = "https://api.jibo.com/upload/asr-binary"
}),
"NewKinesisCredentials" => ProtocolDispatchResult.Ok(new
{
credentials = new
{
AccessKeyId = "fake-access-key",
Expiration = DateTimeOffset.UtcNow.AddHours(1).ToString("O"),
SecretAccessKey = "fake-secret",
SessionToken = "fake-session"
},
region = "us-east-1",
streamName = "openjibo-log-stream"
}),
_ => ProtocolDispatchResult.Ok(new { })
};
}
private ProtocolDispatchResult HandleMedia(string operation, ProtocolEnvelope envelope)
{
var body = envelope.TryParseBody();
if (operation.Equals("List", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(stateStore.ListMedia(
ReadStringArray(body, "loopIds"),
ReadLong(body, "after"),
ReadLong(body, "before")).Select(MapMedia).ToArray());
}
if (operation.Equals("Get", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(stateStore.GetMedia(ReadStringArray(body, "paths")).Select(MapMedia).ToArray());
}
if (operation.Equals("Remove", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(stateStore.RemoveMedia(ReadStringArray(body, "paths")).Select(MapMedia).ToArray());
}
if (!operation.Equals("Create", StringComparison.OrdinalIgnoreCase))
return ProtocolDispatchResult.Ok(Array.Empty<object>());
var loopId = ReadHeader(envelope, "x-loop-id") ?? ReadString(body, "loopId") ?? stateStore.GetLoops()[0].LoopId;
var path = ReadHeader(envelope, "x-path") ?? ReadString(body, "path") ?? $"/media/{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
var type = ReadHeader(envelope, "x-type") ?? ReadString(body, "type") ?? "unknown";
var reference = ReadHeader(envelope, "x-reference") ?? ReadString(body, "reference") ?? string.Empty;
var isEncrypted = ReadBooleanHeader(envelope, "x-encrypted") || ReadBool(body, "isEncrypted");
var meta = ReadObject(body, "meta") ?? new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
var contentType = ReadHeader(envelope, "Content-Type") ?? "application/octet-stream";
meta["contentType"] = contentType;
if (!string.IsNullOrWhiteSpace(envelope.BodyText))
{
meta["bodyText"] = envelope.BodyText;
}
return ProtocolDispatchResult.Ok(MapMedia(stateStore.CreateMedia(loopId, path, type, reference, isEncrypted, meta)));
}
private ProtocolDispatchResult HandlePerson(string operation)
{
return ProtocolDispatchResult.Ok(operation.Equals("ListHolidays", StringComparison.OrdinalIgnoreCase)
? stateStore.GetHolidays()
: []);
}
private ProtocolDispatchResult HandleBackup(string operation)
{
return operation.Equals("List", StringComparison.OrdinalIgnoreCase)
? ProtocolDispatchResult.Ok(stateStore.GetBackups())
: ProtocolDispatchResult.Ok(Array.Empty<object>());
}
private ProtocolDispatchResult HandleKey(string operation, ProtocolEnvelope envelope)
{
var body = envelope.TryParseBody();
var loopId = ReadString(body, "loopId") ?? ReadString(body, "id") ?? stateStore.GetLoops()[0].LoopId;
if (operation.Equals("ShouldCreate", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(new
{
shouldCreate = stateStore.ShouldCreateSymmetricKey(loopId)
});
}
string? symmetricKey;
if (operation.Equals("CreateSymmetricKey", StringComparison.OrdinalIgnoreCase))
{
symmetricKey = stateStore.GetOrCreateSymmetricKey(loopId);
return ProtocolDispatchResult.Ok(new
{
loopId,
key = symmetricKey,
symmetricKey,
created = true
});
}
if (operation is "CreateRequest" or "RequestSymmetricKey")
{
var record = stateStore.CreateKeyRequest(loopId, ReadString(body, "publicKey") ?? string.Empty);
return ProtocolDispatchResult.Ok(new
{
id = record.RequestId,
loopId = record.LoopId
});
}
if (operation.Equals("GetRequest", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(stateStore.GetKeyRequest(loopId, ReadString(body, "id"), ReadString(body, "publicKey")));
}
if (operation.Equals("ListIncomingRequests", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(stateStore.GetIncomingKeyRequests());
}
if (operation.Equals("ListBinaryRequests", StringComparison.OrdinalIgnoreCase))
{
return ProtocolDispatchResult.Ok(stateStore.GetBinaryRequests());
}
if (operation is "Share" or "ShareSymmetricKey" or "ShareBinary")
{
return ProtocolDispatchResult.Ok(new { ok = true });
}
if (!operation.Equals("LoadSymmetricKey", StringComparison.OrdinalIgnoreCase))
return ProtocolDispatchResult.Ok(new { ok = true, operation });
symmetricKey = stateStore.GetOrCreateSymmetricKey(loopId);
return ProtocolDispatchResult.Ok(new
{
loopId,
key = symmetricKey,
symmetricKey
});
}
private ProtocolDispatchResult HandleRobot(string operation, ProtocolEnvelope envelope)
{
var robot = stateStore.GetRobot();
if (operation.Equals("UpdateRobot", StringComparison.OrdinalIgnoreCase))
{
var updated = new DeviceRegistration
{
DeviceId = robot.DeviceId,
RobotId = robot.RobotId,
FriendlyName = robot.FriendlyName,
FirmwareVersion = envelope.FirmwareVersion ?? robot.FirmwareVersion,
ApplicationVersion = envelope.ApplicationVersion ?? robot.ApplicationVersion,
HostMappings = robot.HostMappings
};
stateStore.UpdateRobot(updated);
return ProtocolDispatchResult.Ok(new
{
result = "ok"
});
}
if (!operation.Equals("GetRobot", StringComparison.OrdinalIgnoreCase))
return ProtocolDispatchResult.Ok(new
{
result = "ok"
});
var profile = stateStore.GetRobotProfile();
return ProtocolDispatchResult.Ok(new
{
id = ReadString(envelope.TryParseBody(), "id") ?? profile.RobotId,
payload = profile.Payload,
calibrationPayload = profile.CalibrationPayload,
updated = profile.UpdatedUtc.ToUnixTimeMilliseconds(),
created = profile.CreatedUtc.ToUnixTimeMilliseconds()
});
}
private ProtocolDispatchResult HandleUpdate(string operation, ProtocolEnvelope envelope)
{
var body = envelope.TryParseBody();
var subsystem = ReadString(body, "subsystem");
var filter = ReadString(body, "filter");
var fromVersion = ReadString(body, "fromVersion");
return operation switch
{
"ListUpdates" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter).Select(MapUpdate).ToArray()),
"ListUpdatesFrom" => ProtocolDispatchResult.Ok(stateStore.ListUpdates(subsystem, filter)
.Where(update => fromVersion is null || update.FromVersion.Equals(fromVersion, StringComparison.OrdinalIgnoreCase))
.Select(MapUpdate)
.ToArray()),
"GetUpdateFrom" => HandleGetUpdateFrom(subsystem, fromVersion, filter),
"CreateUpdate" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.CreateUpdate(
fromVersion,
ReadString(body, "toVersion"),
ReadString(body, "changes"),
ReadString(body, "shaHash"),
ReadLong(body, "length"),
subsystem,
filter,
ReadObject(body, "dependencies")))),
"RemoveUpdate" => ProtocolDispatchResult.Ok(MapUpdate(stateStore.RemoveUpdate(ReadString(body, "id")))),
_ => ProtocolDispatchResult.Ok(Array.Empty<object>())
};
}
private ProtocolDispatchResult HandleMediaContent(ProtocolEnvelope envelope)
{
var path = Uri.UnescapeDataString(envelope.Path["/media/".Length..]);
var candidatePaths = new[] { path, $"/{path}" };
var media = stateStore.GetMedia(candidatePaths).FirstOrDefault();
if (media is null || media.IsDeleted)
{
return ProtocolDispatchResult.Raw(404, string.Empty);
}
var contentType = TryReadMetaString(media.Meta, "contentType") ?? "application/octet-stream";
var bodyText = TryReadMetaString(media.Meta, "bodyText") ?? string.Empty;
return ProtocolDispatchResult.Raw(200, bodyText, contentType);
}
private ProtocolDispatchResult HandleGetUpdateFrom(string? subsystem, string? fromVersion, string? filter)
{
var update = stateStore.GetUpdateFrom(subsystem, fromVersion, filter);
return update is null
? ProtocolDispatchResult.Ok(new { })
: ProtocolDispatchResult.Ok(MapUpdate(update));
}
private static object MapUpdate(UpdateManifest update)
{
return new
{
_id = update.UpdateId,
created = update.CreatedUtc.ToUnixTimeMilliseconds(),
accountId = "usr_openjibo_owner",
fromVersion = update.FromVersion,
toVersion = update.ToVersion,
changes = update.Changes,
url = update.Url,
shaHash = update.ShaHash,
length = update.Length,
subsystem = update.Subsystem,
filter = update.Filter,
dependencies = new Dictionary<string, object?>()
};
}
private static object MapMedia(MediaRecord item)
{
return new
{
path = item.Path,
created = item.CreatedUtc.ToUnixTimeMilliseconds(),
type = item.MediaType,
reference = item.Reference,
accountId = item.AccountId,
loopId = item.LoopId,
url = item.Url,
thumbnailUrl = item.Url,
originalUrl = item.Url,
isEncrypted = item.IsEncrypted,
isDeleted = item.IsDeleted,
meta = item.Meta
};
}
private static string? TryReadMetaString(IDictionary<string, object?> meta, string key)
{
return meta.TryGetValue(key, out var value)
? value?.ToString()
: null;
}
private static string? ReadString(JsonElement? element, string propertyName)
{
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
{
return null;
}
return property.ValueKind == JsonValueKind.String
? property.GetString()
: property.ToString();
}
private static long? ReadLong(JsonElement? element, string propertyName)
{
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
{
return null;
}
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out var number))
{
return number;
}
return long.TryParse(property.ToString(), out var parsed) ? parsed : null;
}
private static bool ReadBool(JsonElement? element, string propertyName)
{
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
{
return false;
}
return property.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => bool.TryParse(property.ToString(), out var parsed) && parsed
};
}
private static IReadOnlyList<string> ReadStringArray(JsonElement? element, string propertyName)
{
if (element is null || !element.Value.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
{
return [];
}
return [.. property.EnumerateArray()
.Select(item => item.ValueKind == JsonValueKind.String ? item.GetString() ?? string.Empty : item.ToString())
.Where(item => !string.IsNullOrWhiteSpace(item))];
}
private static IDictionary<string, object?>? ReadObject(JsonElement? element, string propertyName)
{
if (element is null || !element.Value.TryGetProperty(propertyName, out var property))
{
return null;
}
if (property.ValueKind != JsonValueKind.Object)
{
return null;
}
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
foreach (var child in property.EnumerateObject())
{
result[child.Name] = child.Value.ValueKind switch
{
JsonValueKind.String => child.Value.GetString(),
JsonValueKind.Number when child.Value.TryGetInt64(out var longValue) => longValue,
JsonValueKind.Number when child.Value.TryGetDouble(out var doubleValue) => doubleValue,
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => child.Value.ToString()
};
}
return result;
}
private static string? ReadHeader(ProtocolEnvelope envelope, string headerName)
{
return envelope.Headers.TryGetValue(headerName, out var value) ? value : null;
}
private static bool ReadBooleanHeader(ProtocolEnvelope envelope, string headerName)
{
return envelope.Headers.TryGetValue(headerName, out var value) &&
bool.TryParse(value, out var parsed) &&
parsed;
}
}