[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;
|
||||
}
|
||||
|
||||
var res = new RelationshipEntity
|
||||
{
|
||||
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
|
||||
};
|
||||
var res = RenderRelationship(followee);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
@ -236,23 +220,54 @@ public class AccountController(
|
|||
|
||||
await userSvc.UnfollowUserAsync(user, followee);
|
||||
|
||||
var res = new RelationshipEntity
|
||||
{
|
||||
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
|
||||
};
|
||||
var res = RenderRelationship(followee);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/mute")]
|
||||
[Authorize("write:mutes")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(RelationshipEntity))]
|
||||
public async Task<IActionResult> MuteUser(string id, [FromHybrid] AccountSchemas.AccountMuteRequest request)
|
||||
{
|
||||
var user = HttpContext.GetUserOrFail();
|
||||
if (user.Id == id)
|
||||
throw GracefulException.BadRequest("You cannot mute yourself");
|
||||
|
||||
var mutee = await db.Users
|
||||
.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);
|
||||
}
|
||||
|
@ -270,23 +285,7 @@ public class AccountController(
|
|||
.PrecomputeRelationshipData(user)
|
||||
.ToListAsync();
|
||||
|
||||
var res = users.Select(u => 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
|
||||
});
|
||||
var res = users.Select(RenderRelationship);
|
||||
|
||||
return Ok(res);
|
||||
}
|
||||
|
@ -447,23 +446,7 @@ public class AccountController(
|
|||
var relationship = await db.Users.Where(p => id == p.Id)
|
||||
.IncludeCommonProperties()
|
||||
.PrecomputeRelationshipData(user)
|
||||
.Select(u => 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
|
||||
})
|
||||
.Select(u => RenderRelationship(u))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (relationship == null)
|
||||
|
@ -490,23 +473,7 @@ public class AccountController(
|
|||
var relationship = await db.Users.Where(p => id == p.Id)
|
||||
.IncludeCommonProperties()
|
||||
.PrecomputeRelationshipData(user)
|
||||
.Select(u => 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
|
||||
})
|
||||
.Select(u => RenderRelationship(u))
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (relationship == null)
|
||||
|
@ -524,4 +491,25 @@ public class AccountController(
|
|||
var res = await userRenderer.RenderAsync(user);
|
||||
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")]
|
||||
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
|
||||
)
|
||||
{
|
||||
if (job is DriveFileDeleteJob driveFileDeleteJob)
|
||||
switch (job)
|
||||
{
|
||||
if (driveFileDeleteJob.Expire)
|
||||
case DriveFileDeleteJob { Expire: true } driveFileDeleteJob:
|
||||
await ProcessDriveFileExpire(driveFileDeleteJob, scope, token);
|
||||
else
|
||||
break;
|
||||
case DriveFileDeleteJob driveFileDeleteJob:
|
||||
await ProcessDriveFileDelete(driveFileDeleteJob, scope, token);
|
||||
}
|
||||
else if (job is PollExpiryJob pollExpiryJob)
|
||||
{
|
||||
await ProcessPollExpiry(pollExpiryJob, scope, token);
|
||||
break;
|
||||
case PollExpiryJob pollExpiryJob:
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
[ProtoInclude(100, typeof(DriveFileDeleteJob))]
|
||||
[ProtoInclude(101, typeof(PollExpiryJob))]
|
||||
[ProtoInclude(102, typeof(MuteExpiryJob))]
|
||||
public class BackgroundTaskJob : Job;
|
||||
|
||||
[ProtoContract]
|
||||
|
@ -189,4 +208,10 @@ public class DriveFileDeleteJob : BackgroundTaskJob
|
|||
public class PollExpiryJob : BackgroundTaskJob
|
||||
{
|
||||
[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.Types;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Queues;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
|
@ -32,7 +33,8 @@ public class UserService(
|
|||
NotificationService notificationSvc,
|
||||
EmojiService emojiSvc,
|
||||
ActivityPub.MentionsResolver mentionsResolver,
|
||||
ActivityPub.UserRenderer userRenderer
|
||||
ActivityPub.UserRenderer userRenderer,
|
||||
QueueService queueSvc
|
||||
)
|
||||
{
|
||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||
|
@ -187,8 +189,8 @@ public class UserService(
|
|||
{
|
||||
var bgDb = provider.GetRequiredService<DatabaseContext>();
|
||||
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)
|
||||
.ExecuteUpdateAsync(p => p.SetProperty(i => i.UsersCount, i => i.UsersCount + 1));
|
||||
});
|
||||
|
@ -303,7 +305,7 @@ public class UserService(
|
|||
{
|
||||
if (user.Host != null) throw new Exception("This method is only valid for local users");
|
||||
if (user.UserProfile == null) throw new Exception("user.UserProfile must not be null at this stage");
|
||||
|
||||
|
||||
user.Tags = ResolveHashtags(user.UserProfile.Description);
|
||||
|
||||
db.Update(user);
|
||||
|
@ -811,7 +813,7 @@ public class UserService(
|
|||
tags.AddRange(extracted);
|
||||
|
||||
if (tags.Count == 0) return [];
|
||||
|
||||
|
||||
tags = tags.Distinct().ToList();
|
||||
|
||||
_ = followupTaskSvc.ExecuteTask("UpdateHashtagsTable", async provider =>
|
||||
|
@ -826,4 +828,49 @@ public class UserService(
|
|||
|
||||
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