diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-06-01 20:52:19 +0000 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-06-01 20:52:19 +0000 |
| commit | 39fcdcae257d158159d020ff72ef2c5e720f5ffa (patch) | |
| tree | b7cbef18463ddcc4a6b40ff03cfeed5ff4d992d3 | |
| parent | merge: Overhaul trending polls (!1022) (diff) | |
| parent | catch errors from fetchInstance() to resolve test errors (diff) | |
| download | sharkey-39fcdcae257d158159d020ff72ef2c5e720f5ffa.tar.gz sharkey-39fcdcae257d158159d020ff72ef2c5e720f5ffa.tar.bz2 sharkey-39fcdcae257d158159d020ff72ef2c5e720f5ffa.zip | |
merge: Allow unauthenticated (logged-out) users to translate notes (!1055)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1055
Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
| -rw-r--r-- | packages/backend/src/core/RoleService.ts | 2 | ||||
| -rw-r--r-- | packages/backend/src/server/api/ApiCallService.ts | 16 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints.ts | 2 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/notes/translate.ts | 13 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNote.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteDetailed.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkNoteSub.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkNote.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkNoteDetailed.vue | 6 | ||||
| -rw-r--r-- | packages/frontend/src/components/SkNoteSub.vue | 4 | ||||
| -rw-r--r-- | packages/frontend/src/i.ts | 2 | ||||
| -rw-r--r-- | packages/frontend/src/instance.ts | 10 | ||||
| -rw-r--r-- | packages/frontend/src/utility/get-note-menu.ts | 12 |
13 files changed, 49 insertions, 40 deletions
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 6b6479c240..b250eeee21 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -86,7 +86,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canManageCustomEmojis: false, canManageAvatarDecorations: false, canSearchNotes: false, - canUseTranslator: true, + canUseTranslator: false, canHideAds: false, driveCapacityMb: 100, maxFileSizeMb: 25, diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index 0d2dafd556..5c9e5717bb 100644 --- a/packages/backend/src/server/api/ApiCallService.ts +++ b/packages/backend/src/server/api/ApiCallService.ts @@ -344,14 +344,14 @@ export class ApiCallService implements OnApplicationShutdown { } if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { - if (user == null) { + if (user == null && ep.meta.requireCredential !== 'optional') { throw new ApiError({ message: 'Credential required.', code: 'CREDENTIAL_REQUIRED', id: '1384574d-a912-4b81-8601-c7b1c4085df1', httpStatusCode: 401, }); - } else if (user!.isSuspended) { + } else if (user?.isSuspended) { throw new ApiError({ message: 'Your account has been suspended.', code: 'YOUR_ACCOUNT_SUSPENDED', @@ -372,8 +372,8 @@ export class ApiCallService implements OnApplicationShutdown { } } - if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) { - const myRoles = await this.roleService.getUserRoles(user!.id); + if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user?.id)) { + const myRoles = user ? await this.roleService.getUserRoles(user) : []; if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { throw new ApiError({ message: 'You are not assigned to a moderator role.', @@ -392,9 +392,9 @@ export class ApiCallService implements OnApplicationShutdown { } } - if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user!.id)) { - const myRoles = await this.roleService.getUserRoles(user!.id); - const policies = await this.roleService.getUserPolicies(user!.id); + if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user?.id)) { + const myRoles = user ? await this.roleService.getUserRoles(user) : []; + const policies = await this.roleService.getUserPolicies(user ?? null); if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) { throw new ApiError({ message: 'You are not assigned to a required role.', @@ -418,7 +418,7 @@ export class ApiCallService implements OnApplicationShutdown { // Cast non JSON input if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { for (const k of Object.keys(ep.params.properties)) { - const param = ep.params.properties![k]; + const param = ep.params.properties[k]; if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') { try { data[k] = JSON.parse(data[k]); diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 0ba041c536..c7d884cce1 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -92,7 +92,7 @@ export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requir }) | (Omit<IEndpointMetaBase, 'secure'> & { secure: true, }) | (Omit<IEndpointMetaBase, 'requireCredential' | 'kind'> & { - requireCredential: true, + requireCredential: true | 'optional', kind: (typeof permissions)[number], }) | (Omit<IEndpointMetaBase, 'requireModerator' | 'kind'> & { requireModerator: true, diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index e059b5e603..e55168e296 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -20,11 +20,9 @@ import { ApiError } from '../../error.js'; export const meta = { tags: ['notes'], - // TODO allow unauthenticated if default template allows? - // Maybe a value 'optional' that allows unauthenticated OR a token w/ appropriate role. - // This will allow unauthenticated requests without leaking post data to restricted clients. - requireCredential: true, + requireCredential: 'optional', kind: 'read:account', + requiredRolePolicy: 'canUseTranslator', res: { type: 'object', @@ -88,17 +86,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private readonly loggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me) => { - const policies = await this.roleService.getUserPolicies(me.id); - if (!policies.canUseTranslator) { - throw new ApiError(meta.errors.unavailable); - } - const note = await this.getterService.getNote(ps.noteId).catch(err => { if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); throw err; }); - if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) { + if (!(await this.noteEntityService.isVisibleForMe(note, me?.id ?? null))) { throw new ApiError(meta.errors.cannotTranslateInvisibleNote); } diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 366321565d..34c33270bb 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -157,7 +157,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @click.stop="clip()"> <i class="ti ti-paperclip"></i> </button> - <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.footerButton" :disabled="translating || !!translation" @click.stop="translate()"> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.footerButton" :disabled="translating || !!translation" @click.stop="translate()"> <i class="ti ti-language-hiragana"></i> </button> <button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()"> @@ -226,7 +226,7 @@ import { getNoteSummary } from '@/utility/get-note-summary.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/utility/show-moved-dialog.js'; import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; -import { instance, isEnabledUrlPreview } from '@/instance.js'; +import { instance, isEnabledUrlPreview, policies } from '@/instance.js'; import { focusPrev, focusNext } from '@/utility/focus.js'; import { getAppearNote } from '@/utility/get-appear-note.js'; import { prefer } from '@/preferences.js'; @@ -360,7 +360,7 @@ const keymap = { clip(); }, 't': () => { - if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { + if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) { translate(); } }, diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 52cc836926..b2f237a82e 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -169,7 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @click.stop="clip()"> <i class="ti ti-paperclip"></i> </button> - <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> <i class="ti ti-language-hiragana"></i> </button> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()"> @@ -278,7 +278,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; -import { instance, isEnabledUrlPreview } from '@/instance.js'; +import { instance, isEnabledUrlPreview, policies } from '@/instance.js'; import { getAppearNote } from '@/utility/get-appear-note.js'; import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; @@ -388,7 +388,7 @@ const keymap = { clip(); }, 't': () => { - if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { + if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) { translate(); } }, diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 282854c6a8..e6c0e83a8e 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()"> <i class="ti ti-paperclip"></i> </button> - <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> <i class="ti ti-language-hiragana"></i> </button> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()"> @@ -113,7 +113,7 @@ import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; import { prefer } from '@/preferences.js'; import { useNoteCapture } from '@/use/use-note-capture.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; -import { instance } from '@/instance'; +import { instance, policies } from '@/instance'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index 5184cbd801..c4580e9156 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -158,7 +158,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @click.stop="clip()"> <i class="ti ti-paperclip"></i> </button> - <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" class="_button" :class="$style.footerButton" :disabled="translating || !!translation" @click.stop="translate()"> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" class="_button" :class="$style.footerButton" :disabled="translating || !!translation" @click.stop="translate()"> <i class="ti ti-language-hiragana"></i> </button> <button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()"> @@ -226,7 +226,7 @@ import { getNoteSummary } from '@/utility/get-note-summary.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/utility/show-moved-dialog.js'; import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; -import { instance, isEnabledUrlPreview } from '@/instance.js'; +import { instance, isEnabledUrlPreview, policies } from '@/instance.js'; import { focusPrev, focusNext } from '@/utility/focus.js'; import { getAppearNote } from '@/utility/get-appear-note.js'; import { prefer } from '@/preferences.js'; @@ -360,7 +360,7 @@ const keymap = { clip(); }, 't': () => { - if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { + if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) { translate(); } }, diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index b165b95d40..8b495fb873 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -174,7 +174,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @click.stop="clip()"> <i class="ti ti-paperclip"></i> </button> - <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> <i class="ti ti-language-hiragana"></i> </button> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()"> @@ -283,7 +283,7 @@ import MkPagination from '@/components/MkPagination.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; -import { instance, isEnabledUrlPreview } from '@/instance.js'; +import { instance, isEnabledUrlPreview, policies } from '@/instance.js'; import { getAppearNote } from '@/utility/get-appear-note.js'; import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; @@ -394,7 +394,7 @@ const keymap = { clip(); }, 't': () => { - if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { + if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) { translate(); } }, diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue index 775436cb0f..9fa0c11d48 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -70,7 +70,7 @@ SPDX-License-Identifier: AGPL-3.0-only <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()"> <i class="ti ti-paperclip"></i> </button> - <button v-if="prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> + <button v-if="prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable" ref="translationButton" class="_button" :class="$style.noteFooterButton" :disabled="translating || !!translation" @click.stop="translate()"> <i class="ti ti-language-hiragana"></i> </button> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()"> @@ -121,7 +121,7 @@ import { boostMenuItems, computeRenoteTooltip } from '@/utility/boost-quote.js'; import { prefer } from '@/preferences.js'; import { useNoteCapture } from '@/use/use-note-capture.js'; import SkMutedNote from '@/components/SkMutedNote.vue'; -import { instance } from '@/instance'; +import { instance, policies } from '@/instance'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; diff --git a/packages/frontend/src/i.ts b/packages/frontend/src/i.ts index a71ed1671f..0021298d63 100644 --- a/packages/frontend/src/i.ts +++ b/packages/frontend/src/i.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { reactive } from 'vue'; +import { computed, reactive } from 'vue'; import * as Misskey from 'misskey-js'; import { miLocalStorage } from '@/local-storage.js'; diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts index e75e3dfd34..953af6333a 100644 --- a/packages/frontend/src/instance.ts +++ b/packages/frontend/src/instance.ts @@ -3,11 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, reactive } from 'vue'; +import { computed, nextTick, reactive } from 'vue'; import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { miLocalStorage } from '@/local-storage.js'; import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js'; +import { $i } from '@/i'; // TODO: 他のタブと永続化されたstateを同期 @@ -38,6 +39,8 @@ export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFA export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true); +export const policies = computed<Misskey.entities.RolePolicies>(() => $i?.policies ?? instance.policies); + export async function fetchInstance(force = false): Promise<Misskey.entities.MetaDetailed> { if (!force) { const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0; @@ -60,3 +63,8 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met return instance; } + +// instance export can be empty sometimes, which causes problems. +await fetchInstance().catch(err => { + console.warn('Initial meta fetch failed:', err); +}); diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts index f773149fac..2f9b613725 100644 --- a/packages/frontend/src/utility/get-note-menu.ts +++ b/packages/frontend/src/utility/get-note-menu.ts @@ -11,7 +11,7 @@ import type { Ref, ShallowRef } from 'vue'; import type { MenuItem } from '@/types/menu.js'; import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; -import { instance } from '@/instance.js'; +import { instance, policies } from '@/instance.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; @@ -342,7 +342,7 @@ export function getNoteMenu(props: { }); } - if ($i.policies.canUseTranslator && instance.translatorAvailable) { + if (policies.value.canUseTranslator && instance.translatorAvailable) { menuItems.push({ icon: 'ti ti-language-hiragana', text: i18n.ts.translate, @@ -497,6 +497,14 @@ export function getNoteMenu(props: { } else { menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed)); } + + if (policies.value.canUseTranslator && instance.translatorAvailable) { + menuItems.push({ + icon: 'ti ti-language-hiragana', + text: i18n.ts.translate, + action: () => translateNote(appearNote.id, props.translation, props.translating), + }); + } } const noteActions = getPluginHandlers('note_action'); |