[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")
|
||||
.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()
|
||||
|
|
|
@ -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)]
|
||||
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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue