M6: provenance fixes, stored features, community edits that are real edits
- A fetched ObjectRecord no longer wears the signature, key, signed headers and @context of the activity that caused the fetch, and its ReceivedAt is the fetch's own time. Its activity fields now name the trigger. Provenance answers signature null and fetchedBy "instance-actor". Migration _008 clears the old fetched records. - ObjectRecords store their ObjectFeatures extensions and context namespaces (migration _009 fills older records in batches), so statistics can count them. Provenance reads the stored lists. - ForeignAvatar.Features records what an actor document uses (ActorFeatures): featured, shared inbox, FEP-521a, FEP-8b32, identity proofs, Misskey fields, indexable, undiscoverable, locked, key size, and more. - RemoteEdits.Apply and RemoteDeletes.Remove are shared by the Update, Delete and Announce handlers. A community-wrapped Update is now an edit only when newer, refreshes polls and media otherwise, revises the ObjectRecord and re-resolves quotes. A community-wrapped Delete now tombstones the object, removes its ObjectRecord, reblogs and timeline rows, and lowers the reply count. Any deleted quoting post lowers the quoted post's quote count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
1 parent
d37999970c
commit
90a7e38ce9
19 files changed
+505
-109
No files matched your search
@@ -37,6 +37,7 @@ namespace PrivaPub.Federation.Actors
|
||||
public List<CustomEmoji> Emojis { get; init; } = new();
|
||||
public Dictionary<string, string> Fields { get; init; } = new();
|
||||
public IReadOnlyList<ActorKey> Keys { get; init; } = Array.Empty<ActorKey>();
|
||||
public List<string> Features { get; init; } = new();
|
||||
|
||||
public ActorKey Key(string keyId) => Keys.FirstOrDefault(k => k.Id == keyId);
|
||||
|
||||
@@ -54,6 +55,7 @@ namespace PrivaPub.Federation.Actors
|
||||
{
|
||||
Id = id,
|
||||
Type = type,
|
||||
Features = ActorFeatures.Detect(root),
|
||||
PreferredUsername = RemoteActorService.Text(root, "preferredUsername"),
|
||||
Name = RemoteActorService.Text(root, "name"),
|
||||
Summary = RemoteActorService.Text(root, "summary"),
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PrivaPub.Federation.Actors
|
||||
{
|
||||
public static class ActorFeatures
|
||||
{
|
||||
public static List<string> Detect(JsonElement root)
|
||||
{
|
||||
var found = new List<string>();
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
return found;
|
||||
void When(bool condition, string feature)
|
||||
{
|
||||
if (condition)
|
||||
found.Add(feature);
|
||||
}
|
||||
bool Has(string name) => root.TryGetProperty(name, out var value) && value.ValueKind is not (JsonValueKind.Null or JsonValueKind.Undefined);
|
||||
bool True(string name) => root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.True;
|
||||
IEnumerable<JsonElement> Items(string name) => !root.TryGetProperty(name, out var value)
|
||||
? Enumerable.Empty<JsonElement>()
|
||||
: value.ValueKind == JsonValueKind.Array ? value.EnumerateArray() : new[] { value };
|
||||
bool Typed(JsonElement item, string type) =>
|
||||
item.ValueKind == JsonValueKind.Object && item.TryGetProperty("type", out var t) && t.ValueKind == JsonValueKind.String && t.GetString() == type;
|
||||
|
||||
When(Has("featured"), "featured");
|
||||
When(Has("featuredTags"), "featured-tags");
|
||||
When(Items("alsoKnownAs").Any(), "also-known-as");
|
||||
When(Has("movedTo"), "moved-to");
|
||||
When(root.TryGetProperty("endpoints", out var endpoints) && endpoints.ValueKind == JsonValueKind.Object && endpoints.TryGetProperty("sharedInbox", out _), "shared-inbox");
|
||||
When(Has("assertionMethod"), "fep-521a-assertion-method");
|
||||
When(Has("proof"), "fep-8b32-proof");
|
||||
When(Items("attachment").Any(a => Typed(a, "PropertyValue")), "property-value");
|
||||
When(Items("attachment").Any(a => Typed(a, "VerifiableIdentityStatement") || Typed(a, "IdentityProof")), "fep-c390-identity-proof");
|
||||
When(Items("tag").Any(t => Typed(t, "Emoji")), "custom-emoji");
|
||||
When(root.EnumerateObject().Any(p => p.Name.StartsWith("_misskey", StringComparison.Ordinal)), "misskey");
|
||||
When(True("isCat"), "is-cat");
|
||||
When(Has("webfinger"), "fep-2c59-webfinger");
|
||||
When(Has("attributionDomains"), "attribution-domains");
|
||||
When(Has("searchableBy"), "fep-268d-searchable-by");
|
||||
When(True("indexable"), "indexable");
|
||||
When(root.TryGetProperty("discoverable", out var discoverable) && discoverable.ValueKind == JsonValueKind.False, "undiscoverable");
|
||||
When(True("manuallyApprovesFollowers"), "locked");
|
||||
When(True("memorial"), "memorial");
|
||||
When(Has("generator"), "generator");
|
||||
When(root.TryGetProperty("publicKey", out var keys) && keys.ValueKind == JsonValueKind.Array, "public-key-array");
|
||||
if (KeyBits(root) is { } bits)
|
||||
found.Add($"key:rsa-{bits}");
|
||||
return found;
|
||||
}
|
||||
|
||||
static int? KeyBits(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("publicKey", out var key))
|
||||
return default;
|
||||
if (key.ValueKind == JsonValueKind.Array)
|
||||
key = key.EnumerateArray().FirstOrDefault(k => k.ValueKind == JsonValueKind.Object);
|
||||
if (key.ValueKind != JsonValueKind.Object || !key.TryGetProperty("publicKeyPem", out var pem) || pem.ValueKind != JsonValueKind.String)
|
||||
return default;
|
||||
try
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(pem.GetString());
|
||||
return rsa.KeySize;
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or CryptographicException)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,6 +192,7 @@ namespace PrivaPub.Federation.Actors
|
||||
.Modify(a => a.Published, actor.Published)
|
||||
.Modify(a => a.Emojis, actor.Emojis)
|
||||
.Modify(a => a.Fields, actor.Fields)
|
||||
.Modify(a => a.Features, actor.Features)
|
||||
.Modify(a => a.AvatarType, Enum.TryParse<AvatarType>(actor.Type, out var type) ? type : AvatarType.Person)
|
||||
.Modify(a => a.PublicKeyId, key?.Id)
|
||||
.Modify(a => a.PublicKey, key?.Pem)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Domain.Statuses;
|
||||
using PrivaPub.Domain.Social;
|
||||
using PrivaPub.Domain.Timelines;
|
||||
using PrivaPub.Federation.Actors;
|
||||
@@ -28,8 +29,14 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
readonly IFanout _fanout;
|
||||
readonly IRemoteActorService _remoteActors;
|
||||
|
||||
public AnnounceHandler(DbEntities dbEntities, ILocalActorService localActors, IRemotePosts remotePosts, IFanout fanout, IRemoteActorService remoteActors)
|
||||
readonly IObjectRecords _records;
|
||||
readonly IQuoteService _quotes;
|
||||
|
||||
public AnnounceHandler(DbEntities dbEntities, ILocalActorService localActors, IRemotePosts remotePosts, IFanout fanout, IRemoteActorService remoteActors,
|
||||
IObjectRecords records, IQuoteService quotes)
|
||||
{
|
||||
_records = records;
|
||||
_quotes = quotes;
|
||||
_remoteActors = remoteActors;
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
@@ -172,24 +179,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
Arrival.Drop(note == default ? "fetch-failed" : "misattributed");
|
||||
return;
|
||||
}
|
||||
Arrival.Accept("edit");
|
||||
stored.Revisions.Add(new PostRevision
|
||||
{
|
||||
Title = stored.Title,
|
||||
SpoilerText = stored.SpoilerText,
|
||||
ContentHtml = stored.ContentHtml,
|
||||
HasContentWarning = stored.HasContentWarning,
|
||||
EditedAt = stored.EditedAt ?? stored.CreationDate
|
||||
});
|
||||
stored.Title = note.Title;
|
||||
stored.SpoilerText = note.SpoilerText;
|
||||
stored.HasContentWarning = note.Sensitive;
|
||||
stored.Text = note.ContentHtml;
|
||||
stored.ContentHtml = note.ContentHtml;
|
||||
stored.Tags = note.Tags.ToList();
|
||||
stored.Media = note.Attachments.ToList();
|
||||
stored.EditedAt = note.Updated ?? DateTime.UtcNow;
|
||||
await DB.Default.SaveAsync(stored, token);
|
||||
Arrival.Accept(await RemoteEdits.Apply(stored, note, Id(inner), _localActors, _records, _quotes, token) ? "edit" : "refresh");
|
||||
}
|
||||
break;
|
||||
case "Delete" when objectUri != default:
|
||||
@@ -207,8 +197,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
return;
|
||||
}
|
||||
Arrival.Accept("removed");
|
||||
await DB.Default.DeleteAsync<PostEntity>(deleted.ID);
|
||||
await DB.Default.DeleteAsync<TimelineEntry>(e => e.PostId == deleted.ID);
|
||||
await RemoteDeletes.Remove(deleted, objectUri, token);
|
||||
break;
|
||||
default:
|
||||
Arrival.Drop("unsupported");
|
||||
|
||||
@@ -73,11 +73,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
return;
|
||||
}
|
||||
|
||||
await DB.Default.Update<DeletedObject>()
|
||||
.Match(d => d.ObjectURI == objectUri)
|
||||
.Modify(d => d.DeletedAt, DateTime.UtcNow)
|
||||
.Option(o => o.IsUpsert = true)
|
||||
.ExecuteAsync(token);
|
||||
await RemoteDeletes.Tombstone(objectUri, token);
|
||||
var post = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && p.ActorURI == actor.ActorURI).ExecuteFirstAsync(token);
|
||||
if (post == default)
|
||||
{
|
||||
@@ -87,14 +83,9 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
}
|
||||
Arrival.Accept("removed");
|
||||
Arrival.About(post.ObjectType, post.Visibility, post.CreationDate);
|
||||
await DB.Default.DeleteAsync<PostEntity>(post.ID);
|
||||
await DB.Default.DeleteAsync<ObjectRecord>(r => r.PostId == post.ID || r.ObjectURI == objectUri);
|
||||
await DB.Default.DeleteAsync<TimelineEntry>(e => e.PostId == post.ID || e.ReblogOfPostId == post.ID);
|
||||
await RemoteDeletes.Remove(post, objectUri, token);
|
||||
if (!string.IsNullOrEmpty(post.GroupId) && await _localActors.FindById(LocalActorKind.Group, post.GroupId, token) is { IsCircle: false } community)
|
||||
await _groups.Announce(community, activity.AsObject(), post.ObjectURI, isNewPost: false, token);
|
||||
await DB.Default.DeleteAsync<PostEntity>(p => p.ReblogOfPostId == post.ID);
|
||||
if (!string.IsNullOrEmpty(post.AnsweringToPostId))
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.AnsweringToPostId).Modify(b => b.Inc(p => p.RepliesCount, -1)).ExecuteAsync(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
public class UpdateHandler : IActivityHandler
|
||||
{
|
||||
const int MaxRevisions = 20;
|
||||
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
@@ -70,69 +69,10 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
}
|
||||
Arrival.About(note.Type, post.Visibility, post.CreationDate);
|
||||
|
||||
post.Poll = note.Poll ?? post.Poll;
|
||||
post.QuotePolicy = note.QuotePolicy;
|
||||
post.Video = note.Video ?? post.Video;
|
||||
post.Audio = note.Audio ?? post.Audio;
|
||||
post.Event = note.Event ?? post.Event;
|
||||
if (!IsEdit(note, post))
|
||||
{
|
||||
Arrival.Accept("refresh");
|
||||
await DB.Default.SaveAsync(post, token);
|
||||
await _records.Revise(note, Id(activity), token);
|
||||
await Requote(post, note, token);
|
||||
return;
|
||||
}
|
||||
|
||||
Arrival.Accept("edit");
|
||||
post.Revisions.Add(new PostRevision
|
||||
{
|
||||
Title = post.Title,
|
||||
SpoilerText = post.SpoilerText,
|
||||
ContentHtml = post.ContentHtml,
|
||||
HasContentWarning = post.HasContentWarning,
|
||||
EditedAt = post.EditedAt ?? post.CreationDate
|
||||
});
|
||||
if (post.Revisions.Count > MaxRevisions)
|
||||
post.Revisions.RemoveRange(0, post.Revisions.Count - MaxRevisions);
|
||||
|
||||
var mentions = new List<PostMention>();
|
||||
foreach (var mention in note.Mentions)
|
||||
{
|
||||
var local = await _localActors.FindByUri(mention.ActorURI, token);
|
||||
mentions.Add(new PostMention { ActorURI = mention.ActorURI, Handle = mention.Handle, IsLocal = local != default, AccountId = local?.Id });
|
||||
}
|
||||
|
||||
post.Title = note.Title;
|
||||
post.SpoilerText = note.SpoilerText;
|
||||
post.Excerpt = note.Excerpt;
|
||||
post.Source = note.Source;
|
||||
post.Emojis = note.Emojis.ToList();
|
||||
post.CoverURL = note.CoverURL;
|
||||
post.Link = note.Link;
|
||||
post.HasContentWarning = note.Sensitive;
|
||||
post.Text = note.ContentHtml;
|
||||
post.ContentHtml = note.ContentHtml;
|
||||
post.Language = note.Language;
|
||||
post.Tags = note.Tags.ToList();
|
||||
post.Mentions = mentions;
|
||||
post.Media = note.Attachments.ToList();
|
||||
post.EditedAt = note.Updated ?? DateTime.UtcNow;
|
||||
post.UpdateDate = DateTime.UtcNow;
|
||||
await DB.Default.SaveAsync(post, token);
|
||||
await _records.Revise(note, Id(activity), token);
|
||||
await Requote(post, note, token);
|
||||
if (!string.IsNullOrEmpty(post.GroupId) && await _localActors.FindById(Models.Federation.LocalActorKind.Group, post.GroupId, token) is { IsCircle: false } community)
|
||||
var edited = await RemoteEdits.Apply(post, note, Id(activity), _localActors, _records, _quotes, token);
|
||||
Arrival.Accept(edited ? "edit" : "refresh");
|
||||
if (edited && !string.IsNullOrEmpty(post.GroupId) && await _localActors.FindById(Models.Federation.LocalActorKind.Group, post.GroupId, token) is { IsCircle: false } community)
|
||||
await _groups.Announce(community, activity.AsObject(), post.ObjectURI, isNewPost: false, token);
|
||||
}
|
||||
|
||||
static bool IsEdit(NoteDocument note, PostEntity post) =>
|
||||
note.Updated is { } updated && updated > (post.EditedAt ?? post.CreationDate);
|
||||
|
||||
async Task Requote(PostEntity post, NoteDocument note, CancellationToken token)
|
||||
{
|
||||
if (note.QuoteUri != post.QuoteURI || note.QuoteAuthorization != post.QuoteAuthorizationURI || post.QuoteState != QuoteState.Accepted)
|
||||
await _quotes.Resolve(post, note, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Domain.Statuses;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Models.Social;
|
||||
|
||||
using PostEntity = PrivaPub.Models.Post.Post;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox
|
||||
{
|
||||
public static class RemoteEdits
|
||||
{
|
||||
public const int MaxRevisions = 20;
|
||||
|
||||
public static bool IsEdit(NoteDocument note, PostEntity post) =>
|
||||
note.Updated is { } updated && updated > (post.EditedAt ?? post.CreationDate);
|
||||
|
||||
public static async Task<bool> Apply(PostEntity post, NoteDocument note, string activityId, ILocalActorService localActors, IObjectRecords records,
|
||||
IQuoteService quotes, CancellationToken token)
|
||||
{
|
||||
post.Poll = note.Poll ?? post.Poll;
|
||||
post.QuotePolicy = note.QuotePolicy;
|
||||
post.Video = note.Video ?? post.Video;
|
||||
post.Audio = note.Audio ?? post.Audio;
|
||||
post.Event = note.Event ?? post.Event;
|
||||
if (!IsEdit(note, post))
|
||||
{
|
||||
await DB.Default.SaveAsync(post, token);
|
||||
await records.Revise(note, activityId, token);
|
||||
await Requote(post, note, quotes, token);
|
||||
return false;
|
||||
}
|
||||
|
||||
post.Revisions.Add(new PostRevision
|
||||
{
|
||||
Title = post.Title,
|
||||
SpoilerText = post.SpoilerText,
|
||||
ContentHtml = post.ContentHtml,
|
||||
HasContentWarning = post.HasContentWarning,
|
||||
EditedAt = post.EditedAt ?? post.CreationDate
|
||||
});
|
||||
if (post.Revisions.Count > MaxRevisions)
|
||||
post.Revisions.RemoveRange(0, post.Revisions.Count - MaxRevisions);
|
||||
|
||||
var mentions = new List<PostMention>();
|
||||
foreach (var mention in note.Mentions)
|
||||
{
|
||||
var local = await localActors.FindByUri(mention.ActorURI, token);
|
||||
mentions.Add(new PostMention { ActorURI = mention.ActorURI, Handle = mention.Handle, IsLocal = local != default, AccountId = local?.Id });
|
||||
}
|
||||
|
||||
post.Title = note.Title;
|
||||
post.SpoilerText = note.SpoilerText;
|
||||
post.Excerpt = note.Excerpt;
|
||||
post.Source = note.Source;
|
||||
post.Emojis = note.Emojis.ToList();
|
||||
post.CoverURL = note.CoverURL;
|
||||
post.Link = note.Link;
|
||||
post.HasContentWarning = note.Sensitive;
|
||||
post.Text = note.ContentHtml;
|
||||
post.ContentHtml = note.ContentHtml;
|
||||
post.Language = note.Language;
|
||||
post.Tags = note.Tags.ToList();
|
||||
post.Mentions = mentions;
|
||||
post.Media = note.Attachments.ToList();
|
||||
post.EditedAt = note.Updated ?? DateTime.UtcNow;
|
||||
post.UpdateDate = DateTime.UtcNow;
|
||||
await DB.Default.SaveAsync(post, token);
|
||||
await records.Revise(note, activityId, token);
|
||||
await Requote(post, note, quotes, token);
|
||||
return true;
|
||||
}
|
||||
|
||||
static async Task Requote(PostEntity post, NoteDocument note, IQuoteService quotes, CancellationToken token)
|
||||
{
|
||||
if (note.QuoteUri != post.QuoteURI || note.QuoteAuthorization != post.QuoteAuthorizationURI || post.QuoteState != QuoteState.Accepted)
|
||||
await quotes.Resolve(post, note, token);
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoteDeletes
|
||||
{
|
||||
public static async Task Tombstone(string objectUri, CancellationToken token) =>
|
||||
await DB.Default.Update<DeletedObject>()
|
||||
.Match(d => d.ObjectURI == objectUri)
|
||||
.Modify(d => d.DeletedAt, DateTime.UtcNow)
|
||||
.Option(o => o.IsUpsert = true)
|
||||
.ExecuteAsync(token);
|
||||
|
||||
public static async Task Remove(PostEntity post, string objectUri, CancellationToken token)
|
||||
{
|
||||
await Tombstone(objectUri, token);
|
||||
await DB.Default.DeleteAsync<PostEntity>(post.ID);
|
||||
await DB.Default.DeleteAsync<ObjectRecord>(r => r.PostId == post.ID || r.ObjectURI == objectUri);
|
||||
await DB.Default.DeleteAsync<TimelineEntry>(e => e.PostId == post.ID || e.ReblogOfPostId == post.ID);
|
||||
await DB.Default.DeleteAsync<PostEntity>(p => p.ReblogOfPostId == post.ID);
|
||||
if (!string.IsNullOrEmpty(post.AnsweringToPostId))
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.AnsweringToPostId).Modify(b => b.Inc(p => p.RepliesCount, -1)).ExecuteAsync(token);
|
||||
if (post.QuoteState == QuoteState.Accepted && !string.IsNullOrEmpty(post.QuotedPostId))
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.QuotedPostId).Modify(b => b.Inc(p => p.QuotesCount, -1)).ExecuteAsync(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,10 @@ namespace PrivaPub.Federation.Objects
|
||||
public async Task Record(NoteDocument note, PostEntity post, ObjectPath path, bool refetched, CancellationToken token)
|
||||
{
|
||||
var arrival = Arrival.Current;
|
||||
var delivered = path == ObjectPath.Delivered;
|
||||
var raw = Capture(note.Raw);
|
||||
var host = new Uri(note.Id).Host.ToLowerInvariant();
|
||||
var activityContext = !delivered || note.Raw?.ContainsKey("@context") == true || arrival?.Context?.Length > 16 * 1024 ? default : arrival?.Context;
|
||||
var record = new ObjectRecord
|
||||
{
|
||||
ObjectURI = note.Id,
|
||||
@@ -46,20 +48,22 @@ namespace PrivaPub.Federation.Objects
|
||||
RawHash = raw.Hash,
|
||||
RawBytes = raw.Bytes,
|
||||
RawTruncated = raw.Truncated,
|
||||
ActivityContext = note.Raw?.ContainsKey("@context") == true || arrival?.Context?.Length > 16 * 1024 ? default : arrival?.Context,
|
||||
ActivityContext = activityContext,
|
||||
Path = path,
|
||||
Refetched = refetched,
|
||||
ActivityId = arrival?.ActivityId,
|
||||
ActivityType = arrival?.ActivityType,
|
||||
ActivityActorURI = arrival?.ActorURI,
|
||||
Inbox = path == ObjectPath.Delivered ? arrival?.Inbox : default,
|
||||
SignatureScheme = arrival?.KeyId == default ? default : "draft-cavage",
|
||||
KeyId = arrival?.KeyId,
|
||||
Algorithm = arrival?.Algorithm,
|
||||
SignedHeaders = arrival?.SignedHeaders?.ToList() ?? new(),
|
||||
ReceivedAt = arrival?.ReceivedAt ?? DateTime.UtcNow,
|
||||
Inbox = delivered ? arrival?.Inbox : default,
|
||||
SignatureScheme = !delivered || arrival?.KeyId == default ? default : "draft-cavage",
|
||||
KeyId = delivered ? arrival?.KeyId : default,
|
||||
Algorithm = delivered ? arrival?.Algorithm : default,
|
||||
SignedHeaders = delivered ? arrival?.SignedHeaders?.ToList() ?? new() : new(),
|
||||
ReceivedAt = delivered ? arrival?.ReceivedAt ?? DateTime.UtcNow : DateTime.UtcNow,
|
||||
Published = note.Published,
|
||||
Updated = note.Updated
|
||||
Updated = note.Updated,
|
||||
Extensions = ObjectFeatures.Detect(note.Raw),
|
||||
ContextNamespaces = ObjectFeatures.Namespaces(note.Raw, activityContext)
|
||||
};
|
||||
try
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user