[backend/federation] Resolve user pinned posts (ISH-67)
This commit is contained in:
parent
e69bbcb977
commit
81059291c3
9 changed files with 104 additions and 21 deletions
|
@ -14,15 +14,15 @@ public class ObjectResolver(
|
|||
IOptions<Config.InstanceSection> config
|
||||
)
|
||||
{
|
||||
public async Task<ASObject?> ResolveObject(ASObjectBase baseObj, int recurse = 5)
|
||||
public async Task<ASObject?> ResolveObject(ASObjectBase baseObj, int recurse = 5, bool force = false)
|
||||
{
|
||||
if (baseObj is ASActivity { Object.IsUnresolved: true } activity && recurse > 0)
|
||||
{
|
||||
activity.Object = await ResolveObject(activity.Object, --recurse);
|
||||
activity.Object = await ResolveObject(activity.Object, --recurse, force);
|
||||
return await ResolveObject(activity, recurse);
|
||||
}
|
||||
|
||||
if (baseObj is ASObject { IsUnresolved: false } obj)
|
||||
if (baseObj is ASObject { IsUnresolved: false } obj && !force)
|
||||
return obj;
|
||||
if (baseObj.Id == null)
|
||||
{
|
||||
|
|
|
@ -52,8 +52,8 @@ public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseConte
|
|||
Type = type,
|
||||
Inbox = new ASLink($"{id}/inbox"),
|
||||
Outbox = new ASCollection($"{id}/outbox"),
|
||||
Followers = new ASCollection($"{id}/followers"),
|
||||
Following = new ASCollection($"{id}/following"),
|
||||
Followers = new ASOrderedCollection($"{id}/followers"),
|
||||
Following = new ASOrderedCollection($"{id}/following"),
|
||||
SharedInbox = new ASLink($"https://{config.Value.WebDomain}/inbox"),
|
||||
Url = new ASLink(user.GetPublicUrl(config.Value)),
|
||||
Username = user.Username,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"movedTo": {
|
||||
"@id": "https://www.w3.org/ns/activitystreams#movedTo",
|
||||
"@type": "@id"
|
||||
}
|
||||
},
|
||||
"featured": "http://joinmastodon.org/ns#featured"
|
||||
}
|
||||
}
|
|
@ -75,12 +75,12 @@ public class ASActor : ASObject
|
|||
public ASLink? Inbox { get; set; }
|
||||
|
||||
[J($"{Constants.ActivityStreamsNs}#followers")]
|
||||
[JC(typeof(ASCollectionConverter))]
|
||||
public ASCollection? Followers { get; set; }
|
||||
[JC(typeof(ASOrderedCollectionConverter))]
|
||||
public ASOrderedCollection? Followers { get; set; }
|
||||
|
||||
[J($"{Constants.ActivityStreamsNs}#following")]
|
||||
[JC(typeof(ASCollectionConverter))]
|
||||
public ASCollection? Following { get; set; }
|
||||
[JC(typeof(ASOrderedCollectionConverter))]
|
||||
public ASOrderedCollection? Following { get; set; }
|
||||
|
||||
[J($"{Constants.ActivityStreamsNs}#sharedInbox")]
|
||||
[JC(typeof(ASLinkConverter))]
|
||||
|
@ -98,8 +98,8 @@ public class ASActor : ASObject
|
|||
public List<ASLink>? AlsoKnownAs { get; set; }
|
||||
|
||||
[J("http://joinmastodon.org/ns#featured")]
|
||||
[JC(typeof(ASLinkConverter))]
|
||||
public ASLink? Featured { get; set; }
|
||||
[JC(typeof(VC))]
|
||||
public ASOrderedCollection? Featured { get; set; }
|
||||
|
||||
[J("http://joinmastodon.org/ns#featuredTags")]
|
||||
[JC(typeof(ASLinkConverter))]
|
||||
|
|
|
@ -35,6 +35,8 @@ public class ASCollection : ASObject
|
|||
[JC(typeof(ASLinkConverter))]
|
||||
public ASLink? Last { get; set; }
|
||||
|
||||
public new bool IsUnresolved => !TotalItems.HasValue;
|
||||
|
||||
public const string ObjectType = $"{Constants.ActivityStreamsNs}#Collection";
|
||||
}
|
||||
|
||||
|
@ -59,7 +61,7 @@ internal sealed class ASCollectionItemsConverter : JsonConverter
|
|||
var obj = JArray.Load(reader);
|
||||
return obj.Count == 0
|
||||
? null
|
||||
: obj.SelectToken("..@list")?.Children().Select(ASObject.Deserialize).OfType<ASObject>().ToList();
|
||||
: obj.SelectToken("$.[*].@list")?.Children().Select(ASObject.Deserialize).OfType<ASObject>().ToList();
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||
using JC = Newtonsoft.Json.JsonConverterAttribute;
|
||||
using JI = Newtonsoft.Json.JsonIgnoreAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
|
||||
public class ASOrderedCollection : ASCollection
|
||||
{
|
||||
public ASOrderedCollection() => Type = ObjectType;
|
||||
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public ASOrderedCollection(string id) : this() => Id = id;
|
||||
|
||||
|
||||
[J($"{Constants.ActivityStreamsNs}#orderedItems")]
|
||||
public List<ASObject>? OrderedItems { get; set; }
|
||||
|
||||
[JC(typeof(ASCollectionItemsConverter))]
|
||||
[JI]
|
||||
public List<ASObject>? OrderedItems
|
||||
{
|
||||
get => Items;
|
||||
set => Items = value;
|
||||
}
|
||||
|
||||
public new const string ObjectType = $"{Constants.ActivityStreamsNs}#OrderedCollection";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ASOrderedCollectionConverter : ASSerializer.ListSingleObjectConverter<ASOrderedCollection>;
|
|
@ -65,6 +65,14 @@ public class ValueObjectConverter : JsonConverter
|
|||
return val != null ? Convert.ToUInt64(val) : null;
|
||||
}
|
||||
|
||||
if (obj?.Value is string id)
|
||||
{
|
||||
if (objectType == typeof(ASOrderedCollection))
|
||||
return new ASOrderedCollection(id);
|
||||
if (objectType == typeof(ASCollection))
|
||||
return new ASCollection(id);
|
||||
}
|
||||
|
||||
return obj?.Value;
|
||||
}
|
||||
|
||||
|
@ -92,6 +100,18 @@ public class ValueObjectConverter : JsonConverter
|
|||
writer.WritePropertyName("@value");
|
||||
writer.WriteValue(ul);
|
||||
break;
|
||||
case ASOrderedCollection oc:
|
||||
writer.WritePropertyName("@type");
|
||||
writer.WriteValue(ASOrderedCollection.ObjectType);
|
||||
writer.WritePropertyName("@value");
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(oc));
|
||||
break;
|
||||
case ASCollection c:
|
||||
writer.WritePropertyName("@type");
|
||||
writer.WriteValue(ASCollection.ObjectType);
|
||||
writer.WritePropertyName("@value");
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(c));
|
||||
break;
|
||||
default:
|
||||
writer.WritePropertyName("@value");
|
||||
writer.WriteValue(value);
|
||||
|
|
|
@ -38,7 +38,8 @@ public class NoteService(
|
|||
EventService eventSvc,
|
||||
ActivityPub.ActivityRenderer activityRenderer,
|
||||
EmojiService emojiSvc,
|
||||
FollowupTaskService followupTaskSvc
|
||||
FollowupTaskService followupTaskSvc,
|
||||
ActivityPub.ObjectResolver objectResolver
|
||||
)
|
||||
{
|
||||
private readonly List<string> _resolverHistory = [];
|
||||
|
@ -767,4 +768,37 @@ public class NoteService(
|
|||
var dbNote = await ResolveNoteAsync(note) ?? throw new Exception("Cannot unregister like for unknown note");
|
||||
await UnlikeNoteAsync(dbNote, actor);
|
||||
}
|
||||
|
||||
public async Task UpdatePinnedNotesAsync(ASActor actor, User user)
|
||||
{
|
||||
logger.LogDebug("Updating pinned notes for user {user}", user.Id);
|
||||
var collection = actor.Featured;
|
||||
if (collection == null) return;
|
||||
if (collection.IsUnresolved)
|
||||
collection = await objectResolver.ResolveObject(collection, force: true) as ASOrderedCollection;
|
||||
if (collection is not { OrderedItems: not null }) return;
|
||||
|
||||
var items = await collection.OrderedItems.Take(10).Select(p => objectResolver.ResolveObject(p)).AwaitAllAsync();
|
||||
var notes = await items.OfType<ASNote>().Select(p => ResolveNoteAsync(p.Id, p)).AwaitAllNoConcurrencyAsync();
|
||||
var previousPins = await db.Users.Where(p => p.Id == user.Id)
|
||||
.Select(p => p.PinnedNotes.Select(i => i.Id))
|
||||
.FirstOrDefaultAsync() ??
|
||||
throw new Exception("existingPins must not be null at this stage");
|
||||
|
||||
if (previousPins.SequenceEqual(notes.Where(p => p != null).Cast<Note>().Select(p => p.Id))) return;
|
||||
|
||||
var pins = notes.Where(p => p != null)
|
||||
.Cast<Note>()
|
||||
.Select(p => new UserNotePin
|
||||
{
|
||||
Id = IdHelpers.GenerateSlowflakeId(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Note = p,
|
||||
User = user
|
||||
});
|
||||
|
||||
db.RemoveRange(await db.UserNotePins.Where(p => p.User == user).ToListAsync());
|
||||
await db.AddRangeAsync(pins);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
|
@ -153,7 +153,7 @@ public class UserService(
|
|||
FollowersUri = actor.Followers?.Id,
|
||||
Uri = actor.Id,
|
||||
IsCat = actor.IsCat ?? false,
|
||||
Featured = actor.Featured?.Link,
|
||||
Featured = actor.Featured?.Id,
|
||||
//TODO: FollowersCount
|
||||
//TODO: FollowingCount
|
||||
Emojis = emoji.Select(p => p.Id).ToList(),
|
||||
|
@ -185,6 +185,7 @@ public class UserService(
|
|||
await db.SaveChangesAsync();
|
||||
await processPendingDeletes();
|
||||
user = await UpdateProfileMentions(user, actor);
|
||||
UpdateUserPinnedNotesInBackground(actor, user);
|
||||
_ = followupTaskSvc.ExecuteTask("UpdateInstanceUserCounter", async provider =>
|
||||
{
|
||||
var bgDb = provider.GetRequiredService<DatabaseContext>();
|
||||
|
@ -256,7 +257,7 @@ public class UserService(
|
|||
user.IsExplorable = actor.IsDiscoverable ?? false;
|
||||
user.FollowersUri = actor.Followers?.Id;
|
||||
user.IsCat = actor.IsCat ?? false;
|
||||
user.Featured = actor.Featured?.Link;
|
||||
user.Featured = actor.Featured?.Id;
|
||||
|
||||
var emoji = await emojiSvc.ProcessEmojiAsync(actor.Tags?.OfType<ASEmoji>().ToList(),
|
||||
user.Host ??
|
||||
|
@ -301,6 +302,7 @@ public class UserService(
|
|||
await db.SaveChangesAsync();
|
||||
await processPendingDeletes();
|
||||
user = await UpdateProfileMentions(user, actor, force: true);
|
||||
UpdateUserPinnedNotesInBackground(actor, user, force: true);
|
||||
return user;
|
||||
}
|
||||
|
||||
|
@ -702,6 +704,20 @@ public class UserService(
|
|||
// Clean up user list memberships
|
||||
await db.UserListMembers.Where(p => p.UserList.User == user && p.User == followee).ExecuteDeleteAsync();
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameter", Justification = "Method only makes sense for users")]
|
||||
private void UpdateUserPinnedNotesInBackground(ASActor actor, User user, bool force = false)
|
||||
{
|
||||
if (followupTaskSvc.IsBackgroundWorker && !force) return;
|
||||
_ = followupTaskSvc.ExecuteTask("UpdateUserPinnedNotes", async provider =>
|
||||
{
|
||||
var bgDb = provider.GetRequiredService<DatabaseContext>();
|
||||
var bgNoteSvc = provider.GetRequiredService<NoteService>();
|
||||
var bgUser = await bgDb.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == user.Id);
|
||||
if (bgUser == null) return;
|
||||
await bgNoteSvc.UpdatePinnedNotesAsync(actor, bgUser);
|
||||
});
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataQuery", Justification = "Projectables")]
|
||||
[SuppressMessage("ReSharper", "EntityFramework.NPlusOne.IncompleteDataUsage", Justification = "Same as above")]
|
||||
|
|
Loading…
Add table
Reference in a new issue