Communities: - a post addressed to a community (to, cc or audience) is accepted according to its posting policy - followers, anyone, or moderators - and GroupDistributor announces the whole activity with `audience` to the community's followers, plus the object for new posts so Mastodon shows them; updates and deletes of community content are announced too; - top-level posts are Pages with a name (the title, or a headline from the text); /flock counts members, /wardens lists moderators; - a Mastodon client posts into a community by mentioning it, or into a remote group, which sets `audience`; - an Announce of an activity from a remote group a persona follows (Lemmy) is followed through: the object is fetched from its own origin, kept with its AudienceURI, and fanned out to the group's local followers; updates are applied in place and deletes checked against the origin. Circles stop being local-only: an undiscoverable Group actor whose follows are all requests the owner approves; posts addressed to the circle and its /flock and delivered to members' own inboxes, never announced, never public; a remote member's post into the circle is accepted from members only. SignedFetchAuthorizer serves circle posts and collections only to a signed request from a member or a member server's instance actor - 404 for anyone else. Circles never surface in search, lookups, mentions, account ids or profile pages. Federation:SecureMode requires a valid signature on every GET under /peasants except the instance actor. Group forms take a posting policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
400 lines
20 KiB
C#
400 lines
20 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;
|
|
if (Params.Bool("hide_collections") is { } hide)
|
|
avatar.Settings.HideCollections = hide;
|
|
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]");
|
|
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, Kind: not LocalActorKind.Application } ? 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).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(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)
|
|
return Json(Array.Empty<Status>());
|
|
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) && !p.DeletedAt.HasValue).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 != default
|
|
? _dbEntities.Posts.Match(p => p.GroupUserId == local.Id && !p.IsFederatedCopy && !p.DeletedAt.HasValue)
|
|
: _dbEntities.Posts.Match(p => p.ActorURI == remote.ActorURI && p.IsFederatedCopy && !p.DeletedAt.HasValue);
|
|
var viewerFollows = MyId != default && local != default
|
|
&& await _dbEntities.Followings.Match(f => f.AvatarId == MyId && f.TargetAccountId == local.Id && 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)
|
|
query.Match(p => p.InReplyToURI == null);
|
|
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 _dbEntities.Followers.Match(f => f.LocalActorId == local.Id && f.LocalActorKind == local.Kind && f.IsAccepted)
|
|
.Sort(f => f.ID, Order.Descending).Limit(Limit(40, 80)).ExecuteAsync(token);
|
|
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 _dbEntities.Followings.Match(f => f.AvatarId == local.Id && f.State == FollowState.Accepted)
|
|
.Sort(f => f.ID, Order.Descending).Limit(Limit(40, 80)).ExecuteAsync(token);
|
|
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 DB.Default.DeleteAsync<Follower>(f => f.LocalActorId == Me.Id && f.ActorURI == uri);
|
|
if (local != default)
|
|
await DB.Default.DeleteAsync<Following>(f => f.AvatarId == local.Id && f.TargetActorURI == Me.Uri);
|
|
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/{id}/lists"), Scope("read:lists")]
|
|
public IActionResult Lists(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);
|
|
var domainBlocked = remote != default && await DB.Default.Find<AccountDomainBlock>().Match(b => b.AvatarId == MyId && b.Domain == remote.Domain).ExecuteAnyAsync(token);
|
|
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 }
|
|
};
|
|
}
|
|
}
|
|
}
|