From f01478cb40b61f5bd8267e61d75856d9e8f4c8ae Mon Sep 17 00:00:00 2001 From: Luke Gehorsam Date: Mon, 8 Apr 2024 10:50:03 -0400 Subject: [PATCH] add Satori messages API support (#172) --- CHANGELOG.md | 5 + Satori/ApiClient.gen.cs | 395 ++++++++++++++++++++++++++++++++++++++-- Satori/Client.cs | 44 ++++- Satori/IClient.cs | 31 ++++ 4 files changed, 458 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b110dadf..5470dd7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [3.12.0] - 2024-04-08 +### Added +- Satori: Added `IApiLiveEvent.Id` for accessing live event identifiers. +- Satori: Added support for new Satori Messages API: `IClient.GetMessageListAsync`, `IClient.UpdateMessageAsync` and `IClient.DeleteMessageAsync`. + ## [3.11.0] - 2024-03-08 ### Added - Nakama: New `IClient` event called `ReceivedSessionUpdated` when session expires and is refreshed. diff --git a/Satori/ApiClient.gen.cs b/Satori/ApiClient.gen.cs index afddbffc..4619edeb 100644 --- a/Satori/ApiClient.gen.cs +++ b/Satori/ApiClient.gen.cs @@ -40,6 +40,44 @@ public override string ToString() } } + /// + /// The request to update the status of a message. + /// + public interface IApiUpdateMessageRequest + { + + /// + /// The time the message was consumed by the identity. + /// + string ConsumeTime { get; } + + /// + /// The time the message was read at the client. + /// + string ReadTime { get; } + } + + /// + internal class ApiUpdateMessageRequest : IApiUpdateMessageRequest + { + + /// + [DataMember(Name="consume_time"), Preserve] + public string ConsumeTime { get; set; } + + /// + [DataMember(Name="read_time"), Preserve] + public string ReadTime { get; set; } + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "ConsumeTime: ", ConsumeTime, ", "); + output = string.Concat(output, "ReadTime: ", ReadTime, ", "); + return output; + } + } + /// /// Log out a session, invalidate a refresh token, or log out all sessions/refresh tokens for a user. /// @@ -422,6 +460,66 @@ public override string ToString() } } + /// + /// A response containing all the messages for an identity. + /// + public interface IApiGetMessageListResponse + { + + /// + /// Cacheable cursor to list newer messages. Durable and designed to be stored, unlike next/prev cursors. + /// + string CacheableCursor { get; } + + /// + /// The list of messages. + /// + IEnumerable Messages { get; } + + /// + /// The cursor to send when retrieving the next page, if any. + /// + string NextCursor { get; } + + /// + /// The cursor to send when retrieving the previous page, if any. + /// + string PrevCursor { get; } + } + + /// + internal class ApiGetMessageListResponse : IApiGetMessageListResponse + { + + /// + [DataMember(Name="cacheable_cursor"), Preserve] + public string CacheableCursor { get; set; } + + /// + [IgnoreDataMember] + public IEnumerable Messages => _messages ?? new List(0); + [DataMember(Name="messages"), Preserve] + public List _messages { get; set; } + + /// + [DataMember(Name="next_cursor"), Preserve] + public string NextCursor { get; set; } + + /// + [DataMember(Name="prev_cursor"), Preserve] + public string PrevCursor { get; set; } + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "CacheableCursor: ", CacheableCursor, ", "); + output = string.Concat(output, "Messages: [", string.Join(", ", Messages), "], "); + output = string.Concat(output, "NextCursor: ", NextCursor, ", "); + output = string.Concat(output, "PrevCursor: ", PrevCursor, ", "); + return output; + } + } + /// /// Enrich/replace the current session with a new ID. /// @@ -507,6 +605,11 @@ public interface IApiLiveEvent /// string Description { get; } + /// + /// The live event identifier. + /// + string Id { get; } + /// /// Name. /// @@ -534,6 +637,10 @@ internal class ApiLiveEvent : IApiLiveEvent [DataMember(Name="description"), Preserve] public string Description { get; set; } + /// + [DataMember(Name="id"), Preserve] + public string Id { get; set; } + /// [DataMember(Name="name"), Preserve] public string Name { get; set; } @@ -548,6 +655,7 @@ public override string ToString() output = string.Concat(output, "ActiveEndTimeSec: ", ActiveEndTimeSec, ", "); output = string.Concat(output, "ActiveStartTimeSec: ", ActiveStartTimeSec, ", "); output = string.Concat(output, "Description: ", Description, ", "); + output = string.Concat(output, "Id: ", Id, ", "); output = string.Concat(output, "Name: ", Name, ", "); output = string.Concat(output, "Value: ", Value, ", "); return output; @@ -584,6 +692,122 @@ public override string ToString() } } + /// + /// A scheduled message. + /// + public interface IApiMessage + { + + /// + /// The time the message was consumed by the identity. + /// + string ConsumeTime { get; } + + /// + /// The time the message was created. + /// + string CreateTime { get; } + + /// + /// The message's unique identifier. + /// + string Id { get; } + + /// + /// A key-value pairs of metadata. + /// + IDictionary Metadata { get; } + + /// + /// The time the message was read by the client. + /// + string ReadTime { get; } + + /// + /// The identifier of the schedule. + /// + string ScheduleId { get; } + + /// + /// The send time for the message. + /// + string SendTime { get; } + + /// + /// The message's text. + /// + string Text { get; } + + /// + /// The time the message was updated. + /// + string UpdateTime { get; } + } + + /// + internal class ApiMessage : IApiMessage + { + + /// + [DataMember(Name="consume_time"), Preserve] + public string ConsumeTime { get; set; } + + /// + [DataMember(Name="create_time"), Preserve] + public string CreateTime { get; set; } + + /// + [DataMember(Name="id"), Preserve] + public string Id { get; set; } + + /// + [IgnoreDataMember] + public IDictionary Metadata => _metadata ?? new Dictionary(); + [DataMember(Name="metadata"), Preserve] + public Dictionary _metadata { get; set; } + + /// + [DataMember(Name="read_time"), Preserve] + public string ReadTime { get; set; } + + /// + [DataMember(Name="schedule_id"), Preserve] + public string ScheduleId { get; set; } + + /// + [DataMember(Name="send_time"), Preserve] + public string SendTime { get; set; } + + /// + [DataMember(Name="text"), Preserve] + public string Text { get; set; } + + /// + [DataMember(Name="update_time"), Preserve] + public string UpdateTime { get; set; } + + public override string ToString() + { + var output = ""; + output = string.Concat(output, "ConsumeTime: ", ConsumeTime, ", "); + output = string.Concat(output, "CreateTime: ", CreateTime, ", "); + output = string.Concat(output, "Id: ", Id, ", "); + + var metadataString = ""; + foreach (var kvp in Metadata) + { + metadataString = string.Concat(metadataString, "{" + kvp.Key + "=" + kvp.Value + "}"); + } + output = string.Concat(output, "Metadata: [" + metadataString + "]"); + output = string.Concat(output, "ReadTime: ", ReadTime, ", "); + output = string.Concat(output, "ScheduleId: ", ScheduleId, ", "); + output = string.Concat(output, "SendTime: ", SendTime, ", "); + output = string.Concat(output, "Text: ", Text, ", "); + output = string.Concat(output, "UpdateTime: ", UpdateTime, ", "); + return output; + } + } + /// /// Properties associated with an identity. /// @@ -877,9 +1101,11 @@ public async Task SatoriHealthcheckAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -904,9 +1130,11 @@ public async Task SatoriReadycheckAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -937,9 +1165,11 @@ public async Task SatoriAuthenticateAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -976,9 +1206,11 @@ public async Task SatoriAuthenticateLogoutAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1011,9 +1243,11 @@ public async Task SatoriAuthenticateRefreshAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1050,9 +1284,11 @@ public async Task SatoriEventAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1084,9 +1320,11 @@ public async Task SatoriGetExperimentsAsync( queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1119,9 +1357,11 @@ public async Task SatoriGetFlagsAsync( queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1161,9 +1401,11 @@ public async Task SatoriIdentifyAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1191,9 +1433,11 @@ public async Task SatoriDeleteIdentityAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1223,9 +1467,11 @@ public async Task SatoriGetLiveEventsAsync( queryParams = string.Concat(queryParams, "names=", Uri.EscapeDataString(elem), "&"); } + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1239,6 +1485,125 @@ public async Task SatoriGetLiveEventsAsync( return contents.FromJson(); } + /// + /// Get the list of messages for the identity. + /// + public async Task SatoriGetMessageListAsync( + string bearerToken, + int? limit, + bool? forward, + string cursor, + CancellationToken? cancellationToken) + { + + var urlpath = "/v1/message"; + + var queryParams = ""; + if (limit != null) { + queryParams = string.Concat(queryParams, "limit=", limit, "&"); + } + if (forward != null) { + queryParams = string.Concat(queryParams, "forward=", forward.ToString().ToLower(), "&"); + } + if (cursor != null) { + queryParams = string.Concat(queryParams, "cursor=", Uri.EscapeDataString(cursor), "&"); + } + + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + + var uri = new UriBuilder(_baseUri) + { + Path = path, + Query = queryParams + }.Uri; + + var method = "GET"; + var headers = new Dictionary(); + var header = string.Concat("Bearer ", bearerToken); + headers.Add("Authorization", header); + + byte[] content = null; + var contents = await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); + return contents.FromJson(); + } + + /// + /// Deletes a message for an identity. + /// + public async Task SatoriDeleteMessageAsync( + string bearerToken, + string id, + CancellationToken? cancellationToken) + { + if (id == null) + { + throw new ArgumentException("'id' is required but was null."); + } + + var urlpath = "/v1/message/{id}"; + urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); + + var queryParams = ""; + + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + + var uri = new UriBuilder(_baseUri) + { + Path = path, + Query = queryParams + }.Uri; + + var method = "DELETE"; + var headers = new Dictionary(); + var header = string.Concat("Bearer ", bearerToken); + headers.Add("Authorization", header); + + byte[] content = null; + await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); + } + + /// + /// Updates a message for an identity. + /// + public async Task SatoriUpdateMessageAsync( + string bearerToken, + string id, + ApiUpdateMessageRequest body, + CancellationToken? cancellationToken) + { + if (id == null) + { + throw new ArgumentException("'id' is required but was null."); + } + if (body == null) + { + throw new ArgumentException("'body' is required but was null."); + } + + var urlpath = "/v1/message/{id}"; + urlpath = urlpath.Replace("{id}", Uri.EscapeDataString(id)); + + var queryParams = ""; + + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + + var uri = new UriBuilder(_baseUri) + { + Path = path, + Query = queryParams + }.Uri; + + var method = "PUT"; + var headers = new Dictionary(); + var header = string.Concat("Bearer ", bearerToken); + headers.Add("Authorization", header); + + byte[] content = null; + var jsonBody = body.ToJson(); + content = Encoding.UTF8.GetBytes(jsonBody); + await HttpAdapter.SendAsync(method, uri, headers, content, Timeout, cancellationToken); + } + /// /// List properties associated with this identity. /// @@ -1251,9 +1616,11 @@ public async Task SatoriListPropertiesAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; @@ -1284,9 +1651,11 @@ public async Task SatoriUpdatePropertiesAsync( var queryParams = ""; + string path = _baseUri.AbsolutePath.TrimEnd('/') + urlpath; + var uri = new UriBuilder(_baseUri) { - Path = urlpath, + Path = path, Query = queryParams }.Uri; diff --git a/Satori/Client.cs b/Satori/Client.cs index b3fb2aa6..24229b40 100644 --- a/Satori/Client.cs +++ b/Satori/Client.cs @@ -330,14 +330,14 @@ public async Task UpdatePropertiesAsync(ISession session, Dictionary public async Task DeleteIdentityAsync(ISession session, CancellationToken? cancellationToken = default) { @@ -349,5 +349,41 @@ public async Task DeleteIdentityAsync(ISession session, CancellationToken? cance await _apiClient.SatoriDeleteIdentityAsync(session.AuthToken, cancellationToken); } + + /// + public async Task GetMessageListAsync(ISession session, int limit = 1, bool forward = true, string cursor = null, CancellationToken? cancellationToken = default) + { + if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && + session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) + { + await SessionRefreshAsync(session, cancellationToken); + } + + return await _apiClient.SatoriGetMessageListAsync(session.AuthToken, limit, forward, cursor, cancellationToken); + } + + /// + public async Task UpdateMessageAsync(ISession session, string id, string consumeTime, string readTime, CancellationToken? cancellationToken = default) + { + if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && + session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) + { + await SessionRefreshAsync(session, cancellationToken); + } + + await _apiClient.SatoriUpdateMessageAsync(session.AuthToken, id, new ApiUpdateMessageRequest{ConsumeTime = consumeTime, ReadTime = readTime}, cancellationToken); + } + + /// + public async Task DeleteMessageAsync(ISession session, string id, CancellationToken? cancellationToken = default) + { + if (AutoRefreshSession && !string.IsNullOrEmpty(session.RefreshToken) && + session.HasExpired(DateTime.UtcNow.Add(DefaultExpiredTimeSpan))) + { + await SessionRefreshAsync(session, cancellationToken); + } + + await _apiClient.SatoriDeleteMessageAsync(session.AuthToken, id, cancellationToken); + } } -} \ No newline at end of file +} diff --git a/Satori/IClient.cs b/Satori/IClient.cs index cdd9ac83..d12fa8bf 100644 --- a/Satori/IClient.cs +++ b/Satori/IClient.cs @@ -211,5 +211,36 @@ public Task UpdatePropertiesAsync(ISession session, Dictionary d /// The that can be used to cancel the request while mid-flight. /// A task object. public Task DeleteIdentityAsync(ISession session, CancellationToken? cancellationToken = default); + + /// + /// Get all the messages for an identity. + /// + /// The session of the user. + /// Max number of messages to return. Between 1 and 100. + /// True if listing should be older messages to newer, false if reverse. + /// A pagination cursor, if any. + /// The that can be used to cancel the request while mid-flight. + /// A task object which resolves to a list of messages. + public Task GetMessageListAsync(ISession session, int limit = 1, bool forward = true, string cursor = null, CancellationToken? cancellationToken = default); + + /// + /// Update the status of a message. + /// + /// The session of the user. + /// The message's unique identifier. + /// The time the message was consumed by the identity. + /// The time the message was read at the client. + /// The that can be used to cancel the request while mid-flight. + /// A task object. + public Task UpdateMessageAsync(ISession session, string id, string consumeTime, string readTime, CancellationToken? cancellationToken = default); + + /// + /// Delete a scheduled message. + /// + /// The session of the user. + /// The identifier of the message. + /// The that can be used to cancel the request while mid-flight. + /// A task object. + public Task DeleteMessageAsync(ISession session, string id, CancellationToken? cancellationToken = default); } }