[backend/federation] Handle incoming ASMove activities (ISH-118)
This commit is contained in:
parent
f6309148b7
commit
1d9864a214
5 changed files with 90 additions and 2 deletions
|
@ -16,6 +16,30 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
|||
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// This helper method allows consumers to obtain the performance & memory footprint benefits of chunked DB transactions,
|
||||
/// while not requiring them to work with chunks instead of a regular enumerator.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Make sure to call .OrderBy() on the query, otherwise the results will be unpredictable.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// The result set as an IAsyncEnumerable. Makes one DB roundtrip at the start of each chunk.
|
||||
/// Successive items of the chunk are yielded instantaneously.
|
||||
/// </returns>
|
||||
public static async IAsyncEnumerable<T> AsChunkedAsyncEnumerable<T>(this IQueryable<T> query, int chunkSize)
|
||||
{
|
||||
var offset = 0;
|
||||
while (true)
|
||||
{
|
||||
var res = await query.Skip(offset).Take(chunkSize).ToArrayAsync();
|
||||
if (res.Length == 0) break;
|
||||
foreach (var item in res) yield return item;
|
||||
if (res.Length < chunkSize) break;
|
||||
offset += chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
public static IQueryable<T> Paginate<T>(
|
||||
this IQueryable<T> query,
|
||||
MastodonPaginationQuery pq,
|
||||
|
|
|
@ -76,6 +76,7 @@ public class ActivityHandlerService(
|
|||
ASEmojiReact react => HandleReact(react, resolvedActor),
|
||||
ASFollow follow => HandleFollow(follow, resolvedActor),
|
||||
ASLike like => HandleLike(like, resolvedActor),
|
||||
ASMove move => HandleMove(move, resolvedActor),
|
||||
ASReject reject => HandleReject(reject, resolvedActor),
|
||||
ASUndo undo => HandleUndo(undo, resolvedActor),
|
||||
ASUnfollow unfollow => HandleUnfollow(unfollow, resolvedActor),
|
||||
|
@ -204,8 +205,13 @@ public class ActivityHandlerService(
|
|||
.FirstOrDefaultAsync(p => p.Followee == actor && p.FollowerId == ids[0]);
|
||||
|
||||
if (request == null)
|
||||
throw GracefulException
|
||||
.UnprocessableEntity($"No follow request matching follower '{ids[0]}' and followee '{actor.Id}' found");
|
||||
{
|
||||
if (await db.Followings.AnyAsync(p => p.Followee == actor && p.FollowerId == ids[0]))
|
||||
return;
|
||||
|
||||
throw GracefulException.UnprocessableEntity($"No follow request matching follower '{ids[0]}'" +
|
||||
$"and followee '{actor.Id}' found");
|
||||
}
|
||||
|
||||
await userSvc.AcceptFollowRequestAsync(request);
|
||||
}
|
||||
|
@ -478,6 +484,51 @@ public class ActivityHandlerService(
|
|||
await userSvc.BlockUserAsync(resolvedActor, resolvedBlockee);
|
||||
}
|
||||
|
||||
private async Task HandleMove(ASMove activity, User resolvedActor)
|
||||
{
|
||||
if (activity.Target.Id is null) throw GracefulException.UnprocessableEntity("Move target must have an ID");
|
||||
var target = await userResolver.ResolveAsync(activity.Target.Id);
|
||||
var source = await userSvc.UpdateUserAsync(resolvedActor, force: true);
|
||||
target = await userSvc.UpdateUserAsync(target, force: true);
|
||||
|
||||
var sourceUri = source.Uri ?? source.GetPublicUri(config.Value.WebDomain);
|
||||
var targetUri = target.Uri ?? target.GetPublicUri(config.Value.WebDomain);
|
||||
var aliases = target.AlsoKnownAs ?? [];
|
||||
if (!aliases.Contains(sourceUri))
|
||||
throw GracefulException.UnprocessableEntity("Refusing to process move activity:" +
|
||||
"source uri not listed in target aliases");
|
||||
|
||||
source.MovedToUri = targetUri;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var followers = db.Followings
|
||||
.Where(p => p.Followee == source)
|
||||
.Where(p => p.Follower.IsLocalUser)
|
||||
.OrderBy(p => p.Id)
|
||||
.Select(p => p.Follower)
|
||||
.PrecomputeRelationshipData(source)
|
||||
.AsChunkedAsyncEnumerable(50);
|
||||
|
||||
await foreach (var follower in followers)
|
||||
{
|
||||
try
|
||||
{
|
||||
await userSvc.FollowUserAsync(follower, target);
|
||||
|
||||
// We need to transfer the precomputed properties to the source user for each follower so that the unfollow method works correctly
|
||||
source.PrecomputedIsFollowedBy = follower.PrecomputedIsFollowing;
|
||||
source.PrecomputedIsRequestedBy = follower.PrecomputedIsRequested;
|
||||
|
||||
await userSvc.UnfollowUserAsync(follower, source);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Failed to process move ({sourceUri} -> {targetUri}) for user {id}: {error}",
|
||||
sourceUri, targetUri, follower.Id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UnfollowAsync(ASActor followeeActor, User follower)
|
||||
{
|
||||
//TODO: send reject? or do we not want to copy that part of the old ap core
|
||||
|
|
|
@ -34,6 +34,7 @@ public class ASActivity : ASObjectWithId
|
|||
public const string Undo = $"{Ns}#Undo";
|
||||
public const string Like = $"{Ns}#Like";
|
||||
public const string Block = $"{Ns}#Block";
|
||||
public const string Move = $"{Ns}#Move";
|
||||
|
||||
// Extensions
|
||||
public const string Bite = "https://ns.mia.jetzt/as#Bite";
|
||||
|
@ -194,4 +195,14 @@ public class ASEmojiReact : ASActivity
|
|||
[J($"{Constants.ActivityStreamsNs}#tag")]
|
||||
[JC(typeof(ASTagConverter))]
|
||||
public List<ASTag>? Tags { get; set; }
|
||||
}
|
||||
|
||||
public class ASMove : ASActivity
|
||||
{
|
||||
public ASMove() => Type = Types.Move;
|
||||
|
||||
[JR]
|
||||
[J($"{Constants.ActivityStreamsNs}#target")]
|
||||
[JC(typeof(ASLinkConverter))]
|
||||
public required ASLink Target { get; set; }
|
||||
}
|
|
@ -53,6 +53,7 @@ public class ASObject : ASObjectBase
|
|||
ASActivity.Types.Announce => token.ToObject<ASAnnounce>(),
|
||||
ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(),
|
||||
ASActivity.Types.Block => token.ToObject<ASBlock>(),
|
||||
ASActivity.Types.Move => token.ToObject<ASMove>(),
|
||||
_ => token.ToObject<ASObject>()
|
||||
};
|
||||
case JTokenType.Array:
|
||||
|
|
|
@ -251,6 +251,7 @@ public class UserService(
|
|||
public async Task<User> UpdateUserAsync(User user, ASActor? actor = null, bool force = false)
|
||||
{
|
||||
if (!user.NeedsUpdate && actor == null && !force) return user;
|
||||
if (user.IsLocalUser) return user;
|
||||
if (actor is { IsUnresolved: true } or { Username: null })
|
||||
actor = null; // This will trigger a fetch a couple lines down
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue