Merge branch 'develop'
This commit is contained in:
commit
3c5324bbbb
17 changed files with 105 additions and 86 deletions
|
@ -1 +1 @@
|
||||||
v12.1.0
|
v12.6.0
|
||||||
|
|
|
@ -17,6 +17,15 @@ npm i -g ts-node
|
||||||
npm run migrate
|
npm run migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
11.24.1 (2019/07/05)
|
||||||
|
--------------------
|
||||||
|
### 🐛Fixes
|
||||||
|
* WebAuthnでログインできない問題を修正
|
||||||
|
* 絵文字の変更事項のmetaへの反映が最大1時間遅延される問題を修正
|
||||||
|
* ハッシュタグのトレンドの計算を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`してください。
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -127,8 +126,7 @@ export default Vue.extend({
|
||||||
localStorage.setItem('i', res.i);
|
localStorage.setItem('i', res.i);
|
||||||
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;
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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']);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue