[backend/federation] Resolve split domain user hosts exactly once (ISH-201)

This is necessary, since while the current version is handling split domain instances correctly, previous versions (for users who migrated from iceshrimp-js) may have not done so.
Since account domains never change, we only have to do this once.
This commit is contained in:
Laura Hausmann 2024-04-19 00:55:30 +02:00
parent 7ae4dc4c4f
commit ebbec76cfe
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
5 changed files with 123 additions and 22 deletions

View file

@ -4028,6 +4028,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
.HasColumnName("speakAsCat")
.HasComment("Whether to speak as a cat if isCat.");
b.Property<bool>("SplitDomainResolved")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false)
.HasColumnName("splitDomainResolved");
b.Property<List<string>>("Tags")
.IsRequired()
.ValueGeneratedOnAdd()

View file

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Infrastructure;
#nullable disable
namespace Iceshrimp.Backend.Core.Database.Migrations
{
/// <inheritdoc />
[DbContext(typeof(DatabaseContext))]
[Migration("20240418223753_AddUserSplitDomainResolvedColumn")]
public partial class AddUserSplitDomainResolvedColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "splitDomainResolved",
table: "user",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.Sql("""
UPDATE "user" SET "splitDomainResolved" = true WHERE "host" IS NULL;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "splitDomainResolved",
table: "user");
}
}
}

View file

@ -266,6 +266,9 @@ public class User : IEntity
[StringLength(128)]
public string? BannerBlurhash { get; set; }
[Column("splitDomainResolved")]
public bool SplitDomainResolved { get; set; }
[InverseProperty(nameof(AbuseUserReport.Assignee))]
public virtual ICollection<AbuseUserReport> AbuseUserReportAssignees { get; set; } = new List<AbuseUserReport>();
@ -700,6 +703,7 @@ public class UserEntityTypeConfiguration : IEntityTypeConfiguration<User>
.HasComment("The URI of the User. It will be null if the origin of the user is local.");
entity.Property(e => e.Username).HasComment("The username of the User.");
entity.Property(e => e.UsernameLower).HasComment("The username (lowercased) of the User.");
entity.Property(e => e.SplitDomainResolved).HasDefaultValue(false);
entity.HasOne(d => d.Avatar)
.WithOne(p => p.UserAvatar)

View file

@ -84,7 +84,7 @@ public class UserResolver(
return (finalAcct, finalUri);
}
private static string NormalizeQuery(string query)
public static string NormalizeQuery(string query)
{
if (query.StartsWith("https://") || query.StartsWith("http://"))
if (query.Contains('#'))
@ -93,7 +93,7 @@ public class UserResolver(
return query;
else if (query.StartsWith('@'))
query = $"acct:{query[1..]}";
else
else if (!query.StartsWith("acct:"))
query = $"acct:{query}";
return query;

View file

@ -8,6 +8,7 @@ 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.Federation.WebFinger;
using Iceshrimp.Backend.Core.Helpers;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Conversion;
using Iceshrimp.Backend.Core.Helpers.LibMfm.Parsing;
@ -35,7 +36,8 @@ public class UserService(
ActivityPub.MentionsResolver mentionsResolver,
ActivityPub.UserRenderer userRenderer,
QueueService queueSvc,
EventService eventSvc
EventService eventSvc,
WebFingerService webFingerSvc
)
{
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
@ -139,28 +141,29 @@ public class UserService(
user = new User
{
Id = IdHelpers.GenerateSlowflakeId(),
CreatedAt = DateTime.UtcNow,
LastFetchedAt = followupTaskSvc.IsBackgroundWorker ? null : DateTime.UtcNow,
DisplayName = actor.DisplayName,
IsLocked = actor.IsLocked ?? false,
IsBot = actor.IsBot,
Username = actor.Username!,
UsernameLower = actor.Username!.ToLowerInvariant(),
Host = AcctToTuple(acct).Host,
MovedToUri = actor.MovedTo?.Link,
AlsoKnownAs = actor.AlsoKnownAs?.Where(p => p.Link != null).Select(p => p.Link!).ToList(),
IsExplorable = actor.IsDiscoverable ?? false,
Inbox = actor.Inbox?.Link,
SharedInbox = actor.SharedInbox?.Link ?? actor.Endpoints?.SharedInbox?.Id,
FollowersUri = actor.Followers?.Id,
Uri = actor.Id,
IsCat = actor.IsCat ?? false,
Featured = actor.Featured?.Id,
Id = IdHelpers.GenerateSlowflakeId(),
CreatedAt = DateTime.UtcNow,
LastFetchedAt = followupTaskSvc.IsBackgroundWorker ? null : DateTime.UtcNow,
DisplayName = actor.DisplayName,
IsLocked = actor.IsLocked ?? false,
IsBot = actor.IsBot,
Username = actor.Username!,
UsernameLower = actor.Username!.ToLowerInvariant(),
Host = AcctToTuple(acct).Host,
MovedToUri = actor.MovedTo?.Link,
AlsoKnownAs = actor.AlsoKnownAs?.Where(p => p.Link != null).Select(p => p.Link!).ToList(),
IsExplorable = actor.IsDiscoverable ?? false,
Inbox = actor.Inbox?.Link,
SharedInbox = actor.SharedInbox?.Link ?? actor.Endpoints?.SharedInbox?.Id,
FollowersUri = actor.Followers?.Id,
Uri = actor.Id,
IsCat = actor.IsCat ?? false,
Featured = actor.Featured?.Id,
SplitDomainResolved = true,
//TODO: FollowersCount
//TODO: FollowingCount
Emojis = emoji.Select(p => p.Id).ToList(),
Tags = tags
Tags = tags,
};
var profile = new UserProfile
@ -296,6 +299,7 @@ public class UserService(
user.UserProfile.MentionsResolved = false;
user.Tags = ResolveHashtags(user.UserProfile.Description, actor);
user.Host = await UpdateUserHostAsync(user);
db.Update(user);
await db.SaveChangesAsync();
@ -969,4 +973,55 @@ public class UserService(
await deliverSvc.DeliverToAsync(activity, blocker, blockee);
}
}
private async Task<string?> UpdateUserHostAsync(User user)
{
if (user.Host == null || user.Uri == null || user.SplitDomainResolved)
return user.Host;
var res = await webFingerSvc.ResolveAsync(user.Uri);
var match = res?.Links.FirstOrDefault(p => p is { Rel: "self", Type: "application/activity+json" })?.Href;
if (res == null || match != user.Uri)
{
logger.LogWarning("Updating split domain host failed for user {id}: uri mismatch (pass 1) - '{uri}' <> '{match}'",
user.Id, user.Uri, match);
return user.Host;
}
var acct = ActivityPub.UserResolver.NormalizeQuery(res.Subject);
var split = acct.Split('@');
if (split.Length != 2)
{
logger.LogWarning("Updating split domain host failed for user {id}: invalid acct - '{acct}'",
user.Id, acct);
return user.Host;
}
if (user.Host == split[1])
{
user.SplitDomainResolved = true;
return user.Host;
}
logger.LogDebug("Updating split domain for user {id}: {host} -> {newHost}", user.Id, user.Host, split[1]);
res = await webFingerSvc.ResolveAsync(acct);
match = res?.Links.FirstOrDefault(p => p is { Rel: "self", Type: "application/activity+json" })?.Href;
if (res == null || match != user.Uri)
{
logger.LogWarning("Updating split domain host failed for user {id}: uri mismatch (pass 2) - '{uri}' <> '{match}'",
user.Id, user.Uri, match);
return user.Host;
}
if (acct != ActivityPub.UserResolver.NormalizeQuery(res.Subject))
{
logger.LogWarning("Updating split domain host failed for user {id}: subject mismatch - '{acct}' <> '{subject}'",
user.Id, acct, res.Subject.TrimStart('@'));
return user.Host;
}
user.SplitDomainResolved = true;
return split[1];
}
}