[backend/federation] Handle outgoing mentions correctly (ISH-46)
This commit is contained in:
parent
e6079da2ab
commit
16d0199506
5 changed files with 76 additions and 20 deletions
|
@ -3,6 +3,7 @@ using System.Text;
|
|||
using Iceshrimp.Backend.Controllers.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Schemas;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
|
@ -27,9 +28,9 @@ public class ActivityPubController : Controller {
|
|||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||
public async Task<IActionResult> GetNote(string id, [FromServices] DatabaseContext db,
|
||||
[FromServices] NoteRenderer noteRenderer) {
|
||||
var note = await db.Notes.FirstOrDefaultAsync(p => p.Id == id);
|
||||
var note = await db.Notes.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (note == null) return NotFound();
|
||||
var rendered = noteRenderer.RenderAsync(note);
|
||||
var rendered = await noteRenderer.RenderAsync(note);
|
||||
var compacted = LdHelpers.Compact(rendered);
|
||||
return Ok(compacted);
|
||||
}
|
||||
|
@ -43,7 +44,7 @@ public class ActivityPubController : Controller {
|
|||
public async Task<IActionResult> GetUser(string id,
|
||||
[FromServices] DatabaseContext db,
|
||||
[FromServices] UserRenderer userRenderer) {
|
||||
var user = await db.Users.FirstOrDefaultAsync(p => p.Id == id);
|
||||
var user = await db.Users.IncludeCommonProperties().FirstOrDefaultAsync(p => p.Id == id);
|
||||
if (user == null) return NotFound();
|
||||
var rendered = await userRenderer.RenderAsync(user);
|
||||
var compacted = LdHelpers.Compact(rendered);
|
||||
|
|
|
@ -77,7 +77,8 @@ public class MentionsResolver(
|
|||
node.Username = await cache.FetchAsync($"localUserNameCapitalization:{node.Username.ToLowerInvariant()}",
|
||||
TimeSpan.FromHours(24), FetchLocalUserCapitalization);
|
||||
|
||||
node.Acct = $"@{node.Username}";
|
||||
node.Host = config.Value.AccountDomain;
|
||||
node.Acct = $"@{node.Username}@{config.Value.AccountDomain}";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,37 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Extensions;
|
||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||
|
||||
public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter mfmConverter) {
|
||||
public async Task<ASNote> RenderAsync(Note note) {
|
||||
public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter mfmConverter, DatabaseContext db) {
|
||||
public async Task<ASNote> RenderAsync(Note note, List<Note.MentionedUser>? mentions = null) {
|
||||
var id = $"https://{config.Value.WebDomain}/notes/{note.Id}";
|
||||
var userId = $"https://{config.Value.WebDomain}/users/{note.User.Id}";
|
||||
var replyId = note.ReplyId != null
|
||||
? new ASObjectBase($"https://{config.Value.WebDomain}/notes/{note.ReplyId}")
|
||||
: null;
|
||||
|
||||
List<ASObjectBase> to = note.Visibility switch {
|
||||
mentions ??= await db.Users
|
||||
.Where(p => note.Mentions.Contains(p.Id))
|
||||
.IncludeCommonProperties()
|
||||
.Select(p => new Note.MentionedUser {
|
||||
Host = p.Host ?? config.Value.AccountDomain,
|
||||
Username = p.Username,
|
||||
Url = p.UserProfile != null ? p.UserProfile.Url : null,
|
||||
Uri = p.Uri ?? $"https://{config.Value.WebDomain}/users/{p.Id}"
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var to = note.Visibility switch {
|
||||
Note.NoteVisibility.Public => [new ASLink($"{Constants.ActivityStreamsNs}#Public")],
|
||||
Note.NoteVisibility.Followers => [new ASLink($"{userId}/followers")],
|
||||
Note.NoteVisibility.Specified => [], // FIXME
|
||||
Note.NoteVisibility.Specified => mentions.Select(p => new ASObjectBase(p.Uri)).ToList(),
|
||||
_ => []
|
||||
};
|
||||
|
||||
|
@ -26,6 +40,15 @@ public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
|||
_ => []
|
||||
};
|
||||
|
||||
var tags = mentions
|
||||
.Select(mention => new ASMention {
|
||||
Type = $"{Constants.ActivityStreamsNs}#Mention",
|
||||
Name = $"@{mention.Username}@{mention.Host}",
|
||||
Href = new ASObjectBase(mention.Uri)
|
||||
})
|
||||
.Cast<ASTag>()
|
||||
.ToList();
|
||||
|
||||
return new ASNote {
|
||||
Id = id,
|
||||
Content = note.Text != null ? await mfmConverter.ToHtmlAsync(note.Text, []) : null,
|
||||
|
@ -42,7 +65,8 @@ public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
|||
}
|
||||
: null,
|
||||
Cc = cc,
|
||||
To = to
|
||||
To = to,
|
||||
Tags = tags
|
||||
};
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
|||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||
using Iceshrimp.Backend.Core.Helpers;
|
||||
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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -44,6 +46,11 @@ public class NoteService(
|
|||
if (text is { Length: > 100000 })
|
||||
throw GracefulException.BadRequest("Text cannot be longer than 100.000 characters");
|
||||
|
||||
var (mentionedUserIds, mentions, remoteMentions, splitDomainMapping) = await ResolveNoteMentionsAsync(text);
|
||||
|
||||
if (text != null)
|
||||
text = await mentionsResolver.ResolveMentions(text, null, mentions, splitDomainMapping);
|
||||
|
||||
var actor = await userRenderer.RenderAsync(user);
|
||||
|
||||
var note = new Note {
|
||||
|
@ -55,14 +62,18 @@ public class NoteService(
|
|||
UserId = user.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UserHost = null,
|
||||
Visibility = visibility
|
||||
Visibility = visibility,
|
||||
|
||||
Mentions = mentionedUserIds,
|
||||
VisibleUserIds = visibility == Note.NoteVisibility.Specified ? mentionedUserIds : [],
|
||||
MentionedRemoteUsers = remoteMentions,
|
||||
};
|
||||
|
||||
user.NotesCount++;
|
||||
await db.AddAsync(note);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var obj = await noteRenderer.RenderAsync(note);
|
||||
var obj = await noteRenderer.RenderAsync(note, mentions);
|
||||
var activity = ActivityRenderer.RenderCreate(obj, actor);
|
||||
|
||||
await deliverSvc.DeliverToFollowersAsync(activity, user);
|
||||
|
@ -140,7 +151,6 @@ public class NoteService(
|
|||
UserHost = user.Host,
|
||||
Visibility = note.GetVisibility(actor),
|
||||
Reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id) : null
|
||||
//TODO: parse to fields for specified visibility & mentions
|
||||
};
|
||||
|
||||
if (dbNote.Text is { Length: > 100000 })
|
||||
|
@ -150,15 +160,18 @@ public class NoteService(
|
|||
dbNote.Mentions = mentionedUserIds;
|
||||
dbNote.MentionedRemoteUsers = remoteMentions;
|
||||
if (dbNote.Visibility == Note.NoteVisibility.Specified) {
|
||||
var visibleUserIds = note.GetRecipients(actor).Concat(mentionedUserIds).ToList();
|
||||
var visibleUserIds = (await note.GetRecipients(actor)
|
||||
.Select(async p => await userResolver.ResolveAsync(p))
|
||||
.AwaitAllNoConcurrencyAsync())
|
||||
.Select(p => p.Id)
|
||||
.Concat(mentionedUserIds).ToList();
|
||||
if (dbNote.ReplyUserId != null)
|
||||
visibleUserIds.Add(dbNote.ReplyUserId);
|
||||
|
||||
dbNote.VisibleUserIds = visibleUserIds.Distinct().ToList();
|
||||
}
|
||||
|
||||
dbNote.Text =
|
||||
await mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, remoteMentions,
|
||||
dbNote.Text = await mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, remoteMentions,
|
||||
splitDomainMapping);
|
||||
}
|
||||
|
||||
|
@ -175,7 +188,23 @@ public class NoteService(
|
|||
.Select(async p => await userResolver.ResolveAsync(p.Href!.Id!))
|
||||
.AwaitAllNoConcurrencyAsync();
|
||||
|
||||
var userIds = users.Select(p => p.Id).ToList();
|
||||
return ResolveNoteMentions(users);
|
||||
}
|
||||
|
||||
private async Task<MentionQuad> ResolveNoteMentionsAsync(string? text) {
|
||||
var users = text != null
|
||||
? await MfmParser.Parse(text)
|
||||
.SelectMany(p => p.Children.Append(p))
|
||||
.OfType<MfmMentionNode>()
|
||||
.Select(async p => await userResolver.ResolveAsync(p.Acct))
|
||||
.AwaitAllNoConcurrencyAsync()
|
||||
: [];
|
||||
|
||||
return ResolveNoteMentions(users);
|
||||
}
|
||||
|
||||
private MentionQuad ResolveNoteMentions(IReadOnlyCollection<User> users) {
|
||||
var userIds = users.Select(p => p.Id).Distinct().ToList();
|
||||
|
||||
var remoteUsers = users.Where(p => p is { Host: not null, Uri: not null })
|
||||
.ToList();
|
||||
|
|
|
@ -27,11 +27,12 @@ public class UserService(
|
|||
DriveService driveSvc,
|
||||
MfmConverter mfmConverter
|
||||
) {
|
||||
private static (string Username, string? Host) AcctToTuple(string acct) {
|
||||
private (string Username, string? Host) AcctToTuple(string acct) {
|
||||
if (!acct.StartsWith("acct:")) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query");
|
||||
|
||||
var split = acct[5..].Split('@');
|
||||
if (split.Length != 2) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query");
|
||||
if (split.Length != 2)
|
||||
return (split[0], instance.Value.AccountDomain.ToPunycode());
|
||||
|
||||
return (split[0], split[1].ToPunycode());
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue