Blocks, mutes, bookmarks, pins and reports
- 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
This commit is contained in:
1 parent
525b5368a1
commit
8f75317050
20 files changed
+813
-21
No files matched your search
@@ -6,6 +6,7 @@ using PrivaPub.Api.Mastodon.Entities;
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
using PrivaPub.Api.Mastodon.Mappers;
|
||||
using PrivaPub.Domain.Privacy;
|
||||
using PrivaPub.Domain.Relationships;
|
||||
using PrivaPub.Domain.Social;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Outbox;
|
||||
@@ -27,10 +28,12 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
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)
|
||||
IFollowService follows, IOutboxPublisher outbox, IRelationshipService relationships)
|
||||
{
|
||||
_relationships = relationships;
|
||||
_mapper = mapper;
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
@@ -136,7 +139,13 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
if (local == default && remote == default)
|
||||
return NotFoundError();
|
||||
if (Params.Bool("pinned") == true)
|
||||
return Json(Array.Empty<Status>());
|
||||
{
|
||||
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)
|
||||
@@ -220,6 +229,83 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
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)
|
||||
{
|
||||
@@ -280,9 +366,18 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using PrivaPub.Api.Mastodon.Infrastructure;
|
||||
using PrivaPub.Api.Mastodon.Mappers;
|
||||
using PrivaPub.Domain.Relationships;
|
||||
|
||||
namespace PrivaPub.Api.Mastodon.Controllers
|
||||
{
|
||||
public class ReportsController : MastodonController
|
||||
{
|
||||
readonly IReportService _reports;
|
||||
readonly MastodonMapper _mapper;
|
||||
|
||||
public ReportsController(IReportService reports, MastodonMapper mapper)
|
||||
{
|
||||
_reports = reports;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/reports"), Scope("write:reports")]
|
||||
public async Task<IActionResult> Create(CancellationToken token)
|
||||
{
|
||||
var report = await _reports.File(Me, Params.Get("account_id"), Params.List("status_ids"), Params.Get("comment"), Params.Get("category"),
|
||||
Params.Bool("forward") == true, token);
|
||||
if (report == default)
|
||||
return NotFoundError();
|
||||
return Json(new
|
||||
{
|
||||
id = report.ID,
|
||||
action_taken = false,
|
||||
action_taken_at = default(string),
|
||||
category = report.Category,
|
||||
comment = report.Comment ?? string.Empty,
|
||||
forwarded = report.Forwarded,
|
||||
created_at = MastodonJson.Time(report.CreatedAt),
|
||||
status_ids = report.PostIds,
|
||||
rule_ids = Array.Empty<string>(),
|
||||
target_account = await _mapper.Account(report.TargetAccountId, token)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,18 +112,6 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
[HttpGet("/api/v1/followed_tags"), Scope("read:follows")]
|
||||
public IActionResult FollowedTags() => Json(Array.Empty<object>());
|
||||
|
||||
[HttpGet("/api/v1/blocks"), Scope("read:blocks")]
|
||||
public IActionResult Blocks() => Json(Array.Empty<object>());
|
||||
|
||||
[HttpGet("/api/v1/mutes"), Scope("read:mutes")]
|
||||
public IActionResult Mutes() => Json(Array.Empty<object>());
|
||||
|
||||
[HttpGet("/api/v1/domain_blocks"), Scope("read:blocks")]
|
||||
public IActionResult DomainBlocks() => Json(Array.Empty<string>());
|
||||
|
||||
[HttpGet("/api/v1/bookmarks"), Scope("read:bookmarks")]
|
||||
public IActionResult Bookmarks() => Json(Array.Empty<object>());
|
||||
|
||||
[HttpGet("/api/v1/endorsements"), Scope("read:accounts")]
|
||||
public IActionResult Endorsements() => Json(Array.Empty<object>());
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
public class StatusesController : MastodonController
|
||||
{
|
||||
const int MaxContext = 200;
|
||||
public const int MaxPins = 5;
|
||||
|
||||
readonly IStatusService _statuses;
|
||||
readonly MastodonMapper _mapper;
|
||||
@@ -227,10 +228,41 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/statuses/{id}/bookmark"), Scope("write:bookmarks")]
|
||||
public Task<IActionResult> Bookmark(string id, CancellationToken token) => Unchanged(id, token);
|
||||
public async Task<IActionResult> Bookmark(string id, CancellationToken token)
|
||||
{
|
||||
var post = await Visible(id, token);
|
||||
if (post == default)
|
||||
return NotFoundError();
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new Models.Social.Bookmark { AvatarId = MyId, PostId = post.ID }, token);
|
||||
}
|
||||
catch (MongoDB.Driver.MongoWriteException ex) when (ex.WriteError?.Category == MongoDB.Driver.ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
}
|
||||
return Json(await _mapper.Status(post, MyId, token));
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/statuses/{id}/unbookmark"), Scope("write:bookmarks")]
|
||||
public Task<IActionResult> Unbookmark(string id, CancellationToken token) => Unchanged(id, token);
|
||||
public async Task<IActionResult> Unbookmark(string id, CancellationToken token)
|
||||
{
|
||||
await DB.Default.DeleteAsync<Models.Social.Bookmark>(b => b.AvatarId == MyId && b.PostId == id);
|
||||
return await Unchanged(id, token);
|
||||
}
|
||||
|
||||
[HttpGet("/api/v1/bookmarks"), Scope("read:bookmarks")]
|
||||
public async Task<IActionResult> Bookmarks(CancellationToken token)
|
||||
{
|
||||
var bookmarks = await Page.From(Params, Limit()).Fetch(DB.Default.Find<Models.Social.Bookmark>().Match(b => b.AvatarId == MyId), b => b.ID, token);
|
||||
var ids = bookmarks.Select(b => b.PostId).ToList();
|
||||
var posts = (await _dbEntities.Posts.Match(p => ids.Contains(p.ID) && !p.DeletedAt.HasValue).ExecuteAsync(token)).ToDictionary(p => p.ID);
|
||||
var visible = new List<PostEntity>();
|
||||
foreach (var postId in ids.Where(posts.ContainsKey))
|
||||
if (await VisibilityPolicy.CanSee(posts[postId], MyId, token))
|
||||
visible.Add(posts[postId]);
|
||||
Link("/api/v1/bookmarks", bookmarks.LastOrDefault()?.ID, bookmarks.FirstOrDefault()?.ID);
|
||||
return Json(await _mapper.Statuses(visible, MyId, token));
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/statuses/{id}/mute"), Scope("write:mutes")]
|
||||
public Task<IActionResult> Mute(string id, CancellationToken token) => Unchanged(id, token);
|
||||
@@ -239,10 +271,32 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
public Task<IActionResult> Unmute(string id, CancellationToken token) => Unchanged(id, token);
|
||||
|
||||
[HttpPost("/api/v1/statuses/{id}/pin"), Scope("write:accounts")]
|
||||
public IActionResult Pin(string id) => Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: Pinning is not supported yet");
|
||||
public async Task<IActionResult> Pin(string id, CancellationToken token)
|
||||
{
|
||||
var post = await _dbEntities.Posts.Match(p => p.ID == id && p.GroupUserId == MyId && !p.IsFederatedCopy && !p.DeletedAt.HasValue && p.ReblogOfPostId == null)
|
||||
.ExecuteFirstAsync(token);
|
||||
if (post == default)
|
||||
return NotFoundError();
|
||||
if (post.Visibility is not (PostVisibility.Public or PostVisibility.Unlisted))
|
||||
return Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: Only public posts can be pinned");
|
||||
if (await DB.Default.CountAsync<Models.Social.Pin>(p => p.AvatarId == MyId, token) >= MaxPins)
|
||||
return Error(StatusCodes.Status422UnprocessableEntity, $"Validation failed: You can pin at most {MaxPins} posts");
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new Models.Social.Pin { AvatarId = MyId, PostId = post.ID }, token);
|
||||
}
|
||||
catch (MongoDB.Driver.MongoWriteException ex) when (ex.WriteError?.Category == MongoDB.Driver.ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
}
|
||||
return Json(await _mapper.Status(post, MyId, token));
|
||||
}
|
||||
|
||||
[HttpPost("/api/v1/statuses/{id}/unpin"), Scope("write:accounts")]
|
||||
public Task<IActionResult> Unpin(string id, CancellationToken token) => Unchanged(id, token);
|
||||
public async Task<IActionResult> Unpin(string id, CancellationToken token)
|
||||
{
|
||||
await DB.Default.DeleteAsync<Models.Social.Pin>(p => p.AvatarId == MyId && p.PostId == id);
|
||||
return await Unchanged(id, token);
|
||||
}
|
||||
|
||||
async Task<IActionResult> Toggle(Task<StatusOutcome> action, string id, CancellationToken token)
|
||||
{
|
||||
|
||||
@@ -147,6 +147,13 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
var favourited = viewerId == default
|
||||
? new HashSet<string>()
|
||||
: (await _dbEntities.Favourites.Match(f => f.AccountId == viewerId && ids.Contains(f.PostId)).ExecuteAsync(token)).Select(f => f.PostId).ToHashSet();
|
||||
var bookmarked = viewerId == default
|
||||
? new HashSet<string>()
|
||||
: (await DB.Default.Find<Bookmark>().Match(b => b.AvatarId == viewerId && ids.Contains(b.PostId)).ExecuteAsync(token)).Select(b => b.PostId).ToHashSet();
|
||||
var pinned = (await DB.Default.Find<Pin>().Match(p => ids.Contains(p.PostId)).ExecuteAsync(token)).Select(p => p.PostId).ToHashSet();
|
||||
var hidden = viewerId == default
|
||||
? new HashSet<string>()
|
||||
: await Domain.Relationships.Hidden.AuthorsHiddenFrom(viewerId, all.Select(p => p.ActorURI), forNotifications: false, token);
|
||||
var reblogged = viewerId == default
|
||||
? new HashSet<string>()
|
||||
: (await _dbEntities.Posts.Match(p => p.AuthorAccountId == viewerId && ids.Contains(p.ReblogOfPostId) && !p.DeletedAt.HasValue).ExecuteAsync(token))
|
||||
@@ -172,6 +179,8 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
FavouritesCount = post.FavouritesCount,
|
||||
Favourited = favourited.Contains(post.ID),
|
||||
Reblogged = reblogged.Contains(post.ID),
|
||||
Bookmarked = bookmarked.Contains(post.ID),
|
||||
Pinned = pinned.Contains(post.ID),
|
||||
Sensitive = post.HasContentWarning,
|
||||
SpoilerText = post.SpoilerText ?? (post.HasContentWarning ? post.Title ?? ActivityPubRenderer.ContentWarning : string.Empty),
|
||||
Visibility = Visibility(post.Visibility),
|
||||
@@ -186,6 +195,9 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
var mapped = new List<Status>();
|
||||
foreach (var post in posts)
|
||||
{
|
||||
if (hidden.Contains(post.ActorURI) || post.ReblogOfPostId != default && originals.TryGetValue(post.ReblogOfPostId, out var hiddenOriginal)
|
||||
&& hidden.Contains(hiddenOriginal.ActorURI))
|
||||
continue;
|
||||
var status = Map(post);
|
||||
if (status == default)
|
||||
continue;
|
||||
|
||||
@@ -1,10 +1,47 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.ClientModels;
|
||||
using PrivaPub.Extensions;
|
||||
using PrivaPub.Models.Social;
|
||||
|
||||
namespace PrivaPub.Controllers.ClientToServer
|
||||
{
|
||||
[ApiController,
|
||||
Route("clientapi/moderator")]
|
||||
Route("clientapi/moderator"),
|
||||
Authorize(Policy = Policies.IsModerator)]
|
||||
public class ModeratorController : ControllerBase
|
||||
{
|
||||
[HttpGet, Route("/clientapi/moderator/reports")]
|
||||
public async Task<IActionResult> Reports([FromQuery] bool resolved, CancellationToken token) =>
|
||||
Ok((await DB.Default.Find<Report>().Match(r => r.IsResolved == resolved).Sort(r => r.ID, Order.Descending).Limit(100).ExecuteAsync(token))
|
||||
.Select(r => new
|
||||
{
|
||||
r.ID,
|
||||
Reporter = r.ReporterAvatarId != default ? "local" : r.ReporterActorURI,
|
||||
r.TargetAccountId,
|
||||
r.TargetActorURI,
|
||||
r.PostIds,
|
||||
r.ObjectURIs,
|
||||
r.Category,
|
||||
r.Comment,
|
||||
r.Forwarded,
|
||||
r.IsResolved,
|
||||
r.CreatedAt,
|
||||
r.ResolvedAt
|
||||
}));
|
||||
|
||||
[HttpPost, Route("/clientapi/moderator/reports/{id}/resolve")]
|
||||
public async Task<IActionResult> Resolve(string id, CancellationToken token)
|
||||
{
|
||||
var result = await DB.Default.Update<Report>().MatchID(id)
|
||||
.Modify(r => r.IsResolved, true)
|
||||
.Modify(r => r.ResolvedAt, DateTime.UtcNow)
|
||||
.Modify(r => r.ResolvedBy, User.GetUserId())
|
||||
.ExecuteAsync(token);
|
||||
return result.MatchedCount == 0 ? NotFound() : Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Domain.Social;
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Outbox;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Domain.Relationships
|
||||
{
|
||||
public interface IRelationshipService
|
||||
{
|
||||
Task Block(LocalActor me, string targetActorUri, string targetAccountId, CancellationToken token);
|
||||
Task Unblock(LocalActor me, string targetActorUri, CancellationToken token);
|
||||
Task Mute(LocalActor me, string targetActorUri, string targetAccountId, bool hideNotifications, TimeSpan? duration, CancellationToken token);
|
||||
Task Unmute(LocalActor me, string targetActorUri, CancellationToken token);
|
||||
Task BlockDomain(LocalActor me, string domain, CancellationToken token);
|
||||
Task UnblockDomain(LocalActor me, string domain, CancellationToken token);
|
||||
}
|
||||
|
||||
public class RelationshipService : IRelationshipService
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly IFollowService _follows;
|
||||
readonly IDeliveryService _delivery;
|
||||
|
||||
public RelationshipService(DbEntities dbEntities, IFollowService follows, IDeliveryService delivery)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_follows = follows;
|
||||
_delivery = delivery;
|
||||
}
|
||||
|
||||
public async Task Block(LocalActor me, string targetActorUri, string targetAccountId, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new Block { AvatarId = me.Id, TargetAccountId = targetAccountId, TargetActorURI = targetActorUri }, token);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
}
|
||||
|
||||
await _follows.UnfollowAs(me, targetActorUri, token);
|
||||
var follower = await _dbEntities.Followers.Match(f => f.LocalActorId == me.Id && f.ActorURI == targetActorUri).ExecuteFirstAsync(token);
|
||||
if (follower != default)
|
||||
{
|
||||
await DB.Default.DeleteAsync<Follower>(follower.ID);
|
||||
if (!targetActorUri.StartsWith(me.BaseAddress + "/", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(follower.FollowActivityURI))
|
||||
{
|
||||
var reject = new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||
["id"] = me.ActivityUri($"reject-{follower.ID}-{DateTime.UtcNow.Ticks}"),
|
||||
["type"] = "Reject",
|
||||
["actor"] = me.Uri,
|
||||
["object"] = new JsonObject { ["id"] = follower.FollowActivityURI, ["type"] = "Follow", ["actor"] = targetActorUri, ["object"] = me.Uri }
|
||||
};
|
||||
await _delivery.Enqueue(me, new[] { follower.InboxURL }, reject, token);
|
||||
}
|
||||
else
|
||||
await DB.Default.DeleteAsync<Following>(f => f.TargetActorURI == me.Uri && f.AvatarId == targetAccountId);
|
||||
}
|
||||
await Forget(me, targetAccountId, token);
|
||||
}
|
||||
|
||||
public async Task Unblock(LocalActor me, string targetActorUri, CancellationToken token) =>
|
||||
await DB.Default.DeleteAsync<Block>(b => b.AvatarId == me.Id && b.TargetActorURI == targetActorUri);
|
||||
|
||||
public async Task Mute(LocalActor me, string targetActorUri, string targetAccountId, bool hideNotifications, TimeSpan? duration, CancellationToken token)
|
||||
{
|
||||
await DB.Default.Update<Mute>()
|
||||
.Match(m => m.AvatarId == me.Id && m.TargetActorURI == targetActorUri)
|
||||
.Modify(m => m.TargetAccountId, targetAccountId)
|
||||
.Modify(m => m.HideNotifications, hideNotifications)
|
||||
.Modify(m => m.ExpiresAt, duration.HasValue && duration > TimeSpan.Zero ? DateTime.UtcNow + duration : null)
|
||||
.Modify(b => b.SetOnInsert(m => m.CreatedAt, DateTime.UtcNow))
|
||||
.Option(o => o.IsUpsert = true)
|
||||
.ExecuteAsync(token);
|
||||
await Forget(me, targetAccountId, token);
|
||||
}
|
||||
|
||||
public async Task Unmute(LocalActor me, string targetActorUri, CancellationToken token) =>
|
||||
await DB.Default.DeleteAsync<Mute>(m => m.AvatarId == me.Id && m.TargetActorURI == targetActorUri);
|
||||
|
||||
public async Task BlockDomain(LocalActor me, string domain, CancellationToken token)
|
||||
{
|
||||
domain = domain?.Trim().ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(domain) || Uri.CheckHostName(domain) != UriHostNameType.Dns)
|
||||
return;
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new AccountDomainBlock { AvatarId = me.Id, Domain = domain }, token);
|
||||
}
|
||||
catch (MongoWriteException ex) when (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
}
|
||||
foreach (var following in await _dbEntities.Followings.Match(f => f.AvatarId == me.Id && !f.TargetIsLocal).ExecuteAsync(token))
|
||||
if (Hidden.HostMatches(following.TargetActorURI, domain))
|
||||
await _follows.UnfollowAs(me, following.TargetActorURI, token);
|
||||
foreach (var follower in await _dbEntities.Followers.Match(f => f.LocalActorId == me.Id).ExecuteAsync(token))
|
||||
if (Hidden.HostMatches(follower.ActorURI, domain))
|
||||
await DB.Default.DeleteAsync<Follower>(follower.ID);
|
||||
}
|
||||
|
||||
public async Task UnblockDomain(LocalActor me, string domain, CancellationToken token)
|
||||
{
|
||||
domain = domain?.Trim().ToLowerInvariant();
|
||||
await DB.Default.DeleteAsync<AccountDomainBlock>(b => b.AvatarId == me.Id && b.Domain == domain);
|
||||
}
|
||||
|
||||
static async Task Forget(LocalActor me, string targetAccountId, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(targetAccountId))
|
||||
return;
|
||||
await DB.Default.DeleteAsync<TimelineEntry>(e => e.AvatarId == me.Id && e.AuthorAccountId == targetAccountId);
|
||||
await DB.Default.DeleteAsync<Notification>(n => n.AvatarId == me.Id && n.FromAccountId == targetAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Hidden
|
||||
{
|
||||
public static bool HostMatches(string actorUri, string domain) =>
|
||||
Uri.TryCreate(actorUri, UriKind.Absolute, out var uri)
|
||||
&& (uri.Host.Equals(domain, StringComparison.OrdinalIgnoreCase) || uri.Host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
public static async Task<HashSet<string>> AuthorsHiddenFrom(string avatarId, IEnumerable<string> actorUris, bool forNotifications, CancellationToken token)
|
||||
{
|
||||
var uris = actorUris.Where(u => !string.IsNullOrEmpty(u)).Distinct().ToList();
|
||||
var hidden = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (string.IsNullOrEmpty(avatarId) || uris.Count == 0)
|
||||
return hidden;
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var block in await DB.Default.Find<Block>().Match(b => b.AvatarId == avatarId && uris.Contains(b.TargetActorURI)).ExecuteAsync(token))
|
||||
hidden.Add(block.TargetActorURI);
|
||||
foreach (var mute in await DB.Default.Find<Mute>().Match(m => m.AvatarId == avatarId && uris.Contains(m.TargetActorURI)).ExecuteAsync(token))
|
||||
if ((mute.ExpiresAt == null || mute.ExpiresAt > now) && (!forNotifications || mute.HideNotifications))
|
||||
hidden.Add(mute.TargetActorURI);
|
||||
var domains = await DB.Default.Find<AccountDomainBlock>().Match(b => b.AvatarId == avatarId).ExecuteAsync(token);
|
||||
foreach (var uri in uris)
|
||||
if (domains.Any(d => HostMatches(uri, d.Domain)))
|
||||
hidden.Add(uri);
|
||||
return hidden;
|
||||
}
|
||||
|
||||
public static async Task<HashSet<string>> RecipientsHiding(IEnumerable<string> avatarIds, string actorUri, CancellationToken token)
|
||||
{
|
||||
var ids = avatarIds.Distinct().ToList();
|
||||
var hiding = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (ids.Count == 0 || string.IsNullOrEmpty(actorUri))
|
||||
return hiding;
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var block in await DB.Default.Find<Block>().Match(b => ids.Contains(b.AvatarId) && b.TargetActorURI == actorUri).ExecuteAsync(token))
|
||||
hiding.Add(block.AvatarId);
|
||||
foreach (var mute in await DB.Default.Find<Mute>().Match(m => ids.Contains(m.AvatarId) && m.TargetActorURI == actorUri).ExecuteAsync(token))
|
||||
if (mute.ExpiresAt == null || mute.ExpiresAt > now)
|
||||
hiding.Add(mute.AvatarId);
|
||||
foreach (var block in await DB.Default.Find<AccountDomainBlock>().Match(b => ids.Contains(b.AvatarId)).ExecuteAsync(token))
|
||||
if (HostMatches(actorUri, block.Domain))
|
||||
hiding.Add(block.AvatarId);
|
||||
return hiding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Federation.Outbox;
|
||||
using PrivaPub.Federation.Rendering;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Domain.Relationships
|
||||
{
|
||||
public interface IReportService
|
||||
{
|
||||
Task<Report> File(LocalActor reporter, string targetAccountId, IReadOnlyList<string> postIds, string comment, string category, bool forward,
|
||||
CancellationToken token);
|
||||
}
|
||||
|
||||
public class ReportService : IReportService
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
readonly IDeliveryService _delivery;
|
||||
|
||||
public ReportService(DbEntities dbEntities, ILocalActorService localActors, IDeliveryService delivery)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
_delivery = delivery;
|
||||
}
|
||||
|
||||
public async Task<Report> File(LocalActor reporter, string targetAccountId, IReadOnlyList<string> postIds, string comment, string category,
|
||||
bool forward, CancellationToken token)
|
||||
{
|
||||
var localTarget = await _dbEntities.Avatars.MatchID(targetAccountId).ExecuteFirstAsync(token);
|
||||
var remoteTarget = localTarget == default ? await _dbEntities.ForeignAvatars.MatchID(targetAccountId).ExecuteFirstAsync(token) : default;
|
||||
if (localTarget == default && remoteTarget == default)
|
||||
return default;
|
||||
var targetUri = localTarget != default ? _localActors.FromAvatar(localTarget).Uri : remoteTarget.ActorURI;
|
||||
var posts = postIds.Count == 0
|
||||
? new List<Models.Post.Post>()
|
||||
: await _dbEntities.Posts.Match(p => postIds.Contains(p.ID) && p.ActorURI == targetUri).ExecuteAsync(token);
|
||||
|
||||
var report = new Report
|
||||
{
|
||||
ReporterAvatarId = reporter.Id,
|
||||
TargetAccountId = targetAccountId,
|
||||
TargetActorURI = targetUri,
|
||||
PostIds = posts.Select(p => p.ID).ToList(),
|
||||
ObjectURIs = posts.Select(p => p.ObjectURI).ToList(),
|
||||
Comment = comment?.Trim(),
|
||||
Category = category is "spam" or "legal" or "violation" ? category : "other",
|
||||
Forwarded = forward && remoteTarget != default
|
||||
};
|
||||
await DB.Default.SaveAsync(report, token);
|
||||
|
||||
if (report.Forwarded)
|
||||
{
|
||||
var instance = await _localActors.GetInstanceActor(token);
|
||||
var flag = new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||
["id"] = instance.ActivityUri($"flag-{report.ID}"),
|
||||
["type"] = "Flag",
|
||||
["actor"] = instance.Uri,
|
||||
["content"] = report.Comment ?? string.Empty,
|
||||
["object"] = new JsonArray(report.ObjectURIs.Prepend(targetUri).Select(u => (JsonNode)u).ToArray())
|
||||
};
|
||||
await _delivery.Enqueue(instance, new[] { remoteTarget.SharedInboxURL ?? remoteTarget.InboxURL }, flag, token);
|
||||
}
|
||||
return report;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ namespace PrivaPub.Domain.Social
|
||||
{
|
||||
if (string.IsNullOrEmpty(avatarId) || avatarId == fromAccountId)
|
||||
return;
|
||||
if ((await Relationships.Hidden.AuthorsHiddenFrom(avatarId, new[] { fromActorUri }, forNotifications: true, token)).Count > 0)
|
||||
return;
|
||||
try
|
||||
{
|
||||
await DB.Default.SaveAsync(new Notification
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace PrivaPub.Domain.Timelines
|
||||
break;
|
||||
}
|
||||
|
||||
recipients.ExceptWith(await Relationships.Hidden.RecipientsHiding(recipients, post.ActorURI, token));
|
||||
foreach (var avatarId in recipients)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -42,6 +42,8 @@ namespace PrivaPub.Federation.Actors
|
||||
public string Outbox => $"{Uri}/anus";
|
||||
public string Followers => $"{Uri}/groupies";
|
||||
public string Following => $"{Uri}/stalking";
|
||||
public string Featured => $"{Uri}/trophies";
|
||||
public string FeaturedTags => $"{Uri}/tattoos";
|
||||
public string SharedInbox => $"{BaseAddress}/human-centipede";
|
||||
public string Domain => new Uri(BaseAddress).Authority;
|
||||
public string Handle => $"{UserName}@{Domain}";
|
||||
|
||||
@@ -5,6 +5,7 @@ using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Post;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
@@ -112,6 +113,34 @@ namespace PrivaPub.Federation.Controllers
|
||||
return local is not { IsFederated: true } ? NotFound() : Activity(ActivityPubRenderer.OrderedCollection(local.Following, 0, default));
|
||||
}
|
||||
|
||||
[HttpGet, Route("{actor}/trophies")]
|
||||
public async Task<IActionResult> Featured(string actor, CancellationToken token)
|
||||
{
|
||||
var local = await _localActors.FindByUserName(actor, token);
|
||||
if (local is not { IsFederated: true })
|
||||
return NotFound();
|
||||
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 posts = pinIds.Count == 0
|
||||
? new List<PostEntity>()
|
||||
: await _dbEntities.Posts.Match(p => pinIds.Contains(p.ID) && p.GroupUserId == local.Id && p.ReblogOfPostId == null)
|
||||
.Match(VisibilityPolicy.IsPublic).ExecuteAsync(token);
|
||||
var notes = new List<JsonNode>();
|
||||
foreach (var id in pinIds)
|
||||
if (posts.FirstOrDefault(p => p.ID == id) is { } post)
|
||||
notes.Add(ActivityPubRenderer.Note(post, local, default, post.InReplyToURI));
|
||||
return Activity(ActivityPubRenderer.OrderedCollection(local.Featured, notes.Count, notes));
|
||||
}
|
||||
|
||||
[HttpGet, Route("{actor}/tattoos")]
|
||||
public async Task<IActionResult> FeaturedTags(string actor, CancellationToken token)
|
||||
{
|
||||
var local = await _localActors.FindByUserName(actor, token);
|
||||
return local is not { IsFederated: true }
|
||||
? NotFound()
|
||||
: Activity(ActivityPubRenderer.OrderedCollection(local.FeaturedTags, 0, Enumerable.Empty<JsonNode>()));
|
||||
}
|
||||
|
||||
[HttpGet, Route("{actor}/scribbles/{postId}")]
|
||||
public async Task<IActionResult> Post(string actor, string postId, CancellationToken token)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Actors;
|
||||
using PrivaPub.Models.Federation;
|
||||
using PrivaPub.Models.Social;
|
||||
using PrivaPub.Models.User;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
using static PrivaPub.Federation.Objects.ActivityJson;
|
||||
|
||||
namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
public class FlagHandler : IActivityHandler
|
||||
{
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly ILocalActorService _localActors;
|
||||
|
||||
public FlagHandler(DbEntities dbEntities, ILocalActorService localActors)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_localActors = localActors;
|
||||
}
|
||||
|
||||
public string Type => "Flag";
|
||||
|
||||
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
var objects = activity["object"] switch
|
||||
{
|
||||
JsonArray array => array.Select(Id).Where(i => i != default).ToList(),
|
||||
JsonNode single when Id(single) is { } id => new List<string> { id },
|
||||
_ => new List<string>()
|
||||
};
|
||||
|
||||
LocalActor target = default;
|
||||
var postUris = new List<string>();
|
||||
foreach (var uri in objects.Take(50))
|
||||
{
|
||||
var local = await _localActors.FindByUri(uri, token);
|
||||
if (local is { Kind: LocalActorKind.Person } && uri.TrimEnd('/') == local.Uri)
|
||||
target ??= local;
|
||||
else
|
||||
postUris.Add(uri);
|
||||
}
|
||||
var posts = postUris.Count == 0
|
||||
? new List<Models.Post.Post>()
|
||||
: await _dbEntities.Posts.Match(p => postUris.Contains(p.ObjectURI) && !p.IsFederatedCopy).ExecuteAsync(token);
|
||||
if (target == default && posts.Count > 0)
|
||||
target = await _localActors.FindById(LocalActorKind.Person, posts[0].GroupUserId, token);
|
||||
if (target == default)
|
||||
return;
|
||||
|
||||
await DB.Default.SaveAsync(new Report
|
||||
{
|
||||
ReporterActorURI = actor.ActorURI,
|
||||
TargetAccountId = target.Id,
|
||||
TargetActorURI = target.Uri,
|
||||
PostIds = posts.Where(p => p.GroupUserId == target.Id).Select(p => p.ID).ToList(),
|
||||
ObjectURIs = posts.Where(p => p.GroupUserId == target.Id).Select(p => p.ObjectURI).ToList(),
|
||||
Comment = Value(activity, "content")
|
||||
}, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,20 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
var target = await _localActors.FindByUri(Id(follow["object"]), token);
|
||||
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
||||
return;
|
||||
if (target.Kind == LocalActorKind.Person
|
||||
&& await DB.Default.Find<Models.Social.Block>().Match(b => b.AvatarId == target.Id && b.TargetActorURI == follower.ActorURI).ExecuteAnyAsync(token))
|
||||
{
|
||||
var reject = new JsonObject
|
||||
{
|
||||
["@context"] = ActivityPubRenderer.ActivityStreams,
|
||||
["id"] = target.ActivityUri($"reject-{Guid.NewGuid():N}"),
|
||||
["type"] = "Reject",
|
||||
["actor"] = target.Uri,
|
||||
["object"] = follow.DeepClone()
|
||||
};
|
||||
await _delivery.Enqueue(target, new[] { follower.InboxURL }, reject, token);
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = await _dbEntities.Followers
|
||||
.Match(f => f.LocalActorId == target.Id && f.LocalActorKind == target.Kind && f.ActorURI == follower.ActorURI)
|
||||
|
||||
@@ -76,6 +76,8 @@ namespace PrivaPub.Federation.Rendering
|
||||
["outbox"] = actor.Outbox,
|
||||
["followers"] = actor.Followers,
|
||||
["following"] = actor.Following,
|
||||
["featured"] = actor.Featured,
|
||||
["featuredTags"] = actor.FeaturedTags,
|
||||
["published"] = Day(actor.Published),
|
||||
["manuallyApprovesFollowers"] = actor.ManuallyApprovesFollowers,
|
||||
["discoverable"] = actor.Discoverable,
|
||||
|
||||
@@ -87,6 +87,18 @@ namespace PrivaPub.Infrastructure.Data
|
||||
.Key(m => m.Timeline, KeyType.Ascending)
|
||||
.Option(o => o.Unique = true)
|
||||
.CreateAsync(token);
|
||||
foreach (var pair in new (Func<Task>, string)[]
|
||||
{
|
||||
(() => DB.Default.Index<Block>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.TargetActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "block"),
|
||||
(() => DB.Default.Index<Mute>().Key(m => m.AvatarId, KeyType.Ascending).Key(m => m.TargetActorURI, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "mute"),
|
||||
(() => DB.Default.Index<AccountDomainBlock>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.Domain, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "domain block"),
|
||||
(() => DB.Default.Index<Bookmark>().Key(b => b.AvatarId, KeyType.Ascending).Key(b => b.PostId, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "bookmark"),
|
||||
(() => DB.Default.Index<Pin>().Key(p => p.AvatarId, KeyType.Ascending).Key(p => p.PostId, KeyType.Ascending).Option(o => o.Unique = true).CreateAsync(token), "pin")
|
||||
})
|
||||
await pair.Item1();
|
||||
await Plain<Block>(token, b => b.TargetActorURI);
|
||||
await Plain<Mute>(token, m => m.TargetActorURI);
|
||||
await Plain<Report>(token, r => r.IsResolved, r => r.ID);
|
||||
await Unique<DomainBlock>(b => b.Domain, Builders<DomainBlock>.Filter.Type(b => b.Domain, BsonType.String), token);
|
||||
await Unique<RemoteInstance>(i => i.Host, Builders<RemoteInstance>.Filter.Type(i => i.Host, BsonType.String), token);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using PrivaPub.Federation.Inbox;
|
||||
using PrivaPub.Federation.Moderation;
|
||||
using PrivaPub.Federation.Inbox.Handlers;
|
||||
using PrivaPub.Domain.Content;
|
||||
using PrivaPub.Domain.Relationships;
|
||||
using PrivaPub.Domain.Social;
|
||||
using PrivaPub.Domain.Statuses;
|
||||
using PrivaPub.Domain.Timelines;
|
||||
@@ -72,6 +73,7 @@ namespace PrivaPub.Middleware
|
||||
.AddSingleton<IActivityHandler, UndoHandler>()
|
||||
.AddSingleton<IActivityHandler, LikeHandler>()
|
||||
.AddSingleton<IActivityHandler, AnnounceHandler>()
|
||||
.AddSingleton<IActivityHandler, FlagHandler>()
|
||||
.AddSingleton<IActivityHandler, CreateHandler>()
|
||||
.AddSingleton<IActivityHandler, DeleteHandler>()
|
||||
.AddSingleton<IActivityHandler, UpdateHandler>()
|
||||
@@ -138,6 +140,8 @@ namespace PrivaPub.Middleware
|
||||
.AddTransient<IGroupUsersService, GroupUsersService>()
|
||||
.AddTransient<IPostsService, PostsService>()
|
||||
.AddTransient<IStatusService, StatusService>()
|
||||
.AddTransient<IRelationshipService, RelationshipService>()
|
||||
.AddTransient<IReportService, ReportService>()
|
||||
.AddTransient<IFollowService, FollowService>()
|
||||
.AddTransient<ITimelineService, TimelineService>()
|
||||
.AddSingleton<AppConfigurationService>()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
namespace PrivaPub.Models.Social
|
||||
{
|
||||
public class Block : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
public string TargetAccountId { get; set; }
|
||||
public string TargetActorURI { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Mute : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
public string TargetAccountId { get; set; }
|
||||
public string TargetActorURI { get; set; }
|
||||
public bool HideNotifications { get; set; } = true;
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class AccountDomainBlock : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
public string Domain { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Bookmark : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
public string PostId { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Pin : Entity
|
||||
{
|
||||
public string AvatarId { get; set; }
|
||||
public string PostId { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Report : Entity
|
||||
{
|
||||
public string ReporterAvatarId { get; set; }
|
||||
public string ReporterActorURI { get; set; }
|
||||
public string TargetAccountId { get; set; }
|
||||
public string TargetActorURI { get; set; }
|
||||
public List<string> PostIds { get; set; } = new();
|
||||
public List<string> ObjectURIs { get; set; } = new();
|
||||
public string Category { get; set; } = "other";
|
||||
public string Comment { get; set; }
|
||||
public bool Forwarded { get; set; }
|
||||
public bool IsResolved { get; set; }
|
||||
public string ResolvedBy { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? ResolvedAt { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user