2026-04-10 22:04:51 +08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2026-04-11 01:56:47 +08:00
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
2026-04-10 22:04:51 +08:00
|
|
|
using System.Threading.Tasks;
|
2026-04-11 01:56:47 +08:00
|
|
|
using UnityEngine.Networking;
|
2026-04-10 22:04:51 +08:00
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 玩家空间模块。
|
|
|
|
|
/// </summary>
|
2026-04-10 22:04:51 +08:00
|
|
|
public sealed class BriskSpaceModule
|
|
|
|
|
: BriskModuleBase
|
|
|
|
|
{
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按玩家 ID 获取空间数据。
|
|
|
|
|
/// </summary>
|
2026-04-10 22:04:51 +08:00
|
|
|
public async Task<BriskSpaceView> GetByPlayerIdAsync(string playerId)
|
|
|
|
|
{
|
|
|
|
|
ValidatePlayerId(playerId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var data = await context.HttpClient.GetDataAsync($"/spaces/{playerId}", null, true);
|
|
|
|
|
return BriskModelMapper.ToSpaceView(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按登录身份获取空间数据。
|
|
|
|
|
/// </summary>
|
2026-04-10 22:04:51 +08:00
|
|
|
public async Task<BriskSpaceView> GetByLoginIdentityAsync(string loginProvider, string loginUserId)
|
|
|
|
|
{
|
|
|
|
|
ValidateLoginIdentity(loginProvider, loginUserId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var data = await context.HttpClient.GetDataAsync("/spaces/by-login", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
|
|
|
|
return BriskModelMapper.ToSpaceView(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按玩家 ID 获取空间统计。
|
|
|
|
|
/// </summary>
|
2026-04-10 22:04:51 +08:00
|
|
|
public async Task<BriskSpaceStats> GetStatsByPlayerIdAsync(string playerId)
|
|
|
|
|
{
|
|
|
|
|
ValidatePlayerId(playerId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var data = await context.HttpClient.GetDataAsync($"/spaces/{playerId}/stats", null, true);
|
|
|
|
|
return BriskModelMapper.ToSpaceStats(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按登录身份获取空间统计。
|
|
|
|
|
/// </summary>
|
2026-04-10 22:04:51 +08:00
|
|
|
public async Task<BriskSpaceStats> GetStatsByLoginIdentityAsync(string loginProvider, string loginUserId)
|
|
|
|
|
{
|
|
|
|
|
ValidateLoginIdentity(loginProvider, loginUserId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var data = await context.HttpClient.GetDataAsync("/spaces/by-login/stats", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
|
|
|
|
return BriskModelMapper.ToSpaceStats(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 01:56:47 +08:00
|
|
|
/// <summary>
|
2026-04-21 16:02:06 +08:00
|
|
|
/// 按玩家 ID 获取点赞列表。
|
2026-04-11 01:56:47 +08:00
|
|
|
/// </summary>
|
2026-04-21 16:02:06 +08:00
|
|
|
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20, bool currentCycleOnly = false)
|
2026-04-11 01:56:47 +08:00
|
|
|
{
|
|
|
|
|
ValidatePlayerId(playerId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
2026-04-21 16:02:06 +08:00
|
|
|
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLikesQuery(limit, currentCycleOnly), true);
|
2026-04-11 01:56:47 +08:00
|
|
|
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-04-21 16:02:06 +08:00
|
|
|
/// 按登录身份获取点赞列表。
|
2026-04-11 01:56:47 +08:00
|
|
|
/// </summary>
|
2026-04-21 16:02:06 +08:00
|
|
|
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20, bool currentCycleOnly = false)
|
2026-04-11 01:56:47 +08:00
|
|
|
{
|
|
|
|
|
ValidateLoginIdentity(loginProvider, loginUserId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
|
|
|
|
|
query["limit"] = NormalizeLimit(limit);
|
2026-04-21 16:02:06 +08:00
|
|
|
if (currentCycleOnly)
|
|
|
|
|
{
|
|
|
|
|
query["scope"] = "cycle";
|
|
|
|
|
}
|
2026-04-11 01:56:47 +08:00
|
|
|
var data = await context.HttpClient.GetRawDataAsync("/spaces/by-login/likes", query, true);
|
|
|
|
|
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按玩家 ID 点赞空间。
|
|
|
|
|
/// </summary>
|
2026-04-11 01:56:47 +08:00
|
|
|
public async Task<BriskSpaceLikeResult> LikeByPlayerIdAsync(string playerId)
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
|
|
|
|
ValidatePlayerId(playerId);
|
2026-04-11 01:56:47 +08:00
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
2026-04-11 01:56:47 +08:00
|
|
|
var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
|
|
|
|
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
2026-04-10 22:04:51 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按玩家 ID 取消点赞空间。
|
|
|
|
|
/// </summary>
|
2026-04-11 01:56:47 +08:00
|
|
|
public async Task<BriskSpaceLikeResult> UnlikeByPlayerIdAsync(string playerId)
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
|
|
|
|
ValidatePlayerId(playerId);
|
2026-04-11 01:56:47 +08:00
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
2026-04-11 01:56:47 +08:00
|
|
|
var data = await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
|
|
|
|
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
2026-04-10 22:04:51 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按登录身份点赞空间。
|
|
|
|
|
/// </summary>
|
2026-04-11 01:56:47 +08:00
|
|
|
public async Task<BriskSpaceLikeResult> LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
|
|
|
|
ValidateLoginIdentity(loginProvider, loginUserId);
|
2026-04-11 01:56:47 +08:00
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
2026-04-11 01:56:47 +08:00
|
|
|
var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
|
|
|
|
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
2026-04-10 22:04:51 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 按登录身份取消点赞空间。
|
|
|
|
|
/// </summary>
|
2026-04-11 01:56:47 +08:00
|
|
|
public async Task<BriskSpaceLikeResult> UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
|
|
|
|
ValidateLoginIdentity(loginProvider, loginUserId);
|
2026-04-11 01:56:47 +08:00
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var data = await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
|
|
|
|
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 上传当前玩家自己的空间内容。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BriskSpaceContentUpdateResult> UploadMyContentAsync(long? baseVersion, string contentType, string checksum, byte[] bytes)
|
|
|
|
|
{
|
|
|
|
|
RequireNotNull(bytes, nameof(bytes));
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
2026-04-11 01:56:47 +08:00
|
|
|
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
|
|
|
|
var sections = new List<IMultipartFormSection>
|
|
|
|
|
{
|
|
|
|
|
new MultipartFormDataSection("base_version", (baseVersion ?? 0L).ToString()),
|
|
|
|
|
new MultipartFormFileSection("file", bytes, "space-content.bin", string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(contentType))
|
|
|
|
|
{
|
|
|
|
|
sections.Insert(1, new MultipartFormDataSection("content_type", contentType));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(finalChecksum))
|
|
|
|
|
{
|
|
|
|
|
sections.Insert(string.IsNullOrWhiteSpace(contentType) ? 1 : 2, new MultipartFormDataSection("checksum", finalChecksum));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var data = await context.HttpClient.PutMultipartAsync("/spaces/me/content", sections, true);
|
|
|
|
|
return BriskModelMapper.ToSpaceContentUpdateResult(data);
|
2026-04-10 22:04:51 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 更新当前玩家自己的空间内容。
|
|
|
|
|
/// </summary>
|
2026-04-11 01:56:47 +08:00
|
|
|
public Task<BriskSpaceContentUpdateResult> UpdateMyAsync(object payload, long? baseVersion = null, string contentType = null, string checksum = null)
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
|
|
|
|
RequireNotNull(payload, nameof(payload));
|
|
|
|
|
|
2026-04-11 01:56:47 +08:00
|
|
|
if (payload is byte[] bytes)
|
|
|
|
|
{
|
|
|
|
|
return UploadMyContentAsync(baseVersion, contentType ?? "application/octet-stream", checksum, bytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (payload is string text)
|
|
|
|
|
{
|
|
|
|
|
return UploadMyContentAsync(baseVersion, contentType ?? "text/plain", checksum, Encoding.UTF8.GetBytes(text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var json = BriskJson.Serialize(payload);
|
|
|
|
|
return UploadMyContentAsync(baseVersion, contentType ?? "application/json", checksum, Encoding.UTF8.GetBytes(json));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 按玩家 ID 下载空间内容。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BriskSpaceContentDownloadResult> DownloadContentByPlayerIdAsync(string playerId)
|
|
|
|
|
{
|
|
|
|
|
ValidatePlayerId(playerId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
|
|
|
|
var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true);
|
|
|
|
|
return CreateDownloadResult(response);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 按登录身份下载空间内容。
|
|
|
|
|
/// </summary>
|
|
|
|
|
public async Task<BriskSpaceContentDownloadResult> DownloadContentByLoginIdentityAsync(string loginProvider, string loginUserId)
|
|
|
|
|
{
|
|
|
|
|
ValidateLoginIdentity(loginProvider, loginUserId);
|
|
|
|
|
|
|
|
|
|
return await ExecuteAsync(async context =>
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
2026-04-11 01:56:47 +08:00
|
|
|
var response = await context.HttpClient.GetBytesAsync("/spaces/by-login/content", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
|
|
|
|
return CreateDownloadResult(response);
|
2026-04-10 22:04:51 +08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:38:28 +08:00
|
|
|
/// <summary>
|
|
|
|
|
/// 获取我的访客列表。
|
|
|
|
|
/// </summary>
|
2026-04-21 17:07:45 +08:00
|
|
|
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 50)
|
2026-04-10 22:04:51 +08:00
|
|
|
{
|
|
|
|
|
return await ExecuteAsync(async context =>
|
|
|
|
|
{
|
2026-04-11 01:56:47 +08:00
|
|
|
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", CreateLimitQuery(limit), true);
|
2026-04-10 22:04:51 +08:00
|
|
|
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Dictionary<string, string> CreateLoginIdentityQuery(string loginProvider, string loginUserId)
|
|
|
|
|
{
|
|
|
|
|
return new Dictionary<string, string>
|
|
|
|
|
{
|
|
|
|
|
{ "login_provider", loginProvider },
|
|
|
|
|
{ "login_user_id", loginUserId }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 01:56:47 +08:00
|
|
|
private static Dictionary<string, string> CreateLimitQuery(int limit)
|
|
|
|
|
{
|
|
|
|
|
return new Dictionary<string, string>
|
|
|
|
|
{
|
|
|
|
|
{ "limit", NormalizeLimit(limit) }
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 16:02:06 +08:00
|
|
|
private static Dictionary<string, string> CreateLikesQuery(int limit, bool currentCycleOnly)
|
|
|
|
|
{
|
|
|
|
|
var query = CreateLimitQuery(limit);
|
|
|
|
|
if (currentCycleOnly)
|
|
|
|
|
{
|
|
|
|
|
query["scope"] = "cycle";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return query;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 22:04:51 +08:00
|
|
|
private static void ValidatePlayerId(string playerId)
|
|
|
|
|
{
|
|
|
|
|
RequireNotEmpty(playerId, nameof(playerId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ValidateLoginIdentity(string loginProvider, string loginUserId)
|
|
|
|
|
{
|
|
|
|
|
RequireNotEmpty(loginProvider, nameof(loginProvider));
|
|
|
|
|
RequireNotEmpty(loginUserId, nameof(loginUserId));
|
|
|
|
|
}
|
2026-04-11 01:56:47 +08:00
|
|
|
|
|
|
|
|
private static string NormalizeLimit(int limit)
|
|
|
|
|
{
|
|
|
|
|
return (limit > 0 ? limit : 20).ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static BriskSpaceContentDownloadResult CreateDownloadResult(BriskBinaryResponse response)
|
|
|
|
|
{
|
|
|
|
|
return new BriskSpaceContentDownloadResult
|
|
|
|
|
{
|
|
|
|
|
Bytes = response.Bytes,
|
|
|
|
|
Version = ReadHeaderLong(response.Headers, "X-Space-Version"),
|
|
|
|
|
Checksum = ReadHeader(response.Headers, "X-Space-Checksum"),
|
|
|
|
|
ContentType = ReadHeader(response.Headers, "Content-Type")
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ComputeSha256(byte[] bytes)
|
|
|
|
|
{
|
|
|
|
|
using (var sha = SHA256.Create())
|
|
|
|
|
{
|
|
|
|
|
var hash = sha.ComputeHash(bytes);
|
|
|
|
|
var builder = new StringBuilder(hash.Length * 2);
|
|
|
|
|
for (var i = 0; i < hash.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
builder.Append(hash[i].ToString("x2"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return builder.ToString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string NormalizeChecksum(string checksum)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(checksum))
|
|
|
|
|
{
|
|
|
|
|
return checksum;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var value = checksum.Trim();
|
|
|
|
|
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
|
|
|
|
? value.Substring("sha256:".Length)
|
|
|
|
|
: value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static long ReadHeaderLong(Dictionary<string, string> headers, string key)
|
|
|
|
|
{
|
|
|
|
|
var value = ReadHeader(headers, key);
|
|
|
|
|
return long.TryParse(value, out var result) ? result : 0L;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ReadHeader(Dictionary<string, string> headers, string key)
|
|
|
|
|
{
|
|
|
|
|
if (headers == null || string.IsNullOrWhiteSpace(key))
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var pair in headers)
|
|
|
|
|
{
|
|
|
|
|
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return pair.Value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-04-10 22:04:51 +08:00
|
|
|
}
|