Merge branch 'develop'

This commit is contained in:
syuilo 2019-07-05 18:02:42 +09:00
commit 3c5324bbbb
17 changed files with 105 additions and 86 deletions

View file

@ -1 +1 @@
v12.1.0 v12.6.0

View file

@ -17,6 +17,15 @@ npm i -g ts-node
npm run migrate npm run migrate
``` ```
11.24.1 (2019/07/05)
--------------------
### 🐛Fixes
* WebAuthnでログインできない問題を修正
* 絵文字の変更事項のmetaへの反映が最大時間遅延される問題を修正
* ハッシュタグのトレンドの計算を5分単位で丸めるように
* APNGでもMIME typeはimage/pngにするように
* カスタム絵文字リアクションがたまに文字になってしまう問題を修正
11.24.0 (2019/07/05) 11.24.0 (2019/07/05)
-------------------- --------------------
注意: このアップデート後に、`node built/tools/accept-migration Init 1000000000000`してください。 注意: このアップデート後に、`node built/tools/accept-migration Init 1000000000000`してください。

View file

@ -1,4 +1,4 @@
FROM node:12.5-alpine AS base FROM node:12.6-alpine AS base
ENV NODE_ENV=production ENV NODE_ENV=production

View file

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.24.0", "version": "11.24.1",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -55,6 +55,20 @@ export default Vue.extend({
} }
}, },
watch: {
customEmojis() {
if (this.name) {
const customEmoji = this.customEmojis.find(x => x.name == this.name);
if (customEmoji) {
this.customEmoji = customEmoji;
this.url = this.$store.state.device.disableShowingAnimatedImages
? getStaticImageUrl(customEmoji.url)
: customEmoji.url;
}
}
},
},
created() { created() {
if (this.name) { if (this.name) {
const customEmoji = this.customEmojis.find(x => x.name == this.name); const customEmoji = this.customEmojis.find(x => x.name == this.name);
@ -80,7 +94,7 @@ export default Vue.extend({
this.url = `${twemojiBase}/2/svg/${codes.join('-')}.svg`; this.url = `${twemojiBase}/2/svg/${codes.join('-')}.svg`;
} }
} },
}); });
</script> </script>

View file

@ -15,9 +15,14 @@ export default Vue.extend({
}, },
data() { data() {
return { return {
customEmojis: (this.$root.getMetaSync() || { emojis: [] }).emojis || [] customEmojis: []
}; };
}, },
created() {
this.$root.getMeta().then(meta => {
if (meta && meta.emojis) this.customEmojis = meta.emojis;
});
},
computed: { computed: {
str(): any { str(): any {
switch (this.reaction) { switch (this.reaction) {

View file

@ -107,9 +107,8 @@ export default Vue.extend({
})), })),
timeout: 60 * 1000 timeout: 60 * 1000
} }
}).catch(err => { }).catch(() => {
this.queryingKey = false; this.queryingKey = false;
console.warn(err);
return Promise.reject(null); return Promise.reject(null);
}).then(credential => { }).then(credential => {
this.queryingKey = false; this.queryingKey = false;
@ -128,7 +127,6 @@ export default Vue.extend({
location.reload(); location.reload();
}).catch(err => { }).catch(err => {
if (err === null) return; if (err === null) return;
console.error(err);
this.$root.dialog({ this.$root.dialog({
type: 'error', type: 'error',
text: this.$t('login-failed') text: this.$t('login-failed')
@ -142,7 +140,7 @@ export default Vue.extend({
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) { if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
if (window.PublicKeyCredential && this.user.securityKeys) { if (window.PublicKeyCredential && this.user.securityKeys) {
this.$root.api('i/2fa/getkeys', { this.$root.api('signin', {
username: this.username, username: this.username,
password: this.password password: this.password
}).then(res => { }).then(res => {
@ -150,6 +148,14 @@ export default Vue.extend({
this.signing = false; this.signing = false;
this.challengeData = res; this.challengeData = res;
return this.queryKey(); return this.queryKey();
}).catch(() => {
this.$root.dialog({
type: 'error',
text: this.$t('login-failed')
});
this.challengeData = null;
this.totpLogin = false;
this.signing = false;
}); });
} else { } else {
this.totpLogin = true; this.totpLogin = true;

View file

@ -3,6 +3,7 @@ import define from '../../../define';
import { detectUrlMine } from '../../../../../misc/detect-url-mine'; import { detectUrlMine } from '../../../../../misc/detect-url-mine';
import { Emojis } from '../../../../../models'; import { Emojis } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id'; import { genId } from '../../../../../misc/gen-id';
import { getConnection } from 'typeorm';
export const meta = { export const meta = {
desc: { desc: {
@ -43,6 +44,8 @@ export default define(meta, async (ps) => {
type, type,
}); });
await getConnection().queryResultCache!.remove(['meta_emojis']);
return { return {
id: emoji.id id: emoji.id
}; };

View file

@ -2,6 +2,7 @@ import $ from 'cafy';
import define from '../../../define'; import define from '../../../define';
import { ID } from '../../../../../misc/cafy-id'; import { ID } from '../../../../../misc/cafy-id';
import { Emojis } from '../../../../../models'; import { Emojis } from '../../../../../models';
import { getConnection } from 'typeorm';
export const meta = { export const meta = {
desc: { desc: {
@ -26,4 +27,6 @@ export default define(meta, async (ps) => {
if (emoji == null) throw new Error('emoji not found'); if (emoji == null) throw new Error('emoji not found');
await Emojis.delete(emoji.id); await Emojis.delete(emoji.id);
await getConnection().queryResultCache!.remove(['meta_emojis']);
}); });

View file

@ -3,6 +3,7 @@ import define from '../../../define';
import { detectUrlMine } from '../../../../../misc/detect-url-mine'; import { detectUrlMine } from '../../../../../misc/detect-url-mine';
import { ID } from '../../../../../misc/cafy-id'; import { ID } from '../../../../../misc/cafy-id';
import { Emojis } from '../../../../../models'; import { Emojis } from '../../../../../models';
import { getConnection } from 'typeorm';
export const meta = { export const meta = {
desc: { desc: {
@ -47,4 +48,6 @@ export default define(meta, async (ps) => {
url: ps.url, url: ps.url,
type, type,
}); });
await getConnection().queryResultCache!.remove(['meta_emojis']);
}); });

View file

@ -54,8 +54,11 @@ export default define(meta, async () => {
const instance = await fetchMeta(true); const instance = await fetchMeta(true);
const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase()); const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase());
const now = new Date(); // 5分単位で丸めた現在日時
now.setMinutes(Math.round(now.getMinutes() / 5) * 5, 0, 0);
const tagNotes = await Notes.createQueryBuilder('note') const tagNotes = await Notes.createQueryBuilder('note')
.where(`note.createdAt > :date`, { date: new Date(Date.now() - rangeA) }) .where(`note.createdAt > :date`, { date: new Date(now.getTime() - rangeA) })
.andWhere(`note.tags != '{}'`) .andWhere(`note.tags != '{}'`)
.select(['note.tags', 'note.userId']) .select(['note.tags', 'note.userId'])
.cache(60000) // 1 min .cache(60000) // 1 min
@ -106,8 +109,8 @@ export default define(meta, async () => {
countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note') countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
.select('count(distinct note.userId)') .select('count(distinct note.userId)')
.where(':tag = ANY(note.tags)', { tag: tag }) .where(':tag = ANY(note.tags)', { tag: tag })
.andWhere('note.createdAt < :lt', { lt: new Date(Date.now() - (interval * i)) }) .andWhere('note.createdAt < :lt', { lt: new Date(now.getTime() - (interval * i)) })
.andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * (i + 1))) }) .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * (i + 1))) })
.cache(60000) // 1 min .cache(60000) // 1 min
.getRawOne() .getRawOne()
.then(x => parseInt(x.count, 10)) .then(x => parseInt(x.count, 10))
@ -119,7 +122,7 @@ export default define(meta, async () => {
const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note') const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note')
.select('count(distinct note.userId)') .select('count(distinct note.userId)')
.where(':tag = ANY(note.tags)', { tag: tag }) .where(':tag = ANY(note.tags)', { tag: tag })
.andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * range)) }) .andWhere('note.createdAt > :gt', { gt: new Date(now.getTime() - (interval * range)) })
.cache(60000) // 1 min .cache(60000) // 1 min
.getRawOne() .getRawOne()
.then(x => parseInt(x.count, 10)) .then(x => parseInt(x.count, 10))

View file

@ -1,67 +0,0 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import * as crypto from 'crypto';
import define from '../../../define';
import { UserProfiles, UserSecurityKeys, AttestationChallenges } from '../../../../../models';
import { ensure } from '../../../../../prelude/ensure';
import { promisify } from 'util';
import { hash } from '../../../2fa';
import { genId } from '../../../../../misc/gen-id';
export const meta = {
requireCredential: true,
secure: true,
params: {
password: {
validator: $.str
}
}
};
const randomBytes = promisify(crypto.randomBytes);
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne(user.id).then(ensure);
// Compare password
const same = await bcrypt.compare(ps.password, profile.password!);
if (!same) {
throw new Error('incorrect password');
}
const keys = await UserSecurityKeys.find({
userId: user.id
});
if (keys.length === 0) {
throw new Error('no keys found');
}
// 32 byte challenge
const entropy = await randomBytes(32);
const challenge = entropy.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.save({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: false
});
return {
challenge,
challengeId,
securityKeys: keys.map(key => ({
id: key.id
}))
};
});

View file

@ -95,7 +95,7 @@ export const meta = {
export default define(meta, async (ps, me) => { export default define(meta, async (ps, me) => {
const instance = await fetchMeta(true); const instance = await fetchMeta(true);
const emojis = await Emojis.find({ where: { host: null }, cache: 3600000 }); // 1 hour const emojis = await Emojis.find({ where: { host: null }, cache: { id: 'meta_emojis', milliseconds: 3600000 } }); // 1 hour
const response: any = { const response: any = {
maintainerName: instance.maintainerName, maintainerName: instance.maintainerName,

View file

@ -9,6 +9,7 @@ import { ILocalUser } from '../../../models/entities/user';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
import { verifyLogin, hash } from '../2fa'; import { verifyLogin, hash } from '../2fa';
import { randomBytes } from 'crypto';
export default async (ctx: Koa.BaseContext) => { export default async (ctx: Koa.BaseContext) => {
ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Origin', config.url);
@ -99,7 +100,7 @@ export default async (ctx: Koa.BaseContext) => {
}); });
return; return;
} }
} else { } else if (body.credentialId) {
const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex'); const clientDataJSON = Buffer.from(body.clientDataJSON, 'hex');
const clientData = JSON.parse(clientDataJSON.toString('utf-8')); const clientData = JSON.parse(clientDataJSON.toString('utf-8'));
const challenge = await AttestationChallenges.findOne({ const challenge = await AttestationChallenges.findOne({
@ -131,7 +132,7 @@ export default async (ctx: Koa.BaseContext) => {
const securityKey = await UserSecurityKeys.findOne({ const securityKey = await UserSecurityKeys.findOne({
id: Buffer.from( id: Buffer.from(
body.credentialId body.credentialId
.replace(/\-/g, '+') .replace(/-/g, '+')
.replace(/_/g, '/'), .replace(/_/g, '/'),
'base64' 'base64'
).toString('hex') ).toString('hex')
@ -161,7 +162,44 @@ export default async (ctx: Koa.BaseContext) => {
}); });
return; return;
} }
} else {
const keys = await UserSecurityKeys.find({
userId: user.id
});
if (keys.length === 0) {
await fail(403, {
error: 'no keys found'
});
}
// 32 byte challenge
const challenge = randomBytes(32).toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
const challengeId = genId();
await AttestationChallenges.save({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
createdAt: new Date(),
registrationChallenge: false
});
ctx.body = {
challenge,
challengeId,
securityKeys: keys.map(key => ({
id: key.id
}))
};
ctx.status = 200;
return;
} }
await fail(); await fail();
return;
}; };

View file

@ -42,7 +42,7 @@ export default async function(ctx: Koa.BaseContext) {
ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`)); ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`));
ctx.body = InternalStorage.read(key); ctx.body = InternalStorage.read(key);
} else if (isWebpublic) { } else if (isWebpublic) {
ctx.set('Content-Type', file.type); ctx.set('Content-Type', file.type === 'image/apng' ? 'image/png' : file.type);
ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`)); ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`));
ctx.body = InternalStorage.read(key); ctx.body = InternalStorage.read(key);
} else { } else {

View file

@ -207,6 +207,8 @@ export async function generateAlts(path: string, type: string, generateWeb: bool
* Upload to ObjectStorage * Upload to ObjectStorage
*/ */
async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) {
if (type === 'image/apng') type = 'image/png';
const meta = await fetchMeta(); const meta = await fetchMeta();
const minio = new Minio.Client({ const minio = new Minio.Client({

View file

@ -97,6 +97,6 @@ export async function convertToApng(path: string): Promise<IImage> {
return { return {
data, data,
ext: 'apng', ext: 'apng',
type: 'image/vnd.mozilla.apng' type: 'image/apng'
}; };
} }