using PrivaPub.Api.Mastodon.Entities; using PrivaPub.Federation.Actors; using PrivaPub.StaticServices; namespace PrivaPub.Api.Mastodon.Mappers { public class AccountSearch { readonly MastodonMapper _mapper; readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IRemoteActorService _remoteActors; public AccountSearch(MastodonMapper mapper, DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors) { _mapper = mapper; _dbEntities = dbEntities; _localActors = localActors; _remoteActors = remoteActors; } public async Task> Find(string q, bool resolve, int limit, CancellationToken token) => await Find(q, resolve, limit, 0, token); // following=true: only accounts the persona follows, matched by the start of their name, display name or server // (what a client offers when adding to a list), read from what PrivaPub holds and never resolved public async Task> Followed(string viewerId, string q, int limit, CancellationToken token) { q = q?.Trim().TrimStart('@') ?? string.Empty; var ids = (await _dbEntities.Followings.Match(f => f.AvatarId == viewerId && f.State == Models.Social.FollowState.Accepted).ExecuteAsync(token)) .Select(f => f.TargetAccountId).Where(id => !string.IsNullOrEmpty(id)).ToList(); if (ids.Count == 0) return new List(); var parts = q.Split('@'); var localDomain = new Uri(_localActors.BaseAddress).Authority; bool Matches(string userName, string name, string domain) => userName?.StartsWith(parts[0], StringComparison.OrdinalIgnoreCase) == true && (parts.Length < 2 || domain?.StartsWith(parts[1], StringComparison.OrdinalIgnoreCase) == true) || parts.Length == 1 && name?.Contains(q, StringComparison.OrdinalIgnoreCase) == true; var found = (await _dbEntities.Avatars.Match(a => ids.Contains(a.ID) && !a.DeletionAt.HasValue).ExecuteAsync(token)) .Where(a => Matches(a.UserName, a.Name, localDomain)).Select(a => (a.UserName, a.ID)) .Concat((await _dbEntities.ForeignAvatars.Match(f => ids.Contains(f.ID) && !f.DeletionAt.HasValue).ExecuteAsync(token)) .Where(f => Matches(f.UserName, f.Name, f.Domain)).Select(f => (f.UserName, f.ID))) .OrderBy(a => a.UserName, StringComparer.OrdinalIgnoreCase).Take(limit).Select(a => a.ID).ToList(); var accounts = await _mapper.Accounts(found, token); return found.Where(accounts.ContainsKey).Select(id => accounts[id]).ToList(); } // gone accounts (remote ones that deleted themselves, personas of banned or deleted roots) are never found public async Task> Find(string q, bool resolve, int limit, int offset, CancellationToken token) { q = q?.Trim().TrimStart('@'); if (string.IsNullOrEmpty(q)) return new List(); var results = new List(); var parts = q.Split('@'); var localDomain = new Uri(_localActors.BaseAddress).Authority; if (parts.Length == 2 && !parts[1].Equals(localDomain, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(parts[0])) { var userName = parts[0]; var domain = parts[1].ToLowerInvariant(); var known = await _dbEntities.ForeignAvatars.Match(f => f.UserName == userName && f.Domain == domain && !f.DeletionAt.HasValue).ExecuteFirstAsync(token); if (known == default && resolve) { var actorUri = await _remoteActors.ResolveHandle(q, token); known = actorUri == default ? default : await _remoteActors.GetActor(actorUri, refresh: false, token); } if (known is { DeletionAt: null } && offset == 0) results.Add(_mapper.Foreign(known)); return results; } var prefix = parts[0].ToLowerInvariant(); var barred = await Domain.Privacy.Counted.BarredPersonas(token); var locals = await _dbEntities.Avatars .Match(a => a.UserName.StartsWith(prefix) && !a.DeletionAt.HasValue && a.Settings.IsDiscoverable != false && !barred.Contains(a.ID)) .Sort(a => a.UserName, MongoDB.Entities.Order.Ascending) .Limit(offset + limit).ExecuteAsync(token); foreach (var avatar in locals.Skip(offset)) results.Add(await _mapper.Local(_localActors.FromAvatar(avatar), false, token)); var skipRemote = Math.Max(0, offset - locals.Count); if (results.Count < limit) foreach (var foreign in await _dbEntities.ForeignAvatars.Match(f => f.UserName.StartsWith(parts[0]) && f.IsDiscoverable && !f.DeletionAt.HasValue) .Sort(f => f.UserName, MongoDB.Entities.Order.Ascending) .Skip(skipRemote).Limit(limit - results.Count).ExecuteAsync(token)) results.Add(_mapper.Foreign(foreign)); return results; } } }