[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:
parent
7ae4dc4c4f
commit
ebbec76cfe
5 changed files with 123 additions and 22 deletions
|
@ -4028,6 +4028,12 @@ namespace Iceshrimp.Backend.Core.Database.Migrations
|
||||||
.HasColumnName("speakAsCat")
|
.HasColumnName("speakAsCat")
|
||||||
.HasComment("Whether to speak as a cat if isCat.");
|
.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")
|
b.Property<List<string>>("Tags")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -266,6 +266,9 @@ public class User : IEntity
|
||||||
[StringLength(128)]
|
[StringLength(128)]
|
||||||
public string? BannerBlurhash { get; set; }
|
public string? BannerBlurhash { get; set; }
|
||||||
|
|
||||||
|
[Column("splitDomainResolved")]
|
||||||
|
public bool SplitDomainResolved { get; set; }
|
||||||
|
|
||||||
[InverseProperty(nameof(AbuseUserReport.Assignee))]
|
[InverseProperty(nameof(AbuseUserReport.Assignee))]
|
||||||
public virtual ICollection<AbuseUserReport> AbuseUserReportAssignees { get; set; } = new List<AbuseUserReport>();
|
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.");
|
.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.Username).HasComment("The username of the User.");
|
||||||
entity.Property(e => e.UsernameLower).HasComment("The username (lowercased) 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)
|
entity.HasOne(d => d.Avatar)
|
||||||
.WithOne(p => p.UserAvatar)
|
.WithOne(p => p.UserAvatar)
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class UserResolver(
|
||||||
return (finalAcct, finalUri);
|
return (finalAcct, finalUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeQuery(string query)
|
public static string NormalizeQuery(string query)
|
||||||
{
|
{
|
||||||
if (query.StartsWith("https://") || query.StartsWith("http://"))
|
if (query.StartsWith("https://") || query.StartsWith("http://"))
|
||||||
if (query.Contains('#'))
|
if (query.Contains('#'))
|
||||||
|
@ -93,7 +93,7 @@ public class UserResolver(
|
||||||
return query;
|
return query;
|
||||||
else if (query.StartsWith('@'))
|
else if (query.StartsWith('@'))
|
||||||
query = $"acct:{query[1..]}";
|
query = $"acct:{query[1..]}";
|
||||||
else
|
else if (!query.StartsWith("acct:"))
|
||||||
query = $"acct:{query}";
|
query = $"acct:{query}";
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
|
|
|
@ -8,6 +8,7 @@ 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.Extensions;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||||
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.Parsing;
|
||||||
|
@ -35,7 +36,8 @@ public class UserService(
|
||||||
ActivityPub.MentionsResolver mentionsResolver,
|
ActivityPub.MentionsResolver mentionsResolver,
|
||||||
ActivityPub.UserRenderer userRenderer,
|
ActivityPub.UserRenderer userRenderer,
|
||||||
QueueService queueSvc,
|
QueueService queueSvc,
|
||||||
EventService eventSvc
|
EventService eventSvc,
|
||||||
|
WebFingerService webFingerSvc
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o =>
|
||||||
|
@ -139,28 +141,29 @@ public class UserService(
|
||||||
|
|
||||||
user = new User
|
user = new User
|
||||||
{
|
{
|
||||||
Id = IdHelpers.GenerateSlowflakeId(),
|
Id = IdHelpers.GenerateSlowflakeId(),
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
LastFetchedAt = followupTaskSvc.IsBackgroundWorker ? null : DateTime.UtcNow,
|
LastFetchedAt = followupTaskSvc.IsBackgroundWorker ? null : DateTime.UtcNow,
|
||||||
DisplayName = actor.DisplayName,
|
DisplayName = actor.DisplayName,
|
||||||
IsLocked = actor.IsLocked ?? false,
|
IsLocked = actor.IsLocked ?? false,
|
||||||
IsBot = actor.IsBot,
|
IsBot = actor.IsBot,
|
||||||
Username = actor.Username!,
|
Username = actor.Username!,
|
||||||
UsernameLower = actor.Username!.ToLowerInvariant(),
|
UsernameLower = actor.Username!.ToLowerInvariant(),
|
||||||
Host = AcctToTuple(acct).Host,
|
Host = AcctToTuple(acct).Host,
|
||||||
MovedToUri = actor.MovedTo?.Link,
|
MovedToUri = actor.MovedTo?.Link,
|
||||||
AlsoKnownAs = actor.AlsoKnownAs?.Where(p => p.Link != null).Select(p => p.Link!).ToList(),
|
AlsoKnownAs = actor.AlsoKnownAs?.Where(p => p.Link != null).Select(p => p.Link!).ToList(),
|
||||||
IsExplorable = actor.IsDiscoverable ?? false,
|
IsExplorable = actor.IsDiscoverable ?? false,
|
||||||
Inbox = actor.Inbox?.Link,
|
Inbox = actor.Inbox?.Link,
|
||||||
SharedInbox = actor.SharedInbox?.Link ?? actor.Endpoints?.SharedInbox?.Id,
|
SharedInbox = actor.SharedInbox?.Link ?? actor.Endpoints?.SharedInbox?.Id,
|
||||||
FollowersUri = actor.Followers?.Id,
|
FollowersUri = actor.Followers?.Id,
|
||||||
Uri = actor.Id,
|
Uri = actor.Id,
|
||||||
IsCat = actor.IsCat ?? false,
|
IsCat = actor.IsCat ?? false,
|
||||||
Featured = actor.Featured?.Id,
|
Featured = actor.Featured?.Id,
|
||||||
|
SplitDomainResolved = true,
|
||||||
//TODO: FollowersCount
|
//TODO: FollowersCount
|
||||||
//TODO: FollowingCount
|
//TODO: FollowingCount
|
||||||
Emojis = emoji.Select(p => p.Id).ToList(),
|
Emojis = emoji.Select(p => p.Id).ToList(),
|
||||||
Tags = tags
|
Tags = tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
var profile = new UserProfile
|
var profile = new UserProfile
|
||||||
|
@ -296,6 +299,7 @@ public class UserService(
|
||||||
user.UserProfile.MentionsResolved = false;
|
user.UserProfile.MentionsResolved = false;
|
||||||
|
|
||||||
user.Tags = ResolveHashtags(user.UserProfile.Description, actor);
|
user.Tags = ResolveHashtags(user.UserProfile.Description, actor);
|
||||||
|
user.Host = await UpdateUserHostAsync(user);
|
||||||
|
|
||||||
db.Update(user);
|
db.Update(user);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
@ -969,4 +973,55 @@ public class UserService(
|
||||||
await deliverSvc.DeliverToAsync(activity, blocker, blockee);
|
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];
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue