OpenIddict 7.7 (MongoDB stores, keys kept in Mongo so tokens survive
restarts) serves /oauth/authorize, /oauth/token, /oauth/revoke and the
discovery documents, including /.well-known/oauth-authorization-server:
- authorization code (PKCE optional) and client credentials, Mastodon's
scopes including the granular ones, non-expiring reference tokens,
`created_at` in the token response, and the urn:ietf:wg:oauth:2.0:oob
page that shows the code;
- /oauth/login signs the private login into a fifteen-minute cookie that
only /oauth sees (rate limited, antiforgery-protected); /oauth/authorize
then asks which persona the application acts as. The token's subject
is that persona's id and nothing else; no root id reaches a token, an
authorization or a log line;
- the token exchange refuses a persona whose login is banned or deleted,
and every API request checks the same.
/api/v1/apps registers applications dynamically, /api/v1/apps/
verify_credentials, /api/v1/instance (v1 and v2, "4.2.0 (compatible;
PrivaPub)") and verify_credentials answer in Mastodon's shapes: snake_case
with explicit nulls, Rails-style parameters from query, form or JSON,
{"error": ...} on failure, Link paging. CORS exposes Link.
/api goes to OpenIddict validation and everything else to the existing
JWT; the JWT failure handler no longer sends the exception and stack trace
to the client.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
108 lines
4.0 KiB
C#
108 lines
4.0 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,
|
|
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>" });
|
|
}
|
|
}
|