[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.Attributes;
|
||||||
using Iceshrimp.Backend.Controllers.Schemas;
|
using Iceshrimp.Backend.Controllers.Schemas;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
@ -27,9 +28,9 @@ public class ActivityPubController : Controller {
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ErrorResponse))]
|
||||||
public async Task<IActionResult> GetNote(string id, [FromServices] DatabaseContext db,
|
public async Task<IActionResult> GetNote(string id, [FromServices] DatabaseContext db,
|
||||||
[FromServices] NoteRenderer noteRenderer) {
|
[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();
|
if (note == null) return NotFound();
|
||||||
var rendered = noteRenderer.RenderAsync(note);
|
var rendered = await noteRenderer.RenderAsync(note);
|
||||||
var compacted = LdHelpers.Compact(rendered);
|
var compacted = LdHelpers.Compact(rendered);
|
||||||
return Ok(compacted);
|
return Ok(compacted);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ public class ActivityPubController : Controller {
|
||||||
public async Task<IActionResult> GetUser(string id,
|
public async Task<IActionResult> GetUser(string id,
|
||||||
[FromServices] DatabaseContext db,
|
[FromServices] DatabaseContext db,
|
||||||
[FromServices] UserRenderer userRenderer) {
|
[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();
|
if (user == null) return NotFound();
|
||||||
var rendered = await userRenderer.RenderAsync(user);
|
var rendered = await userRenderer.RenderAsync(user);
|
||||||
var compacted = LdHelpers.Compact(rendered);
|
var compacted = LdHelpers.Compact(rendered);
|
||||||
|
|
|
@ -77,7 +77,8 @@ public class MentionsResolver(
|
||||||
node.Username = await cache.FetchAsync($"localUserNameCapitalization:{node.Username.ToLowerInvariant()}",
|
node.Username = await cache.FetchAsync($"localUserNameCapitalization:{node.Username.ToLowerInvariant()}",
|
||||||
TimeSpan.FromHours(24), FetchLocalUserCapitalization);
|
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.Configuration;
|
||||||
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
|
using Iceshrimp.Backend.Core.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
|
|
||||||
public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter mfmConverter) {
|
public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter mfmConverter, DatabaseContext db) {
|
||||||
public async Task<ASNote> RenderAsync(Note note) {
|
public async Task<ASNote> RenderAsync(Note note, List<Note.MentionedUser>? mentions = null) {
|
||||||
var id = $"https://{config.Value.WebDomain}/notes/{note.Id}";
|
var id = $"https://{config.Value.WebDomain}/notes/{note.Id}";
|
||||||
var userId = $"https://{config.Value.WebDomain}/users/{note.User.Id}";
|
var userId = $"https://{config.Value.WebDomain}/users/{note.User.Id}";
|
||||||
var replyId = note.ReplyId != null
|
var replyId = note.ReplyId != null
|
||||||
? new ASObjectBase($"https://{config.Value.WebDomain}/notes/{note.ReplyId}")
|
? new ASObjectBase($"https://{config.Value.WebDomain}/notes/{note.ReplyId}")
|
||||||
: null;
|
: 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.Public => [new ASLink($"{Constants.ActivityStreamsNs}#Public")],
|
||||||
Note.NoteVisibility.Followers => [new ASLink($"{userId}/followers")],
|
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 {
|
return new ASNote {
|
||||||
Id = id,
|
Id = id,
|
||||||
Content = note.Text != null ? await mfmConverter.ToHtmlAsync(note.Text, []) : null,
|
Content = note.Text != null ? await mfmConverter.ToHtmlAsync(note.Text, []) : null,
|
||||||
|
@ -42,7 +65,8 @@ public class NoteRenderer(IOptions<Config.InstanceSection> config, MfmConverter
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
Cc = cc,
|
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.Federation.ActivityStreams.Types;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
|
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.Middleware;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
@ -44,6 +46,11 @@ public class NoteService(
|
||||||
if (text is { Length: > 100000 })
|
if (text is { Length: > 100000 })
|
||||||
throw GracefulException.BadRequest("Text cannot be longer than 100.000 characters");
|
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 actor = await userRenderer.RenderAsync(user);
|
||||||
|
|
||||||
var note = new Note {
|
var note = new Note {
|
||||||
|
@ -55,14 +62,18 @@ public class NoteService(
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
UserHost = null,
|
UserHost = null,
|
||||||
Visibility = visibility
|
Visibility = visibility,
|
||||||
|
|
||||||
|
Mentions = mentionedUserIds,
|
||||||
|
VisibleUserIds = visibility == Note.NoteVisibility.Specified ? mentionedUserIds : [],
|
||||||
|
MentionedRemoteUsers = remoteMentions,
|
||||||
};
|
};
|
||||||
|
|
||||||
user.NotesCount++;
|
user.NotesCount++;
|
||||||
await db.AddAsync(note);
|
await db.AddAsync(note);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var obj = await noteRenderer.RenderAsync(note);
|
var obj = await noteRenderer.RenderAsync(note, mentions);
|
||||||
var activity = ActivityRenderer.RenderCreate(obj, actor);
|
var activity = ActivityRenderer.RenderCreate(obj, actor);
|
||||||
|
|
||||||
await deliverSvc.DeliverToFollowersAsync(activity, user);
|
await deliverSvc.DeliverToFollowersAsync(activity, user);
|
||||||
|
@ -140,7 +151,6 @@ public class NoteService(
|
||||||
UserHost = user.Host,
|
UserHost = user.Host,
|
||||||
Visibility = note.GetVisibility(actor),
|
Visibility = note.GetVisibility(actor),
|
||||||
Reply = note.InReplyTo?.Id != null ? await ResolveNoteAsync(note.InReplyTo.Id) : null
|
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 })
|
if (dbNote.Text is { Length: > 100000 })
|
||||||
|
@ -150,15 +160,18 @@ public class NoteService(
|
||||||
dbNote.Mentions = mentionedUserIds;
|
dbNote.Mentions = mentionedUserIds;
|
||||||
dbNote.MentionedRemoteUsers = remoteMentions;
|
dbNote.MentionedRemoteUsers = remoteMentions;
|
||||||
if (dbNote.Visibility == Note.NoteVisibility.Specified) {
|
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)
|
if (dbNote.ReplyUserId != null)
|
||||||
visibleUserIds.Add(dbNote.ReplyUserId);
|
visibleUserIds.Add(dbNote.ReplyUserId);
|
||||||
|
|
||||||
dbNote.VisibleUserIds = visibleUserIds.Distinct().ToList();
|
dbNote.VisibleUserIds = visibleUserIds.Distinct().ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
dbNote.Text =
|
dbNote.Text = await mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, remoteMentions,
|
||||||
await mentionsResolver.ResolveMentions(dbNote.Text, dbNote.UserHost, remoteMentions,
|
|
||||||
splitDomainMapping);
|
splitDomainMapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +188,23 @@ public class NoteService(
|
||||||
.Select(async p => await userResolver.ResolveAsync(p.Href!.Id!))
|
.Select(async p => await userResolver.ResolveAsync(p.Href!.Id!))
|
||||||
.AwaitAllNoConcurrencyAsync();
|
.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 })
|
var remoteUsers = users.Where(p => p is { Host: not null, Uri: not null })
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
|
@ -27,11 +27,12 @@ public class UserService(
|
||||||
DriveService driveSvc,
|
DriveService driveSvc,
|
||||||
MfmConverter mfmConverter
|
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");
|
if (!acct.StartsWith("acct:")) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query");
|
||||||
|
|
||||||
var split = acct[5..].Split('@');
|
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());
|
return (split[0], split[1].ToPunycode());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue