151 lines
No EOL
5.3 KiB
C#
151 lines
No EOL
5.3 KiB
C#
using System.Collections.Concurrent;
|
|
using Iceshrimp.Backend.Core.Configuration;
|
|
using Iceshrimp.Backend.Core.Database.Tables;
|
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
|
using Iceshrimp.Backend.Core.Federation.Cryptography;
|
|
using Iceshrimp.Backend.Core.Helpers;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using VDS.RDF.JsonLd;
|
|
using VDS.RDF.JsonLd.Syntax;
|
|
|
|
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
|
|
|
public static class LdHelpers
|
|
{
|
|
private static readonly Dictionary<string, RemoteDocument> PreloadedContexts = new()
|
|
{
|
|
["https://purl.archive.org/socialweb/webfinger"] = GetPreloadedContext("wf.json"),
|
|
["https://www.w3.org/ns/activitystreams"] = GetPreloadedContext("as.json"),
|
|
["https://w3id.org/security/v1"] = GetPreloadedContext("security.json"),
|
|
["https://w3id.org/identity/v1"] = GetPreloadedContext("identity.json"),
|
|
["http://joinmastodon.org/ns"] = GetPreloadedContext("toot.json"),
|
|
["https://gotosocial.org/ns"] = GetPreloadedContext("gts.json"),
|
|
["http://schema.org/"] = GetPreloadedContext("schema.json"),
|
|
["litepub-0.1"] = GetPreloadedContext("litepub.json")
|
|
};
|
|
|
|
private static readonly ConcurrentDictionary<string, RemoteDocument> ContextCache = new();
|
|
|
|
private static readonly JToken FederationContext = GetPreloadedDocument("iceshrimp.json");
|
|
|
|
// Nonstandard extensions to the AS context need to be loaded in to fix federation with certain AP implementations
|
|
private static readonly JToken ASExtensions = GetPreloadedDocument("as-extensions.json");
|
|
|
|
private static readonly JsonLdProcessorOptions Options = new()
|
|
{
|
|
DocumentLoader = CustomLoader,
|
|
ExpandContext = ASExtensions,
|
|
ProcessingMode = JsonLdProcessingMode.JsonLd11,
|
|
KeepIRIs = [$"{Constants.ActivityStreamsNs}#Public"],
|
|
ForceArray = ASForceArray.Select(p => $"{Constants.ActivityStreamsNs}#{p}").ToList(),
|
|
|
|
// separated for readability
|
|
RemoveUnusedInlineContextProperties = true
|
|
};
|
|
|
|
public static readonly JsonSerializerSettings JsonSerializerSettings = new()
|
|
{
|
|
NullValueHandling = NullValueHandling.Ignore, DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
|
};
|
|
|
|
private static readonly JsonSerializer JsonSerializer = new()
|
|
{
|
|
NullValueHandling = NullValueHandling.Ignore, DateTimeZoneHandling = DateTimeZoneHandling.Utc
|
|
};
|
|
|
|
private static IEnumerable<string> ASForceArray => ["tag", "attachment", "to", "cc", "bcc", "bto", "alsoKnownAs"];
|
|
|
|
private static JToken GetPreloadedDocument(string filename) =>
|
|
JToken.Parse(AssemblyHelpers.GetEmbeddedResource($"contexts.{filename}"));
|
|
|
|
private static RemoteDocument GetPreloadedContext(string filename) => new()
|
|
{
|
|
Document = GetPreloadedDocument(filename)
|
|
};
|
|
|
|
private static RemoteDocument CustomLoader(Uri uri, JsonLdLoaderOptions jsonLdLoaderOptions)
|
|
{
|
|
var key = uri.AbsolutePath == "/schemas/litepub-0.1.jsonld" ? "litepub-0.1" : uri.ToString();
|
|
if (!PreloadedContexts.TryGetValue(key, out var result))
|
|
ContextCache.TryGetValue(key, out result);
|
|
|
|
if (result != null)
|
|
{
|
|
result.ContextUrl = uri;
|
|
return result;
|
|
}
|
|
|
|
//TODO: cache in postgres with ttl 24h
|
|
result = DefaultDocumentLoader.LoadJson(uri, jsonLdLoaderOptions);
|
|
ContextCache.TryAdd(uri.ToString(), result);
|
|
|
|
// Cleanup to make sure this doesn't take up more and more memory
|
|
while (ContextCache.Count > 20)
|
|
{
|
|
var hit = ContextCache.Keys.FirstOrDefault();
|
|
if (hit == null) break;
|
|
var success = ContextCache.TryRemove(hit, out _);
|
|
if (!success) break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static async Task<string> SignAndCompactAsync(this ASActivity activity, UserKeypair keypair)
|
|
{
|
|
var expanded = Expand(activity) ?? throw new Exception("Failed to expand activity");
|
|
var signed = await LdSignature.SignAsync(expanded, keypair.PrivateKey,
|
|
activity.Actor?.PublicKey?.Id ?? $"{activity.Actor!.Id}#main-key") ??
|
|
throw new Exception("Failed to sign activity");
|
|
var compacted = Compact(signed) ?? throw new Exception("Failed to compact signed activity");
|
|
var payload = JsonConvert.SerializeObject(compacted, JsonSerializerSettings);
|
|
|
|
return payload;
|
|
}
|
|
|
|
public static string CompactToPayload(this ASActivity activity)
|
|
{
|
|
var compacted = Compact(activity) ?? throw new Exception("Failed to compact signed activity");
|
|
var payload = JsonConvert.SerializeObject(compacted, JsonSerializerSettings);
|
|
|
|
return payload;
|
|
}
|
|
|
|
public static JObject Compact(this ASObject obj)
|
|
{
|
|
return Compact((object)obj) ?? throw new Exception("Failed to compact JSON-LD paylaod");
|
|
}
|
|
|
|
public static JObject Compact(object obj)
|
|
{
|
|
return Compact(JToken.FromObject(obj, JsonSerializer)) ??
|
|
throw new Exception("Failed to compact JSON-LD paylaod");
|
|
}
|
|
|
|
public static JArray Expand(object obj)
|
|
{
|
|
return Expand(JToken.FromObject(obj, JsonSerializer)) ??
|
|
throw new Exception("Failed to expand JSON-LD paylaod");
|
|
}
|
|
|
|
public static JObject Compact(JToken? json)
|
|
{
|
|
return JsonLdProcessor.Compact(json, FederationContext, Options);
|
|
}
|
|
|
|
public static JArray Expand(JToken? json)
|
|
{
|
|
return JsonLdProcessor.Expand(json, Options);
|
|
}
|
|
|
|
public static string Canonicalize(JArray json)
|
|
{
|
|
return JsonLdProcessor.Canonicalize(json);
|
|
}
|
|
|
|
public static string Canonicalize(JObject json)
|
|
{
|
|
return JsonLdProcessor.Canonicalize([json]);
|
|
}
|
|
} |