This commit is contained in:
かっこかり 2024-11-05 06:19:14 +00:00 committed by GitHub
commit 55a9d215ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 148 additions and 25 deletions

View file

@ -3,6 +3,7 @@
### General
- Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Fix: センシティブな画像をアイコン・バナーに指定できないように
### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように

9
locales/index.d.ts vendored
View file

@ -5218,6 +5218,15 @@ export interface Locale extends ILocale {
*
*/
"availableRoles": string;
/**
*
*/
"cannotSelectSensitiveMedia": string;
/**
*
*
*/
"cannotSelectSensitiveMediaDescription": string;
"_accountSettings": {
/**
*

View file

@ -1300,6 +1300,8 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "投稿者により、表示に
lockdown: "ロックダウン"
pleaseSelectAccount: "アカウントを選択してください"
availableRoles: "利用可能なロール"
cannotSelectSensitiveMedia: "センシティブなメディアは選択できません"
cannotSelectSensitiveMediaDescription: "自分でセンシティブ設定を行っていないのにこのエラーが出ている場合、自動判定によりセンシティブなメディアとされている可能性があります。\nサーバーの規則に照らして不要な場合は、ファイルのセンシティブ設定を解除してもう一度お試しください。"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"

View file

@ -74,6 +74,18 @@ export const meta = {
id: '75aedb19-2afd-4e6d-87fc-67941256fa60',
},
avatarIsSensitive: {
message: 'The file specified as an avatar is marked as sensitive.',
code: 'AVATAR_IS_SENSITIVE',
id: '71bb5e53-4742-4609-b465-36081e131208',
},
bannerIsSensitive: {
message: 'The file specified as a banner is marked as sensitive.',
code: 'BANNER_IS_SENSITIVE',
id: 'e148b34c-9f33-4300-93e0-7817008fb366',
},
noSuchPage: {
message: 'No such page.',
code: 'NO_SUCH_PAGE',
@ -359,6 +371,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
if (avatar.isSensitive) throw new ApiError(meta.errors.avatarIsSensitive);
updates.avatarId = avatar.id;
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
@ -377,6 +390,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
if (banner.isSensitive) throw new ApiError(meta.errors.bannerIsSensitive);
updates.bannerId = banner.id;
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
:class="[$style.root, { [$style.isSelected]: isSelected }]"
:class="[$style.root, { [$style.isSelected]: isSelected, [$style.isDisabled]: isDisabled }]"
draggable="true"
:title="title"
@click="onClick"
@ -55,9 +55,11 @@ const props = withDefaults(defineProps<{
file: Misskey.entities.DriveFile;
folder: Misskey.entities.DriveFolder | null;
isSelected?: boolean;
isDisabled?: boolean;
selectMode?: boolean;
}>(), {
isSelected: false,
isDisabled: false,
selectMode: false,
});
@ -72,6 +74,8 @@ const isDragging = ref(false);
const title = computed(() => `${props.file.name}\n${props.file.type} ${bytes(props.file.size)}`);
function onClick(ev: MouseEvent) {
if (props.isDisabled) return;
if (props.selectMode) {
emit('chosen', props.file);
} else {
@ -88,6 +92,8 @@ function onContextmenu(ev: MouseEvent) {
}
function onDragstart(ev: DragEvent) {
if (props.isDisabled) return;
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
@ -173,6 +179,23 @@ function onDragend() {
color: #fff;
}
}
&.isDisabled {
cursor: not-allowed;
.thumbnail {
opacity: 0.5;
}
.name {
opacity: 0.5;
}
&:hover,
&:active {
background: none;
}
}
}
.label {

View file

@ -74,6 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:folder="folder"
:selectMode="select === 'file'"
:isSelected="selectedFiles.some(x => x.id === file.id)"
:isDisabled="excludeSensitive && file.isSensitive"
@chosen="chooseFile"
@dragstart="isDragSource = true"
@dragend="isDragSource = false"
@ -115,9 +116,11 @@ const props = withDefaults(defineProps<{
initialFolder?: Misskey.entities.DriveFolder;
type?: string;
multiple?: boolean;
excludeSensitive?: boolean;
select?: 'file' | 'folder' | null;
}>(), {
multiple: false,
excludeSensitive: false,
select: null,
});

View file

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ multiple ? ((type === 'file') ? i18n.ts.selectFiles : i18n.ts.selectFolders) : ((type === 'file') ? i18n.ts.selectFile : i18n.ts.selectFolder) }}
<span v-if="selected.length > 0" style="margin-left: 8px; opacity: 0.5;">({{ number(selected.length) }})</span>
</template>
<XDrive :multiple="multiple" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
<XDrive :multiple="multiple" :excludeSensitive="excludeSensitive" :select="type" @changeSelection="onChangeSelection" @selected="ok()"/>
</MkModalWindow>
</template>
@ -34,6 +34,7 @@ import { i18n } from '@/i18n.js';
withDefaults(defineProps<{
type?: 'file' | 'folder';
multiple: boolean;
excludeSensitive: boolean;
}>(), {
type: 'file',
});

View file

@ -569,11 +569,12 @@ export async function selectUser(opts: { includeSelf?: boolean; localOnly?: bool
});
}
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
export async function selectDriveFile(multiple: boolean, excludeSensitive: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise(resolve => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'file',
multiple,
excludeSensitive,
}, {
done: files => {
if (files) {
@ -585,11 +586,12 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
});
}
export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
export async function selectDriveFolder(multiple: boolean, excludeSensitive: boolean): Promise<Misskey.entities.DriveFolder[]> {
return new Promise(resolve => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'folder',
multiple,
excludeSensitive,
}, {
done: folders => {
if (folders) {

View file

@ -135,6 +135,7 @@ import { defaultStore } from '@/store.js';
import { globalEvents } from '@/events.js';
import MkInfo from '@/components/MkInfo.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
const $i = signinRequired();
@ -223,7 +224,26 @@ function save() {
}
function changeAvatar(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar).then(async (file) => {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.avatar, {
excludeSensitive: true,
additionalMenu: $i.avatarId ? [
{ type: 'divider' },
{
type: 'button',
text: i18n.ts.detach,
icon: 'ti ti-circle-x',
action: () => {
os.apiWithDialog('i/update', {
avatarId: null,
}).then(() => {
$i.avatarId = null;
$i.avatarUrl = null;
globalEvents.emit('requestClearPageCache');
});
},
},
] : undefined,
}).then(async (file) => {
let originalOrCropped = file;
const { canceled } = await os.confirm({
@ -239,18 +259,43 @@ function changeAvatar(ev) {
});
}
const i = await os.apiWithDialog('i/update', {
await os.apiWithDialog('i/update', {
avatarId: originalOrCropped.id,
}, undefined, {
'71bb5e53-4742-4609-b465-36081e131208': {
title: i18n.ts.cannotSelectSensitiveMedia,
text: i18n.ts.cannotSelectSensitiveMediaDescription,
},
}).then(() => {
$i.avatarId = originalOrCropped.id;
$i.avatarUrl = originalOrCropped.url;
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');
});
$i.avatarId = i.avatarId;
$i.avatarUrl = i.avatarUrl;
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');
});
}
function changeBanner(ev) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner).then(async (file) => {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.banner, {
excludeSensitive: true,
additionalMenu: $i.bannerId ? [
{ type: 'divider' },
{
type: 'button',
text: i18n.ts.detach,
icon: 'ti ti-circle-x',
action: () => {
os.apiWithDialog('i/update', {
bannerId: null,
}).then(() => {
$i.bannerId = null;
$i.bannerUrl = null;
globalEvents.emit('requestClearPageCache');
});
},
},
] : undefined,
}).then(async (file) => {
let originalOrCropped = file;
const { canceled } = await os.confirm({
@ -266,12 +311,18 @@ function changeBanner(ev) {
});
}
const i = await os.apiWithDialog('i/update', {
await os.apiWithDialog('i/update', {
bannerId: originalOrCropped.id,
}, undefined, {
'e148b34c-9f33-4300-93e0-7817008fb366': {
title: i18n.ts.cannotSelectSensitiveMedia,
text: i18n.ts.cannotSelectSensitiveMediaDescription,
},
}).then(() => {
$i.bannerId = originalOrCropped.id;
$i.bannerUrl = originalOrCropped.url;
globalEvents.emit('requestClearPageCache');
});
$i.bannerId = i.bannerId;
$i.bannerUrl = i.bannerUrl;
globalEvents.emit('requestClearPageCache');
});
}

View file

@ -5,12 +5,20 @@
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { uploadFile } from '@/scripts/upload.js';
import { deepMerge } from '@/scripts/merge.js';
type SelectFileOptions = {
multiple?: boolean;
excludeSensitive?: boolean;
additionalMenu?: MenuItem[];
};
export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
@ -39,9 +47,9 @@ export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promi
});
}
export function chooseFileFromDrive(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
export function chooseFileFromDrive(multiple: boolean, excludeSensitive: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
os.selectDriveFile(multiple).then(files => {
os.selectDriveFile(multiple, excludeSensitive).then(files => {
res(files);
});
});
@ -80,7 +88,16 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
});
}
function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
function select(src: HTMLElement | EventTarget | null, label: string | null, options?: SelectFileOptions): Promise<Misskey.entities.DriveFile[]> {
const _options = deepMerge(options ?? {}, {
multiple: false,
/** ドライブファイル選択時のみに適用 */
excludeSensitive: false,
additionalMenu: [] as MenuItem[],
});
return new Promise((res, rej) => {
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
@ -94,23 +111,23 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul
}, {
text: i18n.ts.upload,
icon: 'ti ti-upload',
action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)),
action: () => chooseFileFromPc(_options.multiple, keepOriginal.value).then(files => res(files)),
}, {
text: i18n.ts.fromDrive,
icon: 'ti ti-cloud',
action: () => chooseFileFromDrive(multiple).then(files => res(files)),
action: () => chooseFileFromDrive(_options.multiple, _options.excludeSensitive).then(files => res(files)),
}, {
text: i18n.ts.fromUrl,
icon: 'ti ti-link',
action: () => chooseFileFromUrl().then(file => res([file])),
}], src);
}, ...(_options.additionalMenu)], src);
});
}
export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(src, label, false).then(files => files[0]);
export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null, options?: { excludeSensitive?: boolean; additionalMenu?: MenuItem[]; }): Promise<Misskey.entities.DriveFile> {
return select(src, label, { ...(options ? options : {}), multiple: false }).then(files => files[0]);
}
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(src, label, true);
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null, options?: { excludeSensitive?: boolean; additionalMenu?: MenuItem[]; }): Promise<Misskey.entities.DriveFile[]> {
return select(src, label, { ...(options ? options : {}), multiple: true });
}