[backend/core] Add mute support (ISH-169)
This commit is contained in:
parent
a2075d4c63
commit
f41be007d7
5 changed files with 187 additions and 97 deletions
|
@ -197,23 +197,7 @@ public class AccountController(
|
||||||
followee.PrecomputedIsFollowedBy = true;
|
followee.PrecomputedIsFollowedBy = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var res = new RelationshipEntity
|
var res = RenderRelationship(followee);
|
||||||
{
|
|
||||||
Id = followee.Id,
|
|
||||||
Following = followee.PrecomputedIsFollowedBy ?? false,
|
|
||||||
FollowedBy = followee.PrecomputedIsFollowing ?? false,
|
|
||||||
Blocking = followee.PrecomputedIsBlockedBy ?? false,
|
|
||||||
BlockedBy = followee.PrecomputedIsBlocking ?? false,
|
|
||||||
Requested = followee.PrecomputedIsRequestedBy ?? false,
|
|
||||||
RequestedBy = followee.PrecomputedIsRequested ?? false,
|
|
||||||
Muting = followee.PrecomputedIsMutedBy ?? false,
|
|
||||||
Endorsed = false, //FIXME
|
|
||||||
Note = "", //FIXME
|
|
||||||
Notifying = false, //FIXME
|
|
||||||
DomainBlocking = false, //FIXME
|
|
||||||
MutingNotifications = false, //FIXME
|
|
||||||
ShowingReblogs = true //FIXME
|
|
||||||
};
|
|
||||||
|
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
@ -236,23 +220,54 @@ public class AccountController(
|
||||||
|
|
||||||
await userSvc.UnfollowUserAsync(user, followee);
|
await userSvc.UnfollowUserAsync(user, followee);
|
||||||
|
|
||||||
var res = new RelationshipEntity
|
var res = RenderRelationship(followee);
|
||||||
{
|
|
||||||
Id = followee.Id,
|
return Ok(res);
|
||||||
Following = followee.PrecomputedIsFollowedBy ?? false,
|
}
|
||||||
FollowedBy = followee.PrecomputedIsFollowing ?? false,
|
|
||||||
Blocking = followee.PrecomputedIsBlockedBy ?? false,
|
[HttpPost("{id}/mute")]
|
||||||
BlockedBy = followee.PrecomputedIsBlocking ?? false,
|
[Authorize("write:mutes")]
|
||||||
Requested = followee.PrecomputedIsRequestedBy ?? false,
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||||
RequestedBy = followee.PrecomputedIsRequested ?? false,
|
public async Task<IActionResult> MuteUser(string id, [FromHybrid] AccountSchemas.AccountMuteRequest request)
|
||||||
Muting = followee.PrecomputedIsMutedBy ?? false,
|
{
|
||||||
Endorsed = false, //FIXME
|
var user = HttpContext.GetUserOrFail();
|
||||||
Note = "", //FIXME
|
if (user.Id == id)
|
||||||
Notifying = false, //FIXME
|
throw GracefulException.BadRequest("You cannot mute yourself");
|
||||||
DomainBlocking = false, //FIXME
|
|
||||||
MutingNotifications = false, //FIXME
|
var mutee = await db.Users
|
||||||
ShowingReblogs = true //FIXME
|
.Where(p => p.Id == id)
|
||||||
};
|
.IncludeCommonProperties()
|
||||||
|
.PrecomputeRelationshipData(user)
|
||||||
|
.FirstOrDefaultAsync() ??
|
||||||
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
|
//TODO: handle notifications parameter
|
||||||
|
await userSvc.MuteUserAsync(user, mutee, DateTime.UtcNow + TimeSpan.FromSeconds(request.Duration));
|
||||||
|
|
||||||
|
var res = RenderRelationship(mutee);
|
||||||
|
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/unmute")]
|
||||||
|
[Authorize("write:mutes")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||||
|
public async Task<IActionResult> UnmuteUser(string id)
|
||||||
|
{
|
||||||
|
var user = HttpContext.GetUserOrFail();
|
||||||
|
if (user.Id == id)
|
||||||
|
throw GracefulException.BadRequest("You cannot unmute yourself");
|
||||||
|
|
||||||
|
var mutee = await db.Users
|
||||||
|
.Where(p => p.Id == id)
|
||||||
|
.IncludeCommonProperties()
|
||||||
|
.PrecomputeRelationshipData(user)
|
||||||
|
.FirstOrDefaultAsync() ??
|
||||||
|
throw GracefulException.RecordNotFound();
|
||||||
|
|
||||||
|
await userSvc.UnmuteUserAsync(user, mutee);
|
||||||
|
|
||||||
|
var res = RenderRelationship(mutee);
|
||||||
|
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
@ -270,23 +285,7 @@ public class AccountController(
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var res = users.Select(u => new RelationshipEntity
|
var res = users.Select(RenderRelationship);
|
||||||
{
|
|
||||||
Id = u.Id,
|
|
||||||
Following = u.PrecomputedIsFollowedBy ?? false,
|
|
||||||
FollowedBy = u.PrecomputedIsFollowing ?? false,
|
|
||||||
Blocking = u.PrecomputedIsBlockedBy ?? false,
|
|
||||||
BlockedBy = u.PrecomputedIsBlocking ?? false,
|
|
||||||
Requested = u.PrecomputedIsRequestedBy ?? false,
|
|
||||||
RequestedBy = u.PrecomputedIsRequested ?? false,
|
|
||||||
Muting = u.PrecomputedIsMutedBy ?? false,
|
|
||||||
Endorsed = false, //FIXME
|
|
||||||
Note = "", //FIXME
|
|
||||||
Notifying = false, //FIXME
|
|
||||||
DomainBlocking = false, //FIXME
|
|
||||||
MutingNotifications = false, //FIXME
|
|
||||||
ShowingReblogs = true //FIXME
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
@ -447,23 +446,7 @@ public class AccountController(
|
||||||
var relationship = await db.Users.Where(p => id == p.Id)
|
var relationship = await db.Users.Where(p => id == p.Id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.Select(u => new RelationshipEntity
|
.Select(u => RenderRelationship(u))
|
||||||
{
|
|
||||||
Id = u.Id,
|
|
||||||
Following = u.PrecomputedIsFollowedBy ?? false,
|
|
||||||
FollowedBy = u.PrecomputedIsFollowing ?? false,
|
|
||||||
Blocking = u.PrecomputedIsBlockedBy ?? false,
|
|
||||||
BlockedBy = u.PrecomputedIsBlocking ?? false,
|
|
||||||
Requested = u.PrecomputedIsRequestedBy ?? false,
|
|
||||||
RequestedBy = u.PrecomputedIsRequested ?? false,
|
|
||||||
Muting = u.PrecomputedIsMutedBy ?? false,
|
|
||||||
Endorsed = false, //FIXME
|
|
||||||
Note = "", //FIXME
|
|
||||||
Notifying = false, //FIXME
|
|
||||||
DomainBlocking = false, //FIXME
|
|
||||||
MutingNotifications = false, //FIXME
|
|
||||||
ShowingReblogs = true //FIXME
|
|
||||||
})
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (relationship == null)
|
if (relationship == null)
|
||||||
|
@ -490,23 +473,7 @@ public class AccountController(
|
||||||
var relationship = await db.Users.Where(p => id == p.Id)
|
var relationship = await db.Users.Where(p => id == p.Id)
|
||||||
.IncludeCommonProperties()
|
.IncludeCommonProperties()
|
||||||
.PrecomputeRelationshipData(user)
|
.PrecomputeRelationshipData(user)
|
||||||
.Select(u => new RelationshipEntity
|
.Select(u => RenderRelationship(u))
|
||||||
{
|
|
||||||
Id = u.Id,
|
|
||||||
Following = u.PrecomputedIsFollowedBy ?? false,
|
|
||||||
FollowedBy = u.PrecomputedIsFollowing ?? false,
|
|
||||||
Blocking = u.PrecomputedIsBlockedBy ?? false,
|
|
||||||
BlockedBy = u.PrecomputedIsBlocking ?? false,
|
|
||||||
Requested = u.PrecomputedIsRequestedBy ?? false,
|
|
||||||
RequestedBy = u.PrecomputedIsRequested ?? false,
|
|
||||||
Muting = u.PrecomputedIsMutedBy ?? false,
|
|
||||||
Endorsed = false, //FIXME
|
|
||||||
Note = "", //FIXME
|
|
||||||
Notifying = false, //FIXME
|
|
||||||
DomainBlocking = false, //FIXME
|
|
||||||
MutingNotifications = false, //FIXME
|
|
||||||
ShowingReblogs = true //FIXME
|
|
||||||
})
|
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (relationship == null)
|
if (relationship == null)
|
||||||
|
@ -524,4 +491,25 @@ public class AccountController(
|
||||||
var res = await userRenderer.RenderAsync(user);
|
var res = await userRenderer.RenderAsync(user);
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RelationshipEntity RenderRelationship(User u)
|
||||||
|
{
|
||||||
|
return new RelationshipEntity
|
||||||
|
{
|
||||||
|
Id = u.Id,
|
||||||
|
Following = u.PrecomputedIsFollowedBy ?? false,
|
||||||
|
FollowedBy = u.PrecomputedIsFollowing ?? false,
|
||||||
|
Blocking = u.PrecomputedIsBlockedBy ?? false,
|
||||||
|
BlockedBy = u.PrecomputedIsBlocking ?? false,
|
||||||
|
Requested = u.PrecomputedIsRequestedBy ?? false,
|
||||||
|
RequestedBy = u.PrecomputedIsRequested ?? false,
|
||||||
|
Muting = u.PrecomputedIsMutedBy ?? false,
|
||||||
|
Endorsed = false, //FIXME
|
||||||
|
Note = "", //FIXME
|
||||||
|
Notifying = false, //FIXME
|
||||||
|
DomainBlocking = false, //FIXME
|
||||||
|
MutingNotifications = false, //FIXME
|
||||||
|
ShowingReblogs = true //FIXME
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -62,4 +62,13 @@ public abstract class AccountSchemas
|
||||||
[B(Name = "sensitive")]
|
[B(Name = "sensitive")]
|
||||||
public bool? Sensitive { get; set; }
|
public bool? Sensitive { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AccountMuteRequest
|
||||||
|
{
|
||||||
|
[J("notifications")]
|
||||||
|
[B(Name = "notifications")]
|
||||||
|
public bool Notifications { get; set; } = true;
|
||||||
|
|
||||||
|
[J("duration")] [B(Name = "duration")] public long Duration { get; set; } = 0;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -24,16 +24,20 @@ public abstract class BackgroundTaskQueue
|
||||||
CancellationToken token
|
CancellationToken token
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (job is DriveFileDeleteJob driveFileDeleteJob)
|
switch (job)
|
||||||
{
|
{
|
||||||
if (driveFileDeleteJob.Expire)
|
case DriveFileDeleteJob { Expire: true } driveFileDeleteJob:
|
||||||
await ProcessDriveFileExpire(driveFileDeleteJob, scope, token);
|
await ProcessDriveFileExpire(driveFileDeleteJob, scope, token);
|
||||||
else
|
break;
|
||||||
|
case DriveFileDeleteJob driveFileDeleteJob:
|
||||||
await ProcessDriveFileDelete(driveFileDeleteJob, scope, token);
|
await ProcessDriveFileDelete(driveFileDeleteJob, scope, token);
|
||||||
}
|
break;
|
||||||
else if (job is PollExpiryJob pollExpiryJob)
|
case PollExpiryJob pollExpiryJob:
|
||||||
{
|
await ProcessPollExpiry(pollExpiryJob, scope, token);
|
||||||
await ProcessPollExpiry(pollExpiryJob, scope, token);
|
break;
|
||||||
|
case MuteExpiryJob muteExpiryJob:
|
||||||
|
await ProcessMuteExpiry(muteExpiryJob, scope, token);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,11 +175,26 @@ public abstract class BackgroundTaskQueue
|
||||||
await deliverSvc.DeliverToAsync(activity, note.User, voters.ToArray());
|
await deliverSvc.DeliverToAsync(activity, note.User, voters.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task ProcessMuteExpiry(
|
||||||
|
MuteExpiryJob job,
|
||||||
|
IServiceProvider scope,
|
||||||
|
CancellationToken token
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var db = scope.GetRequiredService<DatabaseContext>();
|
||||||
|
var muting = await db.Mutings.FirstOrDefaultAsync(p => p.Id == job.MuteId, token);
|
||||||
|
if (muting is not { ExpiresAt: not null }) return;
|
||||||
|
if (muting.ExpiresAt > DateTime.UtcNow + TimeSpan.FromSeconds(30)) return;
|
||||||
|
db.Remove(muting);
|
||||||
|
await db.SaveChangesAsync(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[ProtoContract]
|
[ProtoContract]
|
||||||
[ProtoInclude(100, typeof(DriveFileDeleteJob))]
|
[ProtoInclude(100, typeof(DriveFileDeleteJob))]
|
||||||
[ProtoInclude(101, typeof(PollExpiryJob))]
|
[ProtoInclude(101, typeof(PollExpiryJob))]
|
||||||
|
[ProtoInclude(102, typeof(MuteExpiryJob))]
|
||||||
public class BackgroundTaskJob : Job;
|
public class BackgroundTaskJob : Job;
|
||||||
|
|
||||||
[ProtoContract]
|
[ProtoContract]
|
||||||
|
@ -190,3 +209,9 @@ public class PollExpiryJob : BackgroundTaskJob
|
||||||
{
|
{
|
||||||
[ProtoMember(1)] public required string NoteId;
|
[ProtoMember(1)] public required string NoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class MuteExpiryJob : BackgroundTaskJob
|
||||||
|
{
|
||||||
|
[ProtoMember(1)] public required string MuteId;
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Types;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Types;
|
||||||
using Iceshrimp.Backend.Core.Middleware;
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
|
using Iceshrimp.Backend.Core.Queues;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
@ -32,7 +33,8 @@ public class UserService(
|
||||||
NotificationService notificationSvc,
|
NotificationService notificationSvc,
|
||||||
EmojiService emojiSvc,
|
EmojiService emojiSvc,
|
||||||
ActivityPub.MentionsResolver mentionsResolver,
|
ActivityPub.MentionsResolver mentionsResolver,
|
||||||
ActivityPub.UserRenderer userRenderer
|
ActivityPub.UserRenderer userRenderer,
|
||||||
|
QueueService queueSvc
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||||
|
@ -188,7 +190,7 @@ public class UserService(
|
||||||
var bgDb = provider.GetRequiredService<DatabaseContext>();
|
var bgDb = provider.GetRequiredService<DatabaseContext>();
|
||||||
var bgInstanceSvc = provider.GetRequiredService<InstanceService>();
|
var bgInstanceSvc = provider.GetRequiredService<InstanceService>();
|
||||||
|
|
||||||
var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(user);
|
var dbInstance = await bgInstanceSvc.GetUpdatedInstanceMetadataAsync(user);
|
||||||
await bgDb.Instances.Where(p => p.Id == dbInstance.Id)
|
await bgDb.Instances.Where(p => p.Id == dbInstance.Id)
|
||||||
.ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount + 1));
|
.ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount + 1));
|
||||||
});
|
});
|
||||||
|
@ -826,4 +828,49 @@ public class UserService(
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task MuteUserAsync(User muter, User mutee, DateTime? expiration)
|
||||||
|
{
|
||||||
|
mutee.PrecomputedIsMutedBy = true;
|
||||||
|
|
||||||
|
var muting = await db.Mutings.FirstOrDefaultAsync(p => p.Muter == muter && p.Mutee == mutee);
|
||||||
|
|
||||||
|
if (muting != null)
|
||||||
|
{
|
||||||
|
if (muting.ExpiresAt == expiration) return;
|
||||||
|
muting.ExpiresAt = expiration;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
if (expiration == null) return;
|
||||||
|
var job = new MuteExpiryJob { MuteId = muting.Id };
|
||||||
|
await queueSvc.BackgroundTaskQueue.ScheduleAsync(job, expiration.Value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
muting = new Muting
|
||||||
|
{
|
||||||
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Mutee = mutee,
|
||||||
|
Muter = muter,
|
||||||
|
ExpiresAt = expiration
|
||||||
|
};
|
||||||
|
await db.AddAsync(muting);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (expiration != null)
|
||||||
|
{
|
||||||
|
var job = new MuteExpiryJob { MuteId = muting.Id };
|
||||||
|
await queueSvc.BackgroundTaskQueue.ScheduleAsync(job, expiration.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UnmuteUserAsync(User muter, User mutee)
|
||||||
|
{
|
||||||
|
if (!mutee.PrecomputedIsMutedBy ?? false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await db.Mutings.Where(p => p.Muter == muter && p.Mutee == mutee).ExecuteDeleteAsync();
|
||||||
|
|
||||||
|
mutee.PrecomputedIsMutedBy = false;
|
||||||
|
}
|
||||||
}
|
}
|
21
Iceshrimp.Backend/Core/Tasks/MuteExpiryTask.cs
Normal file
21
Iceshrimp.Backend/Core/Tasks/MuteExpiryTask.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Services;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Iceshrimp.Backend.Core.Tasks;
|
||||||
|
|
||||||
|
[SuppressMessage("ReSharper", "UnusedType.Global", Justification = "Instantiated at runtime by CronService")]
|
||||||
|
public class MuteExpiryTask : ICronTask
|
||||||
|
{
|
||||||
|
public async Task Invoke(IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var db = provider.GetRequiredService<DatabaseContext>();
|
||||||
|
await db.Mutings.Where(p => p.ExpiresAt != null && p.ExpiresAt < DateTime.UtcNow - TimeSpan.FromMinutes(5))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Midnight
|
||||||
|
public TimeSpan Trigger => TimeSpan.Zero;
|
||||||
|
public CronTaskType Type => CronTaskType.Daily;
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue