- Blocks are per persona and never federate (a Block activity would tell
the other server who blocked whom): the blocked account is removed as a
follower, with a Reject{Follow} if it is remote, unfollowed, cleared from
home and notifications, and refused with Reject if it follows again.
- Mutes (optionally timed, optionally sparing notifications) and per-
persona domain blocks keep authors out of home timelines, notifications
and every status list the API returns; the boosts of a hidden author's
posts are hidden too.
- Bookmarks and pins (at most five, public or unlisted, own posts) with
their Mastodon endpoints and flags; pinned posts are the actor's
`featured` collection at /trophies, and `featuredTags` points at
/tattoos.
- Reports: /api/v1/reports stores the report and, when forwarding to a
remote account, sends Flag from the instance actor, so the reporting
persona is never named to the other server. An inbound Flag about a local
persona or its posts becomes a report; moderators list and resolve them
under /clientapi/moderator/reports.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
138 lines
5.5 KiB
C#
138 lines
5.5 KiB
C#
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<IActionResult> 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<object>() });
|
|
return Json(results);
|
|
}
|
|
}
|
|
|
|
public class StubsController : MastodonController
|
|
{
|
|
[HttpGet("/api/v1/custom_emojis"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult CustomEmojis() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/filters"), Scope("read:filters")]
|
|
public IActionResult FiltersV1() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v2/filters"), Scope("read:filters")]
|
|
public IActionResult FiltersV2() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/lists"), Scope("read:lists")]
|
|
public IActionResult Lists() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/announcements"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult Announcements() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/trends"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult Trends() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/trends/tags"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult TrendTags() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/trends/statuses"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult TrendStatuses() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/trends/links"), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult TrendLinks() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/suggestions"), Scope("read")]
|
|
public IActionResult SuggestionsV1() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v2/suggestions"), Scope("read")]
|
|
public IActionResult SuggestionsV2() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/followed_tags"), Scope("read:follows")]
|
|
public IActionResult FollowedTags() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/endorsements"), Scope("read:accounts")]
|
|
public IActionResult Endorsements() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/featured_tags"), Scope("read:accounts")]
|
|
public IActionResult FeaturedTags() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/scheduled_statuses"), Scope("read:statuses")]
|
|
public IActionResult ScheduledStatuses() => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/preferences"), Scope("read:accounts")]
|
|
public IActionResult Preferences() => Json(new Dictionary<string, object>
|
|
{
|
|
["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");
|
|
}
|
|
}
|