Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/backend/migration/1772983353696-user-acct.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class UserAcct1772983353696 {
name = 'UserAcct1772983353696'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "acct" character varying(512)`);
await queryRunner.query(`COMMENT ON COLUMN "user"."acct" IS 'The last retrieved webfinger subject of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`CREATE INDEX "IDX_0be9d7dcbac33e23aba1637a69" ON "user" ("acct") `);
}

async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_0be9d7dcbac33e23aba1637a69"`);
await queryRunner.query(`COMMENT ON COLUMN "user"."acct" IS 'The last retrieved webfinger subject of the User. It will be null if the origin of the user is local.'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "acct"`);
}
}
12 changes: 6 additions & 6 deletions packages/backend/src/core/AntennaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,15 @@ export class AntennaService implements OnApplicationShutdown {
} else if (antenna.src === 'users') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
return this.utilityService.getFullApAccount({ username, host }).toLowerCase();
});
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
if (!accts.includes(this.utilityService.getFullApAccount(noteUser).toLowerCase())) return false;
} else if (antenna.src === 'users_blacklist') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
return this.utilityService.getFullApAccount(username, host).toLowerCase();
return this.utilityService.getFullApAccount({ username, host }).toLowerCase();
});
if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
if (accts.includes(this.utilityService.getFullApAccount(noteUser).toLowerCase())) return false;
}

const keywords = antenna.keywords
Expand Down Expand Up @@ -225,11 +225,11 @@ export class AntennaService implements OnApplicationShutdown {
// There is a possibility for users to add the srcUser to their antennas, but it's low, so we don't check it.

// Get MiAntenna[] from cache and filter to select antennas with the src user is in the users list
const srcUserAcct = this.utilityService.getFullApAccount(src.username, src.host).toLowerCase();
const srcUserAcct = this.utilityService.getFullApAccount(src).toLowerCase();
const antennasToMigrate = (await this.getAntennas()).filter(antenna => {
return antenna.users.some(user => {
const { username, host } = Acct.parse(user);
return this.utilityService.getFullApAccount(username, host).toLowerCase() === srcUserAcct;
return this.utilityService.getFullApAccount({ username, host }).toLowerCase() === srcUserAcct;
});
});

Expand Down
40 changes: 31 additions & 9 deletions packages/backend/src/core/RemoteUserResolveService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { RemoteLoggerService } from '@/core/RemoteLoggerService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { bindThis } from '@/decorators.js';
import * as Acct from '@/misc/acct.js';

@Injectable()
export class RemoteUserResolveService {
Expand Down Expand Up @@ -67,12 +68,15 @@ export class RemoteUserResolveService {
}) as MiLocalUser;
}

const user = await this.usersRepository.findOneBy({ usernameLower, host }) as MiRemoteUser | null;

const acctLower = `${usernameLower}@${host}`;

const user = await this.usersRepository.findOneBy([
{ usernameLower, host },
{ acct: acctLower },
]) as MiRemoteUser | null;

if (user == null) {
const self = await this.resolveSelf(acctLower);
const { self, subject } = await this.resolveWebfinger(acctLower);

if (this.utilityService.isUriLocal(self.href)) {
const local = this.apDbResolverService.parseUri(self.href);
Expand All @@ -90,8 +94,20 @@ export class RemoteUserResolveService {
}
}

this.logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
return await this.apPersonService.createPerson(self.href);
this.logger.succ(`return new remote user: ${chalk.magenta(subject)}`);
const newUser = await this.apPersonService.createPerson({ uri: self.href, acct: subject });

if (newUser.acct !== subject) {
await this.usersRepository.update({
id: newUser.id,
}, {
acct: subject
});

newUser.acct = subject;
}

return newUser;
}

// ユーザー情報が古い場合は、WebFingerからやりなおして返す
Expand All @@ -102,7 +118,7 @@ export class RemoteUserResolveService {
});

this.logger.info(`try resync: ${acctLower}`);
const self = await this.resolveSelf(acctLower);
const { self, subject } = await this.resolveWebfinger(acctLower);

if (user.uri !== self.href) {
// if uri mismatch, Fix (user@host <=> AP's Person id(RemoteUser.uri)) mapping.
Expand All @@ -120,12 +136,13 @@ export class RemoteUserResolveService {
host: host,
}, {
uri: self.href,
acct: subject,
});
} else {
this.logger.info(`uri is fine: ${acctLower}`);
}

await this.apPersonService.updatePerson(self.href);
await this.apPersonService.updatePerson({ uri: self.href, acct: subject });

this.logger.info(`return resynced remote user: ${acctLower}`);
return await this.usersRepository.findOneBy({ uri: self.href }).then(u => {
Expand All @@ -142,7 +159,7 @@ export class RemoteUserResolveService {
}

@bindThis
private async resolveSelf(acctLower: string): Promise<ILink> {
private async resolveWebfinger(acctLower: string): Promise<{ self: ILink, subject: string | null }> {
this.logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
const finger = await this.webfingerService.webfinger(acctLower).catch(err => {
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ err.statusCode ?? err.message }`);
Expand All @@ -153,6 +170,11 @@ export class RemoteUserResolveService {
this.logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`);
throw new Error('self link not found');
}
return self;
let subject = Acct.validate(finger.subject) ? finger.subject :
Acct.validate(acctLower) ? acctLower : null;
if (subject) {
subject = Acct.parse(subject).toString();
}
return { self, subject };
}
}
2 changes: 2 additions & 0 deletions packages/backend/src/core/UserFollowingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ const logger = new Logger('following/create');
type Local = MiLocalUser | {
id: MiLocalUser['id'];
host: MiLocalUser['host'];
acct: MiLocalUser['acct'];
uri: MiLocalUser['uri']
};
type Remote = MiRemoteUser | {
id: MiRemoteUser['id'];
host: MiRemoteUser['host'];
acct: MiRemoteUser['acct'];
uri: MiRemoteUser['uri'];
inbox: MiRemoteUser['inbox'];
};
Expand Down
9 changes: 7 additions & 2 deletions packages/backend/src/core/UtilityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
import { MiMeta, SoftwareSuspension } from '@/models/Meta.js';
import { MiInstance } from '@/models/Instance.js';
import { MiUser } from '@/models/User.js';

@Injectable()
export class UtilityService {
Expand All @@ -25,8 +26,12 @@ export class UtilityService {
}

@bindThis
public getFullApAccount(username: string, host: string | null): string {
return host ? `${username}@${this.toPuny(host)}` : `${username}@${this.toPuny(this.config.host)}`;
public getFullApAccount(user: { username: string, host: string | null, acct?: string | null }): string {
if (user.acct) {
return user.acct;
}

return user.host ? `${user.username}@${this.toPuny(user.host)}` : `${user.username}@${this.toPuny(this.config.host)}`;
}

@bindThis
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/core/WebhookTestService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
uri: null,
followersUri: null,
token: null,
acct: null,
...override,
};
}
Expand Down Expand Up @@ -411,6 +412,7 @@ export class WebhookTestService {
name: user.name,
username: user.username,
host: user.host,
acct: user.acct,
avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? '',
avatarBlurhash: user.avatarId == null ? null : user.avatarBlurhash,
avatarDecorations: user.avatarDecorations.map(it => ({
Expand Down
34 changes: 19 additions & 15 deletions packages/backend/src/core/activitypub/models/ApPersonService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,21 +301,22 @@ export class ApPersonService implements OnModuleInit {
* Personを作成します。
*/
@bindThis
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
public async createPerson(args: string | { uri: string, acct?: string | null }, resolver?: Resolver): Promise<MiRemoteUser> {
if (typeof args === 'string') args = { uri: args };
if (typeof args.uri !== 'string') throw new Error('uri is not string');

const host = this.utilityService.punyHost(uri);
const host = this.utilityService.punyHost(args.uri);
if (host === this.utilityService.toPuny(this.config.host)) {
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
}

// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = await this.apResolverService.createResolver();

const object = await resolver.resolve(uri);
const object = await resolver.resolve(args.uri);
if (object.id == null) throw new Error('invalid object.id: ' + object.id);

const person = this.validateActor(object, uri);
const person = this.validateActor(object, args.uri);

this.logger.info(`Creating the Person: ${person.id}`);

Expand Down Expand Up @@ -381,6 +382,7 @@ export class ApPersonService implements OnModuleInit {
username: person.preferredUsername,
usernameLower: person.preferredUsername?.toLowerCase(),
host,
acct: args.acct ?? null,
inbox: person.inbox,
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
followersUri: person.followers ? getApId(person.followers) : undefined,
Expand Down Expand Up @@ -488,23 +490,24 @@ export class ApPersonService implements OnModuleInit {
* @param movePreventUris ここに指定されたURIがPersonのmovedToに指定されていたり10回より多く回っている場合これ以上アカウント移行を行わない(無限ループ防止)
*/
@bindThis
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
if (typeof uri !== 'string') throw new Error('uri is not string');
public async updatePerson(args: string | { uri: string, acct?: string | null }, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
if (typeof args === 'string') args = { uri: args };
if (typeof args.uri !== 'string') throw new Error('uri is not string');

// URIがこのサーバーを指しているならスキップ
if (this.utilityService.isUriLocal(uri)) return;
if (this.utilityService.isUriLocal(args.uri)) return;

//#region このサーバーに既に登録されているか
const exist = await this.fetchPerson(uri) as MiRemoteUser | null;
const exist = await this.fetchPerson(args.uri) as MiRemoteUser | null;
if (exist === null) return;
//#endregion

// eslint-disable-next-line no-param-reassign
if (resolver == null) resolver = await this.apResolverService.createResolver();

const object = hint ?? await resolver.resolve(uri);
const object = hint ?? await resolver.resolve(args.uri);

const person = this.validateActor(object, uri);
const person = this.validateActor(object, args.uri);

this.logger.info(`Updating the Person: ${person.id}`);

Expand Down Expand Up @@ -557,6 +560,7 @@ export class ApPersonService implements OnModuleInit {

const updates = {
lastFetchedAt: new Date(),
acct: args.acct,
inbox: person.inbox,
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
followersUri: person.followers ? getApId(person.followers) : undefined,
Expand Down Expand Up @@ -639,7 +643,7 @@ export class ApPersonService implements OnModuleInit {

const updated = { ...exist, ...updates };

this.cacheService.uriPersonCache.set(uri, updated);
this.cacheService.uriPersonCache.set(args.uri, updated);

// 移行処理を行う
if (updated.movedAt && (
Expand All @@ -649,14 +653,14 @@ export class ApPersonService implements OnModuleInit {
// (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく)
exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime()
)) {
this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`);
this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${args.uri})`);
return this.processRemoteMove(updated, movePreventUris)
.then(result => {
this.logger.info(`Processing Move Finished [${result}] @${updated.username}@${updated.host} (${uri})`);
this.logger.info(`Processing Move Finished [${result}] @${updated.username}@${updated.host} (${args.uri})`);
return result;
})
.catch(e => {
this.logger.info(`Processing Move Failed @${updated.username}@${updated.host} (${uri})`, { stack: e });
this.logger.info(`Processing Move Failed @${updated.username}@${updated.host} (${args.uri})`, { stack: e });
});
}

Expand Down
4 changes: 3 additions & 1 deletion packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ export class UserEntityService implements OnModuleInit {
if ((user.host == null || user.host === this.config.host) && user.username.includes('.') && this.meta.iconUrl) { // ローカルのシステムアカウントの場合
return this.meta.iconUrl;
} else {
return `${this.config.url}/identicon/${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
const acct = user.acct?.toLowerCase() ?? `${user.username.toLowerCase()}@${user.host ?? this.config.host}`;
return `${this.config.url}/identicon/${acct}`;
}
}

Expand Down Expand Up @@ -487,6 +488,7 @@ export class UserEntityService implements OnModuleInit {
name: user.name,
username: user.username,
host: user.host,
acct: user.acct,
avatarUrl: (user.avatarId == null ? null : user.avatarUrl) ?? this.getIdenticonUrl(user),
avatarBlurhash: (user.avatarId == null ? null : user.avatarBlurhash),
avatarDecorations: user.avatarDecorations.length > 0 ? this.avatarDecorationService.getAll().then(decorations => user.avatarDecorations.filter(ud => decorations.some(d => d.id === ud.id)).map(ud => ({
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/misc/acct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Acct = {
};

export function parse(acct: string): Acct {
if (acct.startsWith('acct:')) acct = acct.substring(5);
if (acct.startsWith('@')) acct = acct.substring(1);
const split = acct.split('@', 2);
return { username: split[0], host: split[1] ?? null };
Expand All @@ -17,3 +18,7 @@ export function parse(acct: string): Acct {
export function toString(acct: Acct): string {
return acct.host == null ? acct.username : `${acct.username}@${acct.host}`;
}

export function validate(acct: string): boolean {
return acct.match(/^(acct:)?[@]?[^@]+@[^@]+\.[^@]+$/) !== null;
}
11 changes: 11 additions & 0 deletions packages/backend/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ export class MiUser {
})
public host: string | null;

@Index()
@Column('varchar', {
length: 512, nullable: true,
comment: 'The last retrieved webfinger subject of the User. It will be null if the origin of the user is local.',
})
public acct: string | null;

@Column('varchar', {
length: 512, nullable: true,
comment: 'The inbox URL of the User. It will be null if the origin of the user is local.',
Expand Down Expand Up @@ -297,23 +304,27 @@ export class MiUser {
export type MiLocalUser = MiUser & {
host: null;
uri: null;
acct: null;
};

export type MiPartialLocalUser = Partial<MiUser> & {
id: MiUser['id'];
host: null;
uri: null;
acct: null;
};

export type MiRemoteUser = MiUser & {
host: string;
uri: string;
acct: string | null;
};

export type MiPartialRemoteUser = Partial<MiUser> & {
id: MiUser['id'];
host: string;
uri: string;
acct: string | null;
};

export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toString().slice(1, -1) } as const;
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ export const packedUserLiteSchema = {
},
},
},
acct: {
type: 'string',
nullable: true, optional: false,
},
},
} as const;

Expand Down
Loading
Loading