Remote posts take their timeline place on arrival, and an unedited one has no edit date

Found by the first live run against GoToSocial:

- A remote post's id came from its published second plus random bytes, so a reply arriving in the same second as
  the post it answers could sort under it, and a late arrival landed behind a client's since_id. A post published
  within the last hour now gets an id for its arrival; backfill keeps its published time.
- NoteParser.Time returned default(DateTime) for a missing "updated", so every remote post was stored as edited in
  year one. Migration _006 clears the stored ones.
- published now carries milliseconds, so peers that derive ids from it (GoToSocial, Mastodon) keep our posts in
  order within a second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-01 13:19:16 +02:00
1 parent 51f2c10c6f
commit 31542a181e
9 files changed
+56 -6

No files matched your search

@@ -71,7 +71,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
: DateTime.UtcNow;
var reblog = new PostEntity
{
ID = PrivacyIds.At(published),
ID = PrivacyIds.Arrived(published),
ObjectURI = announceId,
ActivityURI = announceId,
ActorURI = actor.ActorURI,
+1 -1
View File
@@ -60,7 +60,7 @@ namespace PrivaPub.Federation.Inbox
return new PostEntity
{
ID = PrivacyIds.At(note.Published),
ID = PrivacyIds.Arrived(note.Published),
ObjectURI = note.Id,
ActorURI = author.ActorURI,
AuthorAccountId = author.ID,
+1 -1
View File
@@ -163,7 +163,7 @@ namespace PrivaPub.Federation.Objects
static DateTime? Time(string value) =>
DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var parsed)
? parsed.UtcDateTime
: default;
: (DateTime?)null;
static int? Int(JsonNode node) =>
node is JsonValue value && value.TryGetValue<int>(out var number) && number is > 0 and < 100_000 ? number : (int?)null;
@@ -48,7 +48,7 @@ namespace PrivaPub.Federation.Rendering
string.IsNullOrEmpty(markdown) ? string.Empty : Markdown.ToHtml(markdown, Pipeline).Trim();
public static string Timestamp(DateTime value) =>
DateTime.SpecifyKind(value, DateTimeKind.Utc).ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
DateTime.SpecifyKind(value, DateTimeKind.Utc).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
public static string Day(DateTime value) =>
DateTime.SpecifyKind(value, DateTimeKind.Utc).Date.ToString("yyyy-MM-ddT00:00:00Z", CultureInfo.InvariantCulture);
@@ -0,0 +1,26 @@
using MongoDB.Entities;
using PrivaPub.Models.Post;
namespace PrivaPub.Infrastructure.Data.Migrations
{
public class _006_unedited_remote_posts_lose_year_one : IMigration
{
public async Task UpgradeAsync()
{
var posts = await DB.Default.Find<Post>()
.Match(p => p.EditedAt == DateTime.MinValue || p.UpdateDate == DateTime.MinValue || p.Revisions.Any(r => r.EditedAt == DateTime.MinValue))
.ExecuteAsync();
foreach (var post in posts)
{
foreach (var revision in post.Revisions.Where(r => r.EditedAt == DateTime.MinValue))
revision.EditedAt = post.CreationDate;
await DB.Default.Update<Post>().MatchID(post.ID)
.Modify(p => p.EditedAt, post.EditedAt == DateTime.MinValue ? null : post.EditedAt)
.Modify(p => p.UpdateDate, post.UpdateDate == DateTime.MinValue ? post.CreationDate : post.UpdateDate)
.Modify(p => p.Revisions, post.Revisions)
.ExecuteAsync();
}
}
}
}
@@ -1,12 +1,19 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
using MongoDB.Bson;
namespace PrivaPub.Infrastructure.Ids
{
public static class PrivacyIds
{
public static string ForDay(DateTime when) => At(DateTime.SpecifyKind(when, DateTimeKind.Utc).Date);
public static string Arrived(DateTime published) =>
DateTime.SpecifyKind(published, DateTimeKind.Utc) > DateTime.UtcNow - LiveWindow ? ObjectId.GenerateNewId().ToString() : At(published);
static readonly TimeSpan LiveWindow = TimeSpan.FromHours(1);
public static string At(DateTime when)
{
var seconds = Math.Clamp(new DateTimeOffset(DateTime.SpecifyKind(when, DateTimeKind.Utc)).ToUnixTimeSeconds(), 0,