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
77 lines
3.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|