using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using MongoDB.Entities; using PrivaPub.Api.Mastodon.Infrastructure; using PrivaPub.Domain.Privacy; using PrivaPub.Federation.Actors; using PrivaPub.Infrastructure; 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; readonly IOptionsMonitor _registrations; public InstanceController(ILocalActorService localActors, IOptionsMonitor registrations) { _localActors = localActors; _registrations = registrations; } bool Open => _registrations.CurrentValue.IsOpen; // the languages this server is set up for (AppConfiguration:SupportedLanguages), "en" when none are named public static IReadOnlyList LanguageCodes(IServiceProvider services) { var configured = services.GetRequiredService>().CurrentValue.SupportedLanguages; var codes = (configured ?? new List()).Where(c => !string.IsNullOrWhiteSpace(c)).Select(c => c.Split('-')[0].ToLowerInvariant()).Distinct().ToList(); return codes.Count == 0 ? new[] { "en" } : codes; } string Domain => new Uri(_localActors.BaseAddress).Authority; // every limit is what the server enforces, read from where it is enforced static object Statuses => new { max_characters = PrivaPub.Domain.Statuses.StatusService.MaxCharacters, max_media_attachments = 4, characters_reserved_per_url = 23 }; object MediaAttachments { get { var media = HttpContext.RequestServices.GetRequiredService>().CurrentValue; return new { supported_mime_types = PrivaPub.Domain.Media.MediaService.SupportedTypes, image_size_limit = media.MaxImageBytes, image_matrix_limit = media.MaxImageSide * media.MaxImageSide, video_size_limit = media.MaxVideoBytes, video_frame_rate_limit = 60, video_matrix_limit = 2_304_000 }; } } static object Polls => new { max_options = PrivaPub.Domain.Statuses.PollService.MaxOptions, max_characters_per_option = PrivaPub.Domain.Statuses.PollService.MaxOptionLength, min_expiration = PrivaPub.Domain.Statuses.PollService.MinExpiration, max_expiration = PrivaPub.Domain.Statuses.PollService.MaxExpiration }; [HttpGet("/api/v1/instance"), AllowAnonymous] public async Task V1(CancellationToken token) { var users = await Counted.Users(token); var statuses = await Counted.LocalPosts(token); var domains = await KnownServers(token); return Json(new { uri = Domain, title = "PrivaPub", short_description = Description, description = Description, email = string.Empty, version = Version, urls = new { },//no streaming API yet, so none is advertised stats = new { user_count = users, status_count = statuses, domain_count = domains }, thumbnail = $"{_localActors.BaseAddress}/media/missing-header.png", languages = LanguageCodes(HttpContext.RequestServices), registrations = Open, approval_required = false, invites_enabled = true,//a group invitation always creates an account configuration = new { accounts = new { max_featured_tags = 0 }, statuses = Statuses, media_attachments = MediaAttachments, polls = Polls }, contact_account = default(object), rules = Array.Empty() }); } // the servers we have exchanged activities with, as the statistics count them. Which ones is not published // (/instance/peers stays empty, as on a Mastodon with its peers API off): on a small server it would say who its // personas talk to. static async Task KnownServers(CancellationToken token) => await DB.Default.CountAsync(i => i.Seen == "touched", token); [HttpGet("/api/v2/instance"), AllowAnonymous] public async Task V2(CancellationToken token) => 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 = await Counted.ActiveUsers(30, token) } }, thumbnail = new { url = $"{_localActors.BaseAddress}/media/missing-header.png" }, languages = LanguageCodes(HttpContext.RequestServices), configuration = new { urls = new { },//no streaming API yet, so none is advertised accounts = new { max_featured_tags = 0, max_pinned_statuses = StatusesController.MaxPins }, statuses = Statuses, media_attachments = MediaAttachments, polls = Polls, translation = new { enabled = false } }, registrations = new { enabled = Open, approval_required = false, message = Open ? default : RegistrationOptions.ClosedMessage }, contact = new { email = string.Empty, account = default(object) }, rules = Array.Empty() }); [HttpGet("/api/v1/instance/peers"), AllowAnonymous] public IActionResult Peers() => Json(Array.Empty()); [HttpGet("/api/v1/instance/activity"), AllowAnonymous] public IActionResult Activity() => Json(Array.Empty()); [HttpGet("/api/v1/instance/rules"), AllowAnonymous] public IActionResult Rules() => Json(Array.Empty()); [HttpGet("/api/v1/instance/extended_description"), AllowAnonymous] public IActionResult ExtendedDescription() => Json(new { updated_at = MastodonJson.Day(DateTime.UtcNow), content = $"

{Description}

" }); } }