diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b5a2ac8..1725c7703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,13 +10,19 @@ ## 12.x.x (unreleased) ### Improvements +- 連合インスタンスページからインスタンス情報再取得を行えるように ### Bugfixes - 投稿のNSFW画像を表示したあとにリアクションが更新されると画像が非表示になる問題を修正 - 「クリップ」ページが開かない問題を修正 - トレンドウィジェットが動作しないのを修正 +- フェデレーションウィジェットが動作しないのを修正 - リアクション設定で絵文字ピッカーが開かないのを修正 - DMページでメンションが含まれる問題を修正 +- 投稿フォームのハッシュタグ保持フィールドが動作しない問題を修正 +- Add `img-src` and `media-src` directives to `Content-Security-Policy` for + files and media proxy +- ensure that specified users does not get duplicates ## 12.102.1 (2022/01/27) ### Bugfixes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27f5598a6..662fa709b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ We're glad you're interested in contributing Misskey! In this document you will **ℹ️ Important:** This project uses Japanese as its major language, **but you do not need to translate and write the Issues/PRs in Japanese.** Also, you might receive comments on your Issue/PR in Japanese, but you do not need to reply to them in Japanese as well.\ -The accuracy of translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. +The accuracy of machine translation into Japanese is not high, so it will be easier for us to understand if you write it in the original language. It will also allow the reader to use the translation tool of their preference if necessary. ## Issues diff --git a/cypress/integration/basic.js b/cypress/integration/basic.js index aca44ef15..7d27b649f 100644 --- a/cypress/integration/basic.js +++ b/cypress/integration/basic.js @@ -176,3 +176,7 @@ describe('After user singed in', () => { cy.contains('Hello, Misskey!'); }); }); + +// TODO: 投稿フォームの公開範囲指定のテスト +// TODO: 投稿フォームのファイル添付のテスト +// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b3279d78b..8fd41e533 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -235,6 +235,8 @@ resetAreYouSure: "リセットしますか?" saved: "保存しました" messaging: "チャット" upload: "アップロード" +keepOriginalUploading: "オリジナル画像を保持" +keepOriginalUploadingDescription: "画像をアップロードする時にオリジナル版を保持します。オフにするとアップロード時にブラウザでWeb公開用画像を生成します。" fromDrive: "ドライブから" fromUrl: "URLから" uploadFromUrl: "URLアップロード" diff --git a/packages/backend/package.json b/packages/backend/package.json index 3d3a901f3..3541e803f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -122,7 +122,7 @@ "langmap": "0.0.16", "mfm-js": "0.21.0", "mime-types": "2.1.34", - "misskey-js": "0.0.13", + "misskey-js": "0.0.14", "mocha": "8.4.0", "ms": "3.0.0-canary.1", "multer": "1.4.4", diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index faa35d12d..362bbb0f5 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -32,7 +32,7 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise((res) => { // Authentication authenticate(body['i']).then(([user, app]) => { // API invoking - call(endpoint.name, user, app, body, (ctx as any).file).then((res: any) => { + call(endpoint.name, user, app, body, ctx).then((res: any) => { reply(res); }).catch((e: ApiError) => { reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e); diff --git a/packages/backend/src/server/api/call.ts b/packages/backend/src/server/api/call.ts index 399ee65bd..5bc7d2f25 100644 --- a/packages/backend/src/server/api/call.ts +++ b/packages/backend/src/server/api/call.ts @@ -1,3 +1,4 @@ +import * as Koa from 'koa'; import { performance } from 'perf_hooks'; import { limiter } from './limiter'; import { User } from '@/models/entities/user'; @@ -12,7 +13,7 @@ const accessDenied = { id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', }; -export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { +export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => { const isSecure = user != null && token == null; const ep = endpoints.find(e => e.name === endpoint); @@ -76,9 +77,20 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac }); } + // Cast non JSON input + if (ep.meta.requireFile && ep.meta.params) { + const body = (ctx!.request as any).body; + for (const k of Object.keys(ep.meta.params)) { + const param = ep.meta.params[k]; + if (['Boolean', 'Number'].includes(param.validator.name) && typeof body[k] === 'string') { + body[k] = JSON.parse(body[k]); + } + } + } + // API invoking const before = performance.now(); - return await ep.exec(data, user, token, file).catch((e: Error) => { + return await ep.exec(data, user, token, ctx!.file).catch((e: Error) => { if (e instanceof ApiError) { throw e; } else { diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index dd65ab061..877e76677 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -39,15 +39,13 @@ export const meta = { }, isSensitive: { - validator: $.optional.either($.bool, $.str), + validator: $.optional.bool, default: false, - transform: (v: any): boolean => v === true || v === 'true', }, force: { - validator: $.optional.either($.bool, $.str), + validator: $.optional.bool, default: false, - transform: (v: any): boolean => v === true || v === 'true', }, }, diff --git a/packages/backend/src/server/file/index.ts b/packages/backend/src/server/file/index.ts index a455acd1c..6fe6110dc 100644 --- a/packages/backend/src/server/file/index.ts +++ b/packages/backend/src/server/file/index.ts @@ -18,7 +18,7 @@ const _dirname = dirname(_filename); const app = new Koa(); app.use(cors()); app.use(async (ctx, next) => { - ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); + ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`); await next(); }); diff --git a/packages/backend/src/server/proxy/index.ts b/packages/backend/src/server/proxy/index.ts index b8993f19f..7a3094311 100644 --- a/packages/backend/src/server/proxy/index.ts +++ b/packages/backend/src/server/proxy/index.ts @@ -11,7 +11,7 @@ import { proxyMedia } from './proxy-media'; const app = new Koa(); app.use(cors()); app.use(async (ctx, next) => { - ctx.set('Content-Security-Policy', `default-src 'none'; style-src 'unsafe-inline'`); + ctx.set('Content-Security-Policy', `default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'`); await next(); }); diff --git a/packages/backend/yarn.lock b/packages/backend/yarn.lock index 99e2e2306..5bf6e05a7 100644 --- a/packages/backend/yarn.lock +++ b/packages/backend/yarn.lock @@ -4967,10 +4967,10 @@ minizlib@^2.0.0, minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -misskey-js@0.0.13: - version "0.0.13" - resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" - integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== +misskey-js@0.0.14: + version "0.0.14" + resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" + integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== dependencies: autobind-decorator "^2.4.0" eventemitter3 "^4.0.7" diff --git a/packages/client/.eslintrc.js b/packages/client/.eslintrc.js index d414f86ed..acbb7c0c6 100644 --- a/packages/client/.eslintrc.js +++ b/packages/client/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { // data の禁止理由: 抽象的すぎるため // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため "id-denylist": ["error", "window", "data", "e"], + 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], "vue/attributes-order": ["error", { "alphabetical": false }], diff --git a/packages/client/package.json b/packages/client/package.json index 71dd89bea..b840bafe8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -69,7 +69,7 @@ "langmap": "0.0.16", "matter-js": "0.18.0", "mfm-js": "0.21.0", - "misskey-js": "0.0.13", + "misskey-js": "0.0.14", "mocha": "8.4.0", "ms": "2.1.3", "nested-property": "4.0.0", diff --git a/packages/client/src/components/chart-tooltip.vue b/packages/client/src/components/chart-tooltip.vue new file mode 100644 index 000000000..b080eaf2b --- /dev/null +++ b/packages/client/src/components/chart-tooltip.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/client/src/components/chart.vue b/packages/client/src/components/chart.vue index d17c0c9f3..3e46c51b4 100644 --- a/packages/client/src/components/chart.vue +++ b/packages/client/src/components/chart.vue @@ -8,7 +8,7 @@ diff --git a/packages/client/src/directives/tooltip.ts b/packages/client/src/directives/tooltip.ts index fffde1487..dd715227a 100644 --- a/packages/client/src/directives/tooltip.ts +++ b/packages/client/src/directives/tooltip.ts @@ -48,7 +48,7 @@ export default { popup(import('@/components/ui/tooltip.vue'), { showing, text: self.text, - source: el + targetElement: el, }, {}, 'closed'); self._close = () => { @@ -56,8 +56,8 @@ export default { }; }; - el.addEventListener('selectstart', e => { - e.preventDefault(); + el.addEventListener('selectstart', ev => { + ev.preventDefault(); }); el.addEventListener(start, () => { diff --git a/packages/client/src/os.ts b/packages/client/src/os.ts index f3be5c68f..95b4e87a1 100644 --- a/packages/client/src/os.ts +++ b/packages/client/src/os.ts @@ -7,8 +7,10 @@ import * as Misskey from 'misskey-js'; import { apiUrl, url } from '@/config'; import MkPostFormDialog from '@/components/post-form-dialog.vue'; import MkWaitingDialog from '@/components/waiting-dialog.vue'; +import { MenuItem } from '@/types/menu'; import { resolve } from '@/router'; import { $i } from '@/account'; +import { defaultStore } from '@/store'; export const pendingApiRequestsCount = ref(0); @@ -470,7 +472,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: }); } -export function popupMenu(items: any[] | Ref, src?: HTMLElement, options?: { +export function popupMenu(items: MenuItem[] | Ref, src?: HTMLElement, options?: { align?: string; width?: number; viaKeyboard?: boolean; @@ -494,7 +496,7 @@ export function popupMenu(items: any[] | Ref, src?: HTMLElement, options? }); } -export function contextMenu(items: any[], ev: MouseEvent) { +export function contextMenu(items: MenuItem[] | Ref, ev: MouseEvent) { ev.preventDefault(); return new Promise((resolve, reject) => { let dispose; @@ -541,7 +543,7 @@ export const uploads = ref<{ img: string; }[]>([]); -export function upload(file: File, folder?: any, name?: string): Promise { +export function upload(file: File, folder?: any, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading): Promise { if (folder && typeof folder == 'object') folder = folder.id; return new Promise((resolve, reject) => { @@ -559,6 +561,8 @@ export function upload(file: File, folder?: any, name?: string): PromiseModeration {{ $ts.stopActivityDelivery }} {{ $ts.blockThisInstance }} + Refresh metadata @@ -111,6 +112,7 @@ import MkChart from '@/components/chart.vue'; import MkObjectView from '@/components/object-view.vue'; import FormLink from '@/components/form/link.vue'; import MkLink from '@/components/link.vue'; +import MkButton from '@/components/ui/button.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; import MkSelect from '@/components/form/select.vue'; @@ -155,6 +157,15 @@ async function toggleSuspend(v) { }); } +function refreshMetadata() { + os.api('admin/federation/refresh-remote-instance-metadata', { + host: instance.host, + }); + os.alert({ + text: 'Refresh requested', + }); +} + fetch(); defineExpose({ diff --git a/packages/client/src/pages/settings/drive.vue b/packages/client/src/pages/settings/drive.vue index f1016ebd8..134fa6330 100644 --- a/packages/client/src/pages/settings/drive.vue +++ b/packages/client/src/pages/settings/drive.vue @@ -28,6 +28,7 @@ + {{ $ts.keepOriginalUploading }} @@ -36,18 +37,21 @@ import { defineComponent } from 'vue'; import * as tinycolor from 'tinycolor2'; import FormLink from '@/components/form/link.vue'; +import FormSwitch from '@/components/form/switch.vue'; import FormSection from '@/components/form/section.vue'; import MkKeyValue from '@/components/key-value.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; import bytes from '@/filters/bytes'; import * as symbols from '@/symbols'; +import { defaultStore } from '@/store'; // TODO: render chart export default defineComponent({ components: { FormLink, + FormSwitch, FormSection, MkKeyValue, FormSplit, @@ -79,7 +83,8 @@ export default defineComponent({ l: 0.5 }) }; - } + }, + keepOriginalUploading: defaultStore.makeGetterSetter('keepOriginalUploading'), }, async created() { diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 56e0b564f..23df4edf5 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -1,3 +1,4 @@ +import { ref } from 'vue'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; @@ -6,12 +7,14 @@ import { DriveFile } from 'misskey-js/built/entities'; function select(src: any, label: string | null, multiple: boolean): Promise { return new Promise((res, rej) => { + const keepOriginal = ref(defaultStore.state.keepOriginalUploading); + const chooseFileFromPc = () => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { - const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder)); + const promises = Array.from(input.files).map(file => os.upload(file, defaultStore.state.uploadFolder, undefined, keepOriginal.value)); Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); @@ -74,6 +77,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise void; + +export type MenuDivider = null; +export type MenuNull = undefined; +export type MenuLabel = { type: 'label', text: string }; +export type MenuLink = { type: 'link', to: string, text: string, icon?: string, indicate?: boolean, avatar?: Misskey.entities.User }; +export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean }; +export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction }; +export type MenuSwitch = { type: 'switch', ref: Ref, text: string, disabled?: boolean }; +export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction }; + +export type MenuPending = { type: 'pending' }; + +type OuterMenuItem = MenuDivider | MenuNull | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; +type OuterPromiseMenuItem = Promise; +export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; +export type InnerMenuItem = MenuDivider | MenuPending | MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton; diff --git a/packages/client/src/widgets/federation.vue b/packages/client/src/widgets/federation.vue index 4c43117e4..5f1131dce 100644 --- a/packages/client/src/widgets/federation.vue +++ b/packages/client/src/widgets/federation.vue @@ -54,13 +54,13 @@ const charts = ref([]); const fetching = ref(true); const fetch = async () => { - const instances = await os.api('federation/instances', { + const fetchedInstances = await os.api('federation/instances', { sort: '+lastCommunicatedAt', limit: 5 }); - const charts = await Promise.all(instances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); - instances.value = instances; - charts.value = charts; + const fetchedCharts = await Promise.all(fetchedInstances.map(i => os.api('charts/instance', { host: i.host, limit: 16, span: 'hour' }))); + instances.value = fetchedInstances; + charts.value = fetchedCharts; fetching.value = false; }; diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock index a45a24ce0..76be45f14 100644 --- a/packages/client/yarn.lock +++ b/packages/client/yarn.lock @@ -4139,10 +4139,10 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -misskey-js@0.0.13: - version "0.0.13" - resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.13.tgz#03a4e469186e28752d599dc4093519eb64647970" - integrity sha512-kBdJdfe281gtykzzsrN3IAxWUQIimzPiJGyKWf863ggWJlWYVPmP9hTFlX2z8oPOaypgVBPEPHyw/jNUdc2DbQ== +misskey-js@0.0.14: + version "0.0.14" + resolved "https://registry.yarnpkg.com/misskey-js/-/misskey-js-0.0.14.tgz#1a616bdfbe81c6ee6900219eaf425bb5c714dd4d" + integrity sha512-bvLx6U3OwQwqHfp/WKwIVwdvNYAAPk0+YblXyxmSG3dwlzCgBRRLcB8o6bNruUDyJgh3t73pLDcOz3myxcUmww== dependencies: autobind-decorator "^2.4.0" eventemitter3 "^4.0.7" diff --git a/packages/shared/.eslintrc.js b/packages/shared/.eslintrc.js index 9d7d4159e..2d3356c3a 100644 --- a/packages/shared/.eslintrc.js +++ b/packages/shared/.eslintrc.js @@ -37,6 +37,7 @@ module.exports = { ] }], */ + 'eqeqeq': ['error', 'always', { 'null': 'ignore' }], 'no-multi-spaces': ['error'], 'no-var': ['error'], 'prefer-arrow-callback': ['error'], @@ -56,7 +57,7 @@ module.exports = { 'object-curly-spacing': ['error', 'always'], 'space-infix-ops': ['error'], 'space-before-blocks': ['error', 'always'], - '@typescript-eslint/no-unnecessary-condition': ['error'], + '@typescript-eslint/no-unnecessary-condition': ['warn'], '@typescript-eslint/no-var-requires': ['warn'], '@typescript-eslint/no-inferrable-types': ['warn'], '@typescript-eslint/no-empty-function': ['off'],