Merge branch 'develop'

This commit is contained in:
syuilo 2019-06-18 19:16:41 +09:00
commit 80d8af84dd
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
23 changed files with 180 additions and 35 deletions

View file

@ -17,6 +17,17 @@ npm i -g ts-node
npm run migrate npm run migrate
``` ```
11.22.0 (2019/06/18)
--------------------
### ✨Improvements
* 管理画面でデータベースの各テーブルのレコード数やサイズを確認できるように
* サーバー情報にPostgreSQLのバージョンを追加
### 🐛Fixes
* リモートファイルのダウンロードに失敗することがある問題を修正
* アンケートの期間を日時指定で選択すると日時がUTCになってしまう問題を修正
* MFMのパースを修正
11.21.0 (2019/06/16) 11.21.0 (2019/06/16)
-------------------- --------------------
### ✨Improvements ### ✨Improvements

View file

@ -1226,8 +1226,12 @@ admin/views/index.vue:
abuse: "スパム報告" abuse: "スパム報告"
queue: "ジョブキュー" queue: "ジョブキュー"
logs: "ログ" logs: "ログ"
db: "データベース"
back-to-misskey: "Misskeyに戻る" back-to-misskey: "Misskeyに戻る"
admin/views/db.vue:
tables: "テーブル"
admin/views/dashboard.vue: admin/views/dashboard.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
accounts: "アカウント" accounts: "アカウント"

View file

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.21.0", "version": "11.22.0",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",
@ -214,7 +214,7 @@
"style-loader": "0.23.1", "style-loader": "0.23.1",
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.2.0", "summaly": "2.3.0",
"systeminformation": "4.11.1", "systeminformation": "4.11.1",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "1.3.0", "terser-webpack-plugin": "1.3.0",

View file

@ -124,7 +124,7 @@ export default Vue.extend({
this.connection = this.$root.stream.useSharedConnection('serverStats'); this.connection = this.$root.stream.useSharedConnection('serverStats');
this.updateStats(); this.updateStats();
this.clock = setInterval(this.updateStats, 1000); this.clock = setInterval(this.updateStats, 3000);
this.$root.getMeta().then(meta => { this.$root.getMeta().then(meta => {
this.meta = meta; this.meta = meta;

View file

@ -0,0 +1,39 @@
<template>
<div>
<ui-card>
<template #title><fa :icon="faDatabase"/> {{ $t('tables') }}</template>
<section v-if="tables">
<div v-for="table in Object.keys(tables)"><b>{{ table }}</b> {{ tables[table].count | number }} {{ tables[table].size | bytes }}</div>
</section>
</ui-card>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faDatabase } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/db.vue'),
data() {
return {
tables: null,
faDatabase
};
},
mounted() {
this.fetch();
},
methods: {
fetch() {
this.$root.api('admin/get-table-stats').then(tables => {
this.tables = tables;
});
},
}
});
</script>

View file

@ -22,6 +22,7 @@
<li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li> <li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li>
<li @click="nav('queue')" :class="{ active: page == 'queue' }"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</li> <li @click="nav('queue')" :class="{ active: page == 'queue' }"><fa :icon="faTasks" fixed-width/>{{ $t('queue') }}</li>
<li @click="nav('logs')" :class="{ active: page == 'logs' }"><fa :icon="faStream" fixed-width/>{{ $t('logs') }}</li> <li @click="nav('logs')" :class="{ active: page == 'logs' }"><fa :icon="faStream" fixed-width/>{{ $t('logs') }}</li>
<li @click="nav('db')" :class="{ active: page == 'db' }"><fa :icon="faDatabase" fixed-width/>{{ $t('db') }}</li>
<li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li> <li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li>
<li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li> <li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li>
<li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li> <li @click="nav('drive')" :class="{ active: page == 'drive' }"><fa icon="cloud" fixed-width/>{{ $t('@.drive') }}</li>
@ -43,6 +44,7 @@
<div v-if="page == 'instance'"><x-instance/></div> <div v-if="page == 'instance'"><x-instance/></div>
<div v-if="page == 'queue'"><x-queue/></div> <div v-if="page == 'queue'"><x-queue/></div>
<div v-if="page == 'logs'"><x-logs/></div> <div v-if="page == 'logs'"><x-logs/></div>
<div v-if="page == 'db'"><x-db/></div>
<div v-if="page == 'moderators'"><x-moderators/></div> <div v-if="page == 'moderators'"><x-moderators/></div>
<div v-if="page == 'users'"><x-users/></div> <div v-if="page == 'users'"><x-users/></div>
<div v-if="page == 'emoji'"><x-emoji/></div> <div v-if="page == 'emoji'"><x-emoji/></div>
@ -59,19 +61,20 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { version } from '../../config'; import { version } from '../../config';
import XDashboard from "./dashboard.vue"; import XDashboard from './dashboard.vue';
import XInstance from "./instance.vue"; import XInstance from './instance.vue';
import XQueue from "./queue.vue"; import XQueue from './queue.vue';
import XLogs from "./logs.vue"; import XLogs from './logs.vue';
import XModerators from "./moderators.vue"; import XDb from './db.vue';
import XEmoji from "./emoji.vue"; import XModerators from './moderators.vue';
import XAnnouncements from "./announcements.vue"; import XEmoji from './emoji.vue';
import XUsers from "./users.vue"; import XAnnouncements from './announcements.vue';
import XDrive from "./drive.vue"; import XUsers from './users.vue';
import XAbuse from "./abuse.vue"; import XDrive from './drive.vue';
import XFederation from "./federation.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'; import { faGrin } from '@fortawesome/free-regular-svg-icons';
// Detect the user agent // Detect the user agent
@ -85,6 +88,7 @@ export default Vue.extend({
XInstance, XInstance,
XQueue, XQueue,
XLogs, XLogs,
XDb,
XModerators, XModerators,
XEmoji, XEmoji,
XAnnouncements, XAnnouncements,
@ -108,7 +112,8 @@ export default Vue.extend({
faGlobe, faGlobe,
faExclamationCircle, faExclamationCircle,
faTasks, faTasks,
faStream faStream,
faDatabase,
}; };
}, },
methods: { methods: {

View file

@ -89,9 +89,7 @@ export default Vue.extend({
get() { get() {
const at = () => { const at = () => {
const [date] = moment(this.atDate).toISOString().split('T'); return moment(`${this.atDate} ${this.atTime}`).valueOf();
const [hour, minute] = this.atTime.split(':');
return moment(`${date}T${hour}:${minute}Z`).valueOf();
}; };
const after = () => { const after = () => {

View file

@ -3,6 +3,7 @@
<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p>
<p>Machine: {{ meta.machine }}</p> <p>Machine: {{ meta.machine }}</p>
<p>Node: {{ meta.node }}</p> <p>Node: {{ meta.node }}</p>
<p>PSQL: {{ meta.psql }}</p>
<p>Version: {{ meta.version }} </p> <p>Version: {{ meta.version }} </p>
</div> </div>
</template> </template>

View file

@ -3,7 +3,6 @@
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import { URL } from 'url';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { Source, Mixin } from './types'; import { Source, Mixin } from './types';
import * as pkg from '../../package.json'; import * as pkg from '../../package.json';

View file

@ -1,5 +1,4 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5'; import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { URL } from 'url';
import { urlRegex } from './prelude'; import { urlRegex } from './prelude';
export function fromHtml(html: string): string { export function fromHtml(html: string): string {

View file

@ -98,13 +98,13 @@ export const mfmLanguage = P.createLanguage({
const text = input.substr(i); const text = input.substr(i);
const match = text.match(/^(\*|_)([a-zA-Z0-9]+?[\s\S]*?)\1/); const match = text.match(/^(\*|_)([a-zA-Z0-9]+?[\s\S]*?)\1/);
if (!match) return P.makeFailure(i, 'not a italic'); 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.makeSuccess(i + match[0].length, match[2]);
}); });
return P.alt(xml, underscore).map(x => createTree('italic', r.inline.atLeast(1).tryParse(x), {})); 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 => { motion: r => {
const paren = P.regexp(/\(\(\(([\s\S]+?)\)\)\)/, 1); const paren = P.regexp(/\(\(\(([\s\S]+?)\)\)\)/, 1);
const xml = P.regexp(/<motion>(.+?)<\/motion>/, 1); const xml = P.regexp(/<motion>(.+?)<\/motion>/, 1);
@ -164,8 +164,10 @@ export const mfmLanguage = P.createLanguage({
} else } else
url = match[0]; url = match[0];
url = removeOrphanedBrackets(url); url = removeOrphanedBrackets(url);
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('.'));
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
}
return P.makeSuccess(i + url.length, url); return P.makeSuccess(i + url.length, url);
}).map(x => createLeaf('url', { url: x })); }).map(x => createLeaf('url', { url: x }));
}, },

View file

@ -1,6 +1,5 @@
import config from '../config'; import config from '../config';
import { toASCII } from 'punycode'; import { toASCII } from 'punycode';
import { URL } from 'url';
export function getFullApAccount(username: string, host: string | null) { export function getFullApAccount(username: string, host: string | null) {
return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`;

View file

@ -25,10 +25,8 @@ export async function downloadUrl(url: string, path: string) {
rej(error); rej(error);
}); });
const requestUrl = new URL(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url;
const req = request({ const req = request({
url: requestUrl, url: new URL(url).href, // https://github.com/syuilo/misskey/issues/2637
proxy: config.proxy, proxy: config.proxy,
timeout: 10 * 1000, timeout: 10 * 1000,
headers: { headers: {

View file

@ -3,7 +3,6 @@ import * as httpSignature from 'http-signature';
import { IRemoteUser } from '../../models/entities/user'; import { IRemoteUser } from '../../models/entities/user';
import perform from '../../remote/activitypub/perform'; import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
import { URL } from 'url';
import { publishApLogStream } from '../../services/stream'; import { publishApLogStream } from '../../services/stream';
import Logger from '../../services/logger'; import Logger from '../../services/logger';
import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc';

View file

@ -6,7 +6,6 @@ import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
import { DriveFile } from '../../../models/entities/drive-file'; import { DriveFile } from '../../../models/entities/drive-file';
import { fromHtml } from '../../../mfm/fromHtml'; import { fromHtml } from '../../../mfm/fromHtml';
import { URL } from 'url';
import { resolveNote, extractEmojis } from './note'; import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import { ITag, extractHashtags } from './tag'; import { ITag, extractHashtags } from './tag';

View file

@ -1,6 +1,5 @@
import { request } from 'https'; import { request } from 'https';
import { sign } from 'http-signature'; import { sign } from 'http-signature';
import { URL } from 'url';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { lookup, IRunOptions } from 'lookup-dns-cache'; import { lookup, IRunOptions } from 'lookup-dns-cache';
import * as promiseAny from 'promise-any'; import * as promiseAny from 'promise-any';

View file

@ -1,7 +1,6 @@
import webFinger from './webfinger'; import webFinger from './webfinger';
import config from '../config'; import config from '../config';
import { createPerson, updatePerson } from './activitypub/models/person'; import { createPerson, updatePerson } from './activitypub/models/person';
import { URL } from 'url';
import { remoteLogger } from './logger'; import { remoteLogger } from './logger';
import chalk from 'chalk'; import chalk from 'chalk';
import { User, IRemoteUser } from '../models/entities/user'; import { User, IRemoteUser } from '../models/entities/user';

View file

@ -1,6 +1,5 @@
import config from '../config'; import config from '../config';
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
import { URL } from 'url';
import { query as urlQuery } from '../prelude/url'; import { query as urlQuery } from '../prelude/url';
type ILink = { type ILink = {

View file

@ -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<string, { count: number; size: number; }>;
for (const rec of recs) {
res[rec.table] = {
count: parseInt(rec.count, 10),
size: parseInt(rec.size, 10),
};
}
return res;
});
return sizes;
});

View file

@ -8,6 +8,7 @@ import * as bcrypt from 'bcryptjs';
import { Users, UserProfiles } from '../../../../models'; import { Users, UserProfiles } from '../../../../models';
import { ensure } from '../../../../prelude/ensure'; import { ensure } from '../../../../prelude/ensure';
import { sendEmail } from '../../../../services/send-email'; import { sendEmail } from '../../../../services/send-email';
import { ApiError } from '../../error';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -27,6 +28,14 @@ export const meta = {
email: { email: {
validator: $.optional.nullable.str 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!); const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error('incorrect password'); throw new ApiError(meta.errors.incorrectPassword);
} }
await UserProfiles.update({ userId: user.id }, { await UserProfiles.update({ userId: user.id }, {

View file

@ -6,6 +6,7 @@ import { fetchMeta } from '../../../misc/fetch-meta';
import * as pkg from '../../../../package.json'; import * as pkg from '../../../../package.json';
import { Emojis } from '../../../models'; import { Emojis } from '../../../models';
import { types, bool } from '../../../misc/schema'; import { types, bool } from '../../../misc/schema';
import { getConnection } from 'typeorm';
export const meta = { export const meta = {
stability: 'stable', stability: 'stable',
@ -114,6 +115,7 @@ export default define(meta, async (ps, me) => {
machine: os.hostname(), machine: os.hostname(),
os: os.platform(), os: os.platform(),
node: process.version, node: process.version,
psql: await getConnection().query('SHOW server_version').then(x => x[0].server_version),
cpu: { cpu: {
model: os.cpus()[0].model, model: os.cpus()[0].model,

View file

@ -163,6 +163,19 @@ export default abstract class Chart<T extends Record<string, any>> {
}, },
...Chart.convertSchemaToFlatColumnDefinitions(schema) ...Chart.convertSchemaToFlatColumnDefinitions(schema)
}, },
indices: [{
columns: ['date']
}, {
columns: ['span']
}, {
columns: ['group']
}, {
columns: ['span', 'date']
}, {
columns: ['date', 'group']
}, {
columns: ['span', 'date', 'group']
}]
}); });
} }

View file

@ -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', () => { it('with comma', () => {
const tokens = parse('https://example.com/foo?bar=a,b'); const tokens = parse('https://example.com/foo?bar=a,b');
assert.deepStrictEqual(tokens, [ 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', () => { describe('italic', () => {
@ -1173,6 +1189,24 @@ describe('MFM', () => {
text('foo_bar_baz'), text('foo_bar_baz'),
]); ]);
}); });
it('require spaces', () => {
const tokens = parse('日目_L38b a_b');
assert.deepStrictEqual(tokens, [
text('日目_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'),
]);
});
}); });
}); });