Files
SocialPub/PrivaPub/Api/Mastodon/Controllers/SuggestionsController.cs
T
thepraandClaude Opus 5.5 87529bca49 R8 the Mastodon API made real: featured tags, endorsements, notes, notify and languages, suggestions, the direct stream
Featured tags per persona (/api/v1/featured_tags, suggestions, tags/:name/feature, accounts' featured tags, max 10),
listed at /tattoos as Hashtags and sent as Add/Remove{Hashtag} to followers on the featured collection, as Mastodon
sends them; remote accounts' tags received and read from featuredTags. Endorsements (pin/endorse) and private notes
(PersonaNote) kept per persona and never federated; the relationship's notifying, languages, endorsed and note, with
follow's notify (status notifications) and languages (home filter). Reading preferences set on /clientapi. Suggestions
v1/v2 with dismissals and staff picks, never the persona or a sibling. conversation events on the direct stream.
translate, annual reports and the admin API answer. New collections backed up and removed with their persona.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-18 08:38:11 +02:00

77 lines
3.1 KiB
C#

using Microsoft.AspNetCore.Mvc;
using MongoDB.Driver;
using MongoDB.Entities;
using PrivaPub.Api.Mastodon.Entities;
using PrivaPub.Api.Mastodon.Infrastructure;
using PrivaPub.Api.Mastodon.Mappers;
using PrivaPub.Domain.Instance;
using PrivaPub.Domain.Relationships;
using PrivaPub.Domain.Social;
using PrivaPub.Models.Social;
namespace PrivaPub.Api.Mastodon.Controllers
{
// Follow suggestions as Mastodon's API has them (completion program R8; who and why in Domain/Social/Suggestions): v1 the
// accounts, v2 each with its reasons; a dismissed account is never suggested to that persona again.
public class SuggestionsController : MastodonController
{
readonly MastodonMapper _mapper;
readonly IInstanceSettings _settings;
public SuggestionsController(MastodonMapper mapper, IInstanceSettings settings)
{
_mapper = mapper;
_settings = settings;
}
[HttpGet("/api/v1/suggestions"), Scope("read")]
public async Task<IActionResult> V1(CancellationToken token) => Json((await Suggested(token)).Select(s => s.Account).ToList());
[HttpGet("/api/v2/suggestions"), Scope("read")]
public async Task<IActionResult> V2(CancellationToken token) => Json(await Suggested(token));
[HttpDelete("/api/v1/suggestions/{accountId}"), Scope("read")]
public async Task<IActionResult> Dismiss(string accountId, CancellationToken token)
{
if (!string.IsNullOrEmpty(accountId) && accountId.Length <= 64)
try
{
await DB.Default.Update<SuggestionDismissal>().Match(d => d.AvatarId == MyId && d.AccountId == accountId)
.Modify(b => b.SetOnInsert(d => d.CreatedAt, DateTime.UtcNow)).Option(o => o.IsUpsert = true).ExecuteAsync(token);
}
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
}
return Json(new { });
}
// the candidates mapped a few at a time, until the page is full: an account no longer shown (deleted, suspended), one
// shown limited (silenced, or its server), a community outside the staff's picks, and one the persona hides (its
// blocked servers) are passed over
async Task<List<Suggestion>> Suggested(CancellationToken token)
{
var limit = Limit(40, 80);
var candidates = await Suggestions.For(Me, _settings.Current.StaffPicks, token);
var suggested = new List<Suggestion>();
for (var start = 0; start < candidates.Count && suggested.Count < limit; start += limit * 2)
{
var chunk = candidates.Skip(start).Take(limit * 2).ToList();
var accounts = await _mapper.Accounts(chunk.Select(c => c.AccountId), token);
var hidden = await Hidden.AuthorsHiddenFrom(MyId, accounts.Values.Select(a => a.Uri), forNotifications: false, token);
foreach (var candidate in chunk)
{
if (accounts.GetValueOrDefault(candidate.AccountId) is not { } account || account.Limited == true || hidden.Contains(account.Uri)
|| account.Group && candidate.Source != Suggestions.Staff)
continue;
suggested.Add(new Suggestion { Source = candidate.Source, Sources = candidate.Sources, Account = account });
if (suggested.Count >= limit)
break;
}
}
return suggested;
}
}
}