[backend/core] Add mute support (ISH-169)

This commit is contained in:
Laura Hausmann 2024-03-14 14:58:07 +01:00
parent a2075d4c63
commit f41be007d7
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
5 changed files with 187 additions and 97 deletions

View file

@ -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
};
}
} }

View file

@ -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;
}
} }

View file

@ -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;
}

View file

@ -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;
}
} }

View 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;
}