From ec2b1ec3f0035466585d9cc2a7842e519e14e31a Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Fri, 20 Jul 2018 02:40:37 +0900 Subject: [PATCH] #1334 --- locales/ja.yml | 10 ++++ .../common/views/components/media-list.vue | 24 ++++++-- .../desktop/views/components/drive.file.vue | 12 ++++ .../desktop/views/components/media-image.vue | 30 +++++++++- .../mobile/views/components/media-image.vue | 30 +++++++++- src/docs/api/entities/drive-file.yaml | 7 +++ src/models/drive-file.ts | 1 + src/remote/activitypub/models/image.ts | 4 +- src/remote/activitypub/renderer/image.ts | 5 +- src/remote/activitypub/renderer/person.ts | 12 +++- src/server/activitypub.ts | 8 +-- .../api/endpoints/drive/files/create.ts | 10 +++- .../api/endpoints/drive/files/update.ts | 59 ++++++++++++++----- src/services/drive/add-file.ts | 6 +- src/services/drive/upload-from-url.ts | 4 +- 15 files changed, 178 insertions(+), 44 deletions(-) diff --git a/locales/ja.yml b/locales/ja.yml index 3575379345..d26d13996d 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -330,6 +330,8 @@ desktop/views/components/drive.file.vue: banner: "バナー" contextmenu: rename: "名前を変更" + mark-as-sensitive: "閲覧注意に設定" + unmark-as-sensitive: "閲覧注意を解除" copy-url: "URLをコピー" download: "ダウンロード" else-files: "その他..." @@ -377,6 +379,10 @@ desktop/views/components/drive.vue: upload: "ファイルをアップロード" url-upload: "URLからアップロード" +desktop/views/components/media-image.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + desktop/views/components/follow-button.vue: following: "フォロー中" follow: "フォロー" @@ -853,6 +859,10 @@ mobile/views/components/drive.file-detail.vue: hash: "ハッシュ (md5)" exif: "EXIF" +mobile/views/components/media-image.vue: + sensitive: "閲覧注意" + click-to-show: "クリックして表示" + mobile/views/components/follow-button.vue: following: "フォロー中" follow: "フォロー" diff --git a/src/client/app/common/views/components/media-list.vue b/src/client/app/common/views/components/media-list.vue index 2f8a1943ad..cdfc2c8d3c 100644 --- a/src/client/app/common/views/components/media-list.vue +++ b/src/client/app/common/views/components/media-list.vue @@ -46,33 +46,45 @@ export default Vue.extend({ display grid grid-gap 4px + > * + overflow hidden + border-radius 4px + &[data-count="1"] grid-template-rows 1fr + &[data-count="2"] grid-template-columns 1fr 1fr grid-template-rows 1fr + &[data-count="3"] grid-template-columns 1fr 0.5fr grid-template-rows 1fr 1fr - :nth-child(1) + + > *:nth-child(1) grid-row 1 / 3 - :nth-child(3) + + > *:nth-child(3) grid-column 2 / 3 grid-row 2 / 3 + &[data-count="4"] grid-template-columns 1fr 1fr grid-template-rows 1fr 1fr - :nth-child(1) + > *:nth-child(1) grid-column 1 / 2 grid-row 1 / 2 - :nth-child(2) + + > *:nth-child(2) grid-column 2 / 3 grid-row 1 / 2 - :nth-child(3) + + > *:nth-child(3) grid-column 1 / 2 grid-row 2 / 3 - :nth-child(4) + + > *:nth-child(4) grid-column 2 / 3 grid-row 2 / 3 diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index 86addb1318..11700d4966 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -68,6 +68,11 @@ export default Vue.extend({ icon: '%fa:i-cursor%', action: this.rename }, { + type: 'item', + text: this.file.isSensitive ? '%i18n:@contextmenu.unmark-as-sensitive%' : '%i18n:@contextmenu.mark-as-sensitive%', + icon: this.file.isSensitive ? '%fa:R eye%' : '%fa:R eye-slash%', + action: this.toggleSensitive + }, null, { type: 'item', text: '%i18n:@contextmenu.copy-url%', icon: '%fa:link%', @@ -149,6 +154,13 @@ export default Vue.extend({ }); }, + toggleSensitive() { + (this as any).api('drive/files/update', { + fileId: this.file.id, + isSensitive: !this.file.isSensitive + }); + }, + copyUrl() { copyToClipboard(this.file.url); (this as any).apis.dialog({ diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue index b98a4707ec..42a31c4c2d 100644 --- a/src/client/app/desktop/views/components/media-image.vue +++ b/src/client/app/desktop/views/components/media-image.vue @@ -1,5 +1,11 @@ <template> -<a class="mk-media-image" +<div class="ldwbgwstjsdgcjruamauqdrffetqudry" v-if="image.isSensitive && hide" @click="hide = false"> + <div> + <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> +</div> +<a class="lcjomzwbohoelkxsnuqjiaccdbdfiazy" v-else :href="image.url" @mousemove="onMousemove" @mouseleave="onMouseleave" @@ -21,6 +27,10 @@ export default Vue.extend({ }, raw: { default: false + }, + hide: { + type: Boolean, + default: true } }, computed: { @@ -56,16 +66,30 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-media-image +.lcjomzwbohoelkxsnuqjiaccdbdfiazy display block cursor zoom-in overflow hidden width 100% height 100% background-position center - border-radius 4px &:not(:hover) background-size cover +.ldwbgwstjsdgcjruamauqdrffetqudry + display flex + justify-content center + align-items center + background #111 + color #fff + + > div + display table-cell + text-align center + font-size 12px + + > b + display block + </style> diff --git a/src/client/app/mobile/views/components/media-image.vue b/src/client/app/mobile/views/components/media-image.vue index c2f9c66e84..1042404c98 100644 --- a/src/client/app/mobile/views/components/media-image.vue +++ b/src/client/app/mobile/views/components/media-image.vue @@ -1,5 +1,11 @@ <template> -<a class="mk-media-image" :href="image.url" target="_blank" :style="style" :title="image.name"></a> +<div class="qjewsnkgzzxlxtzncydssfbgjibiehcy" v-if="image.isSensitive && hide" @click="hide = false"> + <div> + <b>%fa:exclamation-triangle% %i18n:@sensitive%</b> + <span>%i18n:@click-to-show%</span> + </div> +</div> +<a class="gqnyydlzavusgskkfvwvjiattxdzsqlf" v-else :href="image.url" target="_blank" :style="style" :title="image.name"></a> </template> <script lang="ts"> @@ -13,6 +19,10 @@ export default Vue.extend({ }, raw: { default: false + }, + hide: { + type: Boolean, + default: true } }, computed: { @@ -35,13 +45,27 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.mk-media-image +.gqnyydlzavusgskkfvwvjiattxdzsqlf display block overflow hidden width 100% height 100% background-position center background-size cover - border-radius 4px + +.qjewsnkgzzxlxtzncydssfbgjibiehcy + display flex + justify-content center + align-items center + background #111 + color #fff + + > div + display table-cell + text-align center + font-size 12px + + > b + display block </style> diff --git a/src/docs/api/entities/drive-file.yaml b/src/docs/api/entities/drive-file.yaml index bb39e90112..2d14c6e1f5 100644 --- a/src/docs/api/entities/drive-file.yaml +++ b/src/docs/api/entities/drive-file.yaml @@ -81,3 +81,10 @@ props: desc: ja: "フォルダ" en: "The folder of this file" + + sensitive: + type: "boolean" + optional: true + desc: + ja: "このメディアが「閲覧注意」(NSFW)かどうか" + en: "Whether this media is NSFW" diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index 2bdf38f484..3a0390f792 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -33,6 +33,7 @@ export type IMetadata = { url?: string; deletedAt?: Date; isMetaOnly?: boolean; + isSensitive?: boolean; }; export type IDriveFile = { diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts index fb17a7c9e5..8b33187ef5 100644 --- a/src/remote/activitypub/models/image.ts +++ b/src/remote/activitypub/models/image.ts @@ -16,7 +16,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv return null; } - const image = await new Resolver().resolve(value); + const image = await new Resolver().resolve(value) as any; if (image.url == null) { throw new Error('invalid image: url not privided'); @@ -24,7 +24,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv log(`Creating the Image: ${image.url}`); - return await uploadFromUrl(image.url, actor, null, image.url); + return await uploadFromUrl(image.url, actor, null, image.url, image.sensitive); } /** diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts index cf91ce3a0c..69bddd9188 100644 --- a/src/remote/activitypub/renderer/image.ts +++ b/src/remote/activitypub/renderer/image.ts @@ -1,7 +1,8 @@ import config from '../../../config'; import { IDriveFile } from '../../../models/drive-file'; -export default (fileId: IDriveFile['_id']) => ({ +export default (file: IDriveFile) => ({ type: 'Image', - url: `${config.drive_url}/${fileId}` + url: `${config.drive_url}/${file._id}`, + sensitive: file.metadata.isSensitive }); diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index d4b3f40e41..7d828f97ae 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -4,10 +4,16 @@ import config from '../../../config'; import { ILocalUser } from '../../../models/user'; import toHtml from '../../../mfm/html'; import parse from '../../../mfm/parse'; +import DriveFile from '../../../models/drive-file'; -export default (user: ILocalUser) => { +export default async (user: ILocalUser) => { const id = `${config.url}/users/${user._id}`; + const [avatar, banner] = await Promise.all([ + DriveFile.findOne({ _id: user.avatarId }), + DriveFile.findOne({ _id: user.bannerId }) + ]); + return { type: user.isBot ? 'Service' : 'Person', id, @@ -18,8 +24,8 @@ export default (user: ILocalUser) => { preferredUsername: user.username, name: user.name, summary: toHtml(parse(user.description)), - icon: user.avatarId && renderImage(user.avatarId), - image: user.bannerId && renderImage(user.bannerId), + icon: user.avatarId && renderImage(avatar), + image: user.bannerId && renderImage(banner), manuallyApprovesFollowers: user.isLocked, publicKey: renderKey(user) }; diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index 0448ae61b8..17cd34ee6f 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -111,13 +111,13 @@ router.get('/users/:user/publickey', async ctx => { }); // user -function userInfo(ctx: Router.IRouterContext, user: IUser) { +async function userInfo(ctx: Router.IRouterContext, user: IUser) { if (user === null) { ctx.status = 404; return; } - ctx.body = pack(renderPerson(user as ILocalUser)); + ctx.body = pack(await renderPerson(user as ILocalUser)); } router.get('/users/:user', async ctx => { @@ -128,7 +128,7 @@ router.get('/users/:user', async ctx => { host: null }); - userInfo(ctx, user); + await userInfo(ctx, user); }); router.get('/@:user', async (ctx, next) => { @@ -139,7 +139,7 @@ router.get('/@:user', async (ctx, next) => { host: null }); - userInfo(ctx, user); + await userInfo(ctx, user); }); //#endregion diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index ca12be9104..1c5506f6c4 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -29,6 +29,14 @@ export const meta = { desc: { ja: 'フォルダID' } + }), + + isSensitive: $.bool.optional.note({ + default: false, + desc: { + ja: 'このメディアが「閲覧注意」(NSFW)かどうか', + en: 'Whether this media is NSFW' + } }) } }; @@ -68,7 +76,7 @@ export default async (file: any, params: any, user: ILocalUser): Promise<any> => try { // Create file - const driveFile = await create(user, file.path, name, null, ps.folderId); + const driveFile = await create(user, file.path, name, null, ps.folderId, false, false, null, null, ps.isSensitive); cleanup(); diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index 396bc97694..bac04bae78 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -3,6 +3,7 @@ import DriveFolder from '../../../../../models/drive-folder'; import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file'; import { publishDriveStream } from '../../../../../stream'; import { ILocalUser } from '../../../../../models/user'; +import getParams from '../../../get-params'; export const meta = { desc: { @@ -12,18 +13,48 @@ export const meta = { requireCredential: true, - kind: 'drive-write' + kind: 'drive-write', + + params: { + fileId: $.type(ID).note({ + desc: { + ja: '対象のファイルID' + } + }), + + folderId: $.type(ID).optional.nullable.note({ + default: undefined, + desc: { + ja: 'フォルダID' + } + }), + + name: $.str.optional.pipe(validateFileName).note({ + default: undefined, + desc: { + ja: 'ファイル名', + en: 'Name of the file' + } + }), + + isSensitive: $.bool.optional.note({ + default: undefined, + desc: { + ja: 'このメディアが「閲覧注意」(NSFW)かどうか', + en: 'Whether this media is NSFW' + } + }) + } }; export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { - // Get 'fileId' parameter - const [fileId, fileIdErr] = $.type(ID).get(params.fileId); - if (fileIdErr) return rej('invalid fileId param'); + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); // Fetch file const file = await DriveFile .findOne({ - _id: fileId, + _id: ps.fileId, 'metadata.userId': user._id }); @@ -31,23 +62,18 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = return rej('file-not-found'); } - // Get 'name' parameter - const [name, nameErr] = $.str.optional.pipe(validateFileName).get(params.name); - if (nameErr) return rej('invalid name param'); - if (name) file.filename = name; + if (ps.name) file.filename = ps.name; - // Get 'folderId' parameter - const [folderId, folderIdErr] = $.type(ID).optional.nullable.get(params.folderId); - if (folderIdErr) return rej('invalid folderId param'); + if (ps.isSensitive) file.metadata.isSensitive = ps.isSensitive; - if (folderId !== undefined) { - if (folderId === null) { + if (ps.folderId !== undefined) { + if (ps.folderId === null) { file.metadata.folderId = null; } else { // Fetch folder const folder = await DriveFolder .findOne({ - _id: folderId, + _id: ps.folderId, userId: user._id }); @@ -62,7 +88,8 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = await DriveFile.update(file._id, { $set: { filename: file.filename, - 'metadata.folderId': file.metadata.folderId + 'metadata.folderId': file.metadata.folderId, + 'metadata.isSensitive': file.metadata.isSensitive } }); diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 57c6589176..73d5b4962c 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -83,7 +83,8 @@ export default async function( force: boolean = false, metaOnly: boolean = false, url: string = null, - uri: string = null + uri: string = null, + sensitive = false ): Promise<IDriveFile> { // Calc md5 hash const calcHash = new Promise<string>((res, rej) => { @@ -258,7 +259,8 @@ export default async function( folderId: folder !== null ? folder._id : null, comment: comment, properties: properties, - isMetaOnly: metaOnly + isMetaOnly: metaOnly, + isSensitive: sensitive } as IMetadata; if (url !== null) { diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index 711889ea41..4e297d3bb1 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -13,7 +13,7 @@ import * as mongodb from 'mongodb'; const log = debug('misskey:drive:upload-from-url'); -export default async (url: string, user: IUser, folderId: mongodb.ObjectID = null, uri: string = null): Promise<IDriveFile> => { +export default async (url: string, user: IUser, folderId: mongodb.ObjectID = null, uri: string = null, sensitive = false): Promise<IDriveFile> => { log(`REQUESTED: ${url}`); let name = URL.parse(url).pathname.split('/').pop(); @@ -48,7 +48,7 @@ export default async (url: string, user: IUser, folderId: mongodb.ObjectID = nul let error; try { - driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri); + driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri, sensitive); log(`got: ${driveFile._id}`); } catch (e) { error = e;