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 V1(CancellationToken token) => Json((await Suggested(token)).Select(s => s.Account).ToList()); [HttpGet("/api/v2/suggestions"), Scope("read")] public async Task V2(CancellationToken token) => Json(await Suggested(token)); [HttpDelete("/api/v1/suggestions/{accountId}"), Scope("read")] public async Task Dismiss(string accountId, CancellationToken token) { if (!string.IsNullOrEmpty(accountId) && accountId.Length <= 64) try { await DB.Default.Update().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> Suggested(CancellationToken token) { var limit = Limit(40, 80); var candidates = await Suggestions.For(Me, _settings.Current.StaffPicks, token); var suggested = new List(); 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; } } }