Compare commits
1 commit
master
...
atomic-del
Author | SHA1 | Date | |
---|---|---|---|
1d7e141d01 |
14 changed files with 189 additions and 45 deletions
59
.forgejo/workflows/test-federation.yml
Normal file
59
.forgejo/workflows/test-federation.yml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
name: Test (federation)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- packages/misskey-js/**
|
||||||
|
- .forgejo/workflows/test-federation.yml
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- packages/misskey-js/**
|
||||||
|
- .forgejo/workflows/test-federation.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [22.11.0]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
- name: Install FFmpeg
|
||||||
|
uses: https://github.com/FedericoCarboni/setup-ffmpeg@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4.0.3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
- name: Build Misskey
|
||||||
|
run: |
|
||||||
|
corepack enable && corepack prepare
|
||||||
|
pnpm i --frozen-lockfile
|
||||||
|
pnpm build
|
||||||
|
- name: Setup
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
bash ./setup.sh
|
||||||
|
sudo chmod 644 ./certificates/*.test.key
|
||||||
|
- name: Start servers
|
||||||
|
# https://github.com/docker/compose/issues/1294#issuecomment-374847206
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
docker compose up -d --scale tester=0
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
docker compose run --no-deps tester
|
||||||
|
- name: Stop servers
|
||||||
|
run: |
|
||||||
|
cd packages/backend/test-federation
|
||||||
|
docker compose down
|
|
@ -24,7 +24,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
import type { NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta, ActiveUsersRepository } from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
@ -58,8 +58,8 @@ export class ApInboxService {
|
||||||
@Inject(DI.meta)
|
@Inject(DI.meta)
|
||||||
private meta: MiMeta,
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.activeUsersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
@ -380,7 +380,7 @@ export class ApInboxService {
|
||||||
return 'skip: ブロックしようとしているユーザーはローカルユーザーではありません';
|
return 'skip: ブロックしようとしているユーザーはローカルユーザーではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userBlockingService.block(await this.usersRepository.findOneByOrFail({ id: actor.id }), await this.usersRepository.findOneByOrFail({ id: blockee.id }));
|
await this.userBlockingService.block(await this.activeUsersRepository.findOneByOrFail({ id: actor.id }), await this.activeUsersRepository.findOneByOrFail({ id: blockee.id }));
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,7 +509,7 @@ export class ApInboxService {
|
||||||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
const user = await this.activeUsersRepository.findOneBy({ id: actor.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return 'skip: actor not found';
|
return 'skip: actor not found';
|
||||||
} else if (user.isDeleted) {
|
} else if (user.isDeleted) {
|
||||||
|
@ -518,10 +518,6 @@ export class ApInboxService {
|
||||||
|
|
||||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||||
|
|
||||||
await this.usersRepository.update(actor.id, {
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: actor.id });
|
||||||
|
|
||||||
return `ok: queued ${job.name} ${job.id}`;
|
return `ok: queued ${job.name} ${job.id}`;
|
||||||
|
@ -561,7 +557,7 @@ export class ApInboxService {
|
||||||
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||||
.map(uri => uri.split('/').at(-1))
|
.map(uri => uri.split('/').at(-1))
|
||||||
.filter(x => x != null);
|
.filter(x => x != null);
|
||||||
const users = await this.usersRepository.findBy({
|
const users = await this.activeUsersRepository.findBy({
|
||||||
id: In(userIds),
|
id: In(userIds),
|
||||||
});
|
});
|
||||||
if (users.length < 1) return 'skip';
|
if (users.length < 1) return 'skip';
|
||||||
|
@ -715,7 +711,7 @@ export class ApInboxService {
|
||||||
return 'skip: ブロック解除しようとしているユーザーはローカルユーザーではありません';
|
return 'skip: ブロック解除しようとしているユーザーはローカルユーザーではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userBlockingService.unblock(await this.usersRepository.findOneByOrFail({ id: actor.id }), blockee);
|
await this.userBlockingService.unblock(await this.activeUsersRepository.findOneByOrFail({ id: actor.id }), blockee);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
import type { ActiveUsersRepository, FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
|
@ -91,6 +91,9 @@ export class ApPersonService implements OnModuleInit {
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.activeUsersRepository)
|
||||||
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@ -211,13 +214,13 @@ export class ApPersonService implements OnModuleInit {
|
||||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||||
if (uri.startsWith(`${this.config.url}/`)) {
|
if (uri.startsWith(`${this.config.url}/`)) {
|
||||||
const id = uri.split('/').pop();
|
const id = uri.split('/').pop();
|
||||||
const u = await this.usersRepository.findOneBy({ id }) as MiLocalUser | null;
|
const u = await this.activeUsersRepository.findOneBy({ id }) as MiLocalUser | null;
|
||||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||||
return u;
|
return u;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region このサーバーに既に登録されていたらそれを返す
|
//#region このサーバーに既に登録されていたらそれを返す
|
||||||
const exist = await this.usersRepository.findOneBy({ uri }) as MiLocalUser | MiRemoteUser | null;
|
const exist = await this.activeUsersRepository.findOneBy({ uri }) as MiLocalUser | MiRemoteUser | null;
|
||||||
|
|
||||||
if (exist) {
|
if (exist) {
|
||||||
this.cacheService.uriPersonCache.set(uri, exist);
|
this.cacheService.uriPersonCache.set(uri, exist);
|
||||||
|
@ -401,7 +404,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
// duplicate key error
|
// duplicate key error
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||||
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
const u = await this.activeUsersRepository.findOneBy({ uri: person.id });
|
||||||
if (u == null) throw new Error('already registered');
|
if (u == null) throw new Error('already registered');
|
||||||
|
|
||||||
user = u as MiRemoteUser;
|
user = u as MiRemoteUser;
|
||||||
|
@ -560,7 +563,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
await this.usersRepository.update(exist.id, updates);
|
await this.usersRepository.update(exist.id, updates);
|
||||||
|
|
||||||
if (person.publicKey) {
|
if (person.publicKey) {
|
||||||
await this.userPublickeysRepository.update({ userId: exist.id }, {
|
await this.usersRepository.update({ userId: exist.id }, {
|
||||||
keyId: person.publicKey.id,
|
keyId: person.publicKey.id,
|
||||||
keyPem: person.publicKey.publicKeyPem,
|
keyPem: person.publicKey.publicKeyPem,
|
||||||
});
|
});
|
||||||
|
@ -662,7 +665,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
const user = await this.activeUsersRepository.findOneByOrFail({ id: userId });
|
||||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||||
if (!user.featured) return;
|
if (!user.featured) return;
|
||||||
|
|
||||||
|
@ -720,7 +723,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
if (dst && this.userEntityService.isLocalUser(dst)) {
|
if (dst && this.userEntityService.isLocalUser(dst)) {
|
||||||
// targetがローカルユーザーだった場合データベースから引っ張ってくる
|
// targetがローカルユーザーだった場合データベースから引っ張ってくる
|
||||||
dst = await this.usersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser;
|
dst = await this.activeUsersRepository.findOneByOrFail({ uri: src.movedToUri }) as MiLocalUser;
|
||||||
} else if (dst) {
|
} else if (dst) {
|
||||||
if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
|
if (movePreventUris.includes(src.movedToUri)) return 'skip: circular move';
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
passwordSchema,
|
passwordSchema,
|
||||||
} from '@/models/User.js';
|
} from '@/models/User.js';
|
||||||
import type {
|
import type {
|
||||||
|
ActiveUsersRepository,
|
||||||
BlockingsRepository,
|
BlockingsRepository,
|
||||||
FollowingsRepository,
|
FollowingsRepository,
|
||||||
FollowRequestsRepository,
|
FollowRequestsRepository,
|
||||||
|
@ -37,7 +38,6 @@ import type {
|
||||||
UserNotePiningsRepository,
|
UserNotePiningsRepository,
|
||||||
UserProfilesRepository,
|
UserProfilesRepository,
|
||||||
UserSecurityKeysRepository,
|
UserSecurityKeysRepository,
|
||||||
UsersRepository,
|
|
||||||
} from '@/models/_.js';
|
} from '@/models/_.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
@ -101,8 +101,8 @@ export class UserEntityService implements OnModuleInit {
|
||||||
@Inject(DI.redis)
|
@Inject(DI.redis)
|
||||||
private redisClient: Redis.Redis,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.activeUsersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
@Inject(DI.userSecurityKeysRepository)
|
@Inject(DI.userSecurityKeysRepository)
|
||||||
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
private userSecurityKeysRepository: UserSecurityKeysRepository,
|
||||||
|
@ -410,7 +410,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
includeSecrets: false,
|
includeSecrets: false,
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
|
const user = typeof src === 'object' ? src : await this.activeUsersRepository.findOneByOrFail({ id: src });
|
||||||
|
|
||||||
const isDetailed = opts.schema !== 'UserLite';
|
const isDetailed = opts.schema !== 'UserLite';
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
|
@ -662,7 +662,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
|
const _users = users.filter((user): user is MiUser => typeof user !== 'string');
|
||||||
if (_users.length !== users.length) {
|
if (_users.length !== users.length) {
|
||||||
_users.push(
|
_users.push(
|
||||||
...await this.usersRepository.findBy({
|
...await this.activeUsersRepository.findBy({
|
||||||
id: In(users.filter((user): user is string => typeof user === 'string')),
|
id: In(users.filter((user): user is string => typeof user === 'string')),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const DI = {
|
||||||
|
|
||||||
//#region Repositories
|
//#region Repositories
|
||||||
usersRepository: Symbol('usersRepository'),
|
usersRepository: Symbol('usersRepository'),
|
||||||
|
activeUsersRepository: Symbol('activeUsersRepository'),
|
||||||
notesRepository: Symbol('notesRepository'),
|
notesRepository: Symbol('notesRepository'),
|
||||||
announcementsRepository: Symbol('announcementsRepository'),
|
announcementsRepository: Symbol('announcementsRepository'),
|
||||||
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
announcementReadsRepository: Symbol('announcementReadsRepository'),
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
MiAbuseReportNotificationRecipient,
|
MiAbuseReportNotificationRecipient,
|
||||||
MiAbuseUserReport,
|
MiAbuseUserReport,
|
||||||
MiAccessToken,
|
MiAccessToken,
|
||||||
|
MiActiveUser,
|
||||||
MiAd,
|
MiAd,
|
||||||
MiAnnouncement,
|
MiAnnouncement,
|
||||||
MiAnnouncementRead,
|
MiAnnouncementRead,
|
||||||
|
@ -87,6 +88,12 @@ const $usersRepository: Provider = {
|
||||||
inject: [DI.db],
|
inject: [DI.db],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const $activeUsersRepository: Provider = {
|
||||||
|
provide: DI.activeUsersRepository,
|
||||||
|
useFactory: (db: DataSource) => db.getRepository(MiActiveUser).extend(miRepository as MiRepository<MiUser>),
|
||||||
|
inject: [DI.db],
|
||||||
|
};
|
||||||
|
|
||||||
const $notesRepository: Provider = {
|
const $notesRepository: Provider = {
|
||||||
provide: DI.notesRepository,
|
provide: DI.notesRepository,
|
||||||
useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository<MiNote>),
|
useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository<MiNote>),
|
||||||
|
@ -499,6 +506,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
imports: [],
|
imports: [],
|
||||||
providers: [
|
providers: [
|
||||||
$usersRepository,
|
$usersRepository,
|
||||||
|
$activeUsersRepository,
|
||||||
$notesRepository,
|
$notesRepository,
|
||||||
$announcementsRepository,
|
$announcementsRepository,
|
||||||
$announcementReadsRepository,
|
$announcementReadsRepository,
|
||||||
|
@ -570,6 +578,7 @@ const $reversiGamesRepository: Provider = {
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
$usersRepository,
|
$usersRepository,
|
||||||
|
$activeUsersRepository,
|
||||||
$notesRepository,
|
$notesRepository,
|
||||||
$announcementsRepository,
|
$announcementsRepository,
|
||||||
$announcementReadsRepository,
|
$announcementReadsRepository,
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn, ViewEntity } from 'typeorm';
|
||||||
import { id } from './util/id.js';
|
import { id } from './util/id.js';
|
||||||
import { MiDriveFile } from './DriveFile.js';
|
import { MiDriveFile } from './DriveFile.js';
|
||||||
|
|
||||||
|
@ -286,6 +286,15 @@ export class MiUser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewEntity({
|
||||||
|
expression: (db) => db.createQueryBuilder().from(MiUser, 'user').where({
|
||||||
|
isDeleted: false,
|
||||||
|
}),
|
||||||
|
materialized: false,
|
||||||
|
})
|
||||||
|
export class MiActiveUser extends MiUser {
|
||||||
|
}
|
||||||
|
|
||||||
export type MiLocalUser = MiUser & {
|
export type MiLocalUser = MiUser & {
|
||||||
host: null;
|
host: null;
|
||||||
uri: null;
|
uri: null;
|
||||||
|
|
|
@ -57,7 +57,7 @@ import { MiRelay } from '@/models/Relay.js';
|
||||||
import { MiSignin } from '@/models/Signin.js';
|
import { MiSignin } from '@/models/Signin.js';
|
||||||
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
import { MiSwSubscription } from '@/models/SwSubscription.js';
|
||||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiActiveUser, MiUser } from '@/models/User.js';
|
||||||
import { MiUserIp } from '@/models/UserIp.js';
|
import { MiUserIp } from '@/models/UserIp.js';
|
||||||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||||
import { MiUserList } from '@/models/UserList.js';
|
import { MiUserList } from '@/models/UserList.js';
|
||||||
|
@ -172,6 +172,7 @@ export {
|
||||||
MiSignin,
|
MiSignin,
|
||||||
MiSwSubscription,
|
MiSwSubscription,
|
||||||
MiUsedUsername,
|
MiUsedUsername,
|
||||||
|
MiActiveUser,
|
||||||
MiUser,
|
MiUser,
|
||||||
MiUserIp,
|
MiUserIp,
|
||||||
MiUserKeypair,
|
MiUserKeypair,
|
||||||
|
@ -244,6 +245,7 @@ export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>;
|
||||||
export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
|
export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
|
||||||
export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
|
export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
|
||||||
export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
|
export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
|
||||||
|
export type ActiveUsersRepository = Pick<Repository<MiActiveUser> & MiRepository<MiActiveUser>, 'findOneBy' | 'findOneByOrFail' | 'findBy' | 'countBy'>;
|
||||||
export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;
|
export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;
|
||||||
export type UserKeypairsRepository = Repository<MiUserKeypair> & MiRepository<MiUserKeypair>;
|
export type UserKeypairsRepository = Repository<MiUserKeypair> & MiRepository<MiUserKeypair>;
|
||||||
export type UserListsRepository = Repository<MiUserList> & MiRepository<MiUserList>;
|
export type UserListsRepository = Repository<MiUserList> & MiRepository<MiUserList>;
|
||||||
|
|
|
@ -44,9 +44,7 @@ export class DeleteAccountProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async process(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
|
private async processImpl(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
|
||||||
this.logger.info(`Deleting account of ${job.data.user.id} ...`);
|
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -130,4 +128,23 @@ export class DeleteAccountProcessorService {
|
||||||
|
|
||||||
return 'Account deleted';
|
return 'Account deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async process(job: Bull.Job<DbUserDeleteJobData>): Promise<string | void> {
|
||||||
|
this.logger.info(`Deleting account of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
|
// Atomically "soft lock" the entry without actually locking the row
|
||||||
|
if (! (await this.usersRepository.update({ id: job.data.user.id, isDeleted: false }, { isDeleted: true })).affected) {
|
||||||
|
this.logger.debug(`Account of ${job.data.user.id} is already being deleted`);
|
||||||
|
return 'Account deletion already in progress';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.processImpl(job);
|
||||||
|
} catch (e) {
|
||||||
|
await this.usersRepository.update({ id: job.data.user.id }, { isDeleted: false });
|
||||||
|
this.logger.error(`Failed to delete account of ${job.data.user.id}: {e.name}: ${e instanceof Error ? e.message : e}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MoreThan } from 'typeorm';
|
import { MoreThan } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, MiDriveFile } from '@/models/_.js';
|
import type { DriveFilesRepository, MiDriveFile, ActiveUsersRepository } from '@/models/_.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { DriveService } from '@/core/DriveService.js';
|
import { DriveService } from '@/core/DriveService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -20,7 +20,7 @@ export class DeleteDriveFilesProcessorService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
@ -35,7 +35,7 @@ export class DeleteDriveFilesProcessorService {
|
||||||
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
public async process(job: Bull.Job<DbJobDataWithUser>): Promise<void> {
|
||||||
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
this.logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
const user = await this.activeUsersRepository.findOneBy({ id: job.data.user.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { ActiveUsersRepository } from '@/models/_.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -47,14 +47,14 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.activeUsersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.usersRepository.createQueryBuilder('user')
|
const query = this.activeUsersRepository.createQueryBuilder('user')
|
||||||
.where('user.isExplorable = TRUE')
|
.where('user.isExplorable = TRUE')
|
||||||
.andWhere('user.isSuspended = FALSE');
|
.andWhere('user.isSuspended = FALSE');
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
|
import type { UserProfilesRepository, ActiveUsersRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -45,8 +45,8 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.activeUsersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
let users: MiUser[] = [];
|
let users: MiUser[] = [];
|
||||||
|
|
||||||
const nameQuery = this.usersRepository.createQueryBuilder('user')
|
const nameQuery = this.activeUsersRepository.createQueryBuilder('user')
|
||||||
.where(new Brackets(qb => {
|
.where(new Brackets(qb => {
|
||||||
qb.where('user.name &@~ :query', { query: ps.query });
|
qb.where('user.name &@~ :query', { query: ps.query });
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
profQuery.andWhere('prof.userHost IS NOT NULL');
|
profQuery.andWhere('prof.userHost IS NOT NULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = this.usersRepository.createQueryBuilder('user')
|
const query = this.activeUsersRepository.createQueryBuilder('user')
|
||||||
.where(`user.id IN (${ profQuery.getQuery() })`)
|
.where(`user.id IN (${ profQuery.getQuery() })`)
|
||||||
.andWhere(new Brackets(qb => {
|
.andWhere(new Brackets(qb => {
|
||||||
qb
|
qb
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { In, IsNull } from 'typeorm';
|
import { In, IsNull } from 'typeorm';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository } from '@/models/_.js';
|
import type { ActiveUsersRepository } from '@/models/_.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
@ -82,8 +82,8 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.activeUsersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private activeUsersRepository: ActiveUsersRepository,
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private remoteUserResolveService: RemoteUserResolveService,
|
private remoteUserResolveService: RemoteUserResolveService,
|
||||||
|
@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await this.usersRepository.findBy(isModerator ? {
|
const users = await this.activeUsersRepository.findBy(isModerator ? {
|
||||||
id: In(ps.userIds),
|
id: In(ps.userIds),
|
||||||
} : {
|
} : {
|
||||||
id: In(ps.userIds),
|
id: In(ps.userIds),
|
||||||
|
@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
? { id: ps.userId }
|
? { id: ps.userId }
|
||||||
: { usernameLower: ps.username!.toLowerCase(), host: IsNull() };
|
: { usernameLower: ps.username!.toLowerCase(), host: IsNull() };
|
||||||
|
|
||||||
user = await this.usersRepository.findOneBy(q);
|
user = await this.activeUsersRepository.findOneBy(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null || (!isModerator && user.isSuspended)) {
|
if (user == null || (!isModerator && user.isSuspended)) {
|
||||||
|
|
|
@ -450,6 +450,54 @@ describe('User', () => {
|
||||||
strictEqual(followers.length, 0); // Alice's Follow is not processed
|
strictEqual(followers.length, 0); // Alice's Follow is not processed
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Deletion atomicity', () => {
|
||||||
|
const REPS = 5;
|
||||||
|
const N_USERS = 100;
|
||||||
|
let users: LoginUser[];
|
||||||
|
let observer_a: LoginUser, observer_b: LoginUser;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
observer_a = await createAccount('a.test');
|
||||||
|
observer_b = await createAccount('b.test');
|
||||||
|
for (let i = 0; i < N_USERS; i++) {
|
||||||
|
users.push(await createAccount('a.test'));
|
||||||
|
users.push(await createAccount('b.test'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < REPS; i++) {
|
||||||
|
test('Follow all users', async () => {
|
||||||
|
await Promise.all(users.flatMap(async (user, i) => {
|
||||||
|
await observer_a.client.request('following/create', { userId: user.id });
|
||||||
|
await observer_b.client.request('following/create', { userId: user.id });
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete all users while updating them', async () => {
|
||||||
|
await Promise.all(users.flatMap(async (user, i) => {
|
||||||
|
await user.client.request('i/update', { name: `I'm deleting my account ${i}` });
|
||||||
|
await user.client.request('i/delete-account', { password: user.password });
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Check consistency', async () => {
|
||||||
|
await Promise.all(users.flatMap(async (user, i) => {
|
||||||
|
await rejects(
|
||||||
|
async () => await user.client.request('users/show', { userId: user.id }),
|
||||||
|
(err: any) => {
|
||||||
|
strictEqual(err.code, 'NO_SUCH_USER');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}));
|
||||||
|
const following_a = await observer_a.client.request('users/following', { userId: observer_a.id });
|
||||||
|
strictEqual(following_a.length, 0);
|
||||||
|
const following_b = await observer_b.client.request('users/following', { userId: observer_b.id });
|
||||||
|
strictEqual(following_b.length, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Suspension', () => {
|
describe('Suspension', () => {
|
||||||
|
|
Loading…
Reference in a new issue