From 0471b05c17f439cf9c0ce9a0f76c4317e75d84c1 Mon Sep 17 00:00:00 2001 From: nacika-ins Date: Fri, 4 Oct 2024 09:49:38 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20docker-compose=E3=81=A7web=E3=82=92?= =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.local.yml | 30 ++++++------ .../remove-all-following-by-user-id.ts | 49 +++++++++++++++++++ 2 files changed, 64 insertions(+), 15 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/admin/federation/remove-all-following-by-user-id.ts 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/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..b073209a5bc2 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/federation/remove-all-following-by-user-id.ts @@ -0,0 +1,49 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { FollowingsRepository, UsersRepository } from '@/models/index.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + host: { type: 'string' }, + }, + required: ['host'], +} 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.notesRepository) + private followingsRepository: FollowingsRepository, + + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const followings = await this.followingsRepository.findBy({ + followerHost: ps.host, + }); + + const pairs = await Promise.all(followings.map(f => Promise.all([ + this.usersRepository.findOneByOrFail({ id: f.followerId }), + this.usersRepository.findOneByOrFail({ id: f.followeeId }), + ]))); + + for (const pair of pairs) { + this.userFollowingService.unfollow(pair[0], pair[1]); + } + }); + } +} From 01154310f0d1703029ce6bc6b7cb607ac15619be Mon Sep 17 00:00:00 2001 From: nacika-ins Date: Fri, 4 Oct 2024 09:50:06 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=AE?= =?UTF-8?q?=E4=BB=98=E4=B8=8E=E3=82=92=E3=83=A2=E3=83=BC=E3=83=80=E3=83=AB?= =?UTF-8?q?=E3=81=A7=E8=A1=8C=E3=81=86=E3=82=88=E3=81=86=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/scripts/get-user-menu.ts | 123 +++++++++--------- 1 file changed, 60 insertions(+), 63 deletions(-) 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, { From 9a4805af19e104970678e370ef49b76649c3a05a Mon Sep 17 00:00:00 2001 From: nacika-ins Date: Fri, 4 Oct 2024 09:59:45 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=85=A8=E3=81=A6=E3=81=AE?= =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=82=92=E3=82=A2=E3=83=B3?= =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=AD=E3=83=BC=E3=81=99=E3=82=8BAPI?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../remove-all-following-by-user-id.ts | 47 ++++++++++++------- .../admin/federation/remove-all-following.ts | 2 +- packages/frontend/src/pages/user-info.vue | 25 ++++++++++ 5 files changed, 63 insertions(+), 17 deletions(-) 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 index b073209a5bc2..dd5c130a2f7b 100644 --- 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 @@ -1,8 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { FollowingsRepository, UsersRepository } from '@/models/index.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'], @@ -14,9 +15,9 @@ export const meta = { export const paramDef = { type: 'object', properties: { - host: { type: 'string' }, + userId: { type: 'string' }, }, - required: ['host'], + required: ['userId'], } as const; // eslint-disable-next-line import/no-default-export @@ -26,24 +27,38 @@ export default class extends Endpoint { @Inject(DI.usersRepository) private usersRepository: UsersRepository, - @Inject(DI.notesRepository) + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, private userFollowingService: UserFollowingService, ) { super(meta, paramDef, async (ps, me) => { - const followings = await this.followingsRepository.findBy({ - followerHost: ps.host, - }); - - const pairs = await Promise.all(followings.map(f => Promise.all([ - this.usersRepository.findOneByOrFail({ id: f.followerId }), - this.usersRepository.findOneByOrFail({ id: f.followeeId }), - ]))); - - for (const pair of pairs) { - this.userFollowingService.unfollow(pair[0], pair[1]); - } + 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');