Files
SocialPub/PrivaPub/Api/Mastodon/Controllers/InstanceController.cs
T
thepraandClaude Opus 5.5 6f1ab0073c
Build / Build (push) Successful in 35s
Deploy / privapub.thepra.dev (push) Successful in 54s
P6: quote posts, received and sent (FEP-044f and the older keys)
- Received quotes: read from `quote`, `quoteUrl`, `quoteUri`, `_misskey_quote` or a FEP-e232 Link tag; the quoted post
  is fetched once; a quoteAuthorization stamp is verified field by field on the quoted author's origin; a consent
  quote without a stamp is pending; an older-key quote of a public post is shown; Delete of a stamp revokes. Counts
  and a `quote` notification follow the accepted state. The `quote-inline` fallback survives sanitising and is removed
  from content when the real quote is shown.
- Personas quote through `quoted_status_id`: posts that state a quote policy get a QuoteRequest and stay pending until
  an Accept brings a stamp we can verify, then an Update adds quoteAuthorization; posts that state none are quoted
  the older way, without `quote`; another persona's posts cannot be quoted yet (we issue no stamps). Quoting posts
  are delivered to the quoted author too.
- Mastodon API: Status.quote (with the quoted status one level deep), quotes_count, quote_approval from the remote
  policy, GET /api/v1/statuses/:id/quotes, `quote` notifications, and api_versions.mastodon = 7.

Checked live: GoToSocial's author-only quote policy is respected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 18:53:28 +02:00

109 lines
4.1 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using MongoDB.Entities;
using PrivaPub.Api.Mastodon.Infrastructure;
using PrivaPub.Domain.Privacy;
using PrivaPub.Federation.Actors;
using PrivaPub.Models.User;
using PostEntity = PrivaPub.Models.Post.Post;
namespace PrivaPub.Api.Mastodon.Controllers
{
public class InstanceController : MastodonController
{
public const string Version = "4.2.0 (compatible; PrivaPub)";
const string Description = "A small ActivityPub server where one private login keeps several unlinkable public personas.";
readonly ILocalActorService _localActors;
public InstanceController(ILocalActorService localActors)
{
_localActors = localActors;
}
string Domain => new Uri(_localActors.BaseAddress).Authority;
static object Statuses => new { max_characters = 5000, max_media_attachments = 4, characters_reserved_per_url = 23 };
static object MediaAttachments => new
{
supported_mime_types = new[] { "image/jpeg", "image/png", "image/gif", "image/webp", "video/mp4", "video/webm", "audio/mpeg", "audio/ogg" },
image_size_limit = 16 * 1024 * 1024,
image_matrix_limit = 16_777_216,
video_size_limit = 99 * 1024 * 1024,
video_frame_rate_limit = 60,
video_matrix_limit = 2_304_000
};
static object Polls => new { max_options = 4, max_characters_per_option = 50, min_expiration = 300, max_expiration = 2_629_746 };
[HttpGet("/api/v1/instance"), AllowAnonymous]
public async Task<IActionResult> V1(CancellationToken token)
{
var users = await DB.Default.CountAsync<Avatar>(a => !a.DeletionAt.HasValue, token);
var statuses = await DB.Default.CountAsync<PostEntity>(f => f.Where(p => !p.IsFederatedCopy) & f.Where(VisibilityPolicy.IsPublic), token);
var domains = (await DB.Default.Find<ForeignAvatar, string>().Project(a => a.Domain).ExecuteAsync(token)).Distinct().Count();
return Json(new
{
uri = Domain,
title = "PrivaPub",
short_description = Description,
description = Description,
email = string.Empty,
version = Version,
urls = new { streaming_api = $"wss://{Domain}" },
stats = new { user_count = users, status_count = statuses, domain_count = domains },
thumbnail = $"{_localActors.BaseAddress}/media/missing-header.png",
languages = new[] { "en" },
registrations = false,
approval_required = false,
invites_enabled = false,
configuration = new { accounts = new { max_featured_tags = 0 }, statuses = Statuses, media_attachments = MediaAttachments, polls = Polls },
contact_account = default(object),
rules = Array.Empty<object>()
});
}
[HttpGet("/api/v2/instance"), AllowAnonymous]
public IActionResult V2() => Json(new
{
domain = Domain,
title = "PrivaPub",
version = Version,
api_versions = new { mastodon = 7 },
source_url = "https://git.thepra.dev/thepra/SocialPub",
description = Description,
usage = new { users = new { active_month = 0 } },
thumbnail = new { url = $"{_localActors.BaseAddress}/media/missing-header.png" },
languages = new[] { "en" },
configuration = new
{
urls = new { streaming = $"wss://{Domain}" },
accounts = new { max_featured_tags = 0, max_pinned_statuses = 0 },
statuses = Statuses,
media_attachments = MediaAttachments,
polls = Polls,
translation = new { enabled = false }
},
registrations = new { enabled = false, approval_required = false, message = default(string) },
contact = new { email = string.Empty, account = default(object) },
rules = Array.Empty<object>()
});
[HttpGet("/api/v1/instance/peers"), AllowAnonymous]
public IActionResult Peers() => Json(Array.Empty<string>());
[HttpGet("/api/v1/instance/activity"), AllowAnonymous]
public IActionResult Activity() => Json(Array.Empty<object>());
[HttpGet("/api/v1/instance/rules"), AllowAnonymous]
public IActionResult Rules() => Json(Array.Empty<object>());
[HttpGet("/api/v1/instance/extended_description"), AllowAnonymous]
public IActionResult ExtendedDescription() => Json(new { updated_at = MastodonJson.Day(DateTime.UtcNow), content = $"<p>{Description}</p>" });
}
}