Lemmy takes a report only from a person or a service, about one post or comment, addressed to its community, and it answered PrivaPub's Flag (the instance actor's, an Application, with no `to` and the account and posts as its object) 400. A report of a post or comment in a community on a server whose NodeInfo names Lemmy now leaves from `privapub_reports`, a Service with its own key that names nobody: one Flag per post, `to` the community (its own audience, else its thread's), with the persona's words, or the category, in `summary` and `content`, sent to the community's inbox. This is the second exception to "a server's software is for display" (owner decision 2026-10-06, `ReportService.ServiceReportTakers`). Every other server keeps the instance actor's report. An account alone is not reported to Lemmy, which takes no such report, and `forwarded` now says whether anything left. The reporter is read unsigned in SecureMode and answers WebFinger like the instance actor. Nobody follows or mentions it, the Mastodon API has no account for it, and a migration reserves its name. Checked live: Lemmy 1.0 and 0.19 keep the reports of a thread and of a comment, with alice's words, from "Reports from privapub.test", and none names her (69 checks). A sweep of every scenario with this and the next commit: 876 checks pass; Ghost's Network feed listed alice's post too late once, and Ghost passes alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
414 lines
21 KiB
C#
414 lines
21 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
|
|
using MongoDB.Entities;
|
|
|
|
using PrivaPub.Api.Mastodon.Entities;
|
|
using PrivaPub.Api.Mastodon.Infrastructure;
|
|
using PrivaPub.Api.Mastodon.Mappers;
|
|
using PrivaPub.Domain.Media;
|
|
using PrivaPub.Domain.Privacy;
|
|
using PrivaPub.Domain.Relationships;
|
|
using PrivaPub.Domain.Social;
|
|
using PrivaPub.Federation.Actors;
|
|
using PrivaPub.Federation.Outbox;
|
|
using PrivaPub.Models.Federation;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.Models.Social;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using PostEntity = PrivaPub.Models.Post.Post;
|
|
|
|
namespace PrivaPub.Api.Mastodon.Controllers
|
|
{
|
|
public class AccountsController : MastodonController
|
|
{
|
|
readonly MastodonMapper _mapper;
|
|
readonly DbEntities _dbEntities;
|
|
readonly ILocalActorService _localActors;
|
|
readonly IRemoteActorService _remoteActors;
|
|
readonly IFollowService _follows;
|
|
readonly IOutboxPublisher _outbox;
|
|
readonly IRelationshipService _relationships;
|
|
|
|
public AccountsController(MastodonMapper mapper, DbEntities dbEntities, ILocalActorService localActors, IRemoteActorService remoteActors,
|
|
IFollowService follows, IOutboxPublisher outbox, IRelationshipService relationships)
|
|
{
|
|
_relationships = relationships;
|
|
_mapper = mapper;
|
|
_dbEntities = dbEntities;
|
|
_localActors = localActors;
|
|
_remoteActors = remoteActors;
|
|
_follows = follows;
|
|
_outbox = outbox;
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/verify_credentials"), Scope("read:accounts")]
|
|
public async Task<IActionResult> VerifyCredentials(CancellationToken token) => Json(await _mapper.Local(Me, withSource: true, token));
|
|
|
|
[HttpPatch("/api/v1/accounts/update_credentials"), Scope("write:accounts"), RequestSizeLimit(20 * 1024 * 1024),
|
|
RequestFormLimits(MultipartBodyLengthLimit = 20 * 1024 * 1024)]
|
|
public async Task<IActionResult> UpdateCredentials([FromServices] IMediaService media, CancellationToken token)
|
|
{
|
|
var avatar = await _dbEntities.Avatars.MatchID(Me.Id).ExecuteFirstAsync(token);
|
|
if (Request.HasFormContentType)
|
|
{
|
|
var form = await Request.ReadFormAsync(token);
|
|
if (form.Files["avatar"] is { } picture && await media.ProfileImage(picture, 400, 400, token) is { } pictureUrl)
|
|
avatar.PictureURL = pictureUrl;
|
|
if (form.Files["header"] is { } header && await media.ProfileImage(header, 1500, 500, token) is { } headerUrl)
|
|
avatar.ThumbnailURL = headerUrl;
|
|
}
|
|
if (Params.Has("display_name"))
|
|
avatar.Name = Params.Get("display_name")?.Trim();
|
|
if (Params.Has("note"))
|
|
avatar.Biography = Params.Get("note");
|
|
if (Params.Bool("locked") is { } locked)
|
|
avatar.Settings.IsLocked = locked;
|
|
if (Params.Bool("bot") is { } bot)
|
|
avatar.Settings.IsBot = bot;
|
|
if (Params.Bool("discoverable") is { } discoverable)
|
|
avatar.Settings.IsDiscoverable = discoverable;
|
|
if (Params.Bool("indexable") is { } indexable)
|
|
avatar.Settings.IsIndexable = indexable;
|
|
// hide_collections stays true: who follows whom is never listed here, only counted (FEDERATION.md)
|
|
if (Params.Get("source[privacy]") is { } privacy && privacy is "public" or "unlisted" or "private")
|
|
avatar.Settings.DefaultVisibility = privacy;
|
|
if (Params.Bool("source[sensitive]") is { } sensitive)
|
|
avatar.Settings.DefaultSensitive = sensitive;
|
|
if (Params.Has("source[language]"))
|
|
avatar.Settings.DefaultLanguage = Params.Get("source[language]");
|
|
if (Params.Get("source[quote_policy]") is { } quotePolicy && QuotePolicies.IsKnown(quotePolicy))
|
|
avatar.Settings.QuotePolicy = quotePolicy;
|
|
var fields = new Dictionary<string, string>();
|
|
for (var i = 0; i < 4; i++)
|
|
{
|
|
var name = Params.Get($"fields_attributes[{i}][name]") ?? Params.Get($"fields_attributes[][name]");
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
fields[name.Trim()] = Params.Get($"fields_attributes[{i}][value]")?.Trim() ?? string.Empty;
|
|
}
|
|
if (Params.List("fields_attributes[][name]").Count > 0)
|
|
{
|
|
var names = Params.List("fields_attributes[][name]");
|
|
var values = Params.List("fields_attributes[][value]");
|
|
fields = names.Select((n, i) => (n, v: i < values.Count ? values[i] : string.Empty)).Where(f => !string.IsNullOrWhiteSpace(f.n))
|
|
.Take(4).ToDictionary(f => f.n.Trim(), f => f.v.Trim());
|
|
}
|
|
if (fields.Count > 0 || Params.Has("fields_attributes[0][name]"))
|
|
avatar.Fields = fields;
|
|
avatar.UpdatedAt = DateTime.UtcNow;
|
|
await DB.Default.SaveAsync(avatar, token);
|
|
|
|
var actor = _localActors.FromAvatar(avatar);
|
|
await _outbox.PublishProfile(actor, token);
|
|
return Json(await _mapper.Local(actor, withSource: true, token));
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/lookup"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public async Task<IActionResult> Lookup(CancellationToken token)
|
|
{
|
|
var acct = Params.Get("acct")?.Trim().TrimStart('@');
|
|
var parts = acct?.Split('@');
|
|
if (parts is not { Length: 1 or 2 } || string.IsNullOrEmpty(parts[0]))
|
|
return NotFoundError();
|
|
var localDomain = new Uri(_localActors.BaseAddress).Authority;
|
|
if (parts.Length == 1 || parts[1].Equals(localDomain, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var local = await _localActors.FindByUserName(parts[0], token);
|
|
return local is { IsFederated: true, IsCircle: false, IsServerActor: false } ? Json(await _mapper.Local(local, false, token)) : NotFoundError();
|
|
}
|
|
var userName = parts[0];
|
|
var domain = parts[1].ToLowerInvariant();
|
|
var foreign = await _dbEntities.ForeignAvatars.Match(f => f.UserName == userName && f.Domain == domain && !f.DeletionAt.HasValue).ExecuteFirstAsync(token);
|
|
return foreign == default ? NotFoundError() : Json(_mapper.Foreign(foreign));
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/relationships"), Scope("read:follows")]
|
|
public async Task<IActionResult> Relationships(CancellationToken token)
|
|
{
|
|
var relationships = new List<Relationship>();
|
|
foreach (var id in Params.List("id").Distinct().Take(40))
|
|
relationships.Add(await Relationship(id, token));
|
|
return Json(relationships);
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/search"), Scope("read:accounts")]
|
|
public async Task<IActionResult> Search([FromServices] AccountSearch search, CancellationToken token) =>
|
|
Json(Params.Bool("following") == true && MyId != default
|
|
? await search.Followed(MyId, Params.Get("q"), Limit(), token)
|
|
: await search.Find(Params.Get("q"), Params.Bool("resolve") == true && MyId != default, Limit(), token));
|
|
|
|
[HttpGet("/api/v1/accounts/{id}"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public async Task<IActionResult> Get(string id, CancellationToken token)
|
|
{
|
|
var account = await _mapper.Account(id, token);
|
|
return account == default ? NotFoundError() : Json(account);
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/{id}/statuses"), Scope("read:statuses", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public async Task<IActionResult> Statuses(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default)
|
|
return NotFoundError();
|
|
if (Params.Bool("pinned") == true)
|
|
{
|
|
if (local == default)
|
|
{
|
|
var featured = (await DB.Default.Find<RemoteFeatured>().Match(f => f.ActorURI == remote.ActorURI).Sort(f => f.Position, Order.Ascending)
|
|
.Limit(Federation.Actors.FeaturedPosts.MaxFeatured).ExecuteAsync(token)).Select(f => f.PostId).ToList();
|
|
var featuredPosts = await _dbEntities.Posts.Match(p => featured.Contains(p.ID) && !p.DeletedAt.HasValue).Match(VisibilityPolicy.IsPublic).ExecuteAsync(token);
|
|
return Json(await _mapper.Statuses(featured.Select(id => featuredPosts.FirstOrDefault(p => p.ID == id)).Where(p => p != default).ToList(), MyId, token));
|
|
}
|
|
var pinIds = (await DB.Default.Find<Pin>().Match(p => p.AvatarId == local.Id).Sort(p => p.ID, Order.Descending).ExecuteAsync(token)).Select(p => p.PostId).ToList();
|
|
var pinnedPosts = await _dbEntities.Posts.Match(p => pinIds.Contains(p.ID)).Match(VisibilityPolicy.IsPublic).ExecuteAsync(token);
|
|
return Json(await _mapper.Statuses(pinIds.Select(id => pinnedPosts.FirstOrDefault(p => p.ID == id)).Where(p => p != default).ToList(), MyId, token));
|
|
}
|
|
|
|
var query = local is { Kind: LocalActorKind.Group }
|
|
? _dbEntities.Posts.Match(p => p.GroupId == local.Id).Match(VisibilityPolicy.IsShown)
|
|
: local != default
|
|
? _dbEntities.Posts.Match(p => p.GroupUserId == local.Id && !p.IsFederatedCopy).Match(VisibilityPolicy.IsShown)
|
|
: _dbEntities.Posts.Match(p => p.ActorURI == remote.ActorURI && p.IsFederatedCopy).Match(VisibilityPolicy.IsShown);
|
|
var viewerFollows = MyId != default && (local != default
|
|
? await _dbEntities.Followings.Match(f => f.AvatarId == MyId && f.TargetAccountId == local.Id && f.State == FollowState.Accepted).ExecuteAnyAsync(token)
|
|
: await _dbEntities.Followings.Match(f => f.AvatarId == MyId && f.TargetActorURI == remote.ActorURI && f.State == FollowState.Accepted).ExecuteAnyAsync(token));
|
|
if (MyId != id)
|
|
query.Match(viewerFollows
|
|
? p => p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted || p.Visibility == PostVisibility.FollowersOnly
|
|
: p => p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted);
|
|
else
|
|
query.Match(p => p.Visibility != PostVisibility.LocalGeo);
|
|
if (Params.Bool("exclude_replies") == true)
|
|
{
|
|
var self = local?.Id ?? remote.ID;
|
|
query.Match(p => p.InReplyToURI == null || p.InReplyToAccountId == self);
|
|
}
|
|
if (Params.Bool("exclude_reblogs") == true)
|
|
query.Match(p => p.ReblogOfPostId == null);
|
|
if (Params.Bool("only_media") == true)
|
|
query.Match(p => p.Media.Count > 0);
|
|
if (Params.Get("tagged") is { } tag)
|
|
query.Match(p => p.Tags.Contains(tag.ToLowerInvariant()));
|
|
|
|
var posts = await Page.From(Params, Limit()).Fetch(query, p => p.ID, token);
|
|
var statuses = await _mapper.Statuses(posts, MyId, token);
|
|
Link($"/api/v1/accounts/{id}/statuses", posts.LastOrDefault()?.ID, posts.FirstOrDefault()?.ID);
|
|
return Json(statuses);
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/{id}/followers"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public async Task<IActionResult> Followers(string id, CancellationToken token)
|
|
{
|
|
var (local, _) = await Find(id, token);
|
|
if (local == default || local.Id != MyId)
|
|
return Json(Array.Empty<Account>());
|
|
var followers = await Page.From(Params, Limit(40, 80))
|
|
.Fetch(_dbEntities.Followers.Match(f => f.LocalActorId == local.Id && f.LocalActorKind == local.Kind && f.IsAccepted), f => f.ID, token);
|
|
Link($"/api/v1/accounts/{id}/followers", followers.LastOrDefault()?.ID, followers.FirstOrDefault()?.ID);
|
|
return Json(await AccountsFor(followers.Select(f => f.ActorURI), token));
|
|
}
|
|
|
|
[HttpGet("/api/v1/accounts/{id}/following"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public async Task<IActionResult> Following(string id, CancellationToken token)
|
|
{
|
|
var (local, _) = await Find(id, token);
|
|
if (local == default || local.Id != MyId)
|
|
return Json(Array.Empty<Account>());
|
|
var following = await Page.From(Params, Limit(40, 80))
|
|
.Fetch(_dbEntities.Followings.Match(f => f.AvatarId == local.Id && f.State == FollowState.Accepted), f => f.ID, token);
|
|
Link($"/api/v1/accounts/{id}/following", following.LastOrDefault()?.ID, following.FirstOrDefault()?.ID);
|
|
var accounts = await _mapper.Accounts(following.Select(f => f.TargetAccountId), token);
|
|
return Json(following.Select(f => accounts.GetValueOrDefault(f.TargetAccountId)).Where(a => a != default).ToList());
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/follow"), Scope("write:follows")]
|
|
public async Task<IActionResult> Follow(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default)
|
|
return NotFoundError();
|
|
var following = await _follows.FollowAs(Me, local?.Uri ?? remote.ActorURI, Params.Bool("reblogs") != false, token);
|
|
return following == default ? Error(StatusCodes.Status403Forbidden, "This action is not allowed") : Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/unfollow"), Scope("write:follows")]
|
|
public async Task<IActionResult> Unfollow(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default)
|
|
return NotFoundError();
|
|
await _follows.UnfollowAs(Me, local?.Uri ?? remote.ActorURI, token);
|
|
return Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/remove_from_followers"), Scope("write:follows")]
|
|
public async Task<IActionResult> RemoveFromFollowers(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
var uri = local?.Uri ?? remote?.ActorURI;
|
|
if (uri == default)
|
|
return NotFoundError();
|
|
await _relationships.RemoveFollower(Me, uri, id, token);
|
|
return Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/block"), Scope("write:blocks")]
|
|
public async Task<IActionResult> Block(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default || local?.Id == MyId)
|
|
return NotFoundError();
|
|
await _relationships.Block(Me, local?.Uri ?? remote.ActorURI, id, token);
|
|
return Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/unblock"), Scope("write:blocks")]
|
|
public async Task<IActionResult> Unblock(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default)
|
|
return NotFoundError();
|
|
await _relationships.Unblock(Me, local?.Uri ?? remote.ActorURI, token);
|
|
return Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/mute"), Scope("write:mutes")]
|
|
public async Task<IActionResult> Mute(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default || local?.Id == MyId)
|
|
return NotFoundError();
|
|
var duration = Params.Int("duration") is { } seconds and > 0 ? TimeSpan.FromSeconds(seconds) : (TimeSpan?)null;
|
|
await _relationships.Mute(Me, local?.Uri ?? remote.ActorURI, id, Params.Bool("notifications") != false, duration, token);
|
|
return Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/accounts/{id}/unmute"), Scope("write:mutes")]
|
|
public async Task<IActionResult> Unmute(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
if (local == default && remote == default)
|
|
return NotFoundError();
|
|
await _relationships.Unmute(Me, local?.Uri ?? remote.ActorURI, token);
|
|
return Json(await Relationship(id, token));
|
|
}
|
|
|
|
[HttpGet("/api/v1/blocks"), Scope("read:blocks")]
|
|
public async Task<IActionResult> Blocks(CancellationToken token)
|
|
{
|
|
var blocks = await Page.From(Params, Limit(40, 80)).Fetch(DB.Default.Find<Block>().Match(b => b.AvatarId == MyId), b => b.ID, token);
|
|
var accounts = await _mapper.Accounts(blocks.Select(b => b.TargetAccountId), token);
|
|
Link("/api/v1/blocks", blocks.LastOrDefault()?.ID, blocks.FirstOrDefault()?.ID);
|
|
return Json(blocks.Select(b => accounts.GetValueOrDefault(b.TargetAccountId)).Where(a => a != default).ToList());
|
|
}
|
|
|
|
[HttpGet("/api/v1/mutes"), Scope("read:mutes")]
|
|
public async Task<IActionResult> Mutes(CancellationToken token)
|
|
{
|
|
var mutes = await Page.From(Params, Limit(40, 80)).Fetch(DB.Default.Find<Mute>().Match(m => m.AvatarId == MyId), m => m.ID, token);
|
|
var accounts = await _mapper.Accounts(mutes.Select(m => m.TargetAccountId), token);
|
|
Link("/api/v1/mutes", mutes.LastOrDefault()?.ID, mutes.FirstOrDefault()?.ID);
|
|
return Json(mutes.Select(m => accounts.GetValueOrDefault(m.TargetAccountId)).Where(a => a != default).ToList());
|
|
}
|
|
|
|
[HttpGet("/api/v1/domain_blocks"), Scope("read:blocks")]
|
|
public async Task<IActionResult> DomainBlocks(CancellationToken token) =>
|
|
Json((await DB.Default.Find<AccountDomainBlock>().Match(b => b.AvatarId == MyId).Sort(b => b.Domain, Order.Ascending).ExecuteAsync(token)).Select(b => b.Domain).ToList());
|
|
|
|
[HttpPost("/api/v1/domain_blocks"), Scope("write:blocks")]
|
|
public async Task<IActionResult> BlockDomain(CancellationToken token)
|
|
{
|
|
await _relationships.BlockDomain(Me, Params.Get("domain"), token);
|
|
return Json(new { });
|
|
}
|
|
|
|
[HttpDelete("/api/v1/domain_blocks"), Scope("write:blocks")]
|
|
public async Task<IActionResult> UnblockDomain(CancellationToken token)
|
|
{
|
|
await _relationships.UnblockDomain(Me, Params.Get("domain"), token);
|
|
return Json(new { });
|
|
}
|
|
|
|
[HttpGet("/api/v1/follow_requests"), Scope("read:follows")]
|
|
public async Task<IActionResult> FollowRequests(CancellationToken token)
|
|
{
|
|
var requests = await _dbEntities.Followers.Match(f => f.LocalActorId == Me.Id && f.LocalActorKind == LocalActorKind.Person && !f.IsAccepted)
|
|
.Sort(f => f.ID, Order.Descending).Limit(Limit(40, 80)).ExecuteAsync(token);
|
|
return Json(await AccountsFor(requests.Select(r => r.ActorURI), token));
|
|
}
|
|
|
|
[HttpPost("/api/v1/follow_requests/{id}/authorize"), Scope("write:follows")]
|
|
public async Task<IActionResult> Authorize(string id, CancellationToken token) =>
|
|
await _follows.Decide(Me, id, accept: true, token) ? Json(await Relationship(id, token)) : NotFoundError();
|
|
|
|
[HttpPost("/api/v1/follow_requests/{id}/reject"), Scope("write:follows")]
|
|
public async Task<IActionResult> Reject(string id, CancellationToken token) =>
|
|
await _follows.Decide(Me, id, accept: false, token) ? Json(await Relationship(id, token)) : NotFoundError();
|
|
|
|
[HttpGet("/api/v1/accounts/{id}/featured_tags"), Scope("read:accounts", requiresUser: false), Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
|
public IActionResult FeaturedTags(string id) => Json(Array.Empty<object>());
|
|
|
|
[HttpGet("/api/v1/accounts/familiar_followers"), Scope("read:follows")]
|
|
public IActionResult FamiliarFollowers() => Json(Params.List("id").Select(id => new { id, accounts = Array.Empty<Account>() }).ToList());
|
|
|
|
async Task<List<Account>> AccountsFor(IEnumerable<string> actorUris, CancellationToken token)
|
|
{
|
|
var accounts = new List<Account>();
|
|
foreach (var uri in actorUris)
|
|
{
|
|
var local = await _localActors.FindByUri(uri, token);
|
|
if (local != default)
|
|
{
|
|
accounts.Add(await _mapper.Local(local, false, token));
|
|
continue;
|
|
}
|
|
var foreign = await _dbEntities.ForeignAvatars.Match(f => f.ActorURI == uri).ExecuteFirstAsync(token);
|
|
if (foreign != default)
|
|
accounts.Add(_mapper.Foreign(foreign));
|
|
}
|
|
return accounts;
|
|
}
|
|
|
|
async Task<(LocalActor Local, ForeignAvatar Remote)> Find(string id, CancellationToken token)
|
|
{
|
|
var avatar = await _dbEntities.Avatars.MatchID(id).ExecuteFirstAsync(token);
|
|
if (avatar is { DeletionAt: null })
|
|
return (_localActors.FromAvatar(avatar), default);
|
|
var group = await _dbEntities.Groups.MatchID(id).ExecuteFirstAsync(token);
|
|
if (group is { DeletionAt: null } && _localActors.FromGroup(group) is { IsFederated: true, IsCircle: false } community)
|
|
return (community, default);
|
|
return (default, await _dbEntities.ForeignAvatars.MatchID(id).ExecuteFirstAsync(token));
|
|
}
|
|
|
|
async Task<Relationship> Relationship(string id, CancellationToken token)
|
|
{
|
|
var (local, remote) = await Find(id, token);
|
|
var uri = local?.Uri ?? remote?.ActorURI;
|
|
var following = uri == default ? default : await _dbEntities.Followings.Match(f => f.AvatarId == MyId && f.TargetActorURI == uri).ExecuteFirstAsync(token);
|
|
var followedBy = uri == default ? default : await _dbEntities.Followers.Match(f => f.LocalActorId == MyId && f.ActorURI == uri).ExecuteFirstAsync(token);
|
|
var blocking = uri != default && await DB.Default.Find<Block>().Match(b => b.AvatarId == MyId && b.TargetActorURI == uri).ExecuteAnyAsync(token);
|
|
var mute = uri == default ? default : await DB.Default.Find<Mute>().Match(m => m.AvatarId == MyId && m.TargetActorURI == uri).ExecuteFirstAsync(token);
|
|
var blockedBy = local != default
|
|
? await DB.Default.Find<Block>().Match(b => b.AvatarId == local.Id && b.TargetActorURI == Me.Uri).ExecuteAnyAsync(token)
|
|
: remote != default && await DB.Default.Find<BlockedBy>().Match(b => b.AvatarId == MyId && b.ActorURI == remote.ActorURI).ExecuteAnyAsync(token);
|
|
var domainBlocked = remote != default && (await DB.Default.Find<AccountDomainBlock>().Match(b => b.AvatarId == MyId).ExecuteAsync(token))
|
|
.Any(b => Hidden.Blocks(b.Domain, remote.ActorURI, remote.Domain));
|
|
return new Relationship
|
|
{
|
|
Id = id,
|
|
Blocking = blocking,
|
|
BlockedBy = blockedBy,
|
|
Muting = mute != default && (mute.ExpiresAt == default || mute.ExpiresAt > DateTime.UtcNow),
|
|
MutingNotifications = mute?.HideNotifications == true,
|
|
DomainBlocking = domainBlocked,
|
|
Following = following?.State == FollowState.Accepted,
|
|
Requested = following?.State == FollowState.Requested,
|
|
ShowingReblogs = following?.ShowReblogs ?? false,
|
|
FollowedBy = followedBy?.IsAccepted == true,
|
|
RequestedBy = followedBy is { IsAccepted: false }
|
|
};
|
|
}
|
|
}
|
|
}
|