using System.Net; using System.Net.Http.Json; using System.Net.Mail; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; using decePubClient.Models; using decePubClient.Services; using PrivaPub.ClientModels; namespace decePubClient.Helpers { public static class SUtility { public const string dateTimeFormat = "dd/MM/yyyy"; public const string timeFormat = "HH:mm"; public const string dateTimeFormatExt = "dd/MM/yyyy HH:mm"; /// Marks a response HttpService made up itself (a classified failure), not one the server sent. public const string FailureHeader = "X-DecePub-Failure"; /// /// The JSON options for /clientapi payloads and localStorage: web defaults, enums by name as PrivaPub writes them /// (numbers still read), ClientJsonContext first, reflection after. /// public static JsonSerializerOptions DefaultSerializer { get; } = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverter() }, TypeInfoResolver = JsonTypeInfoResolver.Combine(ClientJsonContext.Default, new DefaultJsonTypeInfoResolver()) }; /// The JSON options for PrivaPub's Mastodon API and /oauth: snake_case, MastodonJsonContext first. public static JsonSerializerOptions MastodonSerializer { get; } = new(JsonSerializerDefaults.Web) { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, IgnoreReadOnlyProperties = true, TypeInfoResolver = JsonTypeInfoResolver.Combine(MastodonJsonContext.Default, new DefaultJsonTypeInfoResolver()) }; /// What a cancelled request turns into: HTTP 410, which AddResponseError ignores. public static WebResult DefaultTaskCancelledResult => new WebResult().Invalidate("Request cancelled, not an error.", (int)HttpStatusCode.Gone, FailureCodes.Cancelled); public static HttpResponseMessage DefaultTaskCancelled() => ClassifiedFailure(FailureCodes.Cancelled, DefaultTaskCancelledResult.ErrorMessage, HttpStatusCode.Gone); public static HttpResponseMessage DefaultUnauthorized(string message) => ClassifiedFailure(FailureCodes.Unauthorized, message, HttpStatusCode.Unauthorized); /// A response carrying an invalid WebResult, tagged with FailureHeader so ReadWebResult reads it as is. public static HttpResponseMessage ClassifiedFailure(string failureCode, string message, HttpStatusCode statusCode) { var content = JsonContent.Create(new WebResult().Invalidate(message, (int)statusCode, failureCode), options: DefaultSerializer); content.Headers.Add(FailureHeader, failureCode); return new(statusCode) { Content = content }; } /// The value a cancelled read returns: default for value types, an empty list for list interfaces. public static T CancellationDefault() { var type = typeof(T); if (type.IsValueType) return default; if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() is var definition && (definition == typeof(IEnumerable<>) || definition == typeof(ICollection<>) || definition == typeof(IList<>) || definition == typeof(IReadOnlyCollection<>) || definition == typeof(IReadOnlyList<>))) return (T)Activator.CreateInstance(typeof(List<>).MakeGenericType(type.GetGenericArguments()[0])); if (type.IsInterface || type.IsAbstract || type.GetConstructor(Type.EmptyTypes) is null) return default; return Activator.CreateInstance(); } #region Toasts /// Shows a failed result: connectivity failures as warnings, the rest as errors; cancellations not at all. public static void AddResponseError(this ToastService toast, WebResult result) { if (result.StatusCode is (int)HttpStatusCode.Gone) return; if (FailureCodes.IsConnectivity(result.ErrorCode)) { toast.Add(ToastSeverity.Warning, result.ErrorMessage); return; } toast.Add(ToastSeverity.Error, $"{result.StatusCode} - {result.ErrorMessage}"); } public static void AddResponseError(this ToastService toast, string message) => toast.Add(ToastSeverity.Error, message); public static void AddResponseSuccess(this ToastService toast, string message) => toast.Add(ToastSeverity.Success, message, TimeSpan.FromSeconds(5)); public static void AddWarning(this ToastService toast, string message) => toast.Add(ToastSeverity.Warning, message); public static void AddException(this ToastService toast, Exception exception) => toast.Add(ToastSeverity.Error, exception.Message); #endregion public static T IfTrueThen(bool conditionIsTrue, T ifTrue, T ifFalse = default) => conditionIsTrue ? ifTrue : ifFalse; public static bool TimeBetween(this DateTime datetime, TimeSpan start, TimeSpan end) { // convert datetime to a TimeSpan var now = datetime.TimeOfDay; // see if start comes before end if (start < end) return start <= now && now <= end; // start is after end, so do the inverse comparison return !(end < now && now < start); } public static string GetFileIcon(string fileName) { switch (Path.GetExtension(fileName)) { case ".odp": case ".pptx": return "file-powerpoint"; case ".ods": case ".xlsx": return "file-excel"; case ".odt": case ".docx": return "file-word"; case ".pdf": return "file-pdf"; case ".jpg": case ".jpeg": case ".png": return "file-image"; default: return "file"; } } public static bool IsValidEmail(string email) { try { var addr = new MailAddress(email); return addr.Address == email; } catch { return false; } } } }