diff --git a/locales/en-US.yml b/locales/en-US.yml index ec2b0f9c0..7783ccc4e 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -763,6 +763,7 @@ contact: "Contact" useSystemFont: "Use the system's default font" clips: "Clips" experimentalFeatures: "Experimental features" +misskeyExperimentalFeatures: "Misskey's experimental features" experimental: "Experimental" thisIsExperimentalFeature: "This is an experimental feature. Its functionality is subject to change, and it may not operate as intended." developer: "Developer" diff --git a/locales/index.d.ts b/locales/index.d.ts index bc4a7215b..d1be59ec6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3076,6 +3076,10 @@ export interface Locale extends ILocale { * 実験的機能 */ "experimentalFeatures": string; + /** + * Misskeyの実験的機能 + */ + "misskeyExperimentalFeatures": string; /** * 実験的 */ @@ -5179,6 +5183,38 @@ export interface Locale extends ILocale { * このファイルをドライブに保存する */ "saveThisFile": string; + /** + * 휴가 모드 + */ + "vacationMode": string; + /** + * 휴가 모드는, 관리자가 잠시 직무를 내려놓고 일반 유저로 이용할 수 있도록 임시로 권한을 제거하는 모드입니다. + */ + "vacationModeDescription": string; + /** + * 휴가 모드를 사용하기 + */ + "useVacationMode": string; + /** + * 인터넷과 잠시 거리두기 + */ + "mindControl": string; + /** + * 필요한 경우, Misskey에서 피로감을 덜 느끼도록 몇 가지 설정을 조정할 수 있습니다. + */ + "mindControlDescription": string; + /** + * 모든 카운터를 가리기 + */ + "hideCounters": string; + /** + * 유저 페이지의 노트, 팔로잉, 팔로워 수 및 이러한 통계를 모두 숨깁니다. + */ + "hideCountersDescription": string; + /** + * 현재 휴가 모드를 사용 중입니다. 이 메시지를 닫으려면 여기를 클릭하세요. + */ + "youAreOnVacation": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3cefb4971..895e32252 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -765,6 +765,7 @@ contact: "連絡先" useSystemFont: "システムのデフォルトのフォントを使う" clips: "クリップ" experimentalFeatures: "実験的機能" +misskeyExperimentalFeatures: "Misskeyの実験的機能" experimental: "実験的" thisIsExperimentalFeature: "これは実験的な機能です。仕様が変更されたり、正常に動作しなかったりする可能性があります。" developer: "開発者" @@ -1290,6 +1291,14 @@ areYouSureToRemoveAllFollowings: "本当に{host}とのすべてのフォロー mutualLink: "相互リンク" youNeedToEnableTwoFactor: "モデレーター権限を利用するには、まず二要素認証を有効にする必要があります。" saveThisFile: "このファイルをドライブに保存する" +vacationMode: "휴가 모드" +vacationModeDescription: "휴가 모드는, 관리자가 잠시 직무를 내려놓고 일반 유저로 이용할 수 있도록 임시로 권한을 제거하는 모드입니다." +useVacationMode: "휴가 모드를 사용하기" +mindControl: "인터넷과 잠시 거리두기" +mindControlDescription: "필요한 경우, Misskey에서 피로감을 덜 느끼도록 몇 가지 설정을 조정할 수 있습니다." +hideCounters: "모든 카운터를 가리기" +hideCountersDescription: "유저 페이지의 노트, 팔로잉, 팔로워 수 및 이러한 통계를 모두 숨깁니다." +youAreOnVacation: "현재 휴가 모드를 사용 중입니다. 이 메시지를 닫으려면 여기를 클릭하세요." _bubbleGame: howToPlay: "遊び方" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index df554a810..c4f0ad22a 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -762,6 +762,7 @@ contact: "연락처" useSystemFont: "시스템 기본 글꼴을 사용" clips: "클립" experimentalFeatures: "실험실" +misskeyExperimentalFeatures: "Misskey 실험실" experimental: "실험실" thisIsExperimentalFeature: "이 기능은 실험적인 기능입니다. 사양이 변경되거나 정상적으로 동작하지 않을 가능성이 있습니다." developer: "개발자" @@ -1275,6 +1276,14 @@ areYouSureToRemoveAllFollowings: "정말로 {host}와의 모든 팔로우 관계 mutualLink: "서로링크" youNeedToEnableTwoFactor: "관리 권한을 이용하려면 먼저 2단계 인증을 활성화해야 합니다." saveThisFile: "이 파일을 드라이브에 저장" +vacationMode: "휴가 모드" +vacationModeDescription: "휴가 모드는, 관리자가 잠시 직무를 내려놓고 일반 유저로 이용할 수 있도록 임시로 권한을 제거하는 모드입니다." +useVacationMode: "휴가 모드를 사용하기" +mindControl: "인터넷과 잠시 거리두기" +mindControlDescription: "필요한 경우, Misskey에서 피로감을 덜 느끼도록 몇 가지 설정을 조정할 수 있습니다." +hideCounters: "모든 카운터를 가리기" +hideCountersDescription: "유저 페이지의 노트, 팔로잉, 팔로워 수 및 이러한 통계를 모두 숨깁니다." +youAreOnVacation: "현재 휴가 모드를 사용 중입니다. 이 메시지를 닫으려면 여기를 클릭하세요." _bubbleGame: howToPlay: "설명" hold: "홀드" diff --git a/packages/backend/migration/1724078037441-vacationMode.js b/packages/backend/migration/1724078037441-vacationMode.js new file mode 100644 index 000000000..36a78b452 --- /dev/null +++ b/packages/backend/migration/1724078037441-vacationMode.js @@ -0,0 +1,11 @@ +export class vacationMode1724078037441 { + name = 'vacationMode1724078037441' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "isVacation" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isVacation"`); + } +} diff --git a/packages/backend/src/core/SearchService.ts b/packages/backend/src/core/SearchService.ts index a7871c07f..506d7ff93 100644 --- a/packages/backend/src/core/SearchService.ts +++ b/packages/backend/src/core/SearchService.ts @@ -174,6 +174,7 @@ export class SearchService { if (note.text == null && note.cw == null) return; // if (!['home', 'public'].includes(note.visibility)) return; + const createdAt = this.idService.parse(note.id).date; if (this.meilisearch) { switch (this.meilisearchIndexScope) { case 'global': @@ -192,7 +193,7 @@ export class SearchService { await this.meilisearchNoteIndex?.addDocuments([{ id: note.id, - createdAt: this.idService.parse(note.id).date.getTime(), + createdAt: createdAt.getTime(), userId: note.userId, userHost: note.userHost, channelId: note.channelId, @@ -204,7 +205,7 @@ export class SearchService { }); } else if (this.elasticsearch) { const body = { - createdAt: this.idService.parse(note.id).date.getTime(), + createdAt: createdAt.getTime(), userId: note.userId, userHost: note.userHost, channelId: note.channelId, @@ -213,11 +214,11 @@ export class SearchService { tags: note.tags, }; await this.elasticsearch.index({ - index: this.elasticsearchNoteIndex + `-${new Date().toISOString().slice(0, 7).replace(/-/g, '')}` as string, + index: `${this.elasticsearchNoteIndex}-${createdAt.toISOString().slice(0, 7).replace(/-/g, '')}`, id: note.id, body: body, }).catch((error: any) => { - console.error(error); + this.logger.error(error); }); } } @@ -228,6 +229,13 @@ export class SearchService { if (this.meilisearch) { this.meilisearchNoteIndex?.deleteDocument(note.id); + } else if (this.elasticsearch) { + await this.elasticsearch.delete({ + index: `${this.elasticsearchNoteIndex}-${this.idService.parse(note.id).date.toISOString().slice(0, 7).replace(/-/g, '')}`, + id: note.id, + }).catch((error) => { + this.logger.error(error); + }); } } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 34ce1eda3..0bd847042 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -412,11 +412,13 @@ export class UserEntityService implements OnModuleInit { }, options); const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); + const meObject = await this.usersRepository.findOneByOrFail({ id: me?.id }); + const meProfile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const isDetailed = opts.schema !== 'UserLite'; const meId = me ? me.id : null; const isMe = meId === user.id; - const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; + const iAmModerator = me ? (await this.roleService.isModerator(me as MiUser)) && !meObject.isVacation && meProfile.twoFactorEnabled : false; if (user.isSuspended && !iAmModerator) throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `User ${user.id} has been suspended.`); const profile = isDetailed @@ -576,6 +578,7 @@ export class UserEntityService implements OnModuleInit { isModerator: isModerator, isAdmin: isAdmin, isRoot: isRoot, + isVacation: user.isVacation, injectFeaturedNote: profile?.injectFeaturedNote, receiveAnnouncementEmail: profile?.receiveAnnouncementEmail, alwaysMarkNsfw: profile?.alwaysMarkNsfw, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 4a16d0397..c52dc73f0 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -192,6 +192,12 @@ export class MiUser { }) public isRoot: boolean; + @Column('boolean', { + default: false, + comment: 'Whether the User is on vacation mode.', + }) + public isVacation: boolean; + @Index() @Column('boolean', { default: true, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 8789ecdd9..e6bb86390 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -481,6 +481,10 @@ export const packedMeDetailedOnlySchema = { type: 'boolean', nullable: true, optional: false, }, + isVacation: { + type: 'boolean', + nullable: true, optional: false, + }, injectFeaturedNote: { type: 'boolean', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 798a17240..634597d4b 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -310,7 +310,7 @@ export class ApiCallService implements OnApplicationShutdown { } } - if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) { + if (ep.meta.requireModerator || ep.meta.requireAdmin) { const myRoles = await this.roleService.getUserRoles(user!.id); const isModerator = myRoles.some(r => r.isModerator || r.isAdministrator); const isAdmin = myRoles.some(r => r.isAdministrator); @@ -324,6 +324,14 @@ export class ApiCallService implements OnApplicationShutdown { id: 'abce13fe-1d9f-4e85-8f00-4a5251155470', }); } + if (user?.isVacation) { + throw new ApiError({ + message: 'You are on vacation.', + code: 'VACATION_MODE', + kind: 'permission', + id: 'bbe5ef78-fab6-46a2-9e29-64639747096c', + }); + } if (ep.meta.requireModerator && !isModerator) { throw new ApiError({ message: 'You are not assigned to a proper role.', diff --git a/packages/backend/src/server/api/endpoints/admin/dispose-cache.ts b/packages/backend/src/server/api/endpoints/admin/dispose-cache.ts index 7efb913d4..d4f5c7f5b 100644 --- a/packages/backend/src/server/api/endpoints/admin/dispose-cache.ts +++ b/packages/backend/src/server/api/endpoints/admin/dispose-cache.ts @@ -6,6 +6,8 @@ import { Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { CacheService } from '@/core/CacheService.js'; +import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; +import {AuthenticateService} from "@/server/api/AuthenticateService.js"; export const meta = { tags: ['admin'], @@ -25,9 +27,11 @@ export const paramDef = { export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( private cacheService: CacheService, + private apDbResolverService: ApDbResolverService, ) { super(meta, paramDef, async (ps, me) => { this.cacheService.dispose(); + this.apDbResolverService.dispose(); }); } } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index c055ff3e3..0eb7a6283 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -187,6 +187,7 @@ export const paramDef = { preventAiLearning: { type: 'boolean' }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, + isVacation: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, receiveAnnouncementEmail: { type: 'boolean' }, alwaysMarkNsfw: { type: 'boolean' }, @@ -341,6 +342,7 @@ export default class extends Endpoint { // eslint- if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; + if (typeof ps.isVacation === 'boolean') updates.isVacation = ps.isVacation; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index c1c6f55a3..7e26ed16a 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -24,8 +24,8 @@ const accountData = miLocalStorage.getItem('account'); // TODO: 外部からはreadonlyに export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; -export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true) && $i.twoFactorEnabled; -export const iAmAdmin = $i != null && $i.isAdmin && $i.twoFactorEnabled; +export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true) && $i.twoFactorEnabled && !$i.isVacation; +export const iAmAdmin = $i != null && $i.isAdmin && $i.twoFactorEnabled && !$i.isVacation; export function signinRequired() { if ($i == null) throw new Error('signin required'); diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index fe5a70a25..2a6d49e4a 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -216,53 +216,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - - -
- - {{ i18n.ts._hideSensitiveInformation.use }} - - - - - - - - {{ i18n.ts._hideSensitiveInformation.directMessagesUse }} - - - - - - - - - {{ i18n.ts._hideSensitiveInformation.driveUse }} - - - - - - - - - {{ i18n.ts._hideSensitiveInformation.moderationLogUse }} - - - - - - - - - {{ i18n.ts._hideSensitiveInformation.rolesUse }} - - - -
-
- @@ -370,11 +323,6 @@ const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHori const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer')); const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow')); const sensitiveDoubleClickRequired = computed(defaultStore.makeGetterSetter('sensitiveDoubleClickRequired')); -const privateMode = computed(defaultStore.makeGetterSetter('privateMode')); -const hideDirectMessages = computed(defaultStore.makeGetterSetter('hideDirectMessages')); -const hideDriveFileList = computed(defaultStore.makeGetterSetter('hideDriveFileList')); -const hideModerationLog = computed(defaultStore.makeGetterSetter('hideModerationLog')); -const hideRoleList = computed(defaultStore.makeGetterSetter('hideRoleList')); watch(lang, () => { miLocalStorage.setItem('lang', lang.value as string); @@ -418,11 +366,6 @@ watch([ disableStreamingTimeline, enableSeasonalScreenEffect, alwaysConfirmFollow, - privateMode, - hideDirectMessages, - hideDriveFileList, - hideModerationLog, - hideRoleList, ], async () => { await reloadAsk(); }); diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 0cc2d6be3..68c667b74 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -163,6 +163,11 @@ const menuDef = computed(() => [{ text: `${i18n.ts.accountMigration}`, to: '/settings/migration', active: currentPage.value?.route.name === 'migration', + }, { + icon: 'ti ti-barrier-block', + text: i18n.ts.experimentalFeatures, + to: '/settings/laboratory', + active: currentPage.value?.route.name === 'laboratory', }, { icon: 'ti ti-dots', text: i18n.ts.other, diff --git a/packages/frontend/src/pages/settings/laboratory.vue b/packages/frontend/src/pages/settings/laboratory.vue new file mode 100644 index 000000000..fa0439703 --- /dev/null +++ b/packages/frontend/src/pages/settings/laboratory.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 49a8a05ea..831dc99a9 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -51,17 +51,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - -
- - - -
-
- @@ -90,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only