mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2026-02-09 18:26:12 +00:00
[ShareBubbles] Fixes for 2025r180
This commit is contained in:
parent
13e206cd58
commit
a0a859aa86
14 changed files with 60 additions and 1953 deletions
|
|
@ -1,66 +0,0 @@
|
|||
using System.Net;
|
||||
|
||||
namespace NAK.ShareBubbles.API.Exceptions;
|
||||
|
||||
public class ShareApiException : Exception
|
||||
{
|
||||
public HttpStatusCode StatusCode { get; } // TODO: network back status code to claiming client, to show why the request failed
|
||||
public string UserFriendlyMessage { get; }
|
||||
|
||||
public ShareApiException(HttpStatusCode statusCode, string message, string userFriendlyMessage)
|
||||
: base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
UserFriendlyMessage = userFriendlyMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentNotSharedException : ShareApiException
|
||||
{
|
||||
public ContentNotSharedException(string contentId)
|
||||
: base(HttpStatusCode.BadRequest,
|
||||
$"Content {contentId} is not currently shared",
|
||||
"This content is not currently shared with anyone")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentNotFoundException : ShareApiException
|
||||
{
|
||||
public ContentNotFoundException(string contentId)
|
||||
: base(HttpStatusCode.NotFound,
|
||||
$"Content {contentId} not found",
|
||||
"The specified content could not be found")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class UserOnlyAllowsSharesFromFriendsException : ShareApiException
|
||||
{
|
||||
public UserOnlyAllowsSharesFromFriendsException(string userId)
|
||||
: base(HttpStatusCode.Forbidden,
|
||||
$"User {userId} only accepts shares from friends",
|
||||
"This user only accepts shares from friends")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class UserNotFoundException : ShareApiException
|
||||
{
|
||||
public UserNotFoundException(string userId)
|
||||
: base(HttpStatusCode.NotFound,
|
||||
$"User {userId} not found",
|
||||
"The specified user could not be found")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentAlreadySharedException : ShareApiException
|
||||
{
|
||||
public ContentAlreadySharedException(string contentId, string userId)
|
||||
: base(HttpStatusCode.Conflict,
|
||||
$"Content {contentId} is already shared with user {userId}",
|
||||
"This content is already shared with this user")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
using ABI_RC.Core.Networking.API;
|
||||
using ABI_RC.Core.Networking.API.Responses;
|
||||
using NAK.ShareBubbles.API.Responses;
|
||||
|
||||
namespace NAK.ShareBubbles.API;
|
||||
|
||||
|
|
@ -34,6 +33,8 @@ public static class PedestalInfoBatchProcessor
|
|||
{ PedestalType.Prop, false }
|
||||
};
|
||||
|
||||
// This breaks compile accepting this change.
|
||||
// ReSharper disable once ChangeFieldTypeToSystemThreadingLock
|
||||
private static readonly object _lock = new();
|
||||
private const float BATCH_DELAY = 2f;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace NAK.ShareBubbles.API.Responses;
|
||||
|
||||
public class ActiveSharesResponse
|
||||
{
|
||||
[JsonProperty("value")]
|
||||
public List<ShareUser> Value { get; set; }
|
||||
}
|
||||
|
||||
// Idk why not just reuse UserDetails
|
||||
public class ShareUser
|
||||
{
|
||||
[JsonProperty("image")]
|
||||
public string Image { get; set; }
|
||||
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using ABI_RC.Core.Networking;
|
||||
using ABI_RC.Core.Networking.API;
|
||||
using ABI_RC.Core.Networking.API.Responses;
|
||||
using ABI_RC.Core.Savior;
|
||||
using NAK.ShareBubbles.API.Exceptions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace NAK.ShareBubbles.API;
|
||||
|
||||
/// <summary>
|
||||
/// API for content sharing management.
|
||||
/// </summary>
|
||||
public static class ShareApiHelper
|
||||
{
|
||||
#region Enums
|
||||
|
||||
public enum ShareContentType
|
||||
{
|
||||
Avatar,
|
||||
Spawnable
|
||||
}
|
||||
|
||||
private enum ShareApiOperation
|
||||
{
|
||||
ShareAvatar,
|
||||
ReleaseAvatar,
|
||||
|
||||
ShareSpawnable,
|
||||
ReleaseSpawnable,
|
||||
|
||||
GetAvatarShares,
|
||||
GetSpawnableShares
|
||||
}
|
||||
|
||||
#endregion Enums
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Shares content with a specified user.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of content to share</param>
|
||||
/// <param name="contentId">ID of the content</param>
|
||||
/// <param name="userId">Target user ID</param>
|
||||
/// <returns>Response containing share information</returns>
|
||||
/// <exception cref="ShareApiException">Thrown when API request fails</exception>
|
||||
/// <exception cref="UserNotFoundException">Thrown when target user is not found</exception>
|
||||
/// <exception cref="ContentNotFoundException">Thrown when content is not found</exception>
|
||||
/// <exception cref="UserOnlyAllowsSharesFromFriendsException">Thrown when user only accepts shares from friends</exception>
|
||||
/// <exception cref="ContentAlreadySharedException">Thrown when content is already shared with user</exception>
|
||||
public static Task<BaseResponse<T>> ShareContentAsync<T>(ShareContentType type, string contentId, string userId)
|
||||
{
|
||||
ShareApiOperation operation = type == ShareContentType.Avatar
|
||||
? ShareApiOperation.ShareAvatar
|
||||
: ShareApiOperation.ShareSpawnable;
|
||||
|
||||
ShareRequest data = new()
|
||||
{
|
||||
ContentId = contentId,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
return MakeApiRequestAsync<T>(operation, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases shared content from a specified user.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of content to release</param>
|
||||
/// <param name="contentId">ID of the content</param>
|
||||
/// <param name="userId">Optional user ID. If null, releases share from self</param>
|
||||
/// <returns>Response indicating success</returns>
|
||||
/// <exception cref="ShareApiException">Thrown when API request fails</exception>
|
||||
/// <exception cref="ContentNotSharedException">Thrown when content is not shared</exception>
|
||||
/// <exception cref="ContentNotFoundException">Thrown when content is not found</exception>
|
||||
/// <exception cref="UserNotFoundException">Thrown when specified user is not found</exception>
|
||||
public static Task<BaseResponse<T>> ReleaseShareAsync<T>(ShareContentType type, string contentId, string userId = null)
|
||||
{
|
||||
ShareApiOperation operation = type == ShareContentType.Avatar
|
||||
? ShareApiOperation.ReleaseAvatar
|
||||
: ShareApiOperation.ReleaseSpawnable;
|
||||
|
||||
// If no user ID is provided, release share from self
|
||||
userId ??= MetaPort.Instance.ownerId;
|
||||
|
||||
ShareRequest data = new()
|
||||
{
|
||||
ContentId = contentId,
|
||||
UserId = userId
|
||||
};
|
||||
|
||||
return MakeApiRequestAsync<T>(operation, data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all shares for specified content.
|
||||
/// </summary>
|
||||
/// <param name="type">Type of content</param>
|
||||
/// <param name="contentId">ID of the content</param>
|
||||
/// <returns>Response containing share information</returns>
|
||||
/// <exception cref="ShareApiException">Thrown when API request fails</exception>
|
||||
/// <exception cref="ContentNotFoundException">Thrown when content is not found</exception>
|
||||
public static Task<BaseResponse<T>> GetSharesAsync<T>(ShareContentType type, string contentId)
|
||||
{
|
||||
ShareApiOperation operation = type == ShareContentType.Avatar
|
||||
? ShareApiOperation.GetAvatarShares
|
||||
: ShareApiOperation.GetSpawnableShares;
|
||||
|
||||
ShareRequest data = new() { ContentId = contentId };
|
||||
return MakeApiRequestAsync<T>(operation, data);
|
||||
}
|
||||
|
||||
#endregion Public API
|
||||
|
||||
#region Private Implementation
|
||||
|
||||
[Serializable]
|
||||
private record ShareRequest
|
||||
{
|
||||
public string ContentId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
|
||||
private static async Task<BaseResponse<T>> MakeApiRequestAsync<T>(ShareApiOperation operation, ShareRequest data)
|
||||
{
|
||||
ValidateAuthenticationState();
|
||||
|
||||
(string endpoint, HttpMethod method) = GetApiEndpointAndMethod(operation, data);
|
||||
using HttpRequestMessage request = CreateHttpRequest(endpoint, method, data);
|
||||
|
||||
try
|
||||
{
|
||||
using HttpResponseMessage response = await ApiConnection._client.SendAsync(request);
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
|
||||
return HandleApiResponse<T>(response, content, data.ContentId, data.UserId);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
throw new ShareApiException(
|
||||
HttpStatusCode.ServiceUnavailable,
|
||||
$"Failed to communicate with the server: {ex.Message}",
|
||||
"Unable to connect to the server. Please check your internet connection.");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new ShareApiException(
|
||||
HttpStatusCode.UnprocessableEntity,
|
||||
$"Failed to process response data: {ex.Message}",
|
||||
"Server returned invalid data. Please try again later.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAuthenticationState()
|
||||
{
|
||||
if (!AuthManager.IsAuthenticated)
|
||||
{
|
||||
throw new ShareApiException(
|
||||
HttpStatusCode.Unauthorized,
|
||||
"User is not authenticated",
|
||||
"Please log in to perform this action");
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateHttpRequest(string endpoint, HttpMethod method, ShareRequest data)
|
||||
{
|
||||
HttpRequestMessage request = new(method, endpoint);
|
||||
|
||||
if (method == HttpMethod.Post)
|
||||
{
|
||||
JObject json = JObject.FromObject(data);
|
||||
request.Content = new StringContent(json.ToString(), Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static BaseResponse<T> HandleApiResponse<T>(HttpResponseMessage response, string content, string contentId, string userId)
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
return CreateSuccessResponse<T>(content);
|
||||
|
||||
// Let specific exceptions propagate up to the caller
|
||||
throw response.StatusCode switch
|
||||
{
|
||||
HttpStatusCode.BadRequest => new ContentNotSharedException(contentId),
|
||||
HttpStatusCode.NotFound when userId != null => new UserNotFoundException(userId),
|
||||
HttpStatusCode.NotFound => new ContentNotFoundException(contentId),
|
||||
HttpStatusCode.Forbidden => new UserOnlyAllowsSharesFromFriendsException(userId),
|
||||
HttpStatusCode.Conflict => new ContentAlreadySharedException(contentId, userId),
|
||||
_ => new ShareApiException(
|
||||
response.StatusCode,
|
||||
$"API request failed with status {response.StatusCode}: {content}",
|
||||
"An unexpected error occurred. Please try again later.")
|
||||
};
|
||||
}
|
||||
|
||||
private static BaseResponse<T> CreateSuccessResponse<T>(string content)
|
||||
{
|
||||
var response = new BaseResponse<T>("")
|
||||
{
|
||||
IsSuccessStatusCode = true,
|
||||
HttpStatusCode = HttpStatusCode.OK
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
response.Data = JsonConvert.DeserializeObject<T>(content);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static (string endpoint, HttpMethod method) GetApiEndpointAndMethod(ShareApiOperation operation, ShareRequest data)
|
||||
{
|
||||
string baseUrl = $"{ApiConnection.APIAddress}/{ApiConnection.APIVersion}";
|
||||
string encodedContentId = HttpUtility.UrlEncode(data.ContentId);
|
||||
|
||||
return operation switch
|
||||
{
|
||||
ShareApiOperation.GetAvatarShares => ($"{baseUrl}/avatars/{encodedContentId}/shares", HttpMethod.Get),
|
||||
ShareApiOperation.ShareAvatar => ($"{baseUrl}/avatars/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Post),
|
||||
ShareApiOperation.ReleaseAvatar => ($"{baseUrl}/avatars/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Delete),
|
||||
ShareApiOperation.GetSpawnableShares => ($"{baseUrl}/spawnables/{encodedContentId}/shares", HttpMethod.Get),
|
||||
ShareApiOperation.ShareSpawnable => ($"{baseUrl}/spawnables/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Post),
|
||||
ShareApiOperation.ReleaseSpawnable => ($"{baseUrl}/spawnables/{encodedContentId}/shares/{HttpUtility.UrlEncode(data.UserId)}", HttpMethod.Delete),
|
||||
_ => throw new ArgumentException($"Unknown operation: {operation}")
|
||||
};
|
||||
}
|
||||
|
||||
#endregion Private Implementation
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue