allow unauthenticated (logged-out) users to translate notes

This commit is contained in:
Hazelnoot 2025-05-29 16:56:24 -04:00
parent 5d8a8bba2a
commit 50a64f97df
11 changed files with 44 additions and 38 deletions

View file

@ -344,14 +344,14 @@ export class ApiCallService implements OnApplicationShutdown {
} }
if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) { if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) {
if (user == null) { if (user == null && ep.meta.requireCredential !== 'optional') {
throw new ApiError({ throw new ApiError({
message: 'Credential required.', message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED', code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1', id: '1384574d-a912-4b81-8601-c7b1c4085df1',
httpStatusCode: 401, httpStatusCode: 401,
}); });
} else if (user!.isSuspended) { } else if (user?.isSuspended) {
throw new ApiError({ throw new ApiError({
message: 'Your account has been suspended.', message: 'Your account has been suspended.',
code: 'YOUR_ACCOUNT_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)) { if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user?.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id); const myRoles = user ? await this.roleService.getUserRoles(user) : [];
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) { if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a moderator role.', 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)) { if (ep.meta.requiredRolePolicy != null && (this.meta.rootUserId !== user?.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id); const myRoles = user ? await this.roleService.getUserRoles(user) : [];
const policies = await this.roleService.getUserPolicies(user!.id); const policies = await this.roleService.getUserPolicies(user ?? null);
if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) { if (!policies[ep.meta.requiredRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({ throw new ApiError({
message: 'You are not assigned to a required role.', message: 'You are not assigned to a required role.',
@ -418,7 +418,7 @@ export class ApiCallService implements OnApplicationShutdown {
// Cast non JSON input // Cast non JSON input
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) { if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(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') { if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
try { try {
data[k] = JSON.parse(data[k]); data[k] = JSON.parse(data[k]);

View file

@ -92,7 +92,7 @@ export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requir
}) | (Omit<IEndpointMetaBase, 'secure'> & { }) | (Omit<IEndpointMetaBase, 'secure'> & {
secure: true, secure: true,
}) | (Omit<IEndpointMetaBase, 'requireCredential' | 'kind'> & { }) | (Omit<IEndpointMetaBase, 'requireCredential' | 'kind'> & {
requireCredential: true, requireCredential: true | 'optional',
kind: (typeof permissions)[number], kind: (typeof permissions)[number],
}) | (Omit<IEndpointMetaBase, 'requireModerator' | 'kind'> & { }) | (Omit<IEndpointMetaBase, 'requireModerator' | 'kind'> & {
requireModerator: true, requireModerator: true,

View file

@ -20,11 +20,9 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['notes'], tags: ['notes'],
// TODO allow unauthenticated if default template allows? requireCredential: 'optional',
// 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,
kind: 'read:account', kind: 'read:account',
requiredRolePolicy: 'canUseTranslator',
res: { res: {
type: 'object', type: 'object',
@ -88,17 +86,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private readonly loggerService: ApiLoggerService, private readonly loggerService: ApiLoggerService,
) { ) {
super(meta, paramDef, async (ps, me) => { 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 => { const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err; 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); throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
} }

View file

@ -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()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </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> <i class="ti ti-language-hiragana"></i>
</button> </button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()"> <button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()">
@ -214,7 +214,7 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { checkAnimationFromMfm } from '@/utility/check-animated-mfm.js'; import { checkAnimationFromMfm } from '@/utility/check-animated-mfm.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu, translateNote } from '@/utility/get-note-menu.js'; import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu, translateNote } from '@/utility/get-note-menu.js';
import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js'; import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js';
@ -360,7 +360,7 @@ const keymap = {
clip(); clip();
}, },
't': () => { 't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate(); translate();
} }
}, },

View file

@ -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()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @click.stop="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </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> <i class="ti ti-language-hiragana"></i>
</button> </button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()"> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()">
@ -262,7 +262,7 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu, translateNote } from '@/utility/get-note-menu.js'; import { getNoteClipMenu, getNoteMenu, getRenoteMenu, translateNote } from '@/utility/get-note-menu.js';
import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js'; import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js';
@ -388,7 +388,7 @@ const keymap = {
clip(); clip();
}, },
't': () => { 't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate(); translate();
} }
}, },

View file

@ -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()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </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> <i class="ti ti-language-hiragana"></i>
</button> </button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()"> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()">
@ -100,7 +100,7 @@ import * as os from '@/os.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { checkMutes } from '@/utility/check-word-mute.js'; import { checkMutes } from '@/utility/check-word-mute.js';
import { pleaseLogin } from '@/utility/please-login.js'; import { pleaseLogin } from '@/utility/please-login.js';

View file

@ -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()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </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> <i class="ti ti-language-hiragana"></i>
</button> </button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()"> <button ref="menuButton" :class="$style.footerButton" class="_button" @click.stop="showMenu()">
@ -214,7 +214,7 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { checkAnimationFromMfm } from '@/utility/check-animated-mfm.js'; import { checkAnimationFromMfm } from '@/utility/check-animated-mfm.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, translateNote } from '@/utility/get-note-menu.js'; import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, translateNote } from '@/utility/get-note-menu.js';
import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js'; import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js';
@ -360,7 +360,7 @@ const keymap = {
clip(); clip();
}, },
't': () => { 't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { if (prefer.s.showTranslationButtonInNoteFooter && policies.value.canUseTranslator && instance.translatorAvailable) {
translate(); translate();
} }
}, },

View file

@ -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()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @click.stop="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </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> <i class="ti ti-language-hiragana"></i>
</button> </button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()"> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="showMenu()">
@ -267,7 +267,7 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { reactionPicker } from '@/utility/reaction-picker.js'; import { reactionPicker } from '@/utility/reaction-picker.js';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getNoteClipMenu, getNoteMenu, getRenoteMenu, translateNote } from '@/utility/get-note-menu.js'; import { getNoteClipMenu, getNoteMenu, getRenoteMenu, translateNote } from '@/utility/get-note-menu.js';
import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js'; import { getNoteVersionsMenu } from '@/utility/get-note-versions-menu.js';
@ -394,7 +394,7 @@ const keymap = {
clip(); clip();
}, },
't': () => { 't': () => {
if (prefer.s.showTranslationButtonInNoteFooter && $i?.policies.canUseTranslator && instance.translatorAvailable) { if (prefer.s.showTranslationButtonInNoteFooter && policies.canUseTranslator && instance.translatorAvailable) {
translate(); translate();
} }
}, },

View file

@ -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()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.noteFooterButton" class="_button" @click.stop="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </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> <i class="ti ti-language-hiragana"></i>
</button> </button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()"> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @click.stop="menu()">
@ -108,7 +108,7 @@ import * as os from '@/os.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { checkMutes } from '@/utility/check-word-mute.js'; import { checkMutes } from '@/utility/check-word-mute.js';
import { pleaseLogin } from '@/utility/please-login.js'; import { pleaseLogin } from '@/utility/please-login.js';

View file

@ -3,9 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { reactive } from 'vue'; import { computed, reactive } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { fetchInstance } from '@/instance';
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
@ -29,6 +30,10 @@ export function incNotesCount() {
notesCount++; notesCount++;
} }
// instance export can be empty sometimes, which causes problems.
const instance = await fetchInstance();
export const policies = computed<Misskey.entities.RolePolicies>(() => $i?.policies ?? instance.policies);
if (_DEV_) { if (_DEV_) {
(window as any).$i = $i; (window as any).$i = $i;
} }

View file

@ -9,7 +9,7 @@ import { url } from '@@/js/config.js';
import { claimAchievement } from './achievements.js'; import { claimAchievement } from './achievements.js';
import type { Ref, ShallowRef } from 'vue'; import type { Ref, ShallowRef } from 'vue';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/i.js'; import { $i, policies } from '@/i.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -342,7 +342,7 @@ export function getNoteMenu(props: {
}); });
} }
if ($i.policies.canUseTranslator && instance.translatorAvailable) { if (policies.value.canUseTranslator && instance.translatorAvailable) {
menuItems.push({ menuItems.push({
icon: 'ti ti-language-hiragana', icon: 'ti ti-language-hiragana',
text: i18n.ts.translate, text: i18n.ts.translate,
@ -497,6 +497,14 @@ export function getNoteMenu(props: {
} else { } else {
menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.embed)); 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'); const noteActions = getPluginHandlers('note_action');