Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: デフォルトでフォローするユーザーを指定できるように #14769

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
### General
- Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Feat: ユーザー登録時に自動で特定のユーザーをフォローすることができるように
- フォローさせるユーザーのフォロー解除・ミュート・ブロックができないように指定することもできます
- Enhance: 依存関係の更新
- Enhance: l10nの更新
- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 )
Expand Down
32 changes: 32 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5222,6 +5222,38 @@ export interface Locale extends ILocale {
* 注意事項を理解した上でオンにします。
*/
"acknowledgeNotesAndEnable": string;
/**
* デフォルトでフォローするユーザー (ID)
*/
"defaultFollowedUsers": string;
/**
* 今後アカウントが作成された際に自動でフォローされるユーザー(解除可能)のユーザーIDを改行区切りで指定します。
*/
"defaultFollowedUsersDescription": string;
/**
* 交流を断てないユーザー (ID)
*/
"forciblyFollowedUsers": string;
/**
* 今後アカウントが作成された際には自動でフォローされ、フォローの解除やミュート・ブロックができないユーザーのユーザーIDを改行区切りで指定します。
*/
"forciblyFollowedUsersDescription": string;
/**
* 「デフォルトでフォローするユーザー」と「交流を絶てないユーザー」が重複しています。
*/
"defaultFollowedUsersDuplicated": string;
/**
* サーバー管理者はこのユーザーをフォロー解除することを禁止しています。
*/
"unfollowThisUserIsProhibited": string;
/**
* サーバー管理者はこのユーザーをブロックすることを禁止しています。
*/
"blockThisUserIsProhibited": string;
/**
* サーバー管理者はこのユーザーをミュートすることを禁止しています。
*/
"muteThisUserIsProhibited": string;
"_accountSettings": {
/**
* コンテンツの表示にログインを必須にする
Expand Down
8 changes: 8 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1301,6 +1301,14 @@ lockdown: "ロックダウン"
pleaseSelectAccount: "アカウントを選択してください"
availableRoles: "利用可能なロール"
acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします。"
defaultFollowedUsers: "デフォルトでフォローするユーザー (ID)"
defaultFollowedUsersDescription: "今後アカウントが作成された際に自動でフォローされるユーザー(解除可能)のユーザーIDを改行区切りで指定します。"
forciblyFollowedUsers: "交流を断てないユーザー (ID)"
forciblyFollowedUsersDescription: "今後アカウントが作成された際には自動でフォローされ、フォローの解除やミュート・ブロックができないユーザーのユーザーIDを改行区切りで指定します。"
defaultFollowedUsersDuplicated: "「デフォルトでフォローするユーザー」と「交流を絶てないユーザー」が重複しています。"
unfollowThisUserIsProhibited: "サーバー管理者はこのユーザーをフォロー解除することを禁止しています。"
blockThisUserIsProhibited: "サーバー管理者はこのユーザーをブロックすることを禁止しています。"
muteThisUserIsProhibited: "サーバー管理者はこのユーザーをミュートすることを禁止しています。"

_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/migration/1728986848483-defaultFollowUsers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class DefaultFollowUsers1728986848483 {
name = 'defaultFollowUsers1728986848483'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "defaultFollowedUsers" character varying(1024) array NOT NULL DEFAULT '{}'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "forciblyFollowedUsers" character varying(1024) array NOT NULL DEFAULT '{}'`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "forciblyFollowedUsers"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultFollowedUsers"`);
}
}
19 changes: 19 additions & 0 deletions packages/backend/src/core/SignupService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js';
import UsersChart from '@/core/chart/charts/users.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserService } from '@/core/UserService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';

@Injectable()
export class SignupService {
Expand All @@ -39,6 +40,7 @@ export class SignupService {

private utilityService: UtilityService,
private userService: UserService,
private userFollowingService: UserFollowingService,
private userEntityService: UserEntityService,
private idService: IdService,
private instanceActorService: InstanceActorService,
Expand Down Expand Up @@ -151,6 +153,23 @@ export class SignupService {
});

this.usersChart.update(account, true);

//#region Default following
if (
!isTheFirstUser &&
(this.meta.defaultFollowedUsers.length > 0 || this.meta.forciblyFollowedUsers.length > 0)
) {
const userIdsToFollow = [
...this.meta.defaultFollowedUsers,
...this.meta.forciblyFollowedUsers,
];

await Promise.allSettled(userIdsToFollow.map(async userId => {
await this.userFollowingService.follow(account, { id: userId });
}));
}
//#endregion

this.userService.notifySystemWebhook(account, 'userCreated');

return { account, secret };
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/core/UserBlockingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ModuleRef } from '@nestjs/core';
import { IdService } from '@/core/IdService.js';
import type { MiUser } from '@/models/User.js';
import type { MiBlocking } from '@/models/Blocking.js';
import type { MiMeta } from '@/models/Meta.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { QueueService } from '@/core/QueueService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
Expand All @@ -20,6 +22,7 @@ import { UserWebhookService } from '@/core/UserWebhookService.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { RoleService } from '@/core/RoleService.js';

@Injectable()
export class UserBlockingService implements OnModuleInit {
Expand All @@ -29,6 +32,9 @@ export class UserBlockingService implements OnModuleInit {
constructor(
private moduleRef: ModuleRef,

@Inject(DI.meta)
private serverSettings: MiMeta,

@Inject(DI.followRequestsRepository)
private followRequestsRepository: FollowRequestsRepository,

Expand All @@ -41,6 +47,7 @@ export class UserBlockingService implements OnModuleInit {
@Inject(DI.userListMembershipsRepository)
private userListMembershipsRepository: UserListMembershipsRepository,

private roleService: RoleService,
private cacheService: CacheService,
private userEntityService: UserEntityService,
private idService: IdService,
Expand All @@ -59,6 +66,15 @@ export class UserBlockingService implements OnModuleInit {

@bindThis
public async block(blocker: MiUser, blockee: MiUser, silent = false) {
// フォロー解除できない(=ブロックもできない)ユーザーの場合
if (
blocker.host == null &&
this.serverSettings.forciblyFollowedUsers.includes(blockee.id) &&
!await this.roleService.isModerator(blocker)
) {
throw new IdentifiableError('e2f04d25-0d94-4ac3-a4d8-ba401062741b', 'You cannot block that user due to server policy.');
}

await Promise.all([
this.cancelRequest(blocker, blockee, silent),
this.cancelRequest(blockee, blocker, silent),
Expand Down
13 changes: 12 additions & 1 deletion packages/backend/src/core/UserFollowingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { RoleService } from '@/core/RoleService.js';
import type { ThinUser } from '@/queue/types.js';
import Logger from '../logger.js';

Expand Down Expand Up @@ -73,6 +74,7 @@ export class UserFollowingService implements OnModuleInit {
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,

private roleService: RoleService,
private cacheService: CacheService,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
Expand Down Expand Up @@ -365,13 +367,22 @@ export class UserFollowingService implements OnModuleInit {
@bindThis
public async unfollow(
follower: {
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; isRoot: MiUser['isRoot']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
},
followee: {
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
},
silent = false,
): Promise<void> {
// フォロー解除できないユーザーの場合
if (
follower.host == null &&
this.meta.forciblyFollowedUsers.includes(followee.id) &&
!await this.roleService.isModerator(follower)
) {
throw new IdentifiableError('19f25f61-0141-4683-99dc-217a88d633cb', 'You cannot unfollow that user due to server policy.');
}

const following = await this.followingsRepository.findOne({
relations: {
follower: true,
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/core/UserMutingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,42 @@

import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import type { MutingsRepository, MiMuting } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import type { MiUser } from '@/models/User.js';
import type { MiMeta } from '@/models/Meta.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';

@Injectable()
export class UserMutingService {
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,

@Inject(DI.mutingsRepository)
private mutingsRepository: MutingsRepository,

private roleService: RoleService,
private idService: IdService,
private cacheService: CacheService,
) {
}

@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
// フォロー解除できない(=ミュートもできない)ユーザーの場合
if (
user.host == null &&
this.serverSettings.forciblyFollowedUsers.includes(target.id) &&
!await this.roleService.isModerator(user)
) {
throw new IdentifiableError('15273a89-374d-49fa-8df6-8bb3feeea455', 'You cannot mute that user due to server policy.');
}

await this.mutingsRepository.insert({
id: this.idService.gen(),
expiresAt: expiresAt ?? null,
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/core/UserRenoteMutingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,39 @@ import type { MiRenoteMuting } from '@/models/RenoteMuting.js';

import { IdService } from '@/core/IdService.js';
import type { MiUser } from '@/models/User.js';
import type { MiMeta } from '@/models/Meta.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';

@Injectable()
export class UserRenoteMutingService {
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,

@Inject(DI.renoteMutingsRepository)
private renoteMutingsRepository: RenoteMutingsRepository,

private roleService: RoleService,
private idService: IdService,
private cacheService: CacheService,
) {
}

@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
// フォロー解除できない(=リノートミュートもできない)ユーザーの場合
if (
user.host == null &&
this.serverSettings.forciblyFollowedUsers.includes(target.id) &&
!await this.roleService.isModerator(user)
) {
throw new IdentifiableError('15273a89-374d-49fa-8df6-8bb3feeea455', 'You cannot mute that user due to server policy.');
}

await this.renoteMutingsRepository.insert({
id: this.idService.gen(),
muterId: user.id,
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/src/models/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ export class MiMeta {
})
public pinnedUsers: string[];

/**
* アカウント作成の段階でデフォルトでフォローしているユーザー(あとから解除可能)
*/
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public defaultFollowedUsers: string[];

/**
* デフォルトでフォローしていて、フォロー解除・ブロック・ミュートができないユーザー
*/
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public forciblyFollowedUsers: string[];

@Column('varchar', {
length: 1024, array: true, default: '{}',
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbUserImportJobData } from '../types.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';

@Injectable()
export class ImportMutingProcessorService {
Expand Down Expand Up @@ -90,7 +91,13 @@ export class ImportMutingProcessorService {

this.logger.info(`Mute[${linenum}] ${target.id} ...`);

await this.userMutingService.mute(user, target);
await this.userMutingService.mute(user, target).catch((err) => {
if (err instanceof IdentifiableError && err.id === '15273a89-374d-49fa-8df6-8bb3feeea455') {
// フォロー解除できない(=ミュートもできない)ユーザー。動作は正常のため、エラーを無視する
return;
}
throw err;
});
} catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { RelationshipJobData } from '../types.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import { IdentifiableError } from '@/misc/identifiable-error.js';

@Injectable()
export class RelationshipProcessorService {
Expand Down Expand Up @@ -50,7 +51,13 @@ export class RelationshipProcessorService {
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
await this.userFollowingService.unfollow(follower, followee, job.data.silent);
await this.userFollowingService.unfollow(follower, followee, job.data.silent).catch((err) => {
if (err instanceof IdentifiableError && err.id === '19f25f61-0141-4683-99dc-217a88d633cb') {
// フォロー解除できないユーザー。動作は正常のため、エラーを無視する
return;
}
throw err;
});
return 'ok';
}

Expand All @@ -61,7 +68,13 @@ export class RelationshipProcessorService {
this.usersRepository.findOneByOrFail({ id: job.data.from.id }),
this.usersRepository.findOneByOrFail({ id: job.data.to.id }),
]);
await this.userBlockingService.block(blockee, blocker, job.data.silent);
await this.userBlockingService.block(blockee, blocker, job.data.silent).catch((err) => {
if (err instanceof IdentifiableError && err.id === 'e2f04d25-0d94-4ac3-a4d8-ba401062741b') {
// フォロー解除できない(=ブロックもできない)ユーザー。動作は正常のため、エラーを無視する
return;
}
throw err;
});
return 'ok';
}

Expand Down
Loading
Loading