diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs index 8e5a4175..764c5dc7 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/LDValueObject.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Extensions; using Newtonsoft.Json; @@ -6,10 +7,113 @@ using J = Newtonsoft.Json.JsonPropertyAttribute; namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; +public class LDLocalizedString +{ + /// + /// key: BCP 47 language code, with empty string meaning "unknown" + /// value: content, in that language + /// + public Dictionary Values { get; set; } + + public LDLocalizedString() + { + Values = []; + } + + public LDLocalizedString(string? language, string? value) + { + Values = []; + + // this is required to create a non-Map field for non-JsonLD remotes. + Values.Add("", value); + + if (language != null) + { + language = NormalizeLanguageCode(language); + + if (language != null && language != "") + Values.Add(language, value); + } + } + + /// + /// If the remote sends different content to multiple languages, try and guess which one they prefer "by default". + /// + /// This idea was taken from Sharkey's implementation. + /// https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/401#note_1174 + /// + /// The guessed language + /// The value, in the guessed language + public string? GuessPreferredValue(out string? language) + { + if (Values.Count > 1) + { + var unknownValue = GetUnknownValue(); + if (unknownValue != null) + { + var preferred = Values.FirstOrDefault(i => !IsUnknownLanguage(i.Key) && i.Value == unknownValue); + if (preferred.Value != null) + { + language = NormalizeLanguageCode(preferred.Key); + return preferred.Value; + } + } + } + else if (Values.Count == 0) + { + language = null; + return null; + } + + var first = Values.FirstOrDefault(i => !IsUnknownLanguage(i.Key)); + if (first.Value == null) + { + first = Values.FirstOrDefault(); + if (first.Value == null) + { + language = null; + return null; + } + } + + language = IsUnknownLanguage(first.Key) ? null : NormalizeLanguageCode(first.Key); + return first.Value; + } + + public static string? NormalizeLanguageCode(string lang) + { + try + { + return CultureInfo.GetCultureInfo(lang).ToString(); + } + catch (CultureNotFoundException) + { + // invalid language code + return null; + } + } + + // Akkoma forces all non-localized text to be in the "und" language by adding { "@language":"und" } to it's context + public static bool IsUnknownLanguage(string? lang) => lang == null || lang == "" || lang == "und"; + public string? GetUnknownValue() + { + string? value; + + if (Values.TryGetValue("", out value)) + return value; + + if (Values.TryGetValue("und", out value)) + return value; + + return null; + } +} + public class LDValueObject { - [J("@type")] public string? Type { get; set; } - [J("@value")] public required T Value { get; set; } + [J("@type")] public string? Type { get; set; } + [J("@value")] public required T Value { get; set; } + [J("@language")] public string? Language { get; set; } } public class ValueObjectConverter : JsonConverter @@ -157,4 +261,53 @@ public sealed class XsdString(string? str) public static implicit operator string?(XsdString? s) => s?.Str; public override string? ToString() => str; +} + +public class LocalizedValueObjectConverter : JsonConverter +{ + public override LDLocalizedString? ReadJson(JsonReader reader, Type objectType, LDLocalizedString? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var obj = JArray.Load(reader); + var list = obj.ToObject>>(); + if (list == null || list.Count == 0) + return null; + + var localized = new LDLocalizedString(); + + foreach (var item in list) + { + localized.Values.Add(item.Language ?? "", item.Value); + } + + return localized; + } + + public override void WriteJson(JsonWriter writer, LDLocalizedString? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + writer.WriteStartArray(); + + foreach (var item in value.Values) + { + writer.WriteStartObject(); + + if (!LDLocalizedString.IsUnknownLanguage(item.Key)) + { + writer.WritePropertyName("@language"); + writer.WriteValue(item.Key); + } + + writer.WritePropertyName("@value"); + writer.WriteValue(item.Value); + + writer.WriteEndObject(); + } + + writer.WriteEndArray(); + } } \ No newline at end of file