using Microsoft.AspNetCore.Mvc; using PrivaPub.Api.Mastodon.Entities; using PrivaPub.Api.Mastodon.Infrastructure; using PrivaPub.Api.Mastodon.Mappers; using PrivaPub.Domain.Privacy; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Inbox; using PrivaPub.Federation.Outbox; using PrivaPub.Domain.Social; using PrivaPub.Models.Post; using PrivaPub.StaticServices; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Api.Mastodon.Controllers { public class SearchController : MastodonController { readonly MastodonMapper _mapper; readonly DbEntities _dbEntities; readonly ILocalActorService _localActors; readonly IRemoteActorService _remoteActors; readonly IRemotePosts _remotePosts; readonly AccountSearch _search; public SearchController(MastodonMapper mapper, DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors, IRemotePosts remotePosts, AccountSearch search) { _mapper = mapper; _dbEntities = dbEntities; _localActors = localActors; _remoteActors = remoteActors; _remotePosts = remotePosts; _search = search; } [HttpGet("/api/v2/search"), Scope("read:search", requiresUser: false)] public async Task Search(CancellationToken token) { var q = Params.Get("q")?.Trim(); var type = Params.Get("type"); var resolve = Params.Bool("resolve") == true && MyId != default; var results = new SearchResults(); if (string.IsNullOrEmpty(q)) return Json(results); if (q.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { var post = await _dbEntities.Posts.Match(p => (p.ObjectURI == q || p.Url == q) && !p.DeletedAt.HasValue).ExecuteFirstAsync(token); if (post == default && resolve && type is null or "statuses") post = await _remotePosts.StoreContext(q, 0, token); if (post != default && type is null or "statuses" && await VisibilityPolicy.CanSee(post, MyId, token)) results.Statuses.Add(await _mapper.Status(post, MyId, token)); if (results.Statuses.Count == 0 && type is null or "accounts") { var local = await _localActors.FindByUri(q, token); var foreign = local == default ? (resolve ? await _remoteActors.GetActor(q, refresh: false, token) : default) : default; if (local is { IsFederated: true }) results.Accounts.Add(await _mapper.Local(local, false, token)); else if (foreign != default) results.Accounts.Add(_mapper.Foreign(foreign)); } return Json(results); } if (type is null or "accounts") { results.Accounts = await _search.Find(q, resolve, Limit(), token); } if (type is null or "hashtags" && q.TrimStart('#') is { Length: > 0 } tag) results.Hashtags.Add(new { name = tag.ToLowerInvariant(), url = $"{_localActors.BaseAddress}/tags/{Uri.EscapeDataString(tag.ToLowerInvariant())}", history = Array.Empty() }); return Json(results); } } public class StubsController : MastodonController { [HttpGet("/api/v1/custom_emojis"), Microsoft.AspNetCore.Authorization.AllowAnonymous] public IActionResult CustomEmojis() => Json(Array.Empty()); [HttpGet("/api/v1/filters"), Scope("read:filters")] public IActionResult FiltersV1() => Json(Array.Empty()); [HttpGet("/api/v2/filters"), Scope("read:filters")] public IActionResult FiltersV2() => Json(Array.Empty()); [HttpGet("/api/v1/lists"), Scope("read:lists")] public IActionResult Lists() => Json(Array.Empty()); [HttpGet("/api/v1/announcements"), Microsoft.AspNetCore.Authorization.AllowAnonymous] public IActionResult Announcements() => Json(Array.Empty()); [HttpGet("/api/v1/trends"), Microsoft.AspNetCore.Authorization.AllowAnonymous] public IActionResult Trends() => Json(Array.Empty()); [HttpGet("/api/v1/trends/tags"), Microsoft.AspNetCore.Authorization.AllowAnonymous] public IActionResult TrendTags() => Json(Array.Empty()); [HttpGet("/api/v1/trends/statuses"), Microsoft.AspNetCore.Authorization.AllowAnonymous] public IActionResult TrendStatuses() => Json(Array.Empty()); [HttpGet("/api/v1/trends/links"), Microsoft.AspNetCore.Authorization.AllowAnonymous] public IActionResult TrendLinks() => Json(Array.Empty()); [HttpGet("/api/v1/suggestions"), Scope("read")] public IActionResult SuggestionsV1() => Json(Array.Empty()); [HttpGet("/api/v2/suggestions"), Scope("read")] public IActionResult SuggestionsV2() => Json(Array.Empty()); [HttpGet("/api/v1/followed_tags"), Scope("read:follows")] public IActionResult FollowedTags() => Json(Array.Empty()); [HttpGet("/api/v1/endorsements"), Scope("read:accounts")] public IActionResult Endorsements() => Json(Array.Empty()); [HttpGet("/api/v1/featured_tags"), Scope("read:accounts")] public IActionResult FeaturedTags() => Json(Array.Empty()); [HttpGet("/api/v1/scheduled_statuses"), Scope("read:statuses")] public IActionResult ScheduledStatuses() => Json(Array.Empty()); [HttpGet("/api/v1/preferences"), Scope("read:accounts")] public IActionResult Preferences() => Json(new Dictionary { ["posting:default:visibility"] = Me.Settings.DefaultVisibility ?? "public", ["posting:default:sensitive"] = Me.Settings.DefaultSensitive, ["posting:default:language"] = Me.Settings.DefaultLanguage, ["reading:expand:media"] = "default", ["reading:expand:spoilers"] = false }); [HttpGet("/api/v1/push/subscription"), Scope("push")] public IActionResult PushSubscription() => Error(StatusCodes.Status404NotFound, "Record not found"); } }