diff --git a/CHANGELOG.md b/CHANGELOG.md index 2da31c0f2..cbd6e095b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,17 @@ npm i -g ts-node npm run migrate ``` +11.22.0 (2019/06/18) +-------------------- +### ✨Improvements +* 管理画面でデータベースの各テーブルのレコード数やサイズを確認できるように +* サーバー情報にPostgreSQLのバージョンを追加 + +### 🐛Fixes +* リモートファイルのダウンロードに失敗することがある問題を修正 +* アンケートの期間を日時指定で選択すると日時がUTCになってしまう問題を修正 +* MFMのパースを修正 + 11.21.0 (2019/06/16) -------------------- ### ✨Improvements diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7053360a1..d9223b1c4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1226,8 +1226,12 @@ admin/views/index.vue: abuse: "スパム報告" queue: "ジョブキュー" logs: "ログ" + db: "データベース" back-to-misskey: "Misskeyに戻る" +admin/views/db.vue: + tables: "テーブル" + admin/views/dashboard.vue: dashboard: "ダッシュボード" accounts: "アカウント" diff --git a/package.json b/package.json index 22cb43cb3..e5946f04f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "misskey", "author": "syuilo ", - "version": "11.21.0", + "version": "11.22.0", "codename": "daybreak", "repository": { "type": "git", @@ -214,7 +214,7 @@ "style-loader": "0.23.1", "stylus": "0.54.5", "stylus-loader": "3.0.2", - "summaly": "2.2.0", + "summaly": "2.3.0", "systeminformation": "4.11.1", "syuilo-password-strength": "0.0.1", "terser-webpack-plugin": "1.3.0", diff --git a/src/client/app/admin/views/dashboard.vue b/src/client/app/admin/views/dashboard.vue index 639ec190e..15bd853c1 100644 --- a/src/client/app/admin/views/dashboard.vue +++ b/src/client/app/admin/views/dashboard.vue @@ -124,7 +124,7 @@ export default Vue.extend({ this.connection = this.$root.stream.useSharedConnection('serverStats'); this.updateStats(); - this.clock = setInterval(this.updateStats, 1000); + this.clock = setInterval(this.updateStats, 3000); this.$root.getMeta().then(meta => { this.meta = meta; diff --git a/src/client/app/admin/views/db.vue b/src/client/app/admin/views/db.vue new file mode 100644 index 000000000..7818546e7 --- /dev/null +++ b/src/client/app/admin/views/db.vue @@ -0,0 +1,39 @@ + + + diff --git a/src/client/app/admin/views/index.vue b/src/client/app/admin/views/index.vue index 43e47038f..8a13fe1bf 100644 --- a/src/client/app/admin/views/index.vue +++ b/src/client/app/admin/views/index.vue @@ -22,6 +22,7 @@
  • {{ $t('instance') }}
  • {{ $t('queue') }}
  • {{ $t('logs') }}
  • +
  • {{ $t('db') }}
  • {{ $t('moderators') }}
  • {{ $t('users') }}
  • {{ $t('@.drive') }}
  • @@ -43,6 +44,7 @@
    +
    @@ -59,19 +61,20 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { version } from '../../config'; -import XDashboard from "./dashboard.vue"; -import XInstance from "./instance.vue"; -import XQueue from "./queue.vue"; -import XLogs from "./logs.vue"; -import XModerators from "./moderators.vue"; -import XEmoji from "./emoji.vue"; -import XAnnouncements from "./announcements.vue"; -import XUsers from "./users.vue"; -import XDrive from "./drive.vue"; -import XAbuse from "./abuse.vue"; -import XFederation from "./federation.vue"; +import XDashboard from './dashboard.vue'; +import XInstance from './instance.vue'; +import XQueue from './queue.vue'; +import XLogs from './logs.vue'; +import XDb from './db.vue'; +import XModerators from './moderators.vue'; +import XEmoji from './emoji.vue'; +import XAnnouncements from './announcements.vue'; +import XUsers from './users.vue'; +import XDrive from './drive.vue'; +import XAbuse from './abuse.vue'; +import XFederation from './federation.vue'; -import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks, faStream } from '@fortawesome/free-solid-svg-icons'; +import { faHeadset, faArrowLeft, faGlobe, faExclamationCircle, faTasks, faStream, faDatabase } from '@fortawesome/free-solid-svg-icons'; import { faGrin } from '@fortawesome/free-regular-svg-icons'; // Detect the user agent @@ -85,6 +88,7 @@ export default Vue.extend({ XInstance, XQueue, XLogs, + XDb, XModerators, XEmoji, XAnnouncements, @@ -108,7 +112,8 @@ export default Vue.extend({ faGlobe, faExclamationCircle, faTasks, - faStream + faStream, + faDatabase, }; }, methods: { diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue index ed1d02aa2..f7a4d3af8 100644 --- a/src/client/app/common/views/components/poll-editor.vue +++ b/src/client/app/common/views/components/poll-editor.vue @@ -89,9 +89,7 @@ export default Vue.extend({ get() { const at = () => { - const [date] = moment(this.atDate).toISOString().split('T'); - const [hour, minute] = this.atTime.split(':'); - return moment(`${date}T${hour}:${minute}Z`).valueOf(); + return moment(`${this.atDate} ${this.atTime}`).valueOf(); }; const after = () => { diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue index a97b4ec49..41ccd23bf 100644 --- a/src/client/app/common/views/widgets/server.info.vue +++ b/src/client/app/common/views/widgets/server.info.vue @@ -3,6 +3,7 @@

    Maintainer: {{ meta.maintainerName }}

    Machine: {{ meta.machine }}

    Node: {{ meta.node }}

    +

    PSQL: {{ meta.psql }}

    Version: {{ meta.version }}

    diff --git a/src/config/load.ts b/src/config/load.ts index 26b25eab4..aeed54d74 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -3,7 +3,6 @@ */ import * as fs from 'fs'; -import { URL } from 'url'; import * as yaml from 'js-yaml'; import { Source, Mixin } from './types'; import * as pkg from '../../package.json'; diff --git a/src/mfm/fromHtml.ts b/src/mfm/fromHtml.ts index 5fc4a1641..60293b07f 100644 --- a/src/mfm/fromHtml.ts +++ b/src/mfm/fromHtml.ts @@ -1,5 +1,4 @@ import { parseFragment, DefaultTreeDocumentFragment } from 'parse5'; -import { URL } from 'url'; import { urlRegex } from './prelude'; export function fromHtml(html: string): string { diff --git a/src/mfm/language.ts b/src/mfm/language.ts index 003ae348a..4750ea338 100644 --- a/src/mfm/language.ts +++ b/src/mfm/language.ts @@ -98,13 +98,13 @@ export const mfmLanguage = P.createLanguage({ const text = input.substr(i); const match = text.match(/^(\*|_)([a-zA-Z0-9]+?[\s\S]*?)\1/); if (!match) return P.makeFailure(i, 'not a italic'); - if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a italic'); + if (input[i - 1] != null && input[i - 1] != ' ' && input[i - 1] != '\n') return P.makeFailure(i, 'not a italic'); return P.makeSuccess(i + match[0].length, match[2]); }); return P.alt(xml, underscore).map(x => createTree('italic', r.inline.atLeast(1).tryParse(x), {})); }, - strike: r => P.regexp(/~~(.+?)~~/, 1).map(x => createTree('strike', r.inline.atLeast(1).tryParse(x), {})), + strike: r => P.regexp(/~~([^\n~]+?)~~/, 1).map(x => createTree('strike', r.inline.atLeast(1).tryParse(x), {})), motion: r => { const paren = P.regexp(/\(\(\(([\s\S]+?)\)\)\)/, 1); const xml = P.regexp(/(.+?)<\/motion>/, 1); @@ -164,8 +164,10 @@ export const mfmLanguage = P.createLanguage({ } else url = match[0]; url = removeOrphanedBrackets(url); - if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.')); - if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(',')); + while (url.endsWith('.') || url.endsWith(',')) { + if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.')); + if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(',')); + } return P.makeSuccess(i + url.length, url); }).map(x => createLeaf('url', { url: x })); }, diff --git a/src/misc/convert-host.ts b/src/misc/convert-host.ts index a5fb15c66..ad52e1258 100644 --- a/src/misc/convert-host.ts +++ b/src/misc/convert-host.ts @@ -1,6 +1,5 @@ import config from '../config'; import { toASCII } from 'punycode'; -import { URL } from 'url'; export function getFullApAccount(username: string, host: string | null) { return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; diff --git a/src/misc/donwload-url.ts b/src/misc/donwload-url.ts index 167e01fdd..e2f10c0e4 100644 --- a/src/misc/donwload-url.ts +++ b/src/misc/donwload-url.ts @@ -25,10 +25,8 @@ export async function downloadUrl(url: string, path: string) { rej(error); }); - const requestUrl = new URL(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url; - const req = request({ - url: requestUrl, + url: new URL(url).href, // https://github.com/syuilo/misskey/issues/2637 proxy: config.proxy, timeout: 10 * 1000, headers: { diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts index 21a51c962..e71181ee7 100644 --- a/src/queue/processors/inbox.ts +++ b/src/queue/processors/inbox.ts @@ -3,7 +3,6 @@ import * as httpSignature from 'http-signature'; import { IRemoteUser } from '../../models/entities/user'; import perform from '../../remote/activitypub/perform'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; -import { URL } from 'url'; import { publishApLogStream } from '../../services/stream'; import Logger from '../../services/logger'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index dcd64e0cd..bfcad100f 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -6,7 +6,6 @@ import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; import { DriveFile } from '../../../models/entities/drive-file'; import { fromHtml } from '../../../mfm/fromHtml'; -import { URL } from 'url'; import { resolveNote, extractEmojis } from './note'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; import { ITag, extractHashtags } from './tag'; diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index bde4921c3..6d18e5328 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -1,6 +1,5 @@ import { request } from 'https'; import { sign } from 'http-signature'; -import { URL } from 'url'; import * as crypto from 'crypto'; import { lookup, IRunOptions } from 'lookup-dns-cache'; import * as promiseAny from 'promise-any'; diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 0e2b88f05..5253d684d 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,7 +1,6 @@ import webFinger from './webfinger'; import config from '../config'; import { createPerson, updatePerson } from './activitypub/models/person'; -import { URL } from 'url'; import { remoteLogger } from './logger'; import chalk from 'chalk'; import { User, IRemoteUser } from '../models/entities/user'; diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts index 800673943..101a31aab 100644 --- a/src/remote/webfinger.ts +++ b/src/remote/webfinger.ts @@ -1,6 +1,5 @@ import config from '../config'; import * as request from 'request-promise-native'; -import { URL } from 'url'; import { query as urlQuery } from '../prelude/url'; type ILink = { diff --git a/src/server/api/endpoints/admin/get-table-stats.ts b/src/server/api/endpoints/admin/get-table-stats.ts new file mode 100644 index 000000000..1abea1849 --- /dev/null +++ b/src/server/api/endpoints/admin/get-table-stats.ts @@ -0,0 +1,37 @@ +import define from '../../define'; +import { getConnection } from 'typeorm'; + +export const meta = { + requireCredential: false, + + desc: { + 'en-US': 'Get table stats' + }, + + tags: ['meta'], + + params: { + }, +}; + +export default define(meta, async () => { + const sizes = await + getConnection().query(` + SELECT relname AS "table", reltuples as "count", pg_total_relation_size(C.oid) AS "size" + FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) + WHERE nspname NOT IN ('pg_catalog', 'information_schema') + AND C.relkind <> 'i' + AND nspname !~ '^pg_toast';`) + .then(recs => { + const res = {} as Record; + for (const rec of recs) { + res[rec.table] = { + count: parseInt(rec.count, 10), + size: parseInt(rec.size, 10), + }; + } + return res; + }); + + return sizes; +}); diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts index 56284499d..ca95e612a 100644 --- a/src/server/api/endpoints/i/update-email.ts +++ b/src/server/api/endpoints/i/update-email.ts @@ -8,6 +8,7 @@ import * as bcrypt from 'bcryptjs'; import { Users, UserProfiles } from '../../../../models'; import { ensure } from '../../../../prelude/ensure'; import { sendEmail } from '../../../../services/send-email'; +import { ApiError } from '../../error'; export const meta = { requireCredential: true, @@ -27,6 +28,14 @@ export const meta = { email: { validator: $.optional.nullable.str }, + }, + + errors: { + incorrectPassword: { + message: 'Incorrect password.', + code: 'INCORRECT_PASSWORD', + id: 'e54c1d7e-e7d6-4103-86b6-0a95069b4ad3' + }, } }; @@ -37,7 +46,7 @@ export default define(meta, async (ps, user) => { const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { - throw new Error('incorrect password'); + throw new ApiError(meta.errors.incorrectPassword); } await UserProfiles.update({ userId: user.id }, { diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index d18543f56..4da0c7476 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -6,6 +6,7 @@ import { fetchMeta } from '../../../misc/fetch-meta'; import * as pkg from '../../../../package.json'; import { Emojis } from '../../../models'; import { types, bool } from '../../../misc/schema'; +import { getConnection } from 'typeorm'; export const meta = { stability: 'stable', @@ -114,6 +115,7 @@ export default define(meta, async (ps, me) => { machine: os.hostname(), os: os.platform(), node: process.version, + psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version), cpu: { model: os.cpus()[0].model, diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts index 6a69a21b7..6564ef781 100644 --- a/src/services/chart/core.ts +++ b/src/services/chart/core.ts @@ -163,6 +163,19 @@ export default abstract class Chart> { }, ...Chart.convertSchemaToFlatColumnDefinitions(schema) }, + indices: [{ + columns: ['date'] + }, { + columns: ['span'] + }, { + columns: ['group'] + }, { + columns: ['span', 'date'] + }, { + columns: ['date', 'group'] + }, { + columns: ['span', 'date', 'group'] + }] }); } diff --git a/test/mfm.ts b/test/mfm.ts index 89b414eba..be8b65264 100644 --- a/test/mfm.ts +++ b/test/mfm.ts @@ -804,6 +804,14 @@ describe('MFM', () => { ]); }); + it('ignore trailing periods', () => { + const tokens = parse('https://example.com...'); + assert.deepStrictEqual(tokens, [ + leaf('url', { url: 'https://example.com' }), + text('...') + ]); + }); + it('with comma', () => { const tokens = parse('https://example.com/foo?bar=a,b'); assert.deepStrictEqual(tokens, [ @@ -1116,6 +1124,14 @@ describe('MFM', () => { ], {}), ]); }); + + // https://misskey.io/notes/7u1kv5dmia + it('ignore internal tilde', () => { + const tokens = parse('~~~~~'); + assert.deepStrictEqual(tokens, [ + text('~~~~~') + ]); + }); }); describe('italic', () => { @@ -1173,6 +1189,24 @@ describe('MFM', () => { text('foo_bar_baz'), ]); }); + + it('require spaces', () => { + const tokens = parse('4日目_L38b a_b'); + assert.deepStrictEqual(tokens, [ + text('4日目_L38b a_b'), + ]); + }); + + it('newline sandwich', () => { + const tokens = parse('foo\n_bar_\nbaz'); + assert.deepStrictEqual(tokens, [ + text('foo\n'), + tree('italic', [ + text('bar') + ], {}), + text('\nbaz'), + ]); + }); }); });