Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 14 additions & 33 deletions apps/windows/Cascade/Services/Dto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,13 @@ public static class CascadeJson
[JsonDerivedType(typeof(SetListeningTrackingCommand), "setListeningTracking")]
[JsonDerivedType(typeof(RestoreCommand), "restore")]
[JsonDerivedType(typeof(BeginListeningSyncCommand), "beginListeningSync")]
[JsonDerivedType(typeof(ListeningSyncSucceededCommand), "listeningSyncSucceeded")]
[JsonDerivedType(typeof(ListeningSyncFailedCommand), "listeningSyncFailed")]
[JsonDerivedType(typeof(ResetListeningDataCommand), "resetListeningData")]
[JsonDerivedType(typeof(RequestSignInLinkCommand), "requestSignInLink")]
[JsonDerivedType(typeof(SubmitSignInLinkCommand), "submitSignInLink")]
[JsonDerivedType(typeof(SignOutCommand), "signOut")]
[JsonDerivedType(typeof(DeleteListeningDataCommand), "deleteListeningData")]
[JsonDerivedType(typeof(DeleteAccountCommand), "deleteAccount")]
[JsonDerivedType(typeof(SignInLinkSentCommand), "signInLinkSent")]
[JsonDerivedType(typeof(SignInVerifiedCommand), "signInVerified")]
[JsonDerivedType(typeof(ListeningDataDeletedCommand), "listeningDataDeleted")]
[JsonDerivedType(typeof(AccountDeletedCommand), "accountDeleted")]
[JsonDerivedType(typeof(AccountRequestFailedCommand), "accountRequestFailed")]
[JsonDerivedType(typeof(ServerResponseCommand), "serverResponse")]
public abstract record CascadeCommand;

public sealed record PlayCommand : CascadeCommand;
Expand All @@ -69,25 +63,21 @@ public sealed record SetListeningTrackingCommand(bool Enabled) : CascadeCommand;
// AccountJson is the stored account blob, verbatim; "" for none.
public sealed record RestoreCommand(string SettingsJson, string ListeningJson, string FallbackDeviceId, string AccountJson) : CascadeCommand;
public sealed record BeginListeningSyncCommand(SyncReason Reason) : CascadeCommand;
public sealed record ListeningSyncSucceededCommand(ulong ServerTotalMs) : CascadeCommand;
public sealed record ListeningSyncFailedCommand(bool Unauthorized) : CascadeCommand;
public sealed record ResetListeningDataCommand(string NewDeviceId) : CascadeCommand;

// What the user does with the account. Every one but SignOut is ignored while
// an account request is pending (account.busy).
public sealed record RequestSignInLinkCommand(string Email) : CascadeCommand;
// Platform names the app the emailed link hands off to; null for none.
public sealed record RequestSignInLinkCommand(string Email, string? Platform) : CascadeCommand;
public sealed record SubmitSignInLinkCommand(string Input) : CascadeCommand;
public sealed record SignOutCommand : CascadeCommand;
public sealed record DeleteListeningDataCommand : CascadeCommand;
public sealed record DeleteAccountCommand : CascadeCommand;
// NewDeviceId is a fresh id the core rotates to once the server confirms.
public sealed record DeleteListeningDataCommand(string NewDeviceId) : CascadeCommand;
public sealed record DeleteAccountCommand(string NewDeviceId) : CascadeCommand;

// How an account request settled: one command per success, one shared failure
// (unauthorized = HTTP 401).
public sealed record SignInLinkSentCommand : CascadeCommand;
public sealed record SignInVerifiedCommand(string SessionToken, string Email) : CascadeCommand;
public sealed record ListeningDataDeletedCommand(string NewDeviceId) : CascadeCommand;
public sealed record AccountDeletedCommand(string NewDeviceId) : CascadeCommand;
public sealed record AccountRequestFailedCommand(bool Unauthorized) : CascadeCommand;
// Settles a ServerRequestEffect: its id, the HTTP status (0 = not sent or no
// response) and the response body verbatim ("" for none).
public sealed record ServerResponseCommand(ulong Id, ushort Status, string Body) : CascadeCommand;

/// <summary>
/// Why the shell is asking to sync. The shell decides when it can talk; the
Expand Down Expand Up @@ -125,12 +115,7 @@ public override void Write(System.Text.Json.Utf8JsonWriter writer, SyncReason va
[JsonDerivedType(typeof(SetPlatformVolumeEffect), "setPlatformVolume")]
[JsonDerivedType(typeof(PersistSettingsEffect), "persistSettings")]
[JsonDerivedType(typeof(PersistListeningEffect), "persistListening")]
[JsonDerivedType(typeof(PushListeningEffect), "pushListening")]
[JsonDerivedType(typeof(SendSignInLinkEffect), "sendSignInLink")]
[JsonDerivedType(typeof(VerifySignInTokenEffect), "verifySignInToken")]
[JsonDerivedType(typeof(RevokeSessionEffect), "revokeSession")]
[JsonDerivedType(typeof(DeleteServerListeningEffect), "deleteServerListening")]
[JsonDerivedType(typeof(DeleteServerAccountEffect), "deleteServerAccount")]
[JsonDerivedType(typeof(ServerRequestEffect), "serverRequest")]
[JsonDerivedType(typeof(PersistAccountEffect), "persistAccount")]
public abstract record CascadeEffect;

Expand All @@ -140,14 +125,10 @@ public sealed record PausePlaybackEffect : CascadeEffect;
public sealed record SetPlatformVolumeEffect(double Gain) : CascadeEffect;
public sealed record PersistSettingsEffect(string Json) : CascadeEffect;
public sealed record PersistListeningEffect(string Json) : CascadeEffect;
public sealed record PushListeningEffect(string DeviceId, ulong DeviceTotalMs, string SessionToken) : CascadeEffect;
// Account requests: carry each over HTTP and settle it with its success
// command or AccountRequestFailed. RevokeSession is fire-and-forget.
public sealed record SendSignInLinkEffect(string Email) : CascadeEffect;
public sealed record VerifySignInTokenEffect(string Token) : CascadeEffect;
public sealed record RevokeSessionEffect(string SessionToken) : CascadeEffect;
public sealed record DeleteServerListeningEffect(string SessionToken) : CascadeEffect;
public sealed record DeleteServerAccountEffect(string SessionToken) : CascadeEffect;
// One request to the sync server, sent exactly as described: Method to the
// base URL plus Path, with a Bearer token and a JSON Body when given. Every
// one is settled with a ServerResponseCommand carrying the same Id.
public sealed record ServerRequestEffect(ulong Id, string Method, string Path, string? BearerToken, string? Body) : CascadeEffect;
// Store verbatim; "" means delete the stored account.
public sealed record PersistAccountEffect(string Json) : CascadeEffect;

Expand Down
85 changes: 0 additions & 85 deletions apps/windows/Cascade/Services/SyncApi.cs

This file was deleted.

61 changes: 61 additions & 0 deletions apps/windows/Cascade/Services/SyncServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Cascade.Services;

/// <summary>Base URL of cascade-sync-server. Empty disables the sync feature.</summary>
public static class SyncConfig
{
public const string ApiBase = "https://sync.cascade.stephens.page";
public static bool Available => !string.IsNullOrEmpty(ApiBase);
}

/// <summary>
/// Carries the core's <see cref="ServerRequestEffect"/>s to the sync server.
/// The core owns the request table; this sends exactly what it is given and
/// reports the status and body verbatim, with no per-request knowledge.
/// </summary>
public sealed class SyncServer
{
private static readonly HttpClient DefaultHttp = new() { Timeout = TimeSpan.FromSeconds(15) };

private readonly string _baseUrl;
private readonly HttpClient _http;

public SyncServer(string baseUrl = SyncConfig.ApiBase, HttpClient? http = null)
{
_baseUrl = baseUrl;
_http = http ?? DefaultHttp;
}

/// <summary>
/// Send one request and answer it with the <see cref="ServerResponseCommand"/>
/// that settles it. Never throws: no sync server, a network error, a
/// timeout or a cancellation all settle with status 0.
/// </summary>
public async Task<ServerResponseCommand> CarryAsync(ServerRequestEffect request)
{
if (string.IsNullOrEmpty(_baseUrl)) return NoResponse(request);
try
{
using var req = new HttpRequestMessage(new HttpMethod(request.Method), _baseUrl + request.Path);
if (request.BearerToken is not null)
req.Headers.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", request.BearerToken);
if (request.Body is not null)
req.Content = new StringContent(request.Body, Encoding.UTF8, "application/json");
using var resp = await _http.SendAsync(req);
return new ServerResponseCommand(request.Id, (ushort)resp.StatusCode,
await resp.Content.ReadAsStringAsync());
}
catch (Exception)
{
return NoResponse(request);
}
}

private static ServerResponseCommand NoResponse(ServerRequestEffect request) =>
new(request.Id, 0, "");
}
Loading
Loading