[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
|
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>(
|
public static IQueryable<T> Paginate<T>(
|
||||||
this IQueryable<T> query,
|
this IQueryable<T> query,
|
||||||
MastodonPaginationQuery pq,
|
MastodonPaginationQuery pq,
|
||||||
|
|
|
@ -76,6 +76,7 @@ public class ActivityHandlerService(
|
||||||
ASEmojiReact react => HandleReact(react, resolvedActor),
|
ASEmojiReact react => HandleReact(react, resolvedActor),
|
||||||
ASFollow follow => HandleFollow(follow, resolvedActor),
|
ASFollow follow => HandleFollow(follow, resolvedActor),
|
||||||
ASLike like => HandleLike(like, resolvedActor),
|
ASLike like => HandleLike(like, resolvedActor),
|
||||||
|
ASMove move => HandleMove(move, resolvedActor),
|
||||||
ASReject reject => HandleReject(reject, resolvedActor),
|
ASReject reject => HandleReject(reject, resolvedActor),
|
||||||
ASUndo undo => HandleUndo(undo, resolvedActor),
|
ASUndo undo => HandleUndo(undo, resolvedActor),
|
||||||
ASUnfollow unfollow => HandleUnfollow(unfollow, resolvedActor),
|
ASUnfollow unfollow => HandleUnfollow(unfollow, resolvedActor),
|
||||||
|
@ -204,8 +205,13 @@ public class ActivityHandlerService(
|
||||||
.FirstOrDefaultAsync(p => p.Followee == actor && p.FollowerId == ids[0]);
|
.FirstOrDefaultAsync(p => p.Followee == actor && p.FollowerId == ids[0]);
|
||||||
|
|
||||||
if (request == null)
|
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);
|
await userSvc.AcceptFollowRequestAsync(request);
|
||||||
}
|
}
|
||||||
|
@ -478,6 +484,51 @@ public class ActivityHandlerService(
|
||||||
await userSvc.BlockUserAsync(resolvedActor, resolvedBlockee);
|
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)
|
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
|
//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 Undo = $"{Ns}#Undo";
|
||||||
public const string Like = $"{Ns}#Like";
|
public const string Like = $"{Ns}#Like";
|
||||||
public const string Block = $"{Ns}#Block";
|
public const string Block = $"{Ns}#Block";
|
||||||
|
public const string Move = $"{Ns}#Move";
|
||||||
|
|
||||||
// Extensions
|
// Extensions
|
||||||
public const string Bite = "https://ns.mia.jetzt/as#Bite";
|
public const string Bite = "https://ns.mia.jetzt/as#Bite";
|
||||||
|
@ -195,3 +196,13 @@ public class ASEmojiReact : ASActivity
|
||||||
[JC(typeof(ASTagConverter))]
|
[JC(typeof(ASTagConverter))]
|
||||||
public List<ASTag>? Tags { get; set; }
|
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.Announce => token.ToObject<ASAnnounce>(),
|
||||||
ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(),
|
ASActivity.Types.EmojiReact => token.ToObject<ASEmojiReact>(),
|
||||||
ASActivity.Types.Block => token.ToObject<ASBlock>(),
|
ASActivity.Types.Block => token.ToObject<ASBlock>(),
|
||||||
|
ASActivity.Types.Move => token.ToObject<ASMove>(),
|
||||||
_ => token.ToObject<ASObject>()
|
_ => token.ToObject<ASObject>()
|
||||||
};
|
};
|
||||||
case JTokenType.Array:
|
case JTokenType.Array:
|
||||||
|
|
|
@ -251,6 +251,7 @@ public class UserService(
|
||||||
public async Task<User> UpdateUserAsync(User user, ASActor? actor = null, bool force = false)
|
public async Task<User> UpdateUserAsync(User user, ASActor? actor = null, bool force = false)
|
||||||
{
|
{
|
||||||
if (!user.NeedsUpdate && actor == null && !force) return user;
|
if (!user.NeedsUpdate && actor == null && !force) return user;
|
||||||
|
if (user.IsLocalUser) return user;
|
||||||
if (actor is { IsUnresolved: true } or { Username: null })
|
if (actor is { IsUnresolved: true } or { Username: null })
|
||||||
actor = null; // This will trigger a fetch a couple lines down
|
actor = null; // This will trigger a fetch a couple lines down
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue