From a478a620cc494d8f0516ffb4c6a807dbaec0facc Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 10 Apr 2018 20:53:29 +0000 Subject: [PATCH 01/34] fix(package): update @types/node to version 9.6.4 Closes #1444 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a60a794ba..2193ff3c89 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@types/morgan": "1.7.35", "@types/ms": "0.7.30", "@types/multer": "1.3.6", - "@types/node": "9.6.2", + "@types/node": "9.6.4", "@types/nopt": "3.0.29", "@types/proxy-addr": "2.0.0", "@types/pug": "2.0.4", From 4166fd87c241f651932f99fd2909fa44acee4446 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 11 Apr 2018 18:24:42 +0900 Subject: [PATCH 02/34] wip #1443 --- src/models/note.ts | 32 ++++++++++++++++++++++++++++++++ src/models/user.ts | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/models/note.ts b/src/models/note.ts index f509fa66c8..a11da196cd 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -69,6 +69,38 @@ export type INote = { }; }; +// TODO +export async function physicalDelete(note: string | mongo.ObjectID | INote) { + let n: INote; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(note)) { + n = await Note.findOne({ + _id: note + }); + } else if (typeof note === 'string') { + n = await Note.findOne({ + _id: new mongo.ObjectID(note) + }); + } else { + n = note as INote; + } + + if (n == null) return; + + // この投稿の返信をすべて削除 + const replies = await Note.find({ + replyId: n._id + }); + await Promise.all(replies.map(r => physicalDelete(r))); + + // この投稿のWatchをすべて削除 + + // この投稿のReactionをすべて削除 + + // この投稿に対するFavoriteをすべて削除 +} + /** * Pack a note for API response * diff --git a/src/models/user.ts b/src/models/user.ts index adc9e6da99..a2800a3808 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -2,7 +2,7 @@ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); import rap from '@prezzemolo/rap'; import db from '../db/mongodb'; -import { INote, pack as packNote } from './note'; +import Note, { INote, pack as packNote, physicalDelete as physicalDeleteNote } from './note'; import Following from './following'; import Mute from './mute'; import getFriends from '../server/api/common/get-friends'; @@ -121,6 +121,40 @@ export function init(user): IUser { return user; } +// TODO +export async function physicalDelete(user: string | mongo.ObjectID | IUser) { + let u: IUser; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(user)) { + u = await User.findOne({ + _id: user + }); + } else if (typeof user === 'string') { + u = await User.findOne({ + _id: new mongo.ObjectID(user) + }); + } else { + u = user as IUser; + } + + if (u == null) return; + + // このユーザーが行った投稿をすべて削除 + const notes = await Note.find({ userId: u._id }); + await Promise.all(notes.map(n => physicalDeleteNote(n))); + + // このユーザーのお気に入りをすべて削除 + + // このユーザーが行ったメッセージをすべて削除 + + // このユーザーのドライブのファイルをすべて削除 + + // このユーザーに関するfollowingをすべて削除 + + // このユーザーを削除 +} + /** * Pack a user for API response * From 3569a7820ef233adc2ef8cd9914724e8cd1f19f6 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 11 Apr 2018 21:05:47 +0900 Subject: [PATCH 03/34] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E5=90=8D=E3=81=AE=E5=B0=91=E3=81=AA=E3=81=8F=E3=81=A8=E3=82=82?= =?UTF-8?q?3=E6=96=87=E5=AD=97=E4=BB=A5=E4=B8=8A=E3=81=A8=E3=81=84?= =?UTF-8?q?=E3=81=86=E5=88=B6=E9=99=90=E3=82=92=E6=92=A4=E5=BB=83=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=81=A6=E3=81=84=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/app/common/views/components/signup.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 30fe7b7ad0..40262b54d3 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -77,7 +77,7 @@ export default Vue.extend({ const err = !this.username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' : - this.username.length < 3 ? 'min-range' : + this.username.length < 1 ? 'min-range' : this.username.length > 20 ? 'max-range' : null; From 39d87305bae56e90fadabff52424872c6c2b6fc6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 02:48:06 +0900 Subject: [PATCH 04/34] Update index.ts --- src/server/file/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/file/index.ts b/src/server/file/index.ts index 658117e3ac..95e7867f01 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -56,11 +56,12 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend { const readable: stream.Readable = (() => { // 動画であれば if (/^video\/.*$/.test(type)) { - // 実装は先延ばし + // TODO // 使わないことになったストリームはしっかり取り壊す data.destroy(); return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); // 画像であれば + // Note: SVGはapplication/xml } else if (/^image\/.*$/.test(type) || type == 'application/xml') { // 0フレーム目を送る try { From c32385b616ba431a46331138b630fcc427039ee9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 02:53:48 +0900 Subject: [PATCH 05/34] Add missing index --- src/models/user.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/user.ts b/src/models/user.ts index a2800a3808..cdf9a564fc 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -11,6 +11,7 @@ import config from '../config'; const User = db.get('users'); User.createIndex('username'); +User.createIndex('usernameLower'); User.createIndex('token'); User.createIndex('uri', { sparse: true, unique: true }); From 553fccd719690a659f5985e7956e875af92d17e0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 03:46:32 +0900 Subject: [PATCH 06/34] wip --- src/models/access-token.ts | 31 +++++++++++++++++++++-- src/models/favorite.ts | 31 +++++++++++++++++++++-- src/models/note-reaction.ts | 31 ++++++++++++++++++++--- src/models/note-watching.ts | 27 ++++++++++++++++++++ src/models/note.ts | 43 ++++++++++++++++++++++++-------- src/models/user.ts | 49 +++++++++++++++++++++++++++++-------- 6 files changed, 184 insertions(+), 28 deletions(-) diff --git a/src/models/access-token.ts b/src/models/access-token.ts index 4451ca140d..9909ea01ad 100644 --- a/src/models/access-token.ts +++ b/src/models/access-token.ts @@ -1,12 +1,12 @@ import * as mongo from 'mongodb'; import db from '../db/mongodb'; -const AccessToken = db.get('accessTokens'); +const AccessToken = db.get('accessTokens'); AccessToken.createIndex('token'); AccessToken.createIndex('hash'); export default AccessToken; -export type IAccessTokens = { +export type IAccessToken = { _id: mongo.ObjectID; createdAt: Date; appId: mongo.ObjectID; @@ -14,3 +14,30 @@ export type IAccessTokens = { token: string; hash: string; }; + +/** + * AccessTokenを物理削除します + */ +export async function deleteAccessToken(accessToken: string | mongo.ObjectID | IAccessToken) { + let a: IAccessToken; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(accessToken)) { + a = await AccessToken.findOne({ + _id: accessToken + }); + } else if (typeof accessToken === 'string') { + a = await AccessToken.findOne({ + _id: new mongo.ObjectID(accessToken) + }); + } else { + a = accessToken as IAccessToken; + } + + if (a == null) return; + + // このAccessTokenを削除 + await AccessToken.remove({ + _id: a._id + }); +} diff --git a/src/models/favorite.ts b/src/models/favorite.ts index 73f8881926..b2c5828088 100644 --- a/src/models/favorite.ts +++ b/src/models/favorite.ts @@ -1,8 +1,8 @@ import * as mongo from 'mongodb'; import db from '../db/mongodb'; -const Favorites = db.get('favorites'); -export default Favorites; +const Favorite = db.get('favorites'); +export default Favorite; export type IFavorite = { _id: mongo.ObjectID; @@ -10,3 +10,30 @@ export type IFavorite = { userId: mongo.ObjectID; noteId: mongo.ObjectID; }; + +/** + * Favoriteを物理削除します + */ +export async function deleteFavorite(favorite: string | mongo.ObjectID | IFavorite) { + let f: IFavorite; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(favorite)) { + f = await Favorite.findOne({ + _id: favorite + }); + } else if (typeof favorite === 'string') { + f = await Favorite.findOne({ + _id: new mongo.ObjectID(favorite) + }); + } else { + f = favorite as IFavorite; + } + + if (f == null) return; + + // このFavoriteを削除 + await Favorite.remove({ + _id: f._id + }); +} diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts index d499442de9..9bf467f222 100644 --- a/src/models/note-reaction.ts +++ b/src/models/note-reaction.ts @@ -16,12 +16,35 @@ export interface INoteReaction { reaction: string; } +/** + * NoteReactionを物理削除します + */ +export async function deleteNoteReaction(noteReaction: string | mongo.ObjectID | INoteReaction) { + let n: INoteReaction; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(noteReaction)) { + n = await NoteReaction.findOne({ + _id: noteReaction + }); + } else if (typeof noteReaction === 'string') { + n = await NoteReaction.findOne({ + _id: new mongo.ObjectID(noteReaction) + }); + } else { + n = noteReaction as INoteReaction; + } + + if (n == null) return; + + // このNoteReactionを削除 + await NoteReaction.remove({ + _id: n._id + }); +} + /** * Pack a reaction for API response - * - * @param {any} reaction - * @param {any} me? - * @return {Promise} */ export const pack = ( reaction: any, diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts index b5ef3b61b7..479f92dd44 100644 --- a/src/models/note-watching.ts +++ b/src/models/note-watching.ts @@ -11,3 +11,30 @@ export interface INoteWatching { userId: mongo.ObjectID; noteId: mongo.ObjectID; } + +/** + * NoteWatchingを物理削除します + */ +export async function deleteNoteWatching(noteWatching: string | mongo.ObjectID | INoteWatching) { + let n: INoteWatching; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(noteWatching)) { + n = await NoteWatching.findOne({ + _id: noteWatching + }); + } else if (typeof noteWatching === 'string') { + n = await NoteWatching.findOne({ + _id: new mongo.ObjectID(noteWatching) + }); + } else { + n = noteWatching as INoteWatching; + } + + if (n == null) return; + + // このNoteWatchingを削除 + await NoteWatching.remove({ + _id: n._id + }); +} diff --git a/src/models/note.ts b/src/models/note.ts index a11da196cd..6e7b6cee79 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -6,8 +6,11 @@ import { IUser, pack as packUser } from './user'; import { pack as packApp } from './app'; import { pack as packChannel } from './channel'; import Vote from './poll-vote'; -import Reaction from './note-reaction'; +import Reaction, { deleteNoteReaction } from './note-reaction'; import { pack as packFile } from './drive-file'; +import NoteWatching, { deleteNoteWatching } from './note-watching'; +import NoteReaction from './note-reaction'; +import Favorite, { deleteFavorite } from './favorite'; const Note = db.get('notes'); @@ -69,8 +72,10 @@ export type INote = { }; }; -// TODO -export async function physicalDelete(note: string | mongo.ObjectID | INote) { +/** + * Noteを物理削除します + */ +export async function deleteNote(note: string | mongo.ObjectID | INote) { let n: INote; // Populate @@ -88,17 +93,35 @@ export async function physicalDelete(note: string | mongo.ObjectID | INote) { if (n == null) return; - // この投稿の返信をすべて削除 - const replies = await Note.find({ - replyId: n._id - }); - await Promise.all(replies.map(r => physicalDelete(r))); + // このNoteへの返信をすべて削除 + await Promise.all(( + await Note.find({ replyId: n._id }) + ).map(x => deleteNote(x))); - // この投稿のWatchをすべて削除 + // このNoteのRenoteをすべて削除 + await Promise.all(( + await Note.find({ renoteId: n._id }) + ).map(x => deleteNote(x))); - // この投稿のReactionをすべて削除 + // この投稿に対するNoteWatchingをすべて削除 + await Promise.all(( + await NoteWatching.find({ noteId: n._id }) + ).map(x => deleteNoteWatching(x))); + + // この投稿に対するNoteReactionをすべて削除 + await Promise.all(( + await NoteReaction.find({ noteId: n._id }) + ).map(x => deleteNoteReaction(x))); // この投稿に対するFavoriteをすべて削除 + await Promise.all(( + await Favorite.find({ noteId: n._id }) + ).map(x => deleteFavorite(x))); + + // このNoteを削除 + await Note.remove({ + _id: n._id + }); } /** diff --git a/src/models/user.ts b/src/models/user.ts index cdf9a564fc..b1a68b0827 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -2,11 +2,15 @@ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); import rap from '@prezzemolo/rap'; import db from '../db/mongodb'; -import Note, { INote, pack as packNote, physicalDelete as physicalDeleteNote } from './note'; +import Note, { INote, pack as packNote, deleteNote } from './note'; import Following from './following'; import Mute from './mute'; import getFriends from '../server/api/common/get-friends'; import config from '../config'; +import AccessToken, { deleteAccessToken } from './access-token'; +import NoteWatching, { deleteNoteWatching } from './note-watching'; +import Favorite, { deleteFavorite } from './favorite'; +import NoteReaction, { deleteNoteReaction } from './note-reaction'; const User = db.get('users'); @@ -122,8 +126,10 @@ export function init(user): IUser { return user; } -// TODO -export async function physicalDelete(user: string | mongo.ObjectID | IUser) { +/** + * Userを物理削除します + */ +export async function deleteUser(user: string | mongo.ObjectID | IUser) { let u: IUser; // Populate @@ -141,17 +147,40 @@ export async function physicalDelete(user: string | mongo.ObjectID | IUser) { if (u == null) return; - // このユーザーが行った投稿をすべて削除 - const notes = await Note.find({ userId: u._id }); - await Promise.all(notes.map(n => physicalDeleteNote(n))); + // このユーザーのAccessTokenをすべて削除 + await Promise.all(( + await AccessToken.find({ userId: u._id }) + ).map(x => deleteAccessToken(x))); - // このユーザーのお気に入りをすべて削除 + // このユーザーのNoteをすべて削除 + await Promise.all(( + await Note.find({ userId: u._id }) + ).map(x => deleteNote(x))); - // このユーザーが行ったメッセージをすべて削除 + // このユーザーのNoteReactionをすべて削除 + await Promise.all(( + await NoteReaction.find({ userId: u._id }) + ).map(x => deleteNoteReaction(x))); - // このユーザーのドライブのファイルをすべて削除 + // このユーザーのNoteWatchingをすべて削除 + await Promise.all(( + await NoteWatching.find({ userId: u._id }) + ).map(x => deleteNoteWatching(x))); - // このユーザーに関するfollowingをすべて削除 + // このユーザーのFavoriteをすべて削除 + await Promise.all(( + await Favorite.find({ userId: u._id }) + ).map(x => deleteFavorite(x))); + + // このユーザーのMessageをすべて削除 + + // このユーザーへのMessageをすべて削除 + + // このユーザーのDriveFileをすべて削除 + + // このユーザーのFollowingをすべて削除 + + // このユーザーへのFollowingをすべて削除 // このユーザーを削除 } From 92dd4b3e5a701823ebb5a453aa42202da8a326ea Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 04:05:03 +0900 Subject: [PATCH 07/34] wip --- src/models/messaging-history.ts | 27 +++++++++++++++++++++++ src/models/messaging-message.ts | 38 ++++++++++++++++++++++++++++----- src/models/user.ts | 13 +++++++++++ 3 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/models/messaging-history.ts b/src/models/messaging-history.ts index 6864e22d2f..5367f81412 100644 --- a/src/models/messaging-history.ts +++ b/src/models/messaging-history.ts @@ -11,3 +11,30 @@ export type IMessagingHistory = { partnerId: mongo.ObjectID; messageId: mongo.ObjectID; }; + +/** + * MessagingHistoryを物理削除します + */ +export async function deleteMessagingHistory(messagingHistory: string | mongo.ObjectID | IMessagingHistory) { + let m: IMessagingHistory; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(messagingHistory)) { + m = await MessagingHistory.findOne({ + _id: messagingHistory + }); + } else if (typeof messagingHistory === 'string') { + m = await MessagingHistory.findOne({ + _id: new mongo.ObjectID(messagingHistory) + }); + } else { + m = messagingHistory as IMessagingHistory; + } + + if (m == null) return; + + // このMessagingHistoryを削除 + await MessagingHistory.remove({ + _id: m._id + }); +} diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts index 974ee54ab8..9d62fab4fa 100644 --- a/src/models/messaging-message.ts +++ b/src/models/messaging-message.ts @@ -3,6 +3,7 @@ import deepcopy = require('deepcopy'); import { pack as packUser } from './user'; import { pack as packFile } from './drive-file'; import db from '../db/mongodb'; +import MessagingHistory, { deleteMessagingHistory } from './messaging-history'; const MessagingMessage = db.get('messagingMessages'); export default MessagingMessage; @@ -22,13 +23,40 @@ export function isValidText(text: string): boolean { return text.length <= 1000 && text.trim() != ''; } +/** + * MessagingMessageを物理削除します + */ +export async function deleteMessagingMessage(messagingMessage: string | mongo.ObjectID | IMessagingMessage) { + let m: IMessagingMessage; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(messagingMessage)) { + m = await MessagingMessage.findOne({ + _id: messagingMessage + }); + } else if (typeof messagingMessage === 'string') { + m = await MessagingMessage.findOne({ + _id: new mongo.ObjectID(messagingMessage) + }); + } else { + m = messagingMessage as IMessagingMessage; + } + + if (m == null) return; + + // このMessagingMessageを指すMessagingHistoryをすべて削除 + await Promise.all(( + await MessagingHistory.find({ messageId: m._id }) + ).map(x => deleteMessagingHistory(x))); + + // このMessagingMessageを削除 + await MessagingMessage.remove({ + _id: m._id + }); +} + /** * Pack a messaging message for API response - * - * @param {any} message - * @param {any} me? - * @param {any} options? - * @return {Promise} */ export const pack = ( message: any, diff --git a/src/models/user.ts b/src/models/user.ts index b1a68b0827..6155324be8 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -11,6 +11,8 @@ import AccessToken, { deleteAccessToken } from './access-token'; import NoteWatching, { deleteNoteWatching } from './note-watching'; import Favorite, { deleteFavorite } from './favorite'; import NoteReaction, { deleteNoteReaction } from './note-reaction'; +import MessagingMessage, { deleteMessagingMessage } from './messaging-message'; +import MessagingHistory, { deleteMessagingHistory } from './messaging-history'; const User = db.get('users'); @@ -173,8 +175,19 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { ).map(x => deleteFavorite(x))); // このユーザーのMessageをすべて削除 + await Promise.all(( + await MessagingMessage.find({ userId: u._id }) + ).map(x => deleteMessagingMessage(x))); // このユーザーへのMessageをすべて削除 + await Promise.all(( + await MessagingMessage.find({ recipientId: u._id }) + ).map(x => deleteMessagingMessage(x))); + + // このユーザーの関わるMessagingHistoryをすべて削除 + await Promise.all(( + await MessagingHistory.find({ $or: [{ partnerId: u._id }, { userId: u._id }] }) + ).map(x => deleteMessagingHistory(x))); // このユーザーのDriveFileをすべて削除 From 53415e9ba4bbee35b337afd97940e23eb4523d2c Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 04:22:06 +0900 Subject: [PATCH 08/34] wip --- src/models/drive-file.ts | 67 ++++++++++++++++++++++++++++++++------ src/models/drive-folder.ts | 49 +++++++++++++++++++++++++--- src/models/user.ts | 10 ++++++ 3 files changed, 112 insertions(+), 14 deletions(-) diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index c86570f0f7..ff31ba05dd 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -1,8 +1,11 @@ -import * as mongodb from 'mongodb'; +import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); import { pack as packFolder } from './drive-folder'; import config from '../config'; import monkDb, { nativeDbConn } from '../db/mongodb'; +import Note, { deleteNote } from './note'; +import MessagingMessage, { deleteMessagingMessage } from './messaging-message'; +import User from './user'; const DriveFile = monkDb.get('driveFiles.files'); @@ -10,9 +13,9 @@ DriveFile.createIndex('metadata.uri', { sparse: true, unique: true }); export default DriveFile; -const getGridFSBucket = async (): Promise => { +const getGridFSBucket = async (): Promise => { const db = await nativeDbConn(); - const bucket = new mongodb.GridFSBucket(db, { + const bucket = new mongo.GridFSBucket(db, { bucketName: 'driveFiles' }); return bucket; @@ -22,14 +25,14 @@ export { getGridFSBucket }; export type IMetadata = { properties: any; - userId: mongodb.ObjectID; - folderId: mongodb.ObjectID; + userId: mongo.ObjectID; + folderId: mongo.ObjectID; comment: string; uri: string; }; export type IDriveFile = { - _id: mongodb.ObjectID; + _id: mongo.ObjectID; uploadDate: Date; md5: string; filename: string; @@ -47,12 +50,56 @@ export function validateFileName(name: string): boolean { ); } +/** + * DriveFileを物理削除します + */ +export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriveFile) { + let d: IDriveFile; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(driveFile)) { + d = await DriveFile.findOne({ + _id: driveFile + }); + } else if (typeof driveFile === 'string') { + d = await DriveFile.findOne({ + _id: new mongo.ObjectID(driveFile) + }); + } else { + d = driveFile as IDriveFile; + } + + if (d == null) return; + + // このDriveFileを添付しているNoteをすべて削除 + await Promise.all(( + await Note.find({ mediaIds: d._id }) + ).map(x => deleteNote(x))); + + // このDriveFileを添付しているMessagingMessageをすべて削除 + await Promise.all(( + await MessagingMessage.find({ fileId: d._id }) + ).map(x => deleteMessagingMessage(x))); + + // このDriveFileがアバターやバナーに使われていたらそれらのプロパティをnullにする + const u = await User.findOne({ _id: d.metadata.userId }); + if (u) { + if (u.avatarId.equals(d._id)) { + await User.update({ _id: u._id }, { $set: { avatarId: null } }); + } + if (u.bannerId.equals(d._id)) { + await User.update({ _id: u._id }, { $set: { bannerId: null } }); + } + } + + // このDriveFileを削除 + await DriveFile.remove({ + _id: d._id + }); +} + /** * Pack a drive file for API response - * - * @param {any} file - * @param {any} options? - * @return {Promise} */ export const pack = ( file: any, diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts index 45cc9c9649..e7961936aa 100644 --- a/src/models/drive-folder.ts +++ b/src/models/drive-folder.ts @@ -21,12 +21,53 @@ export function isValidFolderName(name: string): boolean { ); } +/** + * DriveFolderを物理削除します + */ +export async function deleteDriveFolder(driveFolder: string | mongo.ObjectID | IDriveFolder) { + let d: IDriveFolder; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(driveFolder)) { + d = await DriveFolder.findOne({ + _id: driveFolder + }); + } else if (typeof driveFolder === 'string') { + d = await DriveFolder.findOne({ + _id: new mongo.ObjectID(driveFolder) + }); + } else { + d = driveFolder as IDriveFolder; + } + + if (d == null) return; + + // このDriveFolderに格納されているDriveFileがあればすべてルートに移動 + await DriveFile.update({ + 'metadata.folderId': d._id + }, { + $set: { + 'metadata.folderId': null + } + }); + + // このDriveFolderに格納されているDriveFolderがあればすべてルートに移動 + await DriveFolder.update({ + parentId: d._id + }, { + $set: { + parentId: null + } + }); + + // このDriveFolderを削除 + await DriveFolder.remove({ + _id: d._id + }); +} + /** * Pack a drive folder for API response - * - * @param {any} folder - * @param {any} options? - * @return {Promise} */ export const pack = ( folder: any, diff --git a/src/models/user.ts b/src/models/user.ts index 6155324be8..b56cf03ef8 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -13,6 +13,8 @@ import Favorite, { deleteFavorite } from './favorite'; import NoteReaction, { deleteNoteReaction } from './note-reaction'; import MessagingMessage, { deleteMessagingMessage } from './messaging-message'; import MessagingHistory, { deleteMessagingHistory } from './messaging-history'; +import DriveFile, { deleteDriveFile } from './drive-file'; +import DriveFolder, { deleteDriveFolder } from './drive-folder'; const User = db.get('users'); @@ -190,6 +192,14 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { ).map(x => deleteMessagingHistory(x))); // このユーザーのDriveFileをすべて削除 + await Promise.all(( + await DriveFile.find({ 'metadata.userId': u._id }) + ).map(x => deleteDriveFile(x))); + + // このユーザーのDriveFolderをすべて削除 + await Promise.all(( + await DriveFolder.find({ userId: u._id }) + ).map(x => deleteDriveFolder(x))); // このユーザーのFollowingをすべて削除 From b846eb8afe0376feacb567e86bf157b8fd5f4c36 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 05:50:45 +0900 Subject: [PATCH 09/34] wip --- src/models/mute.ts | 27 +++++++++++++++++++++++++++ src/models/user.ts | 16 +++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/models/mute.ts b/src/models/mute.ts index 8793615967..e068215c94 100644 --- a/src/models/mute.ts +++ b/src/models/mute.ts @@ -11,3 +11,30 @@ export interface IMute { muterId: mongo.ObjectID; muteeId: mongo.ObjectID; } + +/** + * Muteを物理削除します + */ +export async function deleteMute(mute: string | mongo.ObjectID | IMute) { + let m: IMute; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(mute)) { + m = await Mute.findOne({ + _id: mute + }); + } else if (typeof mute === 'string') { + m = await Mute.findOne({ + _id: new mongo.ObjectID(mute) + }); + } else { + m = mute as IMute; + } + + if (m == null) return; + + // このMuteを削除 + await Mute.remove({ + _id: m._id + }); +} diff --git a/src/models/user.ts b/src/models/user.ts index b56cf03ef8..ff1c11e76c 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -4,7 +4,7 @@ import rap from '@prezzemolo/rap'; import db from '../db/mongodb'; import Note, { INote, pack as packNote, deleteNote } from './note'; import Following from './following'; -import Mute from './mute'; +import Mute, { deleteMute } from './mute'; import getFriends from '../server/api/common/get-friends'; import config from '../config'; import AccessToken, { deleteAccessToken } from './access-token'; @@ -201,10 +201,24 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { await DriveFolder.find({ userId: u._id }) ).map(x => deleteDriveFolder(x))); + // このユーザーのMuteをすべて削除 + await Promise.all(( + await Mute.find({ muterId: u._id }) + ).map(x => deleteMute(x))); + + // このユーザーへのMuteをすべて削除 + await Promise.all(( + await Mute.find({ muteeId: u._id }) + ).map(x => deleteMute(x))); + // このユーザーのFollowingをすべて削除 // このユーザーへのFollowingをすべて削除 + // このユーザーのFollowingLogをすべて削除 + + // このユーザーのFollowedLogをすべて削除 + // このユーザーを削除 } From 0f994692435d7be4f94492294113db3b5dbc17a1 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 05:54:54 +0900 Subject: [PATCH 10/34] HSTS Co-Authored-By: tamaina --- src/server/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/server/index.ts b/src/server/index.ts index abb8992da5..9358cce2ac 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -41,6 +41,17 @@ app.use((req, res, next) => { next(); }); +/** + * HSTS + * 6month(15552000sec) + */ +if (config.url.startsWith('https')) { + app.use((req, res, next) => { + res.header('strict-transport-security', 'max-age=15552000; preload'); + next(); + }); +} + // Drop request when without 'Host' header app.use((req, res, next) => { if (!req.headers['host']) { From 70433469a16f6279390c8c0bdb3b9048ccfb3841 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 06:02:34 +0900 Subject: [PATCH 11/34] Clean up --- package.json | 1 - src/config/types.ts | 4 ---- src/server/index.ts | 11 ----------- 3 files changed, 16 deletions(-) diff --git a/package.json b/package.json index 048ef651de..d5233d54a6 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "@types/webpack-stream": "3.2.10", "@types/websocket": "0.0.38", "@types/ws": "4.0.2", - "accesses": "2.5.0", "animejs": "2.2.0", "autosize": "4.0.1", "autwh": "0.1.0", diff --git a/src/config/types.ts b/src/config/types.ts index f802e70d1e..b181f2c8c1 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -41,10 +41,6 @@ export type Source = { secret_key: string; }; accesslog?: string; - accesses?: { - enable: boolean; - port: number; - }; twitter?: { consumer_key: string; consumer_secret: string; diff --git a/src/server/index.ts b/src/server/index.ts index 9358cce2ac..962d3b5f4f 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -7,7 +7,6 @@ import * as http from 'http'; import * as https from 'https'; import * as express from 'express'; import * as morgan from 'morgan'; -import Accesses from 'accesses'; import activityPub from './activitypub'; import webFinger from './webfinger'; @@ -21,16 +20,6 @@ const app = express(); app.disable('x-powered-by'); app.set('trust proxy', 'loopback'); -// Log -if (config.accesses && config.accesses.enable) { - const accesses = new Accesses({ - appName: 'Misskey', - port: config.accesses.port - }); - - app.use(accesses.express); -} - app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', { // create a write stream (in append mode) stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null From cca9963a9ab17fa3f10dab845ea774c72101d1ba Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 06:18:55 +0900 Subject: [PATCH 12/34] :v: --- src/client/app/common/mios.ts | 6 ++++-- src/client/app/desktop/views/components/settings.vue | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts index 5e0c7d2f3b..a09af799be 100644 --- a/src/client/app/common/mios.ts +++ b/src/client/app/common/mios.ts @@ -444,7 +444,7 @@ export default class MiOS extends EventEmitter { // Append a credential if (this.isSignedIn) (data as any).i = this.i.token; - const viaStream = localStorage.getItem('enableExperimental') == 'true'; + const viaStream = localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true; return new Promise((resolve, reject) => { if (viaStream) { @@ -452,6 +452,8 @@ export default class MiOS extends EventEmitter { const id = Math.random().toString(); stream.once(`api-res:${id}`, res => { + if (--pending === 0) spinner.parentNode.removeChild(spinner); + if (res.res) { resolve(res.res); } else { @@ -503,7 +505,7 @@ export default class MiOS extends EventEmitter { reject(body.error); } }).catch(reject); - /*}*/ + } }); } diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 4184ae82c7..2b5aa30246 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -26,6 +26,12 @@ ウィンドウが開かれるとき、ポップアウト(ブラウザ外に切り離す)可能なら自動でポップアウトします。この設定はブラウザに記憶されます。 +
+ 詳細設定 + + この設定をオンにすると、websocket接続を経由してAPIリクエストが行われます(パフォーマンス向上が期待できます)。オフにすると、ネイティブの fetch APIが利用されます。この設定はこのデバイスのみ有効です。 + +
@@ -223,6 +229,7 @@ export default Vue.extend({ checkingForUpdate: false, enableSounds: localStorage.getItem('enableSounds') == 'true', autoPopout: localStorage.getItem('autoPopout') == 'true', + apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true, soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100, lang: localStorage.getItem('lang') || '', preventUpdate: localStorage.getItem('preventUpdate') == 'true', @@ -240,6 +247,9 @@ export default Vue.extend({ autoPopout() { localStorage.setItem('autoPopout', this.autoPopout ? 'true' : 'false'); }, + apiViaStream() { + localStorage.setItem('apiViaStream', this.apiViaStream ? 'true' : 'false'); + }, enableSounds() { localStorage.setItem('enableSounds', this.enableSounds ? 'true' : 'false'); }, From 3ffae40085da560c894e0951ec848c412816fcb7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 06:20:32 +0900 Subject: [PATCH 13/34] =?UTF-8?q?=E3=82=B5=E3=82=A6=E3=83=B3=E3=83=89?= =?UTF-8?q?=E3=81=AE=E3=83=9C=E3=83=AA=E3=83=A5=E3=83=BC=E3=83=A0=E3=81=AF?= =?UTF-8?q?=E3=83=87=E3=83=95=E3=82=A9=E3=83=AB=E3=83=88=E3=81=A750%?= =?UTF-8?q?=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/app/common/views/components/messaging-room.vue | 2 +- src/client/app/common/views/components/othello.game.vue | 4 ++-- src/client/app/desktop/views/components/settings.vue | 4 ++-- src/client/app/desktop/views/components/timeline.vue | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index d30c64d74a..e1b775c33a 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -151,7 +151,7 @@ export default Vue.extend({ // サウンドを再生する if ((this as any).os.isEnableSounds) { const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; sound.play(); } diff --git a/src/client/app/common/views/components/othello.game.vue b/src/client/app/common/views/components/othello.game.vue index b9d946de96..8c646cce07 100644 --- a/src/client/app/common/views/components/othello.game.vue +++ b/src/client/app/common/views/components/othello.game.vue @@ -164,7 +164,7 @@ export default Vue.extend({ // サウンドを再生する if ((this as any).os.isEnableSounds) { const sound = new Audio(`${url}/assets/othello-put-me.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; sound.play(); } @@ -188,7 +188,7 @@ export default Vue.extend({ // サウンドを再生する if ((this as any).os.isEnableSounds && x.color != this.myColor) { const sound = new Audio(`${url}/assets/othello-put-you.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; sound.play(); } }, diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 2b5aa30246..9d074165e5 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -230,7 +230,7 @@ export default Vue.extend({ enableSounds: localStorage.getItem('enableSounds') == 'true', autoPopout: localStorage.getItem('autoPopout') == 'true', apiViaStream: localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true, - soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 100, + soundVolume: localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) : 50, lang: localStorage.getItem('lang') || '', preventUpdate: localStorage.getItem('preventUpdate') == 'true', debug: localStorage.getItem('debug') == 'true', @@ -347,7 +347,7 @@ export default Vue.extend({ }, soundTest() { const sound = new Audio(`${url}/assets/message.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; sound.play(); } } diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index e1f88b62f3..f148e840ad 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -97,7 +97,7 @@ export default Vue.extend({ // サウンドを再生する if ((this as any).os.isEnableSounds) { const sound = new Audio(`${url}/assets/post.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 1; + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; sound.play(); } From a5ab80bf02330db30da7707409164edbb77c245e Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 12 Apr 2018 06:20:53 +0900 Subject: [PATCH 14/34] oops --- src/models/drive-file.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index ff31ba05dd..80fe8e0300 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -114,13 +114,13 @@ export const pack = ( let _file: any; // Populate the file if 'file' is ID - if (mongodb.ObjectID.prototype.isPrototypeOf(file)) { + if (mongo.ObjectID.prototype.isPrototypeOf(file)) { _file = await DriveFile.findOne({ _id: file }); } else if (typeof file === 'string') { _file = await DriveFile.findOne({ - _id: new mongodb.ObjectID(file) + _id: new mongo.ObjectID(file) }); } else { _file = deepcopy(file); From a015524cb5874dd33c884588dd2e35faa63ca08f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?= Date: Thu, 12 Apr 2018 07:13:15 +0900 Subject: [PATCH 15/34] wip --- src/models/following.ts | 27 +++++++++++++++++++++++++++ src/models/user.ts | 8 +++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/models/following.ts b/src/models/following.ts index b4090d8c7e..f10e349ee9 100644 --- a/src/models/following.ts +++ b/src/models/following.ts @@ -11,3 +11,30 @@ export type IFollowing = { followeeId: mongo.ObjectID; followerId: mongo.ObjectID; }; + +/** + * Followingを物理削除します + */ +export async function deleteFollowing(following: string | mongo.ObjectID | IFollowing) { + let f: IFollowing; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(following)) { + f = await Following.findOne({ + _id: following + }); + } else if (typeof following === 'string') { + f = await Following.findOne({ + _id: new mongo.ObjectID(following) + }); + } else { + f = following as IFollowing; + } + + if (f == null) return; + + // このFollowingを削除 + await Following.remove({ + _id: f._id + }); +} diff --git a/src/models/user.ts b/src/models/user.ts index ff1c11e76c..cbc445256b 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -3,7 +3,7 @@ import deepcopy = require('deepcopy'); import rap from '@prezzemolo/rap'; import db from '../db/mongodb'; import Note, { INote, pack as packNote, deleteNote } from './note'; -import Following from './following'; +import Following, { deleteFollowing } from './following'; import Mute, { deleteMute } from './mute'; import getFriends from '../server/api/common/get-friends'; import config from '../config'; @@ -212,8 +212,14 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { ).map(x => deleteMute(x))); // このユーザーのFollowingをすべて削除 + await Promise.all(( + await Following.find({ followerId: u._id }) + ).map(x => deleteFollowing(x))); // このユーザーへのFollowingをすべて削除 + await Promise.all(( + await Following.find({ followeeId: u._id }) + ).map(x => deleteFollowing(x))); // このユーザーのFollowingLogをすべて削除 From 991635f9190976433f9e03cef76eb36454145a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?= Date: Thu, 12 Apr 2018 07:19:28 +0900 Subject: [PATCH 16/34] wip --- src/models/note.ts | 8 +++++++- src/models/poll-vote.ts | 27 +++++++++++++++++++++++++++ src/models/user.ts | 6 ++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/models/note.ts b/src/models/note.ts index 6e7b6cee79..3c1c2e18e1 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -5,12 +5,13 @@ import db from '../db/mongodb'; import { IUser, pack as packUser } from './user'; import { pack as packApp } from './app'; import { pack as packChannel } from './channel'; -import Vote from './poll-vote'; +import Vote, { deletePollVote } from './poll-vote'; import Reaction, { deleteNoteReaction } from './note-reaction'; import { pack as packFile } from './drive-file'; import NoteWatching, { deleteNoteWatching } from './note-watching'; import NoteReaction from './note-reaction'; import Favorite, { deleteFavorite } from './favorite'; +import PollVote from './poll-vote'; const Note = db.get('notes'); @@ -113,6 +114,11 @@ export async function deleteNote(note: string | mongo.ObjectID | INote) { await NoteReaction.find({ noteId: n._id }) ).map(x => deleteNoteReaction(x))); + // この投稿に対するPollVoteをすべて削除 + await Promise.all(( + await PollVote.find({ noteId: n._id }) + ).map(x => deletePollVote(x))); + // この投稿に対するFavoriteをすべて削除 await Promise.all(( await Favorite.find({ noteId: n._id }) diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts index 4d33b100e7..85c8454ddc 100644 --- a/src/models/poll-vote.ts +++ b/src/models/poll-vote.ts @@ -11,3 +11,30 @@ export interface IPollVote { noteId: mongo.ObjectID; choice: number; } + +/** + * PollVoteを物理削除します + */ +export async function deletePollVote(pollVote: string | mongo.ObjectID | IPollVote) { + let p: IPollVote; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(pollVote)) { + p = await PollVote.findOne({ + _id: pollVote + }); + } else if (typeof pollVote === 'string') { + p = await PollVote.findOne({ + _id: new mongo.ObjectID(pollVote) + }); + } else { + p = pollVote as IPollVote; + } + + if (p == null) return; + + // このPollVoteを削除 + await PollVote.remove({ + _id: p._id + }); +} diff --git a/src/models/user.ts b/src/models/user.ts index cbc445256b..d7249c944d 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -15,6 +15,7 @@ import MessagingMessage, { deleteMessagingMessage } from './messaging-message'; import MessagingHistory, { deleteMessagingHistory } from './messaging-history'; import DriveFile, { deleteDriveFile } from './drive-file'; import DriveFolder, { deleteDriveFolder } from './drive-folder'; +import PollVote, { deletePollVote } from './poll-vote'; const User = db.get('users'); @@ -171,6 +172,11 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { await NoteWatching.find({ userId: u._id }) ).map(x => deleteNoteWatching(x))); + // このユーザーのPollVoteをすべて削除 + await Promise.all(( + await PollVote.find({ userId: u._id }) + ).map(x => deletePollVote(x))); + // このユーザーのFavoriteをすべて削除 await Promise.all(( await Favorite.find({ userId: u._id }) From 7b05e01819e214f8eadc64e8cca259f0af95157a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?= Date: Thu, 12 Apr 2018 07:25:46 +0900 Subject: [PATCH 17/34] wip --- src/models/followed-log.ts | 33 ++++++++++++++++++++++++++++++--- src/models/following-log.ts | 33 ++++++++++++++++++++++++++++++--- src/models/user.ts | 8 ++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/models/followed-log.ts b/src/models/followed-log.ts index 9e3ca17822..7d488b9cd3 100644 --- a/src/models/followed-log.ts +++ b/src/models/followed-log.ts @@ -1,12 +1,39 @@ -import { ObjectID } from 'mongodb'; +import * as mongo from 'mongodb'; import db from '../db/mongodb'; const FollowedLog = db.get('followedLogs'); export default FollowedLog; export type IFollowedLog = { - _id: ObjectID; + _id: mongo.ObjectID; createdAt: Date; - userId: ObjectID; + userId: mongo.ObjectID; count: number; }; + +/** + * FollowedLogを物理削除します + */ +export async function deleteFollowedLog(followedLog: string | mongo.ObjectID | IFollowedLog) { + let f: IFollowedLog; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(followedLog)) { + f = await FollowedLog.findOne({ + _id: followedLog + }); + } else if (typeof followedLog === 'string') { + f = await FollowedLog.findOne({ + _id: new mongo.ObjectID(followedLog) + }); + } else { + f = followedLog as IFollowedLog; + } + + if (f == null) return; + + // このFollowedLogを削除 + await FollowedLog.remove({ + _id: f._id + }); +} diff --git a/src/models/following-log.ts b/src/models/following-log.ts index 045ff7bf02..c06a337fd4 100644 --- a/src/models/following-log.ts +++ b/src/models/following-log.ts @@ -1,12 +1,39 @@ -import { ObjectID } from 'mongodb'; +import * as mongo from 'mongodb'; import db from '../db/mongodb'; const FollowingLog = db.get('followingLogs'); export default FollowingLog; export type IFollowingLog = { - _id: ObjectID; + _id: mongo.ObjectID; createdAt: Date; - userId: ObjectID; + userId: mongo.ObjectID; count: number; }; + +/** + * FollowingLogを物理削除します + */ +export async function deleteFollowingLog(followingLog: string | mongo.ObjectID | IFollowingLog) { + let f: IFollowingLog; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(followingLog)) { + f = await FollowingLog.findOne({ + _id: followingLog + }); + } else if (typeof followingLog === 'string') { + f = await FollowingLog.findOne({ + _id: new mongo.ObjectID(followingLog) + }); + } else { + f = followingLog as IFollowingLog; + } + + if (f == null) return; + + // このFollowingLogを削除 + await FollowingLog.remove({ + _id: f._id + }); +} diff --git a/src/models/user.ts b/src/models/user.ts index d7249c944d..a4b7becbd2 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -16,6 +16,8 @@ import MessagingHistory, { deleteMessagingHistory } from './messaging-history'; import DriveFile, { deleteDriveFile } from './drive-file'; import DriveFolder, { deleteDriveFolder } from './drive-folder'; import PollVote, { deletePollVote } from './poll-vote'; +import FollowingLog, { deleteFollowingLog } from './following-log'; +import FollowedLog, { deleteFollowedLog } from './followed-log'; const User = db.get('users'); @@ -228,8 +230,14 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { ).map(x => deleteFollowing(x))); // このユーザーのFollowingLogをすべて削除 + await Promise.all(( + await FollowingLog.find({ userId: u._id }) + ).map(x => deleteFollowingLog(x))); // このユーザーのFollowedLogをすべて削除 + await Promise.all(( + await FollowedLog.find({ userId: u._id }) + ).map(x => deleteFollowedLog(x))); // このユーザーを削除 } From 051ab451816edfb71f54d8aa887a13633cdda187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=93=E3=81=B4=E3=81=AA=E3=81=9F=E3=81=BF=E3=81=BD?= Date: Thu, 12 Apr 2018 07:32:35 +0900 Subject: [PATCH 18/34] wip --- src/models/sw-subscription.ts | 28 ++++++++++++++++++++++++++++ src/models/user.ts | 6 ++++++ 2 files changed, 34 insertions(+) diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts index 743d0d2dd9..621ac8a9b6 100644 --- a/src/models/sw-subscription.ts +++ b/src/models/sw-subscription.ts @@ -11,3 +11,31 @@ export interface ISwSubscription { auth: string; publickey: string; } + +/** + * SwSubscriptionを物理削除します + */ +export async function deleteSwSubscription(swSubscription: string | mongo.ObjectID | ISwSubscription) { + let s: ISwSubscription; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(swSubscription)) { + s = await SwSubscription.findOne({ + _id: swSubscription + }); + } else if (typeof swSubscription === 'string') { + s = await SwSubscription.findOne({ + _id: new mongo.ObjectID(swSubscription) + }); + } else { + s = swSubscription as ISwSubscription; + } + + if (s == null) return; + + // このSwSubscriptionを削除 + await SwSubscription.remove({ + _id: s._id + }); +} + diff --git a/src/models/user.ts b/src/models/user.ts index a4b7becbd2..c121790c31 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -18,6 +18,7 @@ import DriveFolder, { deleteDriveFolder } from './drive-folder'; import PollVote, { deletePollVote } from './poll-vote'; import FollowingLog, { deleteFollowingLog } from './following-log'; import FollowedLog, { deleteFollowedLog } from './followed-log'; +import SwSubscription, { deleteSwSubscription } from './sw-subscription'; const User = db.get('users'); @@ -239,6 +240,11 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { await FollowedLog.find({ userId: u._id }) ).map(x => deleteFollowedLog(x))); + // このユーザーのSwSubscriptionをすべて削除 + await Promise.all(( + await SwSubscription.find({ userId: u._id }) + ).map(x => deleteSwSubscription(x))); + // このユーザーを削除 } From ec3fdbc6db1f513d3abcc5df730dc1bacfc52c45 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 00:07:13 +0900 Subject: [PATCH 19/34] Add home customize link --- src/client/app/desktop/views/components/ui.header.account.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index ec4635f338..61c3019294 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -18,6 +18,9 @@
    +
  • + %fa:wrench%カスタマイズ%fa:angle-right% +
  • %fa:cog%%i18n:desktop.tags.mk-ui-header-account.settings%%fa:angle-right%

  • From a3bd4ba42693b1dc99ef586ef35f61dc53cdf9e9 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 00:51:55 +0900 Subject: [PATCH 20/34] wip --- package.json | 5 + src/server/activitypub.ts | 142 ++++++++++++++++++++++++++++ src/server/activitypub/inbox.ts | 32 ------- src/server/activitypub/index.ts | 18 ---- src/server/activitypub/note.ts | 28 ------ src/server/activitypub/outbox.ts | 28 ------ src/server/activitypub/publickey.ts | 23 ----- src/server/activitypub/user.ts | 19 ---- src/server/index.ts | 69 ++++---------- src/server/log-request.ts | 21 ---- src/server/webfinger.ts | 32 ++++--- 11 files changed, 188 insertions(+), 229 deletions(-) create mode 100644 src/server/activitypub.ts delete mode 100644 src/server/activitypub/inbox.ts delete mode 100644 src/server/activitypub/index.ts delete mode 100644 src/server/activitypub/note.ts delete mode 100644 src/server/activitypub/outbox.ts delete mode 100644 src/server/activitypub/publickey.ts delete mode 100644 src/server/activitypub/user.ts delete mode 100644 src/server/log-request.ts diff --git a/package.json b/package.json index d5233d54a6..e5180fddbd 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,9 @@ "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", "@types/js-yaml": "3.11.1", + "@types/koa": "^2.0.45", + "@types/koa-bodyparser": "^4.2.0", + "@types/koa-router": "^7.0.27", "@types/kue": "^0.11.8", "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", @@ -140,6 +143,8 @@ "is-url": "1.2.4", "js-yaml": "3.11.0", "jsdom": "11.7.0", + "koa": "^2.5.0", + "koa-router": "^7.4.0", "kue": "0.11.6", "license-checker": "18.0.0", "loader-utils": "1.1.0", diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts new file mode 100644 index 0000000000..ed0311af9d --- /dev/null +++ b/src/server/activitypub.ts @@ -0,0 +1,142 @@ +import * as Router from 'koa-router'; +import { parseRequest } from 'http-signature'; + +import { createHttp } from '../queue'; +import context from '../remote/activitypub/renderer/context'; +import render from '../remote/activitypub/renderer/note'; +import Note from '../models/note'; +import User, { isLocalUser } from '../models/user'; +import renderNote from '../remote/activitypub/renderer/note'; +import renderKey from '../remote/activitypub/renderer/key'; +import renderPerson from '../remote/activitypub/renderer/person'; +import renderOrderedCollection from '../remote/activitypub/renderer/ordered-collection'; +//import parseAcct from '../acct/parse'; +import config from '../config'; + +// Init router +const router = new Router(); + +//#region Routing + +// inbox +router.post('/users/:user/inbox', ctx => { + let signature; + + ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; + + try { + signature = parseRequest(ctx.req); + } catch (e) { + ctx.status = 401; + return; + } + + createHttp({ + type: 'processInbox', + activity: ctx.request.body, + signature + }).save(); + + ctx.status = 202; +}); + +// note +router.get('/notes/:note', async (ctx, next) => { + const accepted = ctx.accepts('html', 'application/activity+json', 'application/ld+json'); + if (!['application/activity+json', 'application/ld+json'].includes(accepted as string)) { + next(); + return; + } + + const note = await Note.findOne({ + _id: ctx.params.note + }); + + if (note === null) { + ctx.status = 404; + return; + } + + const rendered = await render(note); + rendered['@context'] = context; + + ctx.body = rendered; +}); + +// outbot +router.get('/users/:user/outbox', async ctx => { + const userId = ctx.params.user; + + const user = await User.findOne({ _id: userId }); + + if (user === null) { + ctx.status = 404; + return; + } + + const notes = await Note.find({ userId: user._id }, { + limit: 10, + sort: { _id: -1 } + }); + + const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); + const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); + rendered['@context'] = context; + + ctx.body = rendered; +}); + +// publickey +router.get('/users/:user/publickey', async ctx => { + const userId = ctx.params.user; + + const user = await User.findOne({ _id: userId }); + + if (user === null) { + ctx.status = 404; + return; + } + + if (isLocalUser(user)) { + const rendered = renderKey(user); + rendered['@context'] = context; + + ctx.body = rendered; + } else { + ctx.status = 400; + } +}); + +// user +router.get('/users/:user', async ctx => { + const userId = ctx.params.user; + + const user = await User.findOne({ _id: userId }); + + if (user === null) { + ctx.status = 404; + return; + } + + const rendered = renderPerson(user); + rendered['@context'] = context; + + ctx.body = rendered; +}); + +// follow form +router.get('/authorize-follow', async ctx => { + /* TODO + const { username, host } = parseAcct(ctx.query.acct); + if (host === null) { + res.sendStatus(422); + return; + } + + const finger = await request(`https://${host}`) + */ +}); + +//#endregion + +export default router; diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts deleted file mode 100644 index 643d2945bd..0000000000 --- a/src/server/activitypub/inbox.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as bodyParser from 'body-parser'; -import * as express from 'express'; -import { parseRequest } from 'http-signature'; -import { createHttp } from '../../queue'; - -const app = express.Router(); - -app.post('/users/:user/inbox', bodyParser.json({ - type() { - return true; - } -}), async (req, res) => { - let signature; - - req.headers.authorization = 'Signature ' + req.headers.signature; - - try { - signature = parseRequest(req); - } catch (exception) { - return res.sendStatus(401); - } - - createHttp({ - type: 'processInbox', - activity: req.body, - signature, - }).save(); - - return res.status(202).end(); -}); - -export default app; diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts deleted file mode 100644 index 042579db9d..0000000000 --- a/src/server/activitypub/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as express from 'express'; - -import user from './user'; -import inbox from './inbox'; -import outbox from './outbox'; -import publicKey from './publickey'; -import note from './note'; - -const app = express(); -app.disable('x-powered-by'); - -app.use(user); -app.use(inbox); -app.use(outbox); -app.use(publicKey); -app.use(note); - -export default app; diff --git a/src/server/activitypub/note.ts b/src/server/activitypub/note.ts deleted file mode 100644 index 1c2e695b80..0000000000 --- a/src/server/activitypub/note.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/note'; -import Note from '../../models/note'; - -const app = express.Router(); - -app.get('/notes/:note', async (req, res, next) => { - const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); - if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { - return next(); - } - - const note = await Note.findOne({ - _id: req.params.note - }); - - if (note === null) { - return res.sendStatus(404); - } - - const rendered = await render(note); - rendered['@context'] = context; - - res.json(rendered); -}); - -export default app; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts deleted file mode 100644 index 1c97c17a2e..0000000000 --- a/src/server/activitypub/outbox.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import renderNote from '../../remote/activitypub/renderer/note'; -import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; -import config from '../../config'; -import Note from '../../models/note'; -import User from '../../models/user'; - -const app = express.Router(); - -app.get('/users/:user/outbox', async (req, res) => { - const userId = req.params.user; - - const user = await User.findOne({ _id: userId }); - - const notes = await Note.find({ userId: user._id }, { - limit: 20, - sort: { _id: -1 } - }); - - const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); - const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); - rendered['@context'] = context; - - res.json(rendered); -}); - -export default app; diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts deleted file mode 100644 index e874b82729..0000000000 --- a/src/server/activitypub/publickey.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/key'; -import User, { isLocalUser } from '../../models/user'; - -const app = express.Router(); - -app.get('/users/:user/publickey', async (req, res) => { - const userId = req.params.user; - - const user = await User.findOne({ _id: userId }); - - if (isLocalUser(user)) { - const rendered = render(user); - rendered['@context'] = context; - - res.json(rendered); - } else { - res.sendStatus(400); - } -}); - -export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts deleted file mode 100644 index 9e98e92b6a..0000000000 --- a/src/server/activitypub/user.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as express from 'express'; -import context from '../../remote/activitypub/renderer/context'; -import render from '../../remote/activitypub/renderer/person'; -import User from '../../models/user'; - -const app = express.Router(); - -app.get('/users/:user', async (req, res) => { - const userId = req.params.user; - - const user = await User.findOne({ _id: userId }); - - const rendered = render(user); - rendered['@context'] = context; - - res.json(rendered); -}); - -export default app; diff --git a/src/server/index.ts b/src/server/index.ts index 962d3b5f4f..e9bfa9e10b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,67 +5,40 @@ import * as fs from 'fs'; import * as http from 'http'; import * as https from 'https'; -import * as express from 'express'; -import * as morgan from 'morgan'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as bodyParser from 'koa-bodyparser'; import activityPub from './activitypub'; import webFinger from './webfinger'; -import log from './log-request'; import config from '../config'; -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); -app.set('trust proxy', 'loopback'); +// Init server +const app = new Koa(); +app.proxy = true; +app.use(bodyParser); -app.use(morgan(process.env.NODE_ENV == 'production' ? 'combined' : 'dev', { - // create a write stream (in append mode) - stream: config.accesslog ? fs.createWriteStream(config.accesslog) : null -})); - -app.use((req, res, next) => { - log(req); - next(); -}); - -/** - * HSTS - * 6month(15552000sec) - */ +// HSTS +// 6months (15552000sec) if (config.url.startsWith('https')) { - app.use((req, res, next) => { - res.header('strict-transport-security', 'max-age=15552000; preload'); + app.use((ctx, next) => { + ctx.set('strict-transport-security', 'max-age=15552000; preload'); next(); }); } -// Drop request when without 'Host' header -app.use((req, res, next) => { - if (!req.headers['host']) { - res.sendStatus(400); - } else { - next(); - } -}); +// Init router +const router = new Router(); -// 互換性のため -app.post('/meta', (req, res) => { - res.header('Access-Control-Allow-Origin', '*'); - res.json({ - version: 'nighthike' - }); -}); +// Routing +router.use('/api', require('./api')); +router.use('/files', require('./file')); +router.use(activityPub.routes()); +router.use(webFinger.routes()); +router.use(require('./web')); -/** - * Register modules - */ -app.use('/api', require('./api')); -app.use('/files', require('./file')); -app.use(activityPub); -app.use(webFinger); -app.use(require('./web')); +// Register router +app.use(router.routes()); function createServer() { if (config.https) { diff --git a/src/server/log-request.ts b/src/server/log-request.ts deleted file mode 100644 index e431aa271d..0000000000 --- a/src/server/log-request.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as crypto from 'crypto'; -import * as express from 'express'; -import * as proxyAddr from 'proxy-addr'; -import Xev from 'xev'; - -const ev = new Xev(); - -export default function(req: express.Request) { - const ip = proxyAddr(req, () => true); - - const md5 = crypto.createHash('md5'); - md5.update(ip); - const hashedIp = md5.digest('hex').substr(0, 3); - - ev.emit('request', { - ip: hashedIp, - method: req.method, - hostname: req.hostname, - path: req.originalUrl - }); -} diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts index dbf0999f3e..e72592351b 100644 --- a/src/server/webfinger.ts +++ b/src/server/webfinger.ts @@ -1,17 +1,19 @@ -import * as express from 'express'; +import * as Router from 'koa-router'; import config from '../config'; import parseAcct from '../acct/parse'; import User from '../models/user'; -const app = express.Router(); +// Init router +const router = new Router(); -app.get('/.well-known/webfinger', async (req, res) => { - if (typeof req.query.resource !== 'string') { - return res.sendStatus(400); +router.get('/.well-known/webfinger', async ctx => { + if (typeof ctx.query.resource !== 'string') { + ctx.status = 400; + return; } - const resourceLower = req.query.resource.toLowerCase(); + const resourceLower = ctx.query.resource.toLowerCase(); const webPrefix = config.url.toLowerCase() + '/@'; let acctLower; @@ -25,15 +27,21 @@ app.get('/.well-known/webfinger', async (req, res) => { const parsedAcctLower = parseAcct(acctLower); if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) { - return res.sendStatus(422); + ctx.status = 422; + return; } - const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null }); + const user = await User.findOne({ + usernameLower: parsedAcctLower.username, + host: null + }); + if (user === null) { - return res.sendStatus(404); + ctx.status = 404; + return; } - return res.json({ + ctx.body = { subject: `acct:${user.username}@${config.host}`, links: [{ rel: 'self', @@ -47,7 +55,7 @@ app.get('/.well-known/webfinger', async (req, res) => { rel: 'http://ostatus.org/schema/1.0/subscribe', template: `${config.url}/authorize-follow?acct={uri}` }] - }); + }; }); -export default app; +export default router; From 3368fe855249f45bdf1e4c1e509d325d44e80fbe Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 06:06:18 +0900 Subject: [PATCH 21/34] wip --- package.json | 10 + src/client/app/boot.js | 2 + src/client/assets/404.js | 25 --- src/index.ts | 6 - src/server/api/api-handler.ts | 17 +- src/server/api/bot/interfaces/line.ts | 117 ++++++----- src/server/api/call.ts | 7 +- src/server/api/common/signin.ts | 17 +- src/server/api/index.ts | 56 +++--- src/server/api/private/signin.ts | 36 ++-- src/server/api/private/signup.ts | 20 +- src/server/api/service/github.ts | 266 ++++++++++++++------------ src/server/api/service/twitter.ts | 181 +++++++++--------- src/server/file/index.ts | 172 ++--------------- src/server/file/pour.ts | 93 +++++++++ src/server/file/send-drive-file.ts | 30 +++ src/server/index.ts | 6 +- src/server/web/docs.ts | 25 ++- src/server/web/index.ts | 97 +++++----- src/server/web/url-preview.ts | 8 +- 20 files changed, 582 insertions(+), 609 deletions(-) delete mode 100644 src/client/assets/404.js create mode 100644 src/server/file/pour.ts create mode 100644 src/server/file/send-drive-file.ts diff --git a/package.json b/package.json index e5180fddbd..3e349203fc 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@fortawesome/fontawesome-free-brands": "5.0.2", "@fortawesome/fontawesome-free-regular": "5.0.2", "@fortawesome/fontawesome-free-solid": "5.0.2", + "@koa/cors": "^2.2.1", "@prezzemolo/rap": "0.1.2", "@prezzemolo/zip": "0.0.3", "@types/bcryptjs": "2.4.1", @@ -58,7 +59,12 @@ "@types/js-yaml": "3.11.1", "@types/koa": "^2.0.45", "@types/koa-bodyparser": "^4.2.0", + "@types/koa-favicon": "^2.0.19", + "@types/koa-mount": "^3.0.1", + "@types/koa-multer": "^1.0.0", "@types/koa-router": "^7.0.27", + "@types/koa-send": "^4.1.1", + "@types/koa__cors": "^2.2.2", "@types/kue": "^0.11.8", "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", @@ -144,7 +150,11 @@ "js-yaml": "3.11.0", "jsdom": "11.7.0", "koa": "^2.5.0", + "koa-favicon": "^2.0.1", + "koa-mount": "^3.0.0", + "koa-multer": "^1.0.2", "koa-router": "^7.4.0", + "koa-send": "^4.1.3", "kue": "0.11.6", "license-checker": "18.0.0", "loader-utils": "1.1.0", diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 0846e4bd55..ef828d9637 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -97,6 +97,8 @@ // Compare versions if (meta.version != ver) { + localStorage.setItem('v', meta.version); + alert( 'Misskeyの新しいバージョンがあります。ページを再度読み込みします。' + '\n\n' + diff --git a/src/client/assets/404.js b/src/client/assets/404.js deleted file mode 100644 index 9e498fe7c2..0000000000 --- a/src/client/assets/404.js +++ /dev/null @@ -1,25 +0,0 @@ -const yn = window.confirm( - 'サーバー上に存在しないスクリプトがリクエストされました。お使いのMisskeyのバージョンが古いことが原因の可能性があります。Misskeyを更新しますか?\n\nA script that does not exist on the server was requested. It may be caused by an old version of Misskey you’re using. Do you want to delete the cache?'); - -const langYn = window.confirm('また、言語を日本語に設定すると解決する場合があります。日本語に設定しますか?\n\nAlso, setting the language to Japanese may solve the problem. Would you like to set it to Japanese?'); - -if (langYn) { - localStorage.setItem('lang', 'ja'); -} - -if (yn) { - // Clear cache (serive worker) - try { - navigator.serviceWorker.controller.postMessage('clear'); - - navigator.serviceWorker.getRegistrations().then(registrations => { - registrations.forEach(registration => registration.unregister()); - }); - } catch (e) { - console.error(e); - } - - localStorage.removeItem('v'); - - location.reload(true); -} diff --git a/src/index.ts b/src/index.ts index 68b289793b..d633fcbbcb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,6 @@ import * as debug from 'debug'; import chalk from 'chalk'; // import portUsed = require('tcp-port-used'); import isRoot = require('is-root'); -import { master } from 'accesses'; import Xev from 'xev'; import Logger from './utils/logger'; @@ -73,11 +72,6 @@ async function masterMain(opt) { Logger.info(chalk.green('Successfully initialized :)')); - // Init accesses - if (config.accesses && config.accesses.enable) { - master(); - } - spawnWorkers(() => { if (!opt['only-processor']) { Logger.info(chalk.bold.green( diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 409069b6a0..2c50234317 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -1,4 +1,4 @@ -import * as express from 'express'; +import * as Koa from 'koa'; import { Endpoint } from './endpoints'; import authenticate from './authenticate'; @@ -6,16 +6,17 @@ import call from './call'; import { IUser } from '../../models/user'; import { IApp } from '../../models/app'; -export default async (endpoint: Endpoint, req: express.Request, res: express.Response) => { +export default async (endpoint: Endpoint, ctx: Koa.Context) => { const reply = (x?: any, y?: any) => { if (x === undefined) { - res.sendStatus(204); + ctx.status = 204; } else if (typeof x === 'number') { - res.status(x).send({ + ctx.status = x; + ctx.body = { error: x === 500 ? 'INTERNAL_ERROR' : y - }); + }; } else { - res.send(x); + ctx.body = x; } }; @@ -24,11 +25,11 @@ export default async (endpoint: Endpoint, req: express.Request, res: express.Res // Authentication try { - [user, app] = await authenticate(req.body['i']); + [user, app] = await authenticate(ctx.body['i']); } catch (e) { return reply(403, 'AUTHENTICATION_FAILED'); } // API invoking - call(endpoint, user, app, req.body, req).then(reply).catch(e => reply(400, e)); + call(endpoint, user, app, ctx.body, ctx.req).then(reply).catch(e => reply(400, e)); }; diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index be3bfe33d3..454630161a 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -1,5 +1,5 @@ import * as EventEmitter from 'events'; -import * as express from 'express'; +import * as Router from 'koa-router'; import * as request from 'request'; import * as crypto from 'crypto'; import User from '../../../../models/user'; @@ -158,82 +158,81 @@ class LineBot extends BotCore { } } -module.exports = async (app: express.Application) => { - if (config.line_bot == null) return; +const handler = new EventEmitter(); - const handler = new EventEmitter(); +handler.on('event', async (ev) => { - handler.on('event', async (ev) => { + const sourceId = ev.source.userId; + const sessionId = `line-bot-sessions:${sourceId}`; - const sourceId = ev.source.userId; - const sessionId = `line-bot-sessions:${sourceId}`; + const session = await redis.get(sessionId); + let bot: LineBot; - const session = await redis.get(sessionId); - let bot: LineBot; - - if (session == null) { - const user = await User.findOne({ - host: null, - 'line': { - userId: sourceId - } - }); - - bot = new LineBot(user); - - bot.on('signin', user => { - User.update(user._id, { - $set: { - 'line': { - userId: sourceId - } - } - }); - }); - - bot.on('signout', user => { - User.update(user._id, { - $set: { - 'line': { - userId: null - } - } - }); - }); - - redis.set(sessionId, JSON.stringify(bot.export())); - } else { - bot = LineBot.import(JSON.parse(session)); - } - - bot.on('updated', () => { - redis.set(sessionId, JSON.stringify(bot.export())); + if (session == null) { + const user = await User.findOne({ + host: null, + 'line': { + userId: sourceId + } }); - if (session != null) bot.refreshUser(); + bot = new LineBot(user); - bot.react(ev); + bot.on('signin', user => { + User.update(user._id, { + $set: { + 'line': { + userId: sourceId + } + } + }); + }); + + bot.on('signout', user => { + User.update(user._id, { + $set: { + 'line': { + userId: null + } + } + }); + }); + + redis.set(sessionId, JSON.stringify(bot.export())); + } else { + bot = LineBot.import(JSON.parse(session)); + } + + bot.on('updated', () => { + redis.set(sessionId, JSON.stringify(bot.export())); }); - app.post('/hooks/line', (req, res, next) => { - // req.headers['x-line-signature'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - const sig1 = req.headers['x-line-signature'] as string; + if (session != null) bot.refreshUser(); + + bot.react(ev); +}); + +// Init router +const router = new Router(); + +if (config.line_bot) { + router.post('/hooks/line', ctx => { + const sig1 = ctx.headers['x-line-signature']; const hash = crypto.createHmac('SHA256', config.line_bot.channel_secret) - .update((req as any).rawBody); + .update(ctx.request.rawBody); const sig2 = hash.digest('base64'); // シグネチャ比較 if (sig1 === sig2) { - req.body.events.forEach(ev => { + ctx.body.events.forEach(ev => { handler.emit('event', ev); }); - - res.sendStatus(200); } else { - res.sendStatus(400); + ctx.status = 400; } }); -}; +} + +module.exports = router; diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 1bfe94bb74..c25f55ed3f 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,11 +1,12 @@ -import * as express from 'express'; +import * as http from 'http'; +import * as multer from 'koa-multer'; import endpoints, { Endpoint } from './endpoints'; import limitter from './limitter'; import { IUser } from '../../models/user'; import { IApp } from '../../models/app'; -export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: express.Request) => new Promise(async (ok, rej) => { +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => { const isSecure = user != null && app == null; //console.log(endpoint, user, app, data); @@ -38,7 +39,7 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, let exec = require(`${__dirname}/endpoints/${ep.name}`); if (ep.withFile && req) { - exec = exec.bind(null, req.file); + exec = exec.bind(null, (req as multer.MulterIncomingMessage).file); } let res; diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index 8bb327694d..f57c38414c 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -1,19 +1,20 @@ -import config from '../../../config'; +import * as Koa from 'koa'; -export default function(res, user, redirect: boolean) { +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) { const expires = 1000 * 60 * 60 * 24 * 365; // One Year - res.cookie('i', user.token, { + ctx.cookies.set('i', user.token, { path: '/', - domain: `.${config.hostname}`, - secure: config.url.substr(0, 5) === 'https', + domain: config.hostname, + secure: config.url.startsWith('https'), httpOnly: false, expires: new Date(Date.now() + expires), maxAge: expires }); if (redirect) { - res.redirect(config.url); - } else { - res.sendStatus(204); + ctx.redirect(config.url); } } diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 5fbacd8a0e..d2427d30ae 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -2,53 +2,41 @@ * API Server */ -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as cors from 'cors'; -import * as multer from 'multer'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as multer from 'koa-multer'; import endpoints from './endpoints'; -/** - * Init app - */ -const app = express(); +const handler = require('./api-handler').default; -app.disable('x-powered-by'); -app.set('etag', false); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ - type: ['application/json', 'text/plain'], - verify: (req, res, buf, encoding) => { - if (buf && buf.length) { - (req as any).rawBody = buf.toString(encoding || 'utf8'); - } - } -})); -app.use(cors()); +// Init app +const app = new Koa(); -app.get('/', (req, res) => { - res.send('YEE HAW'); +// Init multer instance +const upload = multer({ + storage: multer.diskStorage({}) }); +// Init router +const router = new Router(); + /** * Register endpoint handlers */ -endpoints.forEach(endpoint => - endpoint.withFile ? - app.post(`/${endpoint.name}`, - endpoint.withFile ? multer({ storage: multer.diskStorage({}) }).single('file') : null, - require('./api-handler').default.bind(null, endpoint)) : - app.post(`/${endpoint.name}`, - require('./api-handler').default.bind(null, endpoint)) +endpoints.forEach(endpoint => endpoint.withFile + ? router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint)) + : router.post(`/${endpoint.name}`, handler.bind(null, endpoint)) ); -app.post('/signup', require('./private/signup').default); -app.post('/signin', require('./private/signin').default); +router.post('/signup', require('./private/signup').default); +router.post('/signin', require('./private/signin').default); -require('./service/github')(app); -require('./service/twitter')(app); +router.use(require('./service/github').routes()); +router.use(require('./service/twitter').routes()); +router.use(require('./bot/interfaces/line').routes()); -require('./bot/interfaces/line')(app); +// Register router +app.use(router.routes()); module.exports = app; diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 665ee21ebd..55326deeaf 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -1,4 +1,4 @@ -import * as express from 'express'; +import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import User, { ILocalUser } from '../../../models/user'; @@ -7,26 +7,26 @@ import event from '../../../publishers/stream'; import signin from '../common/signin'; import config from '../../../config'; -export default async (req: express.Request, res: express.Response) => { - res.header('Access-Control-Allow-Origin', config.url); - res.header('Access-Control-Allow-Credentials', 'true'); +export default async (ctx: Koa.Context) => { + ctx.set('Access-Control-Allow-Origin', config.url); + ctx.set('Access-Control-Allow-Credentials', 'true'); - const username = req.body['username']; - const password = req.body['password']; - const token = req.body['token']; + const username = ctx.body['username']; + const password = ctx.body['password']; + const token = ctx.body['token']; if (typeof username != 'string') { - res.sendStatus(400); + ctx.status = 400; return; } if (typeof password != 'string') { - res.sendStatus(400); + ctx.status = 400; return; } if (token != null && typeof token != 'string') { - res.sendStatus(400); + ctx.status = 400; return; } @@ -37,12 +37,12 @@ export default async (req: express.Request, res: express.Response) => { }, { fields: { data: false, - 'profile': false + profile: false } }) as ILocalUser; if (user === null) { - res.status(404).send({ + ctx.throw(404, { error: 'user not found' }); return; @@ -60,17 +60,17 @@ export default async (req: express.Request, res: express.Response) => { }); if (verified) { - signin(res, user, false); + signin(ctx, user, false); } else { - res.status(400).send({ + ctx.throw(400, { error: 'invalid token' }); } } else { - signin(res, user, false); + signin(ctx, user, false); } } else { - res.status(400).send({ + ctx.throw(400, { error: 'incorrect password' }); } @@ -79,8 +79,8 @@ export default async (req: express.Request, res: express.Response) => { const record = await Signin.insert({ createdAt: new Date(), userId: user._id, - ip: req.ip, - headers: req.headers, + ip: ctx.ip, + headers: ctx.headers, success: same }); diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index f441e1b754..a4554be4ae 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,5 +1,5 @@ import * as uuid from 'uuid'; -import * as express from 'express'; +import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; import { generate as generateKeypair } from '../../../crypto_key'; import recaptcha = require('recaptcha-promise'); @@ -33,30 +33,30 @@ const home = { ] }; -export default async (req: express.Request, res: express.Response) => { +export default async (ctx: Koa.Context) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - const success = await recaptcha(req.body['g-recaptcha-response']); + const success = await recaptcha(ctx.body['g-recaptcha-response']); if (!success) { - res.status(400).send('recaptcha-failed'); + ctx.throw(400, 'recaptcha-failed'); return; } } - const username = req.body['username']; - const password = req.body['password']; + const username = ctx.body['username']; + const password = ctx.body['password']; // Validate username if (!validateUsername(username)) { - res.sendStatus(400); + ctx.status = 400; return; } // Validate password if (!validatePassword(password)) { - res.sendStatus(400); + ctx.status = 400; return; } @@ -71,7 +71,7 @@ export default async (req: express.Request, res: express.Response) => { // Check username already used if (usernameExist !== 0) { - res.sendStatus(400); + ctx.status = 400; return; } @@ -143,5 +143,5 @@ export default async (req: express.Request, res: express.Response) => { }); // Response - res.send(await pack(account)); + ctx.body = await pack(account); }; diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index bc8d3c6a7d..ee226cc5cc 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -1,140 +1,152 @@ import * as EventEmitter from 'events'; -import * as express from 'express'; +import * as Router from 'koa-router'; import * as request from 'request'; const crypto = require('crypto'); -import User from '../../../models/user'; +import User, { IUser } from '../../../models/user'; import createNote from '../../../services/note/create'; import config from '../../../config'; -module.exports = async (app: express.Application) => { - if (config.github_bot == null) return; +const handler = new EventEmitter(); - const bot = await User.findOne({ - usernameLower: config.github_bot.username.toLowerCase() - }); +let bot: IUser; +const post = async text => { if (bot == null) { - console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); - return; + const account = await User.findOne({ + usernameLower: config.github_bot.username.toLowerCase() + }); + + if (account == null) { + console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); + return; + } else { + bot = account; + } } - const post = text => createNote(bot, { text }); - - const handler = new EventEmitter(); - - app.post('/hooks/github', (req, res, next) => { - // req.headers['x-hub-signature'] および - // req.headers['x-github-event'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) { - handler.emit(req.headers['x-github-event'] as string, req.body); - res.sendStatus(200); - } else { - res.sendStatus(400); - } - }); - - handler.on('status', event => { - const state = event.state; - switch (state) { - case 'error': - case 'failure': - const commit = event.commit; - const parent = commit.parents[0]; - - // Fetch parent status - request({ - url: `${parent.url}/statuses`, - headers: { - 'User-Agent': 'misskey' - } - }, (err, res, body) => { - if (err) { - console.error(err); - return; - } - const parentStatuses = JSON.parse(body); - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - if (stillFailed) { - post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); - } else { - post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); - } - }); - break; - } - }); - - handler.on('push', event => { - const ref = event.ref; - switch (ref) { - case 'refs/heads/master': - const pusher = event.pusher; - const compare = event.compare; - const commits = event.commits; - post([ - `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, - commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), - ].join('\n')); - break; - case 'refs/heads/release': - const commit = event.commits[0]; - post(`RELEASED: ${commit.message}`); - break; - } - }); - - handler.on('issues', event => { - const issue = event.issue; - const action = event.action; - let title: string; - switch (action) { - case 'opened': title = 'Issue opened'; break; - case 'closed': title = 'Issue closed'; break; - case 'reopened': title = 'Issue reopened'; break; - default: return; - } - post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); - }); - - handler.on('issue_comment', event => { - const issue = event.issue; - const comment = event.comment; - const action = event.action; - let text: string; - switch (action) { - case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; - default: return; - } - post(text); - }); - - handler.on('watch', event => { - const sender = event.sender; - post(`⭐️ Starred by **${sender.login}** ⭐️`); - }); - - handler.on('fork', event => { - const repo = event.forkee; - post(`🍴 Forked:\n${repo.html_url} 🍴`); - }); - - handler.on('pull_request', event => { - const pr = event.pull_request; - const action = event.action; - let text: string; - switch (action) { - case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; - case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; - case 'closed': - text = pr.merged - ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` - : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; - break; - default: return; - } - post(text); - }); + createNote(bot, { text }); }; + +// Init router +const router = new Router(); + +if (config.github_bot != null) { + const secret = config.github_bot.hook_secret; + + router.post('/hooks/github', ctx => { + const sig1 = new Buffer(ctx.headers['x-hub-signature']); + const sig2 = new Buffer(`sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(ctx.body)).digest('hex')}`); + if (sig1.equals(sig2)) { + handler.emit(ctx.headers['x-github-event'], ctx.body); + ctx.status = 204; + } else { + ctx.status = 400; + } + }); +} + +module.exports = router; + +handler.on('status', event => { + const state = event.state; + switch (state) { + case 'error': + case 'failure': + const commit = event.commit; + const parent = commit.parents[0]; + + // Fetch parent status + request({ + url: `${parent.url}/statuses`, + headers: { + 'User-Agent': 'misskey' + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + const parentStatuses = JSON.parse(body); + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + if (stillFailed) { + post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); + } else { + post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); + } + }); + break; + } +}); + +handler.on('push', event => { + const ref = event.ref; + switch (ref) { + case 'refs/heads/master': + const pusher = event.pusher; + const compare = event.compare; + const commits = event.commits; + post([ + `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, + commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), + ].join('\n')); + break; + case 'refs/heads/release': + const commit = event.commits[0]; + post(`RELEASED: ${commit.message}`); + break; + } +}); + +handler.on('issues', event => { + const issue = event.issue; + const action = event.action; + let title: string; + switch (action) { + case 'opened': title = 'Issue opened'; break; + case 'closed': title = 'Issue closed'; break; + case 'reopened': title = 'Issue reopened'; break; + default: return; + } + post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); +}); + +handler.on('issue_comment', event => { + const issue = event.issue; + const comment = event.comment; + const action = event.action; + let text: string; + switch (action) { + case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; + default: return; + } + post(text); +}); + +handler.on('watch', event => { + const sender = event.sender; + post(`⭐️ Starred by **${sender.login}** ⭐️`); +}); + +handler.on('fork', event => { + const repo = event.forkee; + post(`🍴 Forked:\n${repo.html_url} 🍴`); +}); + +handler.on('pull_request', event => { + const pr = event.pull_request; + const action = event.action; + let text: string; + switch (action) { + case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; + case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; + case 'closed': + text = pr.merged + ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` + : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; + break; + default: return; + } + post(text); +}); diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index e5239fa171..9fb01b44ef 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -1,160 +1,155 @@ -import * as express from 'express'; -import * as cookie from 'cookie'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; import * as uuid from 'uuid'; -// import * as Twitter from 'twitter'; -// const Twitter = require('twitter'); import autwh from 'autwh'; import redis from '../../../db/redis'; -import User, { pack } from '../../../models/user'; +import User, { pack, ILocalUser } from '../../../models/user'; import event from '../../../publishers/stream'; import config from '../../../config'; import signin from '../common/signin'; -module.exports = (app: express.Application) => { - function getUserToken(req: express.Request) { - // req.headers['cookie'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; +function getUserToken(ctx: Koa.Context) { + return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; +} + +function compareOrigin(ctx: Koa.Context) { + function normalizeUrl(url: string) { + return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; } - function compareOrigin(req: express.Request) { - function normalizeUrl(url: string) { - return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; - } + const referer = ctx.headers['referer']; - // req.headers['referer'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - const referer = req.headers['referer'] as string; + return (normalizeUrl(referer) == normalizeUrl(config.url)); +} - return (normalizeUrl(referer) == normalizeUrl(config.url)); - } - - app.get('/disconnect/twitter', async (req, res): Promise => { - if (!compareOrigin(req)) { - res.status(400).send('invalid origin'); - return; - } - - const userToken = getUserToken(req); - if (userToken == null) return res.send('plz signin'); - - const user = await User.findOneAndUpdate({ - host: null, - 'token': userToken - }, { - $set: { - 'twitter': null - } - }); - - res.send(`Twitterの連携を解除しました :v:`); - - // Publish i updated event - event(user._id, 'i_updated', await pack(user, user, { - detail: true, - includeSecrets: true - })); - }); - - if (config.twitter == null) { - app.get('/connect/twitter', (req, res) => { - res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); - }); - - app.get('/signin/twitter', (req, res) => { - res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); - }); +// Init router +const router = new Router(); +router.get('/disconnect/twitter', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); return; } + const userToken = getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } + + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'twitter': null + } + }); + + ctx.body = `Twitterの連携を解除しました :v:`; + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); +}); + +if (config.twitter == null) { + router.get('/connect/twitter', ctx => { + ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; + }); + + router.get('/signin/twitter', ctx => { + ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; + }); +} else { const twAuth = autwh({ consumerKey: config.twitter.consumer_key, consumerSecret: config.twitter.consumer_secret, callbackUrl: `${config.url}/api/tw/cb` }); - app.get('/connect/twitter', async (req, res): Promise => { - if (!compareOrigin(req)) { - res.status(400).send('invalid origin'); + router.get('/connect/twitter', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); return; } - const userToken = getUserToken(req); - if (userToken == null) return res.send('plz signin'); + const userToken = getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } - const ctx = await twAuth.begin(); - redis.set(userToken, JSON.stringify(ctx)); - res.redirect(ctx.url); + const twCtx = await twAuth.begin(); + redis.set(userToken, JSON.stringify(twCtx)); + ctx.redirect(twCtx.url); }); - app.get('/signin/twitter', async (req, res): Promise => { - const ctx = await twAuth.begin(); + router.get('/signin/twitter', async ctx => { + const twCtx = await twAuth.begin(); const sessid = uuid(); - redis.set(sessid, JSON.stringify(ctx)); + redis.set(sessid, JSON.stringify(twCtx)); const expires = 1000 * 60 * 60; // 1h - res.cookie('signin_with_twitter_session_id', sessid, { + ctx.cookies.set('signin_with_twitter_session_id', sessid, { path: '/', - domain: `.${config.host}`, - secure: config.url.substr(0, 5) === 'https', + domain: config.host, + secure: config.url.startsWith('https'), httpOnly: true, expires: new Date(Date.now() + expires), maxAge: expires }); - res.redirect(ctx.url); + ctx.redirect(twCtx.url); }); - app.get('/tw/cb', (req, res): any => { - const userToken = getUserToken(req); + router.get('/tw/cb', ctx => { + const userToken = getUserToken(ctx); if (userToken == null) { - // req.headers['cookie'] は常に string ですが、型定義の都合上 - // string | string[] になっているので string を明示しています - const cookies = cookie.parse((req.headers['cookie'] as string || '')); + const sessid = ctx.cookies.get('signin_with_twitter_session_id'); - const sessid = cookies['signin_with_twitter_session_id']; - - if (sessid == undefined) { - res.status(400).send('invalid session'); + if (sessid == null) { + ctx.throw(400, 'invalid session'); return; } - redis.get(sessid, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + redis.get(sessid, async (_, twCtx) => { + const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); const user = await User.findOne({ host: null, 'twitter.userId': result.userId - }); + }) as ILocalUser; if (user == null) { - res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); return; } - signin(res, user, true); + signin(ctx, user, true); }); } else { - const verifier = req.query.oauth_verifier; + const verifier = ctx.query.oauth_verifier; if (verifier == null) { - res.status(400).send('invalid session'); + ctx.throw(400, 'invalid session'); return; } - redis.get(userToken, async (_, ctx) => { - const result = await twAuth.done(JSON.parse(ctx), verifier); + redis.get(userToken, async (_, twCtx) => { + const result = await twAuth.done(JSON.parse(twCtx), verifier); const user = await User.findOneAndUpdate({ host: null, - 'token': userToken + token: userToken }, { $set: { - 'twitter': { + twitter: { accessToken: result.accessToken, accessTokenSecret: result.accessTokenSecret, userId: result.userId, @@ -163,7 +158,7 @@ module.exports = (app: express.Application) => { } }); - res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); + ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; // Publish i updated event event(user._id, 'i_updated', await pack(user, user, { @@ -173,4 +168,6 @@ module.exports = (app: express.Application) => { }); } }); -}; +} + +module.exports = router; diff --git a/src/server/file/index.ts b/src/server/file/index.ts index 95e7867f01..d58939f1be 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -3,171 +3,33 @@ */ import * as fs from 'fs'; -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as cors from 'cors'; -import * as mongodb from 'mongodb'; -import * as _gm from 'gm'; -import * as stream from 'stream'; +import * as Koa from 'koa'; +import * as cors from '@koa/cors'; +import * as Router from 'koa-router'; +import pour from './pour'; +import sendDriveFile from './send-drive-file'; -import DriveFile, { getGridFSBucket } from '../../models/drive-file'; - -const gm = _gm.subClass({ - imageMagick: true -}); - -/** - * Init app - */ -const app = express(); - -app.disable('x-powered-by'); -app.locals.cache = true; -app.use(bodyParser.urlencoded({ extended: true })); +// Init app +const app = new Koa(); app.use(cors()); -/** - * Statics - */ -app.use('/assets', express.static(`${__dirname}/assets`, { - maxAge: 1000 * 60 * 60 * 24 * 365 // 一年 -})); +// Init router +const router = new Router(); -app.get('/', (req, res) => { - res.send('yee haw'); -}); - -app.get('/default-avatar.jpg', (req, res) => { +router.get('/default-avatar.jpg', ctx => { const file = fs.createReadStream(`${__dirname}/assets/avatar.jpg`); - send(file, 'image/jpeg', req, res); + pour(file, 'image/jpeg', ctx); }); -app.get('/app-default.jpg', (req, res) => { +router.get('/app-default.jpg', ctx => { const file = fs.createReadStream(`${__dirname}/assets/dummy.png`); - send(file, 'image/png', req, res); + pour(file, 'image/png', ctx); }); -interface ISend { - contentType: string; - stream: stream.Readable; -} +router.get('/:id', sendDriveFile); +router.get('/:id/:name', sendDriveFile); -function thumbnail(data: stream.Readable, type: string, resize: number): ISend { - const readable: stream.Readable = (() => { - // 動画であれば - if (/^video\/.*$/.test(type)) { - // TODO - // 使わないことになったストリームはしっかり取り壊す - data.destroy(); - return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); - // 画像であれば - // Note: SVGはapplication/xml - } else if (/^image\/.*$/.test(type) || type == 'application/xml') { - // 0フレーム目を送る - try { - return gm(data).selectFrame(0).stream(); - // だめだったら - } catch (e) { - // 使わないことになったストリームはしっかり取り壊す - data.destroy(); - return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); - } - // 動画か画像以外 - } else { - data.destroy(); - return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); - } - })(); - - let g = gm(readable); - - if (resize) { - g = g.resize(resize, resize); - } - - const stream = g - .compress('jpeg') - .quality(80) - .interlace('line') - .stream(); - - return { - contentType: 'image/jpeg', - stream - }; -} - -const commonReadableHandlerGenerator = (req: express.Request, res: express.Response) => (e: Error): void => { - console.dir(e); - req.destroy(); - res.destroy(e); -}; - -function send(readable: stream.Readable, type: string, req: express.Request, res: express.Response): void { - readable.on('error', commonReadableHandlerGenerator(req, res)); - - const data = ((): ISend => { - if (req.query.thumbnail !== undefined) { - return thumbnail(readable, type, req.query.size); - } - return { - contentType: type, - stream: readable - }; - })(); - - if (readable !== data.stream) { - data.stream.on('error', commonReadableHandlerGenerator(req, res)); - } - - if (req.query.download !== undefined) { - res.header('Content-Disposition', 'attachment'); - } - - res.header('Content-Type', data.contentType); - - data.stream.pipe(res); - - data.stream.on('end', () => { - res.end(); - }); -} - -async function sendFileById(req: express.Request, res: express.Response): Promise { - // Validate id - if (!mongodb.ObjectID.isValid(req.params.id)) { - res.status(400).send('incorrect id'); - return; - } - - const fileId = new mongodb.ObjectID(req.params.id); - - // Fetch (drive) file - const file = await DriveFile.findOne({ _id: fileId }); - - // validate name - if (req.params.name !== undefined && req.params.name !== file.filename) { - res.status(404).send('there is no file has given name'); - return; - } - - if (file == null) { - res.status(404).sendFile(`${__dirname}/assets/dummy.png`); - return; - } - - const bucket = await getGridFSBucket(); - - const readable = bucket.openDownloadStream(fileId); - - send(readable, file.contentType, req, res); -} - -/** - * Routing - */ - -app.get('/:id', sendFileById); -app.get('/:id/:name', sendFileById); +// Register router +app.use(router.routes()); module.exports = app; diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts new file mode 100644 index 0000000000..2a31cb5898 --- /dev/null +++ b/src/server/file/pour.ts @@ -0,0 +1,93 @@ +import * as fs from 'fs'; +import * as stream from 'stream'; +import * as Koa from 'koa'; +import * as Gm from 'gm'; + +const gm = Gm.subClass({ + imageMagick: true +}); + +interface ISend { + contentType: string; + stream: stream.Readable; +} + +function thumbnail(data: stream.Readable, type: string, resize: number): ISend { + const readable: stream.Readable = (() => { + // 動画であれば + if (/^video\/.*$/.test(type)) { + // TODO + // 使わないことになったストリームはしっかり取り壊す + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); + // 画像であれば + // Note: SVGはapplication/xml + } else if (/^image\/.*$/.test(type) || type == 'application/xml') { + // 0フレーム目を送る + try { + return gm(data).selectFrame(0).stream(); + // だめだったら + } catch (e) { + // 使わないことになったストリームはしっかり取り壊す + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/thumbnail-not-available.png`); + } + // 動画か画像以外 + } else { + data.destroy(); + return fs.createReadStream(`${__dirname}/assets/not-an-image.png`); + } + })(); + + let g = gm(readable); + + if (resize) { + g = g.resize(resize, resize); + } + + const stream = g + .compress('jpeg') + .quality(80) + .interlace('line') + .stream(); + + return { + contentType: 'image/jpeg', + stream + }; +} + +const commonReadableHandlerGenerator = (ctx: Koa.Context) => (e: Error): void => { + console.error(e); + ctx.status = 500; +}; + +export default function(readable: stream.Readable, type: string, ctx: Koa.Context): void { + readable.on('error', commonReadableHandlerGenerator(ctx)); + + const data = ((): ISend => { + if (ctx.query.thumbnail !== undefined) { + return thumbnail(readable, type, ctx.query.size); + } + return { + contentType: type, + stream: readable + }; + })(); + + if (readable !== data.stream) { + data.stream.on('error', commonReadableHandlerGenerator(ctx)); + } + + if (ctx.query.download !== undefined) { + ctx.header('Content-Disposition', 'attachment'); + } + + ctx.header('Content-Type', data.contentType); + + data.stream.pipe(ctx.res); + + data.stream.on('end', () => { + ctx.res.end(); + }); +} diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts new file mode 100644 index 0000000000..e6ee19ff1d --- /dev/null +++ b/src/server/file/send-drive-file.ts @@ -0,0 +1,30 @@ +import * as Koa from 'koa'; +import * as send from 'koa-send'; +import * as mongodb from 'mongodb'; +import DriveFile, { getGridFSBucket } from '../../models/drive-file'; +import pour from './pour'; + +export default async function(ctx: Koa.Context) { + // Validate id + if (!mongodb.ObjectID.isValid(ctx.params.id)) { + ctx.throw(400, 'incorrect id'); + return; + } + + const fileId = new mongodb.ObjectID(ctx.params.id); + + // Fetch drive file + const file = await DriveFile.findOne({ _id: fileId }); + + if (file == null) { + ctx.status = 404; + await send(ctx, `${__dirname}/assets/dummy.png`); + return; + } + + const bucket = await getGridFSBucket(); + + const readable = bucket.openDownloadStream(fileId); + + pour(readable, file.contentType, ctx); +} diff --git a/src/server/index.ts b/src/server/index.ts index e9bfa9e10b..f0d60fa651 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -13,7 +13,7 @@ import activityPub from './activitypub'; import webFinger from './webfinger'; import config from '../config'; -// Init server +// Init app const app = new Koa(); app.proxy = true; app.use(bodyParser); @@ -46,9 +46,9 @@ function createServer() { Object.keys(config.https).forEach(k => { certs[k] = fs.readFileSync(config.https[k]); }); - return https.createServer(certs, app); + return https.createServer(certs, app.callback); } else { - return http.createServer(app); + return http.createServer(app.callback); } } diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index 889532e17e..a546d1e88c 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -1,24 +1,21 @@ /** - * Docs Server + * Docs */ import * as path from 'path'; -import * as express from 'express'; +import * as Router from 'koa-router'; +import * as send from 'koa-send'; const docs = path.resolve(`${__dirname}/../../client/docs/`); -/** - * Init app - */ -const app = express(); -app.disable('x-powered-by'); +const router = new Router(); -app.use('/assets', express.static(`${docs}/assets`)); +router.get('/assets', async ctx => { + await send(ctx, `${docs}/assets`); +}); -/** - * Routing - */ -app.get(/^\/([a-z_\-\/]+?)$/, (req, res) => - res.sendFile(`${docs}/${req.params[0]}.html`)); +router.get(/^\/([a-z_\-\/]+?)$/, async ctx => { + await send(ctx, `${docs}/${ctx.params[0]}.html`); +}); -module.exports = app; +module.exports = router; diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 5b1b6409b9..b28ad5592c 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -5,60 +5,71 @@ import * as path from 'path'; import ms = require('ms'); -// express modules -import * as express from 'express'; -import * as bodyParser from 'body-parser'; -import * as favicon from 'serve-favicon'; -import * as compression from 'compression'; +import * as Koa from 'koa'; +import * as Router from 'koa-router'; +import * as send from 'koa-send'; +import * as favicon from 'koa-favicon'; const client = path.resolve(`${__dirname}/../../client/`); -// Create server -const app = express(); -app.disable('x-powered-by'); +// Init app +const app = new Koa(); -app.use('/docs', require('./docs')); +// Serve favicon +app.use(favicon(`${client}/assets/favicon.ico`)); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json({ - type: ['application/json', 'text/plain'] -})); -app.use(compression()); - -app.use((req, res, next) => { - res.header('X-Frame-Options', 'DENY'); +// Common request handler +app.use((ctx, next) => { + // IFrameの中に入れられないようにする + ctx.set('X-Frame-Options', 'DENY'); next(); }); +// Init router +const router = new Router(); + //#region static assets -app.use(favicon(`${client}/assets/favicon.ico`)); -app.get('/apple-touch-icon.png', (req, res) => res.sendFile(`${client}/assets/apple-touch-icon.png`)); -app.use('/assets', express.static(`${client}/assets`, { - maxAge: ms('7 days') -})); -app.use('/assets/*.js', (req, res) => res.sendFile(`${client}/assets/404.js`)); -app.use('/assets', (req, res) => { - res.sendStatus(404); -}); - -// ServiceWroker -app.get(/^\/sw\.(.+?)\.js$/, (req, res) => - res.sendFile(`${client}/assets/sw.${req.params[0]}.js`)); - -// Manifest -app.get('/manifest.json', (req, res) => - res.sendFile(`${client}/assets/manifest.json`)); - -//#endregion - -app.get(/\/api:url/, require('./url-preview')); - -// Render base html for all requests -app.get('*', (req, res) => { - res.sendFile(path.resolve(`${client}/app/base.html`), { - maxAge: ms('7 days') +router.get('/assets', async ctx => { + await send(ctx, ctx.path, { + root: `${client}/assets`, + maxage: ms('7 days'), + immutable: true }); }); +// Apple touch icon +router.get('/apple-touch-icon.png', async ctx => { + await send(ctx, `${client}/assets/apple-touch-icon.png`); +}); + +// ServiceWroker +router.get(/^\/sw\.(.+?)\.js$/, async ctx => { + await send(ctx, `${client}/assets/sw.${ctx.params[0]}.js`); +}); + +// Manifest +router.get('/manifest.json', async ctx => { + await send(ctx, `${client}/assets/manifest.json`); +}); + +//#endregion + +// Docs +router.use('/docs', require('./docs').routes()); + +// URL preview endpoint +router.get('url', require('./url-preview')); + +// Render base html for all requests +router.get('*', async ctx => { + await send(ctx, `${client}/app/base.html`, { + maxage: ms('7 days'), + immutable: true + }); +}); + +// Register router +app.use(router.routes()); + module.exports = app; diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts index 0c5fd8a78e..4b3f44a5da 100644 --- a/src/server/web/url-preview.ts +++ b/src/server/web/url-preview.ts @@ -1,11 +1,11 @@ -import * as express from 'express'; +import * as Koa from 'koa'; import summaly from 'summaly'; -module.exports = async (req: express.Request, res: express.Response) => { - const summary = await summaly(req.query.url); +module.exports = async (ctx: Koa.Context) => { + const summary = await summaly(ctx.query.url); summary.icon = wrap(summary.icon); summary.thumbnail = wrap(summary.thumbnail); - res.send(summary); + ctx.body = summary; }; function wrap(url: string): string { From c54ff5b2bced1dd3d15f98e4a8a40523a0fcea78 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 06:17:14 +0900 Subject: [PATCH 22/34] wip --- package.json | 1 + src/server/index.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3e349203fc..b611f603c9 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,7 @@ "js-yaml": "3.11.0", "jsdom": "11.7.0", "koa": "^2.5.0", + "koa-bodyparser": "^4.2.0", "koa-favicon": "^2.0.1", "koa-mount": "^3.0.0", "koa-multer": "^1.0.2", diff --git a/src/server/index.ts b/src/server/index.ts index f0d60fa651..02362037ee 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,6 +8,7 @@ import * as https from 'https'; import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as bodyParser from 'koa-bodyparser'; +import * as mount from 'koa-mount'; import activityPub from './activitypub'; import webFinger from './webfinger'; @@ -27,15 +28,17 @@ if (config.url.startsWith('https')) { }); } +app.use(mount('/api', require('./api'))); +app.use(mount('/files', require('./file'))); + // Init router const router = new Router(); // Routing -router.use('/api', require('./api')); -router.use('/files', require('./file')); router.use(activityPub.routes()); router.use(webFinger.routes()); -router.use(require('./web')); + +app.use(mount(require('./web'))); // Register router app.use(router.routes()); From 61f21594a9edbb3102d6a0f45c1e95ed82c3f606 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 07:34:27 +0900 Subject: [PATCH 23/34] wip --- src/server/api/index.ts | 2 ++ src/server/file/pour.ts | 5 +++-- src/server/index.ts | 16 +++++++--------- src/server/web/index.ts | 17 ++++++++--------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/server/api/index.ts b/src/server/api/index.ts index d2427d30ae..c383e1cf8d 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -5,6 +5,7 @@ import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as multer from 'koa-multer'; +import * as bodyParser from 'koa-bodyparser'; import endpoints from './endpoints'; @@ -12,6 +13,7 @@ const handler = require('./api-handler').default; // Init app const app = new Koa(); +app.use(bodyParser); // Init multer instance const upload = multer({ diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts index 2a31cb5898..b38b969c2d 100644 --- a/src/server/file/pour.ts +++ b/src/server/file/pour.ts @@ -80,10 +80,11 @@ export default function(readable: stream.Readable, type: string, ctx: Koa.Contex } if (ctx.query.download !== undefined) { - ctx.header('Content-Disposition', 'attachment'); + ctx.set('Content-Disposition', 'attachment'); } - ctx.header('Content-Type', data.contentType); + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + ctx.set('Content-Type', data.contentType); data.stream.pipe(ctx.res); diff --git a/src/server/index.ts b/src/server/index.ts index 02362037ee..a637e8598b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,10 +4,9 @@ import * as fs from 'fs'; import * as http from 'http'; -import * as https from 'https'; +import * as http2 from 'http2'; import * as Koa from 'koa'; import * as Router from 'koa-router'; -import * as bodyParser from 'koa-bodyparser'; import * as mount from 'koa-mount'; import activityPub from './activitypub'; @@ -17,14 +16,13 @@ import config from '../config'; // Init app const app = new Koa(); app.proxy = true; -app.use(bodyParser); // HSTS // 6months (15552000sec) if (config.url.startsWith('https')) { - app.use((ctx, next) => { + app.use(async (ctx, next) => { ctx.set('strict-transport-security', 'max-age=15552000; preload'); - next(); + await next(); }); } @@ -38,20 +36,20 @@ const router = new Router(); router.use(activityPub.routes()); router.use(webFinger.routes()); -app.use(mount(require('./web'))); - // Register router app.use(router.routes()); +app.use(mount(require('./web'))); + function createServer() { if (config.https) { const certs = {}; Object.keys(config.https).forEach(k => { certs[k] = fs.readFileSync(config.https[k]); }); - return https.createServer(certs, app.callback); + return http2.createSecureServer(certs, app.callback()); } else { - return http.createServer(app.callback); + return http.createServer(app.callback()); } } diff --git a/src/server/web/index.ts b/src/server/web/index.ts index b28ad5592c..dd296f875d 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -2,15 +2,13 @@ * Web Client Server */ -import * as path from 'path'; import ms = require('ms'); - import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as send from 'koa-send'; import * as favicon from 'koa-favicon'; -const client = path.resolve(`${__dirname}/../../client/`); +const client = `${__dirname}/../../client/`; // Init app const app = new Koa(); @@ -19,10 +17,10 @@ const app = new Koa(); app.use(favicon(`${client}/assets/favicon.ico`)); // Common request handler -app.use((ctx, next) => { +app.use(async (ctx, next) => { // IFrameの中に入れられないようにする ctx.set('X-Frame-Options', 'DENY'); - next(); + await next(); }); // Init router @@ -30,9 +28,9 @@ const router = new Router(); //#region static assets -router.get('/assets', async ctx => { +router.get('/assets/*', async ctx => { await send(ctx, ctx.path, { - root: `${client}/assets`, + root: client, maxage: ms('7 days'), immutable: true }); @@ -63,8 +61,9 @@ router.get('url', require('./url-preview')); // Render base html for all requests router.get('*', async ctx => { - await send(ctx, `${client}/app/base.html`, { - maxage: ms('7 days'), + await send(ctx, `app/base.html`, { + root: client, + maxage: ms('3 days'), immutable: true }); }); From 22d2f2051c4cbe3da5b9ece674f36a6555f8c953 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 09:44:00 +0900 Subject: [PATCH 24/34] wip --- src/client/app/common/mios.ts | 5 +++-- src/server/api/api-handler.ts | 16 +++++++++++++--- src/server/api/bot/interfaces/line.ts | 2 +- src/server/api/call.ts | 4 +--- src/server/api/index.ts | 4 +++- src/server/api/private/signin.ts | 6 +++--- src/server/api/private/signup.ts | 6 +++--- src/server/api/service/github.ts | 8 ++++++-- src/server/file/index.ts | 7 ++++++- src/server/file/pour.ts | 8 +------- 10 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts index a09af799be..ccc73eebc3 100644 --- a/src/client/app/common/mios.ts +++ b/src/client/app/common/mios.ts @@ -444,9 +444,10 @@ export default class MiOS extends EventEmitter { // Append a credential if (this.isSignedIn) (data as any).i = this.i.token; - const viaStream = localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true; - return new Promise((resolve, reject) => { + const viaStream = this.stream.hasConnection && + (localStorage.getItem('apiViaStream') ? localStorage.getItem('apiViaStream') == 'true' : true); + if (viaStream) { const stream = this.stream.borrow(); const id = Math.random().toString(); diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 2c50234317..947794a20e 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -25,11 +25,21 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => { // Authentication try { - [user, app] = await authenticate(ctx.body['i']); + [user, app] = await authenticate(ctx.request.body['i']); } catch (e) { - return reply(403, 'AUTHENTICATION_FAILED'); + reply(403, 'AUTHENTICATION_FAILED'); + return; } + let res; + // API invoking - call(endpoint, user, app, ctx.body, ctx.req).then(reply).catch(e => reply(400, e)); + try { + res = await call(endpoint, user, app, ctx.request.body, ctx.req); + } catch (e) { + reply(400, e); + return; + } + + reply(res); }; diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index 454630161a..733315391d 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -226,7 +226,7 @@ if (config.line_bot) { // シグネチャ比較 if (sig1 === sig2) { - ctx.body.events.forEach(ev => { + ctx.request.body.events.forEach(ev => { handler.emit('event', ev); }); } else { diff --git a/src/server/api/call.ts b/src/server/api/call.ts index c25f55ed3f..cc40294657 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -6,11 +6,9 @@ import limitter from './limitter'; import { IUser } from '../../models/user'; import { IApp } from '../../models/app'; -export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => { +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => { const isSecure = user != null && app == null; - //console.log(endpoint, user, app, data); - const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint; if (ep.secure && !isSecure) { diff --git a/src/server/api/index.ts b/src/server/api/index.ts index c383e1cf8d..2ea5fccb5b 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -13,7 +13,9 @@ const handler = require('./api-handler').default; // Init app const app = new Koa(); -app.use(bodyParser); +app.use(bodyParser({ + detectJSON: () => true +})); // Init multer instance const upload = multer({ diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 55326deeaf..1737007206 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -11,9 +11,9 @@ export default async (ctx: Koa.Context) => { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); - const username = ctx.body['username']; - const password = ctx.body['password']; - const token = ctx.body['token']; + const username = ctx.request.body['username']; + const password = ctx.request.body['password']; + const token = ctx.request.body['token']; if (typeof username != 'string') { ctx.status = 400; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index a4554be4ae..15257b869f 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -37,7 +37,7 @@ export default async (ctx: Koa.Context) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - const success = await recaptcha(ctx.body['g-recaptcha-response']); + const success = await recaptcha(ctx.request.body['g-recaptcha-response']); if (!success) { ctx.throw(400, 'recaptcha-failed'); @@ -45,8 +45,8 @@ export default async (ctx: Koa.Context) => { } } - const username = ctx.body['username']; - const password = ctx.body['password']; + const username = ctx.request.body['username']; + const password = ctx.request.body['password']; // Validate username if (!validateUsername(username)) { diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index ee226cc5cc..cd9760a36d 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -35,10 +35,14 @@ if (config.github_bot != null) { const secret = config.github_bot.hook_secret; router.post('/hooks/github', ctx => { + const body = JSON.stringify(ctx.request.body); + const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); const sig1 = new Buffer(ctx.headers['x-hub-signature']); - const sig2 = new Buffer(`sha1=${crypto.createHmac('sha1', secret).update(JSON.stringify(ctx.body)).digest('hex')}`); + const sig2 = new Buffer(`sha1=${hash}`); + + // シグネチャ比較 if (sig1.equals(sig2)) { - handler.emit(ctx.headers['x-github-event'], ctx.body); + handler.emit(ctx.headers['x-github-event'], ctx.request.body); ctx.status = 204; } else { ctx.status = 400; diff --git a/src/server/file/index.ts b/src/server/file/index.ts index d58939f1be..d305286d12 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -13,6 +13,11 @@ import sendDriveFile from './send-drive-file'; const app = new Koa(); app.use(cors()); +app.use(async (ctx, next) => { + ctx.set('Cache-Control', 'max-age=31536000, immutable'); + await next(); +}); + // Init router const router = new Router(); @@ -27,7 +32,7 @@ router.get('/app-default.jpg', ctx => { }); router.get('/:id', sendDriveFile); -router.get('/:id/:name', sendDriveFile); +router.get('/:id/*', sendDriveFile); // Register router app.use(router.routes()); diff --git a/src/server/file/pour.ts b/src/server/file/pour.ts index b38b969c2d..0fd0ad0e60 100644 --- a/src/server/file/pour.ts +++ b/src/server/file/pour.ts @@ -83,12 +83,6 @@ export default function(readable: stream.Readable, type: string, ctx: Koa.Contex ctx.set('Content-Disposition', 'attachment'); } - ctx.set('Cache-Control', 'max-age=31536000, immutable'); ctx.set('Content-Type', data.contentType); - - data.stream.pipe(ctx.res); - - data.stream.on('end', () => { - ctx.res.end(); - }); + ctx.body = data.stream; } From e2e7babee0de35385eb74830c82eaccdb28f013a Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 11:44:39 +0900 Subject: [PATCH 25/34] wip --- src/server/api/api-handler.ts | 6 ++++-- src/server/api/call.ts | 7 +++---- src/server/api/common/signin.ts | 4 +++- src/server/api/index.ts | 3 ++- src/server/api/private/signin.ts | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 947794a20e..e716dcdc01 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -7,6 +7,8 @@ import { IUser } from '../../models/user'; import { IApp } from '../../models/app'; export default async (endpoint: Endpoint, ctx: Koa.Context) => { + const body = ctx.is('multipart/form-data') ? (ctx.req as any).body : ctx.request.body; + const reply = (x?: any, y?: any) => { if (x === undefined) { ctx.status = 204; @@ -25,7 +27,7 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => { // Authentication try { - [user, app] = await authenticate(ctx.request.body['i']); + [user, app] = await authenticate(body['i']); } catch (e) { reply(403, 'AUTHENTICATION_FAILED'); return; @@ -35,7 +37,7 @@ export default async (endpoint: Endpoint, ctx: Koa.Context) => { // API invoking try { - res = await call(endpoint, user, app, ctx.request.body, ctx.req); + res = await call(endpoint, user, app, body, (ctx.req as any).file); } catch (e) { reply(400, e); return; diff --git a/src/server/api/call.ts b/src/server/api/call.ts index cc40294657..713add566a 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,4 +1,3 @@ -import * as http from 'http'; import * as multer from 'koa-multer'; import endpoints, { Endpoint } from './endpoints'; @@ -6,7 +5,7 @@ import limitter from './limitter'; import { IUser } from '../../models/user'; import { IApp } from '../../models/app'; -export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, req?: http.IncomingMessage) => new Promise(async (ok, rej) => { +export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, file?: any) => new Promise(async (ok, rej) => { const isSecure = user != null && app == null; const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint; @@ -36,8 +35,8 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, let exec = require(`${__dirname}/endpoints/${ep.name}`); - if (ep.withFile && req) { - exec = exec.bind(null, (req as multer.MulterIncomingMessage).file); + if (ep.withFile && file) { + exec = exec.bind(null, file); } let res; diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index f57c38414c..44e1336f27 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -3,7 +3,7 @@ import * as Koa from 'koa'; import config from '../../../config'; import { ILocalUser } from '../../../models/user'; -export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) { +export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) { const expires = 1000 * 60 * 60 * 24 * 365; // One Year ctx.cookies.set('i', user.token, { path: '/', @@ -16,5 +16,7 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect: boolean) { if (redirect) { ctx.redirect(config.url); + } else { + ctx.status = 204; } } diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 2ea5fccb5b..009c99acae 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -14,7 +14,8 @@ const handler = require('./api-handler').default; // Init app const app = new Koa(); app.use(bodyParser({ - detectJSON: () => true + // リクエストが multipart/form-data でない限りはJSONだと見なす + detectJSON: ctx => !ctx.is('multipart/form-data') })); // Init multer instance diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 1737007206..5450c7ad27 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -60,14 +60,14 @@ export default async (ctx: Koa.Context) => { }); if (verified) { - signin(ctx, user, false); + signin(ctx, user); } else { ctx.throw(400, { error: 'invalid token' }); } } else { - signin(ctx, user, false); + signin(ctx, user); } } else { ctx.throw(400, { From f3c4152a684b28b144caa7a609c98f695e7d04af Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 11:50:36 +0900 Subject: [PATCH 26/34] Clean up --- package.json | 57 ++++++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index b611f603c9..eed5d63702 100644 --- a/package.json +++ b/package.json @@ -30,21 +30,16 @@ "@fortawesome/fontawesome-free-brands": "5.0.2", "@fortawesome/fontawesome-free-regular": "5.0.2", "@fortawesome/fontawesome-free-solid": "5.0.2", - "@koa/cors": "^2.2.1", + "@koa/cors": "2.2.1", "@prezzemolo/rap": "0.1.2", "@prezzemolo/zip": "0.0.3", "@types/bcryptjs": "2.4.1", - "@types/body-parser": "1.16.8", "@types/chai": "4.1.2", "@types/chai-http": "3.0.4", - "@types/compression": "0.0.36", - "@types/cookie": "0.3.1", - "@types/cors": "2.8.3", "@types/debug": "0.0.30", "@types/deep-equal": "1.0.1", "@types/elasticsearch": "5.0.22", "@types/eventemitter3": "2.0.2", - "@types/express": "4.11.1", "@types/gm": "1.17.33", "@types/gulp": "3.8.36", "@types/gulp-htmlmin": "1.3.32", @@ -57,26 +52,23 @@ "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", "@types/js-yaml": "3.11.1", - "@types/koa": "^2.0.45", - "@types/koa-bodyparser": "^4.2.0", - "@types/koa-favicon": "^2.0.19", - "@types/koa-mount": "^3.0.1", - "@types/koa-multer": "^1.0.0", - "@types/koa-router": "^7.0.27", - "@types/koa-send": "^4.1.1", - "@types/koa__cors": "^2.2.2", - "@types/kue": "^0.11.8", + "@types/koa": "2.0.45", + "@types/koa-bodyparser": "4.2.0", + "@types/koa-favicon": "2.0.19", + "@types/koa-mount": "3.0.1", + "@types/koa-multer": "1.0.0", + "@types/koa-router": "7.0.27", + "@types/koa-send": "4.1.1", + "@types/koa__cors": "2.2.2", + "@types/kue": "0.11.8", "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", "@types/mocha": "5.0.0", "@types/mongodb": "3.0.12", "@types/monk": "6.0.0", - "@types/morgan": "1.7.35", "@types/ms": "0.7.30", - "@types/multer": "1.3.6", "@types/node": "9.6.4", "@types/nopt": "3.0.29", - "@types/proxy-addr": "2.0.0", "@types/pug": "2.0.4", "@types/qrcode": "0.8.1", "@types/ratelimiter": "2.1.28", @@ -85,7 +77,6 @@ "@types/request-promise-native": "1.0.14", "@types/rimraf": "2.0.2", "@types/seedrandom": "2.4.27", - "@types/serve-favicon": "2.2.30", "@types/speakeasy": "2.0.2", "@types/tmp": "0.0.33", "@types/uuid": "3.4.3", @@ -97,22 +88,18 @@ "autosize": "4.0.1", "autwh": "0.1.0", "bcryptjs": "2.4.3", - "body-parser": "1.18.2", "bootstrap-vue": "2.0.0-rc.6", "cafy": "3.2.1", "chai": "4.1.2", "chai-http": "4.0.0", "chalk": "2.3.2", - "compression": "1.7.2", - "cookie": "0.3.1", - "cors": "2.8.4", "crc-32": "1.2.0", "css-loader": "0.28.11", "debug": "3.1.0", "deep-equal": "1.0.1", "deepcopy": "0.6.3", "diskusage": "0.2.4", - "dompurify": "^1.0.3", + "dompurify": "1.0.3", "elasticsearch": "14.2.2", "element-ui": "2.3.3", "emojilib": "2.2.12", @@ -121,7 +108,6 @@ "eslint-plugin-vue": "4.4.0", "eventemitter3": "3.0.1", "exif-js": "2.3.0", - "express": "4.16.3", "file-loader": "1.1.11", "file-type": "7.6.0", "fuckadblock": "3.2.1", @@ -143,19 +129,19 @@ "hard-source-webpack-plugin": "0.6.4", "highlight.js": "9.12.0", "html-minifier": "3.5.14", - "http-signature": "^1.2.0", + "http-signature": "1.2.0", "inquirer": "5.2.0", "is-root": "2.0.0", "is-url": "1.2.4", "js-yaml": "3.11.0", "jsdom": "11.7.0", - "koa": "^2.5.0", - "koa-bodyparser": "^4.2.0", - "koa-favicon": "^2.0.1", - "koa-mount": "^3.0.0", - "koa-multer": "^1.0.2", - "koa-router": "^7.4.0", - "koa-send": "^4.1.3", + "koa": "2.5.0", + "koa-bodyparser": "4.2.0", + "koa-favicon": "2.0.1", + "koa-mount": "3.0.0", + "koa-multer": "1.0.2", + "koa-router": "7.4.0", + "koa-send": "4.1.3", "kue": "0.11.6", "license-checker": "18.0.0", "loader-utils": "1.1.0", @@ -165,9 +151,7 @@ "moji": "0.5.1", "mongodb": "3.0.6", "monk": "6.0.5", - "morgan": "1.9.0", "ms": "2.1.1", - "multer": "1.3.0", "nan": "2.10.0", "node-sass": "4.8.3", "node-sass-json-importer": "3.1.6", @@ -178,7 +162,6 @@ "os-utils": "0.0.14", "progress-bar-webpack-plugin": "1.11.0", "prominence": "0.2.0", - "proxy-addr": "2.0.3", "pug": "2.0.3", "punycode": "2.1.0", "qrcode": "1.2.0", @@ -193,7 +176,6 @@ "s-age": "1.1.2", "sass-loader": "6.0.7", "seedrandom": "2.4.3", - "serve-favicon": "2.5.0", "speakeasy": "2.0.0", "style-loader": "0.20.3", "stylus": "0.54.5", @@ -213,7 +195,6 @@ "url-loader": "1.0.1", "uuid": "3.2.1", "v-animate-css": "0.0.2", - "vhost": "3.0.2", "vue": "2.5.16", "vue-cropperjs": "2.2.0", "vue-js-modal": "1.3.12", From b099ad2a30aac43c4dbe36864f9045a687cfd30b Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 12:05:24 +0900 Subject: [PATCH 27/34] wip --- .../common/views/components/url-preview.vue | 2 +- src/client/docs/layout.pug | 2 +- src/server/web/docs.ts | 20 ++++++++++++------- src/server/web/index.ts | 4 +++- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index e91e510550..fd25480f61 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -45,7 +45,7 @@ export default Vue.extend({ } else if (url.hostname == 'youtu.be') { this.youtubeId = url.pathname; } else { - fetch('/api:url?url=' + this.url).then(res => { + fetch('/url?url=' + this.url).then(res => { res.json().then(info => { this.title = info.title; this.description = info.description; diff --git a/src/client/docs/layout.pug b/src/client/docs/layout.pug index 29d2a3ff69..1d9ebcb4cd 100644 --- a/src/client/docs/layout.pug +++ b/src/client/docs/layout.pug @@ -6,7 +6,7 @@ html(lang= lang) meta(name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no") title | #{title} | Misskey Docs - link(rel="stylesheet" href="/assets/style.css") + link(rel="stylesheet" href="/docs/assets/style.css") block meta //- FontAwesome style diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index a546d1e88c..75da010682 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -2,20 +2,26 @@ * Docs */ -import * as path from 'path'; +import ms = require('ms'); import * as Router from 'koa-router'; import * as send from 'koa-send'; -const docs = path.resolve(`${__dirname}/../../client/docs/`); +const docs = `${__dirname}/../../client/docs/`; const router = new Router(); -router.get('/assets', async ctx => { - await send(ctx, `${docs}/assets`); +router.get('/assets/*', async ctx => { + await send(ctx, ctx.path, { + root: docs, + maxage: ms('7 days'), + immutable: true + }); }); -router.get(/^\/([a-z_\-\/]+?)$/, async ctx => { - await send(ctx, `${docs}/${ctx.params[0]}.html`); +router.get('*', async ctx => { + await send(ctx, `${ctx.params[0]}.html`, { + root: docs + }); }); -module.exports = router; +export default router; diff --git a/src/server/web/index.ts b/src/server/web/index.ts index dd296f875d..376aadda73 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -8,6 +8,8 @@ import * as Router from 'koa-router'; import * as send from 'koa-send'; import * as favicon from 'koa-favicon'; +import docs from './docs'; + const client = `${__dirname}/../../client/`; // Init app @@ -54,7 +56,7 @@ router.get('/manifest.json', async ctx => { //#endregion // Docs -router.use('/docs', require('./docs').routes()); +router.use('/docs', docs.routes()); // URL preview endpoint router.get('url', require('./url-preview')); From f618dedfbc1471de19ea72a4ab6ffbad44dc7e56 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 12:08:56 +0900 Subject: [PATCH 28/34] Fix --- src/server/web/docs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index 75da010682..e65cc87b12 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -11,8 +11,8 @@ const docs = `${__dirname}/../../client/docs/`; const router = new Router(); router.get('/assets/*', async ctx => { - await send(ctx, ctx.path, { - root: docs, + await send(ctx, ctx.params[0], { + root: docs + '/assets/', maxage: ms('7 days'), immutable: true }); From 645481c2e8d13123c74271fa86291f10b99b9a55 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 13:36:21 +0900 Subject: [PATCH 29/34] Use http2 --- src/server/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index a637e8598b..5f6d3a84df 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,7 +3,6 @@ */ import * as fs from 'fs'; -import * as http from 'http'; import * as http2 from 'http2'; import * as Koa from 'koa'; import * as Router from 'koa-router'; @@ -49,7 +48,7 @@ function createServer() { }); return http2.createSecureServer(certs, app.callback()); } else { - return http.createServer(app.callback()); + return http2.createServer(app.callback()); } } From 732e07a4b3800a7d5527a8be7331cd0ef44fdc85 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 13:52:25 +0900 Subject: [PATCH 30/34] Clean up --- src/server/api/call.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 713add566a..fd3cea7743 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,5 +1,3 @@ -import * as multer from 'koa-multer'; - import endpoints, { Endpoint } from './endpoints'; import limitter from './limitter'; import { IUser } from '../../models/user'; From 16920caf9cf5340f8e1ea3b40ff145967fc72c0a Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 14:07:42 +0900 Subject: [PATCH 31/34] Revert "Use http2" This reverts commit 645481c2e8d13123c74271fa86291f10b99b9a55. --- src/server/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/index.ts b/src/server/index.ts index 5f6d3a84df..a637e8598b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -3,6 +3,7 @@ */ import * as fs from 'fs'; +import * as http from 'http'; import * as http2 from 'http2'; import * as Koa from 'koa'; import * as Router from 'koa-router'; @@ -48,7 +49,7 @@ function createServer() { }); return http2.createSecureServer(certs, app.callback()); } else { - return http2.createServer(app.callback()); + return http.createServer(app.callback()); } } From 8e0949c66f5108053ebbdeb3381c358bc63f8178 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 14:28:09 +0900 Subject: [PATCH 32/34] compress --- package.json | 2 ++ src/server/index.ts | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/package.json b/package.json index eed5d63702..d82e5d2330 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/js-yaml": "3.11.1", "@types/koa": "2.0.45", "@types/koa-bodyparser": "4.2.0", + "@types/koa-compress": "^2.0.8", "@types/koa-favicon": "2.0.19", "@types/koa-mount": "3.0.1", "@types/koa-multer": "1.0.0", @@ -137,6 +138,7 @@ "jsdom": "11.7.0", "koa": "2.5.0", "koa-bodyparser": "4.2.0", + "koa-compress": "^2.0.0", "koa-favicon": "2.0.1", "koa-mount": "3.0.0", "koa-multer": "1.0.2", diff --git a/src/server/index.ts b/src/server/index.ts index a637e8598b..db41a1dcb5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,9 +5,11 @@ import * as fs from 'fs'; import * as http from 'http'; import * as http2 from 'http2'; +import * as zlib from 'zlib'; import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as mount from 'koa-mount'; +import * as compress from 'koa-compress'; import activityPub from './activitypub'; import webFinger from './webfinger'; @@ -17,6 +19,10 @@ import config from '../config'; const app = new Koa(); app.proxy = true; +app.use(compress({ + flush: zlib.constants.Z_SYNC_FLUSH +})); + // HSTS // 6months (15552000sec) if (config.url.startsWith('https')) { From a9080f87ad07c1960e7865801f9fdea5b8e8514a Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 14:29:01 +0900 Subject: [PATCH 33/34] Fix bug --- src/server/activitypub.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index ed0311af9d..acd10b7886 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -44,7 +44,7 @@ router.post('/users/:user/inbox', ctx => { router.get('/notes/:note', async (ctx, next) => { const accepted = ctx.accepts('html', 'application/activity+json', 'application/ld+json'); if (!['application/activity+json', 'application/ld+json'].includes(accepted as string)) { - next(); + await next(); return; } From 17f4dd69a38097d92d60829e37cacdbb27b61f94 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 13 Apr 2018 14:39:08 +0900 Subject: [PATCH 34/34] Refactor --- src/remote/activitypub/renderer/context.ts | 5 ----- src/remote/activitypub/renderer/index.ts | 7 +++++++ src/server/activitypub.ts | 21 +++++---------------- src/services/following/create.ts | 10 +++------- src/services/following/delete.ts | 6 ++---- src/services/note/create.ts | 5 ++--- src/services/note/reaction/create.ts | 6 ++---- 7 files changed, 21 insertions(+), 39 deletions(-) delete mode 100644 src/remote/activitypub/renderer/context.ts create mode 100644 src/remote/activitypub/renderer/index.ts diff --git a/src/remote/activitypub/renderer/context.ts b/src/remote/activitypub/renderer/context.ts deleted file mode 100644 index b56f727ae7..0000000000 --- a/src/remote/activitypub/renderer/context.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { Hashtag: 'as:Hashtag' } -]; diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts new file mode 100644 index 0000000000..ee7f496162 --- /dev/null +++ b/src/remote/activitypub/renderer/index.ts @@ -0,0 +1,7 @@ +export default (x: any) => Object.assign({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { Hashtag: 'as:Hashtag' } + ] +}, x); diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index acd10b7886..2a99bccfc4 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -2,8 +2,7 @@ import * as Router from 'koa-router'; import { parseRequest } from 'http-signature'; import { createHttp } from '../queue'; -import context from '../remote/activitypub/renderer/context'; -import render from '../remote/activitypub/renderer/note'; +import pack from '../remote/activitypub/renderer'; import Note from '../models/note'; import User, { isLocalUser } from '../models/user'; import renderNote from '../remote/activitypub/renderer/note'; @@ -57,10 +56,7 @@ router.get('/notes/:note', async (ctx, next) => { return; } - const rendered = await render(note); - rendered['@context'] = context; - - ctx.body = rendered; + ctx.body = pack(await renderNote(note)); }); // outbot @@ -81,9 +77,8 @@ router.get('/users/:user/outbox', async ctx => { const renderedNotes = await Promise.all(notes.map(note => renderNote(note))); const rendered = renderOrderedCollection(`${config.url}/users/${userId}/inbox`, user.notesCount, renderedNotes); - rendered['@context'] = context; - ctx.body = rendered; + ctx.body = pack(rendered); }); // publickey @@ -98,10 +93,7 @@ router.get('/users/:user/publickey', async ctx => { } if (isLocalUser(user)) { - const rendered = renderKey(user); - rendered['@context'] = context; - - ctx.body = rendered; + ctx.body = pack(renderKey(user)); } else { ctx.status = 400; } @@ -118,10 +110,7 @@ router.get('/users/:user', async ctx => { return; } - const rendered = renderPerson(user); - rendered['@context'] = context; - - ctx.body = rendered; + ctx.body = pack(renderPerson(user)); }); // follow form diff --git a/src/services/following/create.ts b/src/services/following/create.ts index 31e3be19ed..3289e31294 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -4,7 +4,7 @@ import FollowingLog from '../../models/following-log'; import FollowedLog from '../../models/followed-log'; import event from '../../publishers/stream'; import notify from '../../publishers/notify'; -import context from '../../remote/activitypub/renderer/context'; +import pack from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderAccept from '../../remote/activitypub/renderer/accept'; import { deliver } from '../../queue'; @@ -57,16 +57,12 @@ export default async function(follower: IUser, followee: IUser, activity?) { } if (isLocalUser(follower) && isRemoteUser(followee)) { - const content = renderFollow(follower, followee); - content['@context'] = context; - + const content = pack(renderFollow(follower, followee)); deliver(follower, content, followee.inbox).save(); } if (isRemoteUser(follower) && isLocalUser(followee)) { - const content = renderAccept(activity); - content['@context'] = context; - + const content = pack(renderAccept(activity)); deliver(followee, content, follower.inbox).save(); } } diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts index d79bf64f53..8b6c56816a 100644 --- a/src/services/following/delete.ts +++ b/src/services/following/delete.ts @@ -3,7 +3,7 @@ import Following from '../../models/following'; import FollowingLog from '../../models/following-log'; import FollowedLog from '../../models/followed-log'; import event from '../../publishers/stream'; -import context from '../../remote/activitypub/renderer/context'; +import pack from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; @@ -56,9 +56,7 @@ export default async function(follower: IUser, followee: IUser, activity?) { } if (isLocalUser(follower) && isRemoteUser(followee)) { - const content = renderUndo(renderFollow(follower, followee)); - content['@context'] = context; - + const content = pack(renderUndo(renderFollow(follower, followee))); deliver(follower, content, followee.inbox).save(); } } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 8f0b84bccd..b238cd5607 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -6,7 +6,7 @@ import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; -import context from '../../remote/activitypub/renderer/context'; +import packAp from '../../remote/activitypub/renderer'; import { IDriveFile } from '../../models/drive-file'; import notify from '../../publishers/notify'; import NoteWatching from '../../models/note-watching'; @@ -132,8 +132,7 @@ export default async (user: IUser, data: { const content = data.renote && data.text == null ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote)) : renderCreate(await renderNote(note)); - content['@context'] = context; - return content; + return packAp(content); }; // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 88158034f3..69a14248da 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -8,7 +8,7 @@ import NoteWatching from '../../../models/note-watching'; import watch from '../watch'; import renderLike from '../../../remote/activitypub/renderer/like'; import { deliver } from '../../../queue'; -import context from '../../../remote/activitypub/renderer/context'; +import pack from '../../../remote/activitypub/renderer'; export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => { // Myself @@ -85,9 +85,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise //#region 配信 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 if (isLocalUser(user) && isRemoteUser(note._user)) { - const content = renderLike(user, note); - content['@context'] = context; - + const content = pack(renderLike(user, note)); deliver(user, content, note._user.inbox).save(); } //#endregion