Add WinUI and core source
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:27:13 +08:00
parent f59190251d
commit 7ecc6a8923
262 changed files with 137492 additions and 0 deletions
@@ -0,0 +1,172 @@
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace YMhut.Box.Core.Feedback;
public sealed class FeedbackSubmissionService(HttpClient? httpClient = null) : IFeedbackSubmissionService
{
public const string Endpoint = "https://mail-smtp.ymhut.cn/";
public const string ClientSignatureKey = "ymhut-box-feedback-client-v1";
private readonly HttpClient _httpClient = httpClient ?? new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
public async Task<FeedbackSubmissionResponse> SubmitAsync(
FeedbackRequest request,
FeedbackPackageResult package,
CancellationToken cancellationToken = default)
{
try
{
if (!File.Exists(package.PackagePath))
{
return new FeedbackSubmissionResponse(false, null, "PACKAGE_MISSING", "本地反馈包不存在。");
}
var encrypted = await FeedbackPackageCrypto.EncryptPackageAsync(package.PackagePath, cancellationToken: cancellationToken)
.ConfigureAwait(false);
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(global::System.Globalization.CultureInfo.InvariantCulture);
var nonce = Guid.NewGuid().ToString("N");
var feedbackCode = FeedbackCode.Normalize(request.FeedbackCode);
var payload = JsonSerializer.Serialize(new
{
feedbackCode,
title = request.Title,
type = request.Type,
severity = request.Severity,
contact = request.Contact,
bodyLength = request.Body?.Length ?? 0,
packageEncrypted = true,
encryption = FeedbackPackageCrypto.MagicText,
packageBytes = encrypted.PackageBytes,
packageSha256 = encrypted.PackageSha256,
plainPackageBytes = package.PackageBytes,
plainPackageSha256 = package.PackageSha256,
createdAt = package.CreatedAt
}, new JsonSerializerOptions(JsonSerializerDefaults.Web));
var signature = Sign(timestamp, nonce, encrypted.PackageSha256, payload);
using var form = new MultipartFormDataContent();
form.Add(new StringContent(payload, Encoding.UTF8, "application/json"), "payload");
form.Add(new StringContent(timestamp, Encoding.UTF8), "timestamp");
form.Add(new StringContent(nonce, Encoding.UTF8), "nonce");
form.Add(new StringContent(encrypted.PackageSha256, Encoding.UTF8), "packageSha256");
form.Add(new StringContent(signature, Encoding.UTF8), "signature");
await using var stream = File.OpenRead(encrypted.PackagePath);
using var fileContent = new StreamContent(stream);
fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
form.Add(fileContent, "package", Path.GetFileName(encrypted.PackagePath));
using var response = await _httpClient.PostAsync(Endpoint, form, cancellationToken).ConfigureAwait(false);
var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var parsed = ParseResponse(text);
if (response.IsSuccessStatusCode)
{
return parsed;
}
return parsed with
{
Ok = false,
ErrorCode = parsed.ErrorCode ?? $"HTTP_{(int)response.StatusCode}",
Message = parsed.Message ?? response.ReasonPhrase
};
}
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or OperationCanceledException)
{
return new FeedbackSubmissionResponse(false, null, exception is TaskCanceledException ? "TIMEOUT" : "NETWORK_ERROR", exception.Message);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidDataException or CryptographicException)
{
return new FeedbackSubmissionResponse(false, null, "PACKAGE_ENCRYPT_FAILED", exception.Message);
}
}
public async Task<FeedbackStatusResponse> GetStatusAsync(
string feedbackCode,
CancellationToken cancellationToken = default)
{
var normalized = FeedbackCode.Normalize(feedbackCode);
if (!FeedbackCode.IsValid(normalized))
{
return new FeedbackStatusResponse(false, normalized, null, null, false, null, null, null, "INVALID_CODE", "Invalid feedback code.");
}
try
{
var uri = $"{Endpoint}?api=status&code={Uri.EscapeDataString(normalized)}";
using var response = await _httpClient.GetAsync(uri, cancellationToken).ConfigureAwait(false);
var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var parsed = ParseStatusResponse(text);
if (response.IsSuccessStatusCode)
{
return parsed;
}
return parsed with
{
Ok = false,
ErrorCode = parsed.ErrorCode ?? $"HTTP_{(int)response.StatusCode}",
Message = parsed.Message ?? response.ReasonPhrase
};
}
catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or OperationCanceledException)
{
return new FeedbackStatusResponse(false, normalized, null, null, false, null, null, null, exception is TaskCanceledException ? "TIMEOUT" : "NETWORK_ERROR", exception.Message);
}
}
public static string Sign(string timestamp, string nonce, string packageSha256, string payload)
{
var material = $"{timestamp}\n{nonce}\n{packageSha256}\n{payload}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ClientSignatureKey));
return Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(material))).ToLowerInvariant();
}
private static FeedbackSubmissionResponse ParseResponse(string text)
{
try
{
using var document = JsonDocument.Parse(text);
var root = document.RootElement;
var ok = root.TryGetProperty("ok", out var okElement) && okElement.ValueKind == JsonValueKind.True;
var code = root.TryGetProperty("code", out var codeElement) ? codeElement.GetString() : null;
var error = root.TryGetProperty("error", out var errorElement) ? errorElement.GetString() : null;
var message = root.TryGetProperty("message", out var messageElement) ? messageElement.GetString() : null;
var duplicate = root.TryGetProperty("duplicate", out var duplicateElement) && duplicateElement.ValueKind == JsonValueKind.True;
return new FeedbackSubmissionResponse(ok, code, error, message, duplicate);
}
catch (JsonException)
{
return new FeedbackSubmissionResponse(false, null, "INVALID_RESPONSE", text);
}
}
private static FeedbackStatusResponse ParseStatusResponse(string text)
{
try
{
using var document = JsonDocument.Parse(text);
var root = document.RootElement;
var ok = root.TryGetProperty("ok", out var okElement) && okElement.ValueKind == JsonValueKind.True;
var code = root.TryGetProperty("code", out var codeElement) ? codeElement.GetString() : null;
var status = root.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : null;
var statusLabel = root.TryGetProperty("statusLabel", out var statusLabelElement) ? statusLabelElement.GetString() : null;
var statusDetail = root.TryGetProperty("statusDetail", out var statusDetailElement) ? statusDetailElement.GetString() : null;
var category = root.TryGetProperty("category", out var categoryElement) ? categoryElement.GetString() : null;
var priority = root.TryGetProperty("priority", out var priorityElement) ? priorityElement.GetString() : null;
var hasReply = root.TryGetProperty("hasReply", out var hasReplyElement) && hasReplyElement.ValueKind == JsonValueKind.True;
var reply = root.TryGetProperty("reply", out var replyElement) ? replyElement.GetString() : null;
var receivedAt = root.TryGetProperty("receivedAt", out var receivedAtElement) ? receivedAtElement.GetString() : null;
var updatedAt = root.TryGetProperty("updatedAt", out var updatedAtElement) ? updatedAtElement.GetString() : null;
var mailSent = root.TryGetProperty("mailSent", out var mailSentElement) && mailSentElement.ValueKind == JsonValueKind.True;
var error = root.TryGetProperty("error", out var errorElement) ? errorElement.GetString() : null;
var message = root.TryGetProperty("message", out var messageElement) ? messageElement.GetString() : null;
return new FeedbackStatusResponse(ok, code, status, statusLabel, hasReply, reply, receivedAt, updatedAt, error, message, statusDetail, category, priority, mailSent);
}
catch (JsonException)
{
return new FeedbackStatusResponse(false, null, null, null, false, null, null, null, "INVALID_RESPONSE", text);
}
}
}