diff --git a/docker-compose.local.yml b/docker-compose.local.yml index c5b3ef452602..e5b6cf830711 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,21 +1,21 @@ version: "3" services: - web: - build: . - restart: unless-stopped - links: - - db - - redis -# - es - ports: - - "3000:3000" - networks: - - internal_network - - external_network - volumes: - - ./files:/misskey/files - - ./.config:/misskey/.config:ro +# web: +# build: . +# restart: unless-stopped +# links: +# - db +# - redis +## - es +# ports: +# - "3000:3000" +# networks: +# - internal_network +# - external_network +# volumes: +# - ./files:/misskey/files +# - ./.config:/misskey/.config:ro redis: restart: unless-stopped diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index d0915a5091f2..d4e558133a62 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -33,6 +33,7 @@ import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; +import * as ep___admin_federation_removeAllFollowingByUserId from './endpoints/admin/federation/remove-all-following-by-user-id.js'; import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; @@ -384,6 +385,7 @@ const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useCla const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default }; const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default }; const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default }; +const $admin_federation_removeAllFollowingByUserId: Provider = { provide: 'ep:admin/federation/remove-all-following-by-user-id', useClass: ep___admin_federation_removeAllFollowingByUserId.default }; const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default }; const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default }; const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default }; @@ -739,6 +741,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_federation_deleteAllFiles, $admin_federation_refreshRemoteInstanceMetadata, $admin_federation_removeAllFollowing, + $admin_federation_removeAllFollowingByUserId, $admin_federation_updateInstance, $admin_getIndexStats, $admin_getTableStats, @@ -1088,6 +1091,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $admin_federation_deleteAllFiles, $admin_federation_refreshRemoteInstanceMetadata, $admin_federation_removeAllFollowing, + $admin_federation_removeAllFollowingByUserId, $admin_federation_updateInstance, $admin_getIndexStats, $admin_getTableStats, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 49e4fefb5a38..c29349ea365f 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -33,6 +33,7 @@ import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js'; import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js'; import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js'; import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js'; +import * as ep___admin_federation_removeAllFollowingByUserId from './endpoints/admin/federation/remove-all-following-by-user-id.js'; import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js'; import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'; import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'; @@ -382,6 +383,7 @@ const eps = [ ['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles], ['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata], ['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing], + ['admin/federation/remove-all-following-by-user-id', ep___admin_federation_removeAllFollowingByUserId], ['admin/federation/update-instance', ep___admin_federation_updateInstance], ['admin/get-index-stats', ep___admin_getIndexStats], ['admin/get-table-stats', ep___admin_getTableStats], diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following-by-user-id.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following-by-user-id.ts new file mode 100644 index 000000000000..dd5c130a2f7b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following-by-user-id.ts @@ -0,0 +1,64 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { FollowingsRepository, User, UsersRepository } from '@/models/index.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string' }, + }, + required: ['userId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const follower = await this.usersRepository.findOne({ where: { id: ps.userId } }); + + if (!follower) { + throw new Error(`User not found: ${ps.userId}`); + } + + await this.unFollowAll(follower); + }); + } + + @bindThis + private async unFollowAll(follower: User) { + const followings = await this.followingsRepository.findBy({ + followerId: follower.id, + }); + + for (const following of followings) { + const followee = await this.usersRepository.findOneBy({ + id: following.followeeId, + }); + + if (followee == null) { + throw `Cant find followee ${following.followeeId}`; + } + + await this.userFollowingService.unfollow(follower, followee, true); + } + } +} diff --git a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts index b073209a5bc2..2be598587a70 100644 --- a/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -26,7 +26,7 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.notesRepository) + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, private userFollowingService: UserFollowingService, diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue index b6d0baa03ec1..89f2e4e1e310 100644 --- a/packages/frontend/src/pages/user-info.vue +++ b/packages/frontend/src/pages/user-info.vue @@ -113,6 +113,7 @@ {{ i18n.ts.resetPassword }} + {{ i18n.ts.unfollowAll }} {{ i18n.ts.deleteAccount }} @@ -401,6 +402,30 @@ async function deleteAccount() { } } +async function unfollowAll() { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.ts.unfollowAllConfirm, + }); + if (confirm.canceled) return; + + const typed = await os.inputText({ + text: i18n.t('typeToConfirm', { x: user?.username }), + }); + if (typed.canceled) return; + + if (typed.result === user?.username) { + await os.apiWithDialog('admin/federation/remove-all-following-by-user-id', { + userId: user.id, + }); + } else { + os.alert({ + type: 'error', + text: 'input not match', + }); + } +} + async function assignRole() { const roles = await os.api('admin/roles/list'); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index bb2cb0a6c4a9..6308d742f311 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -59,6 +59,59 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router }); } + async function assignRole() { + const roles = await os.api('admin/roles/list'); + + const { canceled, result: roleId } = await os.select({ + title: i18n.ts._role.chooseRoleToAssign, + items: roles.map((r) => ({ text: r.name, value: r.id })), + }); + if (canceled) return; + + const { canceled: canceled2, result: period } = await os.select({ + title: i18n.ts.period, + items: [ + { + value: 'indefinitely', + text: i18n.ts.indefinitely, + }, + { + value: 'oneHour', + text: i18n.ts.oneHour, + }, + { + value: 'oneDay', + text: i18n.ts.oneDay, + }, + { + value: 'oneWeek', + text: i18n.ts.oneWeek, + }, + { + value: 'oneMonth', + text: i18n.ts.oneMonth, + }, + ], + default: 'indefinitely', + }); + if (canceled2) return; + + const expiresAt = + period === 'indefinitely' + ? null + : period === 'oneHour' + ? Date.now() + 1000 * 60 * 60 + : period === 'oneDay' + ? Date.now() + 1000 * 60 * 60 * 24 + : period === 'oneWeek' + ? Date.now() + 1000 * 60 * 60 * 24 * 7 + : period === 'oneMonth' + ? Date.now() + 1000 * 60 * 60 * 24 * 30 + : null; + + await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id, expiresAt }); + } + async function toggleMute() { if (user.isMuted) { os.apiWithDialog('mute/delete', { @@ -216,6 +269,13 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router action: inviteGroup, } : undefined, + iAmModerator + ? { + icon: 'ti ti-users', + text: i18n.ts.assignRole, + action: assignRole, + } + : undefined, null, { type: 'parent', @@ -238,69 +298,6 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router ] as any; if ($i && meId !== user.id) { - if (iAmModerator) { - menu = menu.concat([ - { - type: 'parent', - icon: 'ti ti-badges', - text: i18n.ts.roles, - children: async () => { - const roles = await os.api('admin/roles/list'); - - return roles - .filter((r) => r.target === 'manual') - .map((r) => ({ - text: r.name, - action: async () => { - const { canceled, result: period } = await os.select({ - title: i18n.ts.period, - items: [ - { - value: 'indefinitely', - text: i18n.ts.indefinitely, - }, - { - value: 'oneHour', - text: i18n.ts.oneHour, - }, - { - value: 'oneDay', - text: i18n.ts.oneDay, - }, - { - value: 'oneWeek', - text: i18n.ts.oneWeek, - }, - { - value: 'oneMonth', - text: i18n.ts.oneMonth, - }, - ], - default: 'indefinitely', - }); - if (canceled) return; - - const expiresAt = - period === 'indefinitely' - ? null - : period === 'oneHour' - ? Date.now() + 1000 * 60 * 60 - : period === 'oneDay' - ? Date.now() + 1000 * 60 * 60 * 24 - : period === 'oneWeek' - ? Date.now() + 1000 * 60 * 60 * 24 * 7 - : period === 'oneMonth' - ? Date.now() + 1000 * 60 * 60 * 24 * 30 - : null; - - os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt }); - }, - })); - }, - }, - ]); - } - menu = menu.concat([ null, {