using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Entities; using PrivaPub.Api.Mastodon.Infrastructure; using PrivaPub.Domain.Privacy; using PrivaPub.Federation.Objects; using PrivaPub.Models.Federation; using PrivaPub.Models.Jobs; using PrivaPub.Models.Post; using PrivaPub.StaticServices; using System.Text.Json.Nodes; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Api.Mastodon.Controllers { public class ProvenanceController : MastodonController { readonly DbEntities _dbEntities; public ProvenanceController(DbEntities dbEntities) => _dbEntities = dbEntities; [HttpGet("/api/privapub/v1/statuses/{id}/provenance"), Scope("read:statuses", requiresUser: false), AllowAnonymous] public async Task Status(string id, CancellationToken token) { var post = await _dbEntities.Posts.Match(p => p.ID == id).Match(VisibilityPolicy.IsShown).ExecuteFirstAsync(token); if (post?.ReblogOfPostId != default) post = await _dbEntities.Posts.Match(p => p.ID == post.ReblogOfPostId).Match(VisibilityPolicy.IsShown).ExecuteFirstAsync(token); if (post == default || post.Visibility == PostVisibility.LocalGeo || !await VisibilityPolicy.CanSee(post, MyId, token)) return NotFoundError(); if (!post.IsFederatedCopy) return Json(new Provenance { ObjectUri = post.ObjectURI, Url = post.Url, Path = "local" }); var record = await DB.Default.Find().Match(r => r.PostId == post.ID).ExecuteFirstAsync(token) ?? await DB.Default.Find().Match(r => r.ObjectURI == post.ObjectURI).ExecuteFirstAsync(token); var host = record?.Host ?? (Uri.TryCreate(post.ObjectURI, UriKind.Absolute, out var uri) ? uri.Host.ToLowerInvariant() : default); var instance = host == default ? default : await DB.Default.Find().Match(i => i.Host == host).ExecuteFirstAsync(token); return Json(Describe(post, record, instance)); } [HttpGet("/api/privapub/v1/instances/{host}"), Scope("read", requiresUser: false), AllowAnonymous] public async Task Instance(string host, CancellationToken token) { host = host?.Trim().ToLowerInvariant(); var instance = await DB.Default.Find().Match(i => i.Host == host).ExecuteFirstAsync(token); return instance == default ? NotFoundError() : Json(Describe(instance)); } static Provenance Describe(PostEntity post, ObjectRecord record, RemoteInstance instance) { var raw = record?.Raw == default ? default : JsonNode.Parse(record.Raw) as JsonObject; return new Provenance { ObjectUri = post.ObjectURI, ObjectType = record?.ObjectType ?? post.ObjectType, Url = post.Url, Path = record == default ? "unrecorded" : record.Path == ObjectPath.Delivered ? "delivered" : "fetched", Refetched = record?.Refetched == true, Activity = record?.ActivityId == default ? default : new ProvenanceActivity { Id = record.ActivityId, Type = record.ActivityType, Actor = record.ActivityActorURI }, Inbox = record?.Inbox, FetchedBy = record?.Path == ObjectPath.Fetched ? "instance-actor" : default, Signature = record?.KeyId == default || record.Path == ObjectPath.Fetched ? default : new ProvenanceSignature { Scheme = record.SignatureScheme, KeyId = record.KeyId, Algorithm = record.Algorithm, Headers = record.SignedHeaders }, ReceivedAt = record == default ? default : MastodonJson.Time(record.ReceivedAt), Published = record?.Published is { } published ? MastodonJson.Time(published) : default, Updated = record?.Updated is { } updated ? MastodonJson.Time(updated) : default, Extensions = record?.Extensions ?? ObjectFeatures.Detect(raw), ContextNamespaces = record?.ContextNamespaces ?? ObjectFeatures.Namespaces(raw, record?.ActivityContext), Raw = raw, RawBytes = record?.RawBytes ?? 0, RawHash = record?.RawHash, RawTruncated = record?.RawTruncated == true, Revisions = record?.Revisions.Select(r => new ProvenanceRevision { ActivityId = r.ActivityId, Updated = r.Updated is { } at ? MastodonJson.Time(at) : default, ReceivedAt = MastodonJson.Time(r.ReceivedAt), RawHash = r.RawHash, Raw = r.Raw == default ? default : JsonNode.Parse(r.Raw) }).ToList() ?? new(), Instance = instance == default ? default : Describe(instance) }; } static InstanceDescription Describe(RemoteInstance instance) => new() { Host = instance.Host, Software = instance.Software, Version = instance.SoftwareVersion, NodeName = instance.NodeName, Protocols = instance.Protocols ?? new(), OpenRegistrations = instance.OpenRegistrations, DescribedAt = instance.DescribedAt is { } described ? MastodonJson.Time(described) : default, DescriptionError = instance.DescriptionError, NodeInfo = instance.NodeInfo == default ? default : JsonNode.Parse(instance.NodeInfo), Delivery = new InstanceDelivery { ConsecutiveFailures = instance.ConsecutiveFailures, UnavailableUntil = instance.UnavailableUntil is { } until ? MastodonJson.Time(until) : default, LastSuccessAt = instance.LastSuccessAt is { } success ? MastodonJson.Time(success) : default, LastFailureAt = instance.LastFailureAt is { } failure ? MastodonJson.Time(failure) : default } }; public class Provenance { public string ObjectUri { get; set; } public string ObjectType { get; set; } public string Url { get; set; } public string Path { get; set; } public bool Refetched { get; set; } public ProvenanceActivity Activity { get; set; } public string Inbox { get; set; } public string FetchedBy { get; set; } public ProvenanceSignature Signature { get; set; } public string ReceivedAt { get; set; } public string Published { get; set; } public string Updated { get; set; } public List Extensions { get; set; } = new(); public List ContextNamespaces { get; set; } = new(); public JsonNode Raw { get; set; } public int RawBytes { get; set; } public string RawHash { get; set; } public bool RawTruncated { get; set; } public List Revisions { get; set; } = new(); public InstanceDescription Instance { get; set; } } public class ProvenanceActivity { public string Id { get; set; } public string Type { get; set; } public string Actor { get; set; } } public class ProvenanceSignature { public string Scheme { get; set; } public string KeyId { get; set; } public string Algorithm { get; set; } public List Headers { get; set; } = new(); } public class ProvenanceRevision { public string ActivityId { get; set; } public string Updated { get; set; } public string ReceivedAt { get; set; } public string RawHash { get; set; } public JsonNode Raw { get; set; } } public class InstanceDescription { public string Host { get; set; } public string Software { get; set; } public string Version { get; set; } public string NodeName { get; set; } public List Protocols { get; set; } = new(); public bool? OpenRegistrations { get; set; } public string DescribedAt { get; set; } public string DescriptionError { get; set; } public JsonNode NodeInfo { get; set; } public InstanceDelivery Delivery { get; set; } } public class InstanceDelivery { public int ConsecutiveFailures { get; set; } public string UnavailableUntil { get; set; } public string LastSuccessAt { get; set; } public string LastFailureAt { get; set; } } } }