summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2018-02-05 03:59:29 +0900
committersyuilo <syuilotan@yahoo.co.jp>2018-02-05 03:59:29 +0900
commitdbd3cdb308d2edf600b20a8b632045c6163ae326 (patch)
treede3630065fcddeb1916668ef3b0b43a219340e2e /src
parentFix (diff)
parentMerge pull request #1097 from syuilo/refactor (diff)
downloadsharkey-dbd3cdb308d2edf600b20a8b632045c6163ae326.tar.gz
sharkey-dbd3cdb308d2edf600b20a8b632045c6163ae326.tar.bz2
sharkey-dbd3cdb308d2edf600b20a8b632045c6163ae326.zip
Merge remote-tracking branch 'refs/remotes/origin/master' into vue-#972
Diffstat (limited to 'src')
-rw-r--r--src/api/common/add-file-to-drive.ts74
-rw-r--r--src/api/common/notify.ts18
-rw-r--r--src/api/endpoints.ts38
-rw-r--r--src/api/endpoints/aggregation/posts/reactions.ts11
-rw-r--r--src/api/endpoints/aggregation/users.ts13
-rw-r--r--src/api/endpoints/app/create.ts6
-rw-r--r--src/api/endpoints/app/show.ts5
-rw-r--r--src/api/endpoints/auth/session/show.ts5
-rw-r--r--src/api/endpoints/auth/session/userkey.ts4
-rw-r--r--src/api/endpoints/channels.ts21
-rw-r--r--src/api/endpoints/channels/create.ts4
-rw-r--r--src/api/endpoints/channels/posts.ts21
-rw-r--r--src/api/endpoints/channels/show.ts5
-rw-r--r--src/api/endpoints/drive/files.ts21
-rw-r--r--src/api/endpoints/drive/files/create.ts17
-rw-r--r--src/api/endpoints/drive/files/find.ts5
-rw-r--r--src/api/endpoints/drive/files/show.ts5
-rw-r--r--src/api/endpoints/drive/files/update.ts6
-rw-r--r--src/api/endpoints/drive/files/upload_from_url.ts5
-rw-r--r--src/api/endpoints/drive/folders.ts21
-rw-r--r--src/api/endpoints/drive/folders/create.ts6
-rw-r--r--src/api/endpoints/drive/folders/find.ts5
-rw-r--r--src/api/endpoints/drive/folders/show.ts5
-rw-r--r--src/api/endpoints/drive/folders/update.ts6
-rw-r--r--src/api/endpoints/drive/stream.ts21
-rw-r--r--src/api/endpoints/following/create.ts7
-rw-r--r--src/api/endpoints/following/delete.ts5
-rw-r--r--src/api/endpoints/i.ts5
-rw-r--r--src/api/endpoints/i/2fa/done.ts37
-rw-r--r--src/api/endpoints/i/2fa/register.ts48
-rw-r--r--src/api/endpoints/i/2fa/unregister.ts28
-rw-r--r--src/api/endpoints/i/authorized_apps.ts4
-rw-r--r--src/api/endpoints/i/favorites.ts4
-rw-r--r--src/api/endpoints/i/notifications.ts43
-rw-r--r--src/api/endpoints/i/pin.ts4
-rw-r--r--src/api/endpoints/i/signin_history.ts21
-rw-r--r--src/api/endpoints/i/update.ts6
-rw-r--r--src/api/endpoints/messaging/history.ts15
-rw-r--r--src/api/endpoints/messaging/messages.ts20
-rw-r--r--src/api/endpoints/messaging/messages/create.ts16
-rw-r--r--src/api/endpoints/messaging/unread.ts10
-rw-r--r--src/api/endpoints/mute/create.ts61
-rw-r--r--src/api/endpoints/mute/delete.ts63
-rw-r--r--src/api/endpoints/mute/list.ts73
-rw-r--r--src/api/endpoints/my/apps.ts5
-rw-r--r--src/api/endpoints/notifications/get_unread_count.ts10
-rw-r--r--src/api/endpoints/posts.ts21
-rw-r--r--src/api/endpoints/posts/context.ts5
-rw-r--r--src/api/endpoints/posts/create.ts28
-rw-r--r--src/api/endpoints/posts/mentions.ts20
-rw-r--r--src/api/endpoints/posts/polls/recommendation.ts5
-rw-r--r--src/api/endpoints/posts/reactions.ts5
-rw-r--r--src/api/endpoints/posts/reactions/create.ts9
-rw-r--r--src/api/endpoints/posts/replies.ts5
-rw-r--r--src/api/endpoints/posts/reposts.ts21
-rw-r--r--src/api/endpoints/posts/search.ts394
-rw-r--r--src/api/endpoints/posts/show.ts5
-rw-r--r--src/api/endpoints/posts/timeline.ts55
-rw-r--r--src/api/endpoints/posts/trend.ts5
-rw-r--r--src/api/endpoints/users.ts21
-rw-r--r--src/api/endpoints/users/followers.ts4
-rw-r--r--src/api/endpoints/users/following.ts4
-rw-r--r--src/api/endpoints/users/get_frequently_replied_users.ts5
-rw-r--r--src/api/endpoints/users/posts.ts31
-rw-r--r--src/api/endpoints/users/recommendation.ts5
-rw-r--r--src/api/endpoints/users/search.ts7
-rw-r--r--src/api/endpoints/users/search_by_username.ts5
-rw-r--r--src/api/endpoints/users/show.ts5
-rw-r--r--src/api/models/app.ts96
-rw-r--r--src/api/models/auth-session.ts44
-rw-r--r--src/api/models/channel.ts66
-rw-r--r--src/api/models/drive-file.ts91
-rw-r--r--src/api/models/drive-folder.ts69
-rw-r--r--src/api/models/messaging-message.ts71
-rw-r--r--src/api/models/mute.ts3
-rw-r--r--src/api/models/notification.ts64
-rw-r--r--src/api/models/post-reaction.ts50
-rw-r--r--src/api/models/post.ts193
-rw-r--r--src/api/models/signin.ts28
-rw-r--r--src/api/models/user.ts199
-rw-r--r--src/api/private/signin.ts32
-rw-r--r--src/api/private/signup.ts6
-rw-r--r--src/api/serializers/app.ts83
-rw-r--r--src/api/serializers/auth-session.ts40
-rw-r--r--src/api/serializers/channel.ts66
-rw-r--r--src/api/serializers/drive-file.ts74
-rw-r--r--src/api/serializers/drive-folder.ts64
-rw-r--r--src/api/serializers/drive-tag.ts35
-rw-r--r--src/api/serializers/messaging-message.ts68
-rw-r--r--src/api/serializers/notification.ts65
-rw-r--r--src/api/serializers/post-reaction.ts43
-rw-r--r--src/api/serializers/post.ts192
-rw-r--r--src/api/serializers/signin.ts23
-rw-r--r--src/api/serializers/user.ts173
-rw-r--r--src/api/server.ts11
-rw-r--r--src/api/service/twitter.ts74
-rw-r--r--src/api/stream/home.ts37
-rw-r--r--src/common/build/fa.ts57
-rw-r--r--src/common/build/i18n.ts50
-rw-r--r--src/common/build/license.ts13
-rw-r--r--src/config.ts4
-rw-r--r--src/const.json1
-rw-r--r--src/db/mongodb.ts15
-rw-r--r--src/docs/api/entities/post.pug149
-rw-r--r--src/docs/api/entities/user.pug122
-rw-r--r--src/docs/api/getting-started.md73
-rw-r--r--src/docs/api/library.md8
-rw-r--r--src/docs/index.md4
-rw-r--r--src/docs/link-to-twitter.md9
-rw-r--r--src/docs/tou.md4
-rw-r--r--src/file/server.ts10
-rw-r--r--src/utils/dependencyInfo.ts2
-rw-r--r--src/web/app/app.styl38
-rw-r--r--src/web/app/base.pug3
-rw-r--r--src/web/app/common/scripts/api.ts2
-rw-r--r--src/web/app/common/scripts/parse-search-query.ts53
-rw-r--r--src/web/app/common/tags/authorized-apps.tag4
-rw-r--r--src/web/app/common/tags/copyright.tag7
-rw-r--r--src/web/app/common/tags/index.ts1
-rw-r--r--src/web/app/common/tags/introduction.tag2
-rw-r--r--src/web/app/common/tags/messaging/room.tag2
-rw-r--r--src/web/app/common/tags/nav-links.tag5
-rw-r--r--src/web/app/common/tags/signin-history.tag120
-rw-r--r--src/web/app/common/tags/signin.tag11
-rw-r--r--src/web/app/common/tags/signup.tag4
-rw-r--r--src/web/app/common/tags/twitter-setting.tag2
-rw-r--r--src/web/app/desktop/router.ts4
-rw-r--r--src/web/app/desktop/style.styl65
-rw-r--r--src/web/app/desktop/tags/drive/browser.tag6
-rw-r--r--src/web/app/desktop/tags/drive/file.tag16
-rw-r--r--src/web/app/desktop/tags/home-widgets/mentions.tag2
-rw-r--r--src/web/app/desktop/tags/home-widgets/timeline.tag4
-rw-r--r--src/web/app/desktop/tags/image-dialog.tag61
-rw-r--r--src/web/app/desktop/tags/images-viewer.tag45
-rw-r--r--src/web/app/desktop/tags/images.tag172
-rw-r--r--src/web/app/desktop/tags/index.ts3
-rw-r--r--src/web/app/desktop/tags/notifications.tag2
-rw-r--r--src/web/app/desktop/tags/pages/entrance.tag6
-rw-r--r--src/web/app/desktop/tags/post-detail-sub.tag11
-rw-r--r--src/web/app/desktop/tags/post-detail.tag7
-rw-r--r--src/web/app/desktop/tags/post-form.tag69
-rw-r--r--src/web/app/desktop/tags/search-posts.tag21
-rw-r--r--src/web/app/desktop/tags/select-file-from-drive-window.tag10
-rw-r--r--src/web/app/desktop/tags/settings.tag369
-rw-r--r--src/web/app/desktop/tags/sub-post-content.tag2
-rw-r--r--src/web/app/desktop/tags/timeline.tag7
-rw-r--r--src/web/app/desktop/tags/ui.tag7
-rw-r--r--src/web/app/desktop/tags/user-timeline.tag4
-rw-r--r--src/web/app/desktop/tags/user.tag27
-rw-r--r--src/web/app/desktop/ui.styl122
-rw-r--r--src/web/app/mobile/router.ts9
-rw-r--r--src/web/app/mobile/tags/drive.tag17
-rw-r--r--src/web/app/mobile/tags/drive/file-viewer.tag59
-rw-r--r--src/web/app/mobile/tags/drive/file.tag202
-rw-r--r--src/web/app/mobile/tags/drive/folder.tag60
-rw-r--r--src/web/app/mobile/tags/home-timeline.tag2
-rw-r--r--src/web/app/mobile/tags/images-viewer.tag26
-rw-r--r--src/web/app/mobile/tags/images.tag82
-rw-r--r--src/web/app/mobile/tags/index.ts3
-rw-r--r--src/web/app/mobile/tags/notifications.tag2
-rw-r--r--src/web/app/mobile/tags/page/entrance.tag4
-rw-r--r--src/web/app/mobile/tags/page/settings.tag1
-rw-r--r--src/web/app/mobile/tags/page/settings/api.tag36
-rw-r--r--src/web/app/mobile/tags/post-detail.tag2
-rw-r--r--src/web/app/mobile/tags/post-form.tag76
-rw-r--r--src/web/app/mobile/tags/search-posts.tag18
-rw-r--r--src/web/app/mobile/tags/sub-post-content.tag2
-rw-r--r--src/web/app/mobile/tags/timeline.tag2
-rw-r--r--src/web/app/mobile/tags/ui.tag6
-rw-r--r--src/web/app/mobile/tags/user-timeline.tag2
-rw-r--r--src/web/assets/code-highlight.css93
-rw-r--r--src/web/assets/recover.html2
-rw-r--r--src/web/const.styl4
-rw-r--r--src/web/docs/about.en.pug3
-rw-r--r--src/web/docs/about.ja.pug3
-rw-r--r--src/web/docs/api.ja.pug103
-rw-r--r--src/web/docs/api/endpoints/posts/create.yaml53
-rw-r--r--src/web/docs/api/endpoints/posts/timeline.yaml32
-rw-r--r--src/web/docs/api/endpoints/style.styl21
-rw-r--r--src/web/docs/api/endpoints/view.pug32
-rw-r--r--src/web/docs/api/entities/drive-file.yaml73
-rw-r--r--src/web/docs/api/entities/post.yaml124
-rw-r--r--src/web/docs/api/entities/style.styl1
-rw-r--r--src/web/docs/api/entities/user.yaml153
-rw-r--r--src/web/docs/api/entities/view.pug20
-rw-r--r--src/web/docs/api/gulpfile.ts188
-rw-r--r--src/web/docs/api/mixins.pug37
-rw-r--r--src/web/docs/api/style.styl11
-rw-r--r--src/web/docs/gulpfile.ts77
-rw-r--r--src/web/docs/index.en.pug3
-rw-r--r--src/web/docs/index.ja.pug3
-rw-r--r--src/web/docs/layout.pug41
-rw-r--r--src/web/docs/license.en.pug17
-rw-r--r--src/web/docs/license.ja.pug17
-rw-r--r--src/web/docs/mute.ja.pug13
-rw-r--r--src/web/docs/search.ja.pug116
-rw-r--r--src/web/docs/server.ts21
-rw-r--r--src/web/docs/style.styl120
-rw-r--r--src/web/docs/tou.ja.pug3
-rw-r--r--src/web/docs/ui.styl19
-rw-r--r--src/web/docs/vars.ts64
-rw-r--r--src/web/server.ts5
-rw-r--r--src/web/style.styl35
203 files changed, 4974 insertions, 2519 deletions
diff --git a/src/api/common/add-file-to-drive.ts b/src/api/common/add-file-to-drive.ts
index 2a649788af..1ee455c092 100644
--- a/src/api/common/add-file-to-drive.ts
+++ b/src/api/common/add-file-to-drive.ts
@@ -5,17 +5,21 @@ import * as stream from 'stream';
import * as mongodb from 'mongodb';
import * as crypto from 'crypto';
-import * as gm from 'gm';
+import * as _gm from 'gm';
import * as debug from 'debug';
import fileType = require('file-type');
import prominence = require('prominence');
import DriveFile, { getGridFSBucket } from '../models/drive-file';
import DriveFolder from '../models/drive-folder';
-import serialize from '../serializers/drive-file';
+import { pack } from '../models/drive-file';
import event, { publishDriveStream } from '../event';
import config from '../../conf';
+const gm = _gm.subClass({
+ imageMagick: true
+});
+
const log = debug('misskey:register-drive-file');
const tmpFile = (): Promise<string> => new Promise((resolve, reject) => {
@@ -106,8 +110,8 @@ const addFile = async (
}
}
- const [properties, folder] = await Promise.all([
- // properties
+ const [wh, averageColor, folder] = await Promise.all([
+ // Width and height (when image)
(async () => {
// 画像かどうか
if (!/^image\/.*$/.test(mime)) {
@@ -116,22 +120,49 @@ const addFile = async (
const imageType = mime.split('/')[1];
- // 画像でもPNGかJPEGでないならスキップ
- if (imageType != 'png' && imageType != 'jpeg') {
+ // 画像でもPNGかJPEGかGIFでないならスキップ
+ if (imageType != 'png' && imageType != 'jpeg' && imageType != 'gif') {
return null;
}
- // If the file is an image, calculate width and height to save in property
+ log('calculate image width and height...');
+
+ // Calculate width and height
const g = gm(fs.createReadStream(path), name);
const size = await prominence(g).size();
- const properties = {
- width: size.width,
- height: size.height
- };
- log('image width and height is calculated');
+ log(`image width and height is calculated: ${size.width}, ${size.height}`);
+
+ return [size.width, size.height];
+ })(),
+ // average color (when image)
+ (async () => {
+ // 画像かどうか
+ if (!/^image\/.*$/.test(mime)) {
+ return null;
+ }
+
+ const imageType = mime.split('/')[1];
+
+ // 画像でもPNGかJPEGでないならスキップ
+ if (imageType != 'png' && imageType != 'jpeg') {
+ return null;
+ }
+
+ log('calculate average color...');
+
+ const buffer = await prominence(gm(fs.createReadStream(path), name)
+ .setFormat('ppm')
+ .resize(1, 1)) // 1pxのサイズに縮小して平均色を取得するというハック
+ .toBuffer();
- return properties;
+ const r = buffer.readUInt8(buffer.length - 3);
+ const g = buffer.readUInt8(buffer.length - 2);
+ const b = buffer.readUInt8(buffer.length - 1);
+
+ log(`average color is calculated: ${r}, ${g}, ${b}`);
+
+ return [r, g, b];
})(),
// folder
(async () => {
@@ -181,6 +212,17 @@ const addFile = async (
const readable = fs.createReadStream(path);
+ const properties = {};
+
+ if (wh) {
+ properties['width'] = wh[0];
+ properties['height'] = wh[1];
+ }
+
+ if (averageColor) {
+ properties['average_color'] = averageColor;
+ }
+
return addToGridFS(detectedName, readable, mime, {
user_id: user._id,
folder_id: folder !== null ? folder._id : null,
@@ -224,11 +266,11 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
}
rej(new Error('un-compatible file.'));
})
- .then(([path, remove]): Promise<any> => new Promise((res, rej) => {
+ .then(([path, shouldCleanup]): Promise<any> => new Promise((res, rej) => {
addFile(user, path, ...args)
.then(file => {
res(file);
- if (remove) {
+ if (shouldCleanup) {
fs.unlink(path, (e) => {
if (e) log(e.stack);
});
@@ -240,7 +282,7 @@ export default (user: any, file: string | stream.Readable, ...args) => new Promi
log(`drive file has been created ${file._id}`);
resolve(file);
- serialize(file).then(serializedFile => {
+ pack(file).then(serializedFile => {
// Publish drive_file_created event
event(user._id, 'drive_file_created', serializedFile);
publishDriveStream(user._id, 'file_created', serializedFile);
diff --git a/src/api/common/notify.ts b/src/api/common/notify.ts
index 4b3e6a5d54..ae5669b84c 100644
--- a/src/api/common/notify.ts
+++ b/src/api/common/notify.ts
@@ -1,7 +1,8 @@
import * as mongo from 'mongodb';
import Notification from '../models/notification';
+import Mute from '../models/mute';
import event from '../event';
-import serialize from '../serializers/notification';
+import { pack } from '../models/notification';
export default (
notifiee: mongo.ObjectID,
@@ -26,13 +27,24 @@ export default (
// Publish notification event
event(notifiee, 'notification',
- await serialize(notification));
+ await pack(notification));
// 3秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
setTimeout(async () => {
const fresh = await Notification.findOne({ _id: notification._id }, { is_read: true });
if (!fresh.is_read) {
- event(notifiee, 'unread_notification', await serialize(notification));
+ //#region ただしミュートしているユーザーからの通知なら無視
+ const mute = await Mute.find({
+ muter_id: notifiee,
+ deleted_at: { $exists: false }
+ });
+ const mutedUserIds = mute.map(m => m.mutee_id.toString());
+ if (mutedUserIds.indexOf(notifier.toString()) != -1) {
+ return;
+ }
+ //#endregion
+
+ event(notifiee, 'unread_notification', await pack(notification));
}
}, 3000);
});
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index 06fb9a64ae..e846381578 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -156,6 +156,21 @@ const endpoints: Endpoint[] = [
withCredential: true
},
{
+ name: 'i/2fa/register',
+ withCredential: true,
+ secure: true
+ },
+ {
+ name: 'i/2fa/unregister',
+ withCredential: true,
+ secure: true
+ },
+ {
+ name: 'i/2fa/done',
+ withCredential: true,
+ secure: true
+ },
+ {
name: 'i/update',
withCredential: true,
limit: {
@@ -171,11 +186,13 @@ const endpoints: Endpoint[] = [
},
{
name: 'i/change_password',
- withCredential: true
+ withCredential: true,
+ secure: true
},
{
name: 'i/regenerate_token',
- withCredential: true
+ withCredential: true,
+ secure: true
},
{
name: 'i/pin',
@@ -205,6 +222,23 @@ const endpoints: Endpoint[] = [
withCredential: true,
kind: 'notification-read'
},
+
+ {
+ name: 'mute/create',
+ withCredential: true,
+ kind: 'account/write'
+ },
+ {
+ name: 'mute/delete',
+ withCredential: true,
+ kind: 'account/write'
+ },
+ {
+ name: 'mute/list',
+ withCredential: true,
+ kind: 'account/read'
+ },
+
{
name: 'notifications/get_unread_count',
withCredential: true,
diff --git a/src/api/endpoints/aggregation/posts/reactions.ts b/src/api/endpoints/aggregation/posts/reactions.ts
index 2cd4588ae1..790b523be9 100644
--- a/src/api/endpoints/aggregation/posts/reactions.ts
+++ b/src/api/endpoints/aggregation/posts/reactions.ts
@@ -35,10 +35,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
{ deleted_at: { $gt: startTime } }
]
}, {
- _id: false,
- post_id: false
- }, {
- sort: { created_at: -1 }
+ sort: {
+ _id: -1
+ },
+ fields: {
+ _id: false,
+ post_id: false
+ }
});
const graph = [];
diff --git a/src/api/endpoints/aggregation/users.ts b/src/api/endpoints/aggregation/users.ts
index 9eb2d035ec..e38ce92ff9 100644
--- a/src/api/endpoints/aggregation/users.ts
+++ b/src/api/endpoints/aggregation/users.ts
@@ -17,11 +17,14 @@ module.exports = params => new Promise(async (res, rej) => {
const users = await User
.find({}, {
- _id: false,
- created_at: true,
- deleted_at: true
- }, {
- sort: { created_at: -1 }
+ sort: {
+ _id: -1
+ },
+ fields: {
+ _id: false,
+ created_at: true,
+ deleted_at: true
+ }
});
const graph = [];
diff --git a/src/api/endpoints/app/create.ts b/src/api/endpoints/app/create.ts
index ca684de02d..0f688792a7 100644
--- a/src/api/endpoints/app/create.ts
+++ b/src/api/endpoints/app/create.ts
@@ -3,9 +3,7 @@
*/
import rndstr from 'rndstr';
import $ from 'cafy';
-import App from '../../models/app';
-import { isValidNameId } from '../../models/app';
-import serialize from '../../serializers/app';
+import App, { isValidNameId, pack } from '../../models/app';
/**
* @swagger
@@ -106,5 +104,5 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
});
// Response
- res(await serialize(app));
+ res(await pack(app));
});
diff --git a/src/api/endpoints/app/show.ts b/src/api/endpoints/app/show.ts
index 054aab8596..8bc3dda42c 100644
--- a/src/api/endpoints/app/show.ts
+++ b/src/api/endpoints/app/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import App from '../../models/app';
-import serialize from '../../serializers/app';
+import App, { pack } from '../../models/app';
/**
* @swagger
@@ -67,7 +66,7 @@ module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) =>
}
// Send response
- res(await serialize(app, user, {
+ res(await pack(app, user, {
includeSecret: isSecure && app.user_id.equals(user._id)
}));
});
diff --git a/src/api/endpoints/auth/session/show.ts b/src/api/endpoints/auth/session/show.ts
index ede8a67634..73ac3185f6 100644
--- a/src/api/endpoints/auth/session/show.ts
+++ b/src/api/endpoints/auth/session/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import AuthSess from '../../../models/auth-session';
-import serialize from '../../../serializers/auth-session';
+import AuthSess, { pack } from '../../../models/auth-session';
/**
* @swagger
@@ -67,5 +66,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
// Response
- res(await serialize(session, user));
+ res(await pack(session, user));
});
diff --git a/src/api/endpoints/auth/session/userkey.ts b/src/api/endpoints/auth/session/userkey.ts
index afd3250b04..fc989bf8c2 100644
--- a/src/api/endpoints/auth/session/userkey.ts
+++ b/src/api/endpoints/auth/session/userkey.ts
@@ -5,7 +5,7 @@ import $ from 'cafy';
import App from '../../../models/app';
import AuthSess from '../../../models/auth-session';
import AccessToken from '../../../models/access-token';
-import serialize from '../../../serializers/user';
+import { pack } from '../../../models/user';
/**
* @swagger
@@ -102,7 +102,7 @@ module.exports = (params) => new Promise(async (res, rej) => {
// Response
res({
access_token: accessToken.token,
- user: await serialize(session.user_id, null, {
+ user: await pack(session.user_id, null, {
detail: true
})
});
diff --git a/src/api/endpoints/channels.ts b/src/api/endpoints/channels.ts
index e10c943896..b9a7d1b788 100644
--- a/src/api/endpoints/channels.ts
+++ b/src/api/endpoints/channels.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Channel from '../models/channel';
-import serialize from '../serializers/channel';
+import Channel, { pack } from '../models/channel';
/**
* Get all channels
@@ -21,13 +20,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Construct query
@@ -40,9 +39,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -55,5 +54,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(channels.map(async channel =>
- await serialize(channel, me))));
+ await pack(channel, me))));
});
diff --git a/src/api/endpoints/channels/create.ts b/src/api/endpoints/channels/create.ts
index a8d7c29dc1..695b4515b3 100644
--- a/src/api/endpoints/channels/create.ts
+++ b/src/api/endpoints/channels/create.ts
@@ -4,7 +4,7 @@
import $ from 'cafy';
import Channel from '../../models/channel';
import Watching from '../../models/channel-watching';
-import serialize from '../../serializers/channel';
+import { pack } from '../../models/channel';
/**
* Create a channel
@@ -28,7 +28,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
});
// Response
- res(await serialize(channel));
+ res(await pack(channel));
// Create Watching
await Watching.insert({
diff --git a/src/api/endpoints/channels/posts.ts b/src/api/endpoints/channels/posts.ts
index 5c071a124f..d722589c20 100644
--- a/src/api/endpoints/channels/posts.ts
+++ b/src/api/endpoints/channels/posts.ts
@@ -3,8 +3,7 @@
*/
import $ from 'cafy';
import { default as Channel, IChannel } from '../../models/channel';
-import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import Post, { pack } from '../../models/post';
/**
* Show a posts of a channel
@@ -22,13 +21,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Get 'channel_id' parameter
@@ -58,9 +57,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
//#endregion Construct query
@@ -74,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(posts.map(async (post) =>
- await serialize(post, user)
+ await pack(post, user)
)));
});
diff --git a/src/api/endpoints/channels/show.ts b/src/api/endpoints/channels/show.ts
index 8861e54594..332da64675 100644
--- a/src/api/endpoints/channels/show.ts
+++ b/src/api/endpoints/channels/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import { default as Channel, IChannel } from '../../models/channel';
-import serialize from '../../serializers/channel';
+import Channel, { IChannel, pack } from '../../models/channel';
/**
* Show a channel
@@ -27,5 +26,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
// Serialize
- res(await serialize(channel, user));
+ res(await pack(channel, user));
});
diff --git a/src/api/endpoints/drive/files.ts b/src/api/endpoints/drive/files.ts
index b2e094775c..89915331ea 100644
--- a/src/api/endpoints/drive/files.ts
+++ b/src/api/endpoints/drive/files.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFile from '../../models/drive-file';
-import serialize from '../../serializers/drive-file';
+import DriveFile, { pack } from '../../models/drive-file';
/**
* Get drive files
@@ -22,13 +21,13 @@ module.exports = async (params, user, app) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) throw 'invalid since_id param';
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) throw 'invalid max_id param';
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) throw 'invalid until_id param';
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- throw 'cannot set since_id and max_id';
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ throw 'cannot set since_id and until_id';
}
// Get 'folder_id' parameter
@@ -52,9 +51,9 @@ module.exports = async (params, user, app) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
if (type) {
@@ -69,6 +68,6 @@ module.exports = async (params, user, app) => {
});
// Serialize
- const _files = await Promise.all(files.map(file => serialize(file)));
+ const _files = await Promise.all(files.map(file => pack(file)));
return _files;
};
diff --git a/src/api/endpoints/drive/files/create.ts b/src/api/endpoints/drive/files/create.ts
index 7546eca309..96bcace886 100644
--- a/src/api/endpoints/drive/files/create.ts
+++ b/src/api/endpoints/drive/files/create.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { validateFileName, pack } from '../../../models/drive-file';
import create from '../../../common/add-file-to-drive';
/**
@@ -38,9 +37,15 @@ module.exports = async (file, params, user): Promise<any> => {
const [folderId = null, folderIdErr] = $(params.folder_id).optional.nullable.id().$;
if (folderIdErr) throw 'invalid folder_id param';
- // Create file
- const driveFile = await create(user, file.path, name, null, folderId);
+ try {
+ // Create file
+ const driveFile = await create(user, file.path, name, null, folderId);
- // Serialize
- return serialize(driveFile);
+ // Serialize
+ return pack(driveFile);
+ } catch (e) {
+ console.error(e);
+
+ throw e;
+ }
};
diff --git a/src/api/endpoints/drive/files/find.ts b/src/api/endpoints/drive/files/find.ts
index a1cdf1643e..e026afe936 100644
--- a/src/api/endpoints/drive/files/find.ts
+++ b/src/api/endpoints/drive/files/find.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import DriveFile, { pack } from '../../../models/drive-file';
/**
* Find a file(s)
@@ -31,5 +30,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(files.map(async file =>
- await serialize(file))));
+ await pack(file))));
});
diff --git a/src/api/endpoints/drive/files/show.ts b/src/api/endpoints/drive/files/show.ts
index 3c7cf774f9..21664f7ba4 100644
--- a/src/api/endpoints/drive/files/show.ts
+++ b/src/api/endpoints/drive/files/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import DriveFile, { pack } from '../../../models/drive-file';
/**
* Show a file
@@ -29,7 +28,7 @@ module.exports = async (params, user) => {
}
// Serialize
- const _file = await serialize(file, {
+ const _file = await pack(file, {
detail: true
});
diff --git a/src/api/endpoints/drive/files/update.ts b/src/api/endpoints/drive/files/update.ts
index f39a420d6e..83da462113 100644
--- a/src/api/endpoints/drive/files/update.ts
+++ b/src/api/endpoints/drive/files/update.ts
@@ -3,9 +3,7 @@
*/
import $ from 'cafy';
import DriveFolder from '../../../models/drive-folder';
-import DriveFile from '../../../models/drive-file';
-import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import DriveFile, { validateFileName, pack } from '../../../models/drive-file';
import { publishDriveStream } from '../../../event';
/**
@@ -67,7 +65,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- const fileObj = await serialize(file);
+ const fileObj = await pack(file);
// Response
res(fileObj);
diff --git a/src/api/endpoints/drive/files/upload_from_url.ts b/src/api/endpoints/drive/files/upload_from_url.ts
index 519e0bdf65..68428747ef 100644
--- a/src/api/endpoints/drive/files/upload_from_url.ts
+++ b/src/api/endpoints/drive/files/upload_from_url.ts
@@ -3,8 +3,7 @@
*/
import * as URL from 'url';
import $ from 'cafy';
-import { validateFileName } from '../../../models/drive-file';
-import serialize from '../../../serializers/drive-file';
+import { validateFileName, pack } from '../../../models/drive-file';
import create from '../../../common/add-file-to-drive';
import * as debug from 'debug';
import * as tmp from 'tmp';
@@ -63,5 +62,5 @@ module.exports = async (params, user): Promise<any> => {
if (e) log(e.stack);
});
- return serialize(driveFile);
+ return pack(driveFile);
};
diff --git a/src/api/endpoints/drive/folders.ts b/src/api/endpoints/drive/folders.ts
index d49ef0af03..428bde3507 100644
--- a/src/api/endpoints/drive/folders.ts
+++ b/src/api/endpoints/drive/folders.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFolder from '../../models/drive-folder';
-import serialize from '../../serializers/drive-folder';
+import DriveFolder, { pack } from '../../models/drive-folder';
/**
* Get drive folders
@@ -22,13 +21,13 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Get 'folder_id' parameter
@@ -48,9 +47,9 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -63,5 +62,5 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(folders.map(async folder =>
- await serialize(folder))));
+ await pack(folder))));
});
diff --git a/src/api/endpoints/drive/folders/create.ts b/src/api/endpoints/drive/folders/create.ts
index be847b2153..03f396ddc9 100644
--- a/src/api/endpoints/drive/folders/create.ts
+++ b/src/api/endpoints/drive/folders/create.ts
@@ -2,9 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName } from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
import { publishDriveStream } from '../../../event';
/**
@@ -47,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- const folderObj = await serialize(folder);
+ const folderObj = await pack(folder);
// Response
res(folderObj);
diff --git a/src/api/endpoints/drive/folders/find.ts b/src/api/endpoints/drive/folders/find.ts
index a5eb8e015d..fc84766bc8 100644
--- a/src/api/endpoints/drive/folders/find.ts
+++ b/src/api/endpoints/drive/folders/find.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import DriveFolder, { pack } from '../../../models/drive-folder';
/**
* Find a folder(s)
@@ -30,5 +29,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- res(await Promise.all(folders.map(folder => serialize(folder))));
+ res(await Promise.all(folders.map(folder => pack(folder))));
});
diff --git a/src/api/endpoints/drive/folders/show.ts b/src/api/endpoints/drive/folders/show.ts
index 9b1c04ca3c..e07d14d20d 100644
--- a/src/api/endpoints/drive/folders/show.ts
+++ b/src/api/endpoints/drive/folders/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import DriveFolder, { pack } from '../../../models/drive-folder';
/**
* Show a folder
@@ -29,7 +28,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
// Serialize
- res(await serialize(folder, {
+ res(await pack(folder, {
detail: true
}));
});
diff --git a/src/api/endpoints/drive/folders/update.ts b/src/api/endpoints/drive/folders/update.ts
index ff673402ab..d3df8bdae5 100644
--- a/src/api/endpoints/drive/folders/update.ts
+++ b/src/api/endpoints/drive/folders/update.ts
@@ -2,9 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFolder from '../../../models/drive-folder';
-import { isValidFolderName } from '../../../models/drive-folder';
-import serialize from '../../../serializers/drive-folder';
+import DriveFolder, { isValidFolderName, pack } from '../../../models/drive-folder';
import { publishDriveStream } from '../../../event';
/**
@@ -91,7 +89,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- const folderObj = await serialize(folder);
+ const folderObj = await pack(folder);
// Response
res(folderObj);
diff --git a/src/api/endpoints/drive/stream.ts b/src/api/endpoints/drive/stream.ts
index 7ee255e5d1..8352c7dd4c 100644
--- a/src/api/endpoints/drive/stream.ts
+++ b/src/api/endpoints/drive/stream.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import DriveFile from '../../models/drive-file';
-import serialize from '../../serializers/drive-file';
+import DriveFile, { pack } from '../../models/drive-file';
/**
* Get drive stream
@@ -21,13 +20,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Get 'type' parameter
@@ -46,9 +45,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
if (type) {
@@ -64,5 +63,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(files.map(async file =>
- await serialize(file))));
+ await pack(file))));
});
diff --git a/src/api/endpoints/following/create.ts b/src/api/endpoints/following/create.ts
index b4a2217b16..8e1aa34713 100644
--- a/src/api/endpoints/following/create.ts
+++ b/src/api/endpoints/following/create.ts
@@ -2,11 +2,10 @@
* Module dependencies
*/
import $ from 'cafy';
-import User from '../../models/user';
+import User, { pack as packUser } from '../../models/user';
import Following from '../../models/following';
import notify from '../../common/notify';
import event from '../../event';
-import serializeUser from '../../serializers/user';
/**
* Follow a user
@@ -77,8 +76,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Publish follow event
- event(follower._id, 'follow', await serializeUser(followee, follower));
- event(followee._id, 'followed', await serializeUser(follower, followee));
+ event(follower._id, 'follow', await packUser(followee, follower));
+ event(followee._id, 'followed', await packUser(follower, followee));
// Notify
notify(followee._id, follower._id, 'follow');
diff --git a/src/api/endpoints/following/delete.ts b/src/api/endpoints/following/delete.ts
index aa1639ef6c..b68cec09dd 100644
--- a/src/api/endpoints/following/delete.ts
+++ b/src/api/endpoints/following/delete.ts
@@ -2,10 +2,9 @@
* Module dependencies
*/
import $ from 'cafy';
-import User from '../../models/user';
+import User, { pack as packUser } from '../../models/user';
import Following from '../../models/following';
import event from '../../event';
-import serializeUser from '../../serializers/user';
/**
* Unfollow a user
@@ -78,5 +77,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Publish follow event
- event(follower._id, 'unfollow', await serializeUser(followee, follower));
+ event(follower._id, 'unfollow', await packUser(followee, follower));
});
diff --git a/src/api/endpoints/i.ts b/src/api/endpoints/i.ts
index ae75f11d54..7efdbcd7c9 100644
--- a/src/api/endpoints/i.ts
+++ b/src/api/endpoints/i.ts
@@ -1,8 +1,7 @@
/**
* Module dependencies
*/
-import User from '../models/user';
-import serialize from '../serializers/user';
+import User, { pack } from '../models/user';
/**
* Show myself
@@ -15,7 +14,7 @@ import serialize from '../serializers/user';
*/
module.exports = (params, user, _, isSecure) => new Promise(async (res, rej) => {
// Serialize
- res(await serialize(user, user, {
+ res(await pack(user, user, {
detail: true,
includeSecrets: isSecure
}));
diff --git a/src/api/endpoints/i/2fa/done.ts b/src/api/endpoints/i/2fa/done.ts
new file mode 100644
index 0000000000..0b36033bb6
--- /dev/null
+++ b/src/api/endpoints/i/2fa/done.ts
@@ -0,0 +1,37 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as speakeasy from 'speakeasy';
+import User from '../../../models/user';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+ // Get 'token' parameter
+ const [token, tokenErr] = $(params.token).string().$;
+ if (tokenErr) return rej('invalid token param');
+
+ const _token = token.replace(/\s/g, '');
+
+ if (user.two_factor_temp_secret == null) {
+ return rej('二段階認証の設定が開始されていません');
+ }
+
+ const verified = (speakeasy as any).totp.verify({
+ secret: user.two_factor_temp_secret,
+ encoding: 'base32',
+ token: _token
+ });
+
+ if (!verified) {
+ return rej('not verified');
+ }
+
+ await User.update(user._id, {
+ $set: {
+ two_factor_secret: user.two_factor_temp_secret,
+ two_factor_enabled: true
+ }
+ });
+
+ res();
+});
diff --git a/src/api/endpoints/i/2fa/register.ts b/src/api/endpoints/i/2fa/register.ts
new file mode 100644
index 0000000000..c2b5037a29
--- /dev/null
+++ b/src/api/endpoints/i/2fa/register.ts
@@ -0,0 +1,48 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import * as speakeasy from 'speakeasy';
+import * as QRCode from 'qrcode';
+import User from '../../../models/user';
+import config from '../../../../conf';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+ // Get 'password' parameter
+ const [password, passwordErr] = $(params.password).string().$;
+ if (passwordErr) return rej('invalid password param');
+
+ // Compare password
+ const same = await bcrypt.compare(password, user.password);
+
+ if (!same) {
+ return rej('incorrect password');
+ }
+
+ // Generate user's secret key
+ const secret = speakeasy.generateSecret({
+ length: 32
+ });
+
+ await User.update(user._id, {
+ $set: {
+ two_factor_temp_secret: secret.base32
+ }
+ });
+
+ // Get the data URL of the authenticator URL
+ QRCode.toDataURL(speakeasy.otpauthURL({
+ secret: secret.base32,
+ encoding: 'base32',
+ label: user.username,
+ issuer: config.host
+ }), (err, data_url) => {
+ res({
+ qr: data_url,
+ secret: secret.base32,
+ label: user.username,
+ issuer: config.host
+ });
+ });
+});
diff --git a/src/api/endpoints/i/2fa/unregister.ts b/src/api/endpoints/i/2fa/unregister.ts
new file mode 100644
index 0000000000..6bee6a26f2
--- /dev/null
+++ b/src/api/endpoints/i/2fa/unregister.ts
@@ -0,0 +1,28 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import * as bcrypt from 'bcryptjs';
+import User from '../../../models/user';
+
+module.exports = async (params, user) => new Promise(async (res, rej) => {
+ // Get 'password' parameter
+ const [password, passwordErr] = $(params.password).string().$;
+ if (passwordErr) return rej('invalid password param');
+
+ // Compare password
+ const same = await bcrypt.compare(password, user.password);
+
+ if (!same) {
+ return rej('incorrect password');
+ }
+
+ await User.update(user._id, {
+ $set: {
+ two_factor_secret: null,
+ two_factor_enabled: false
+ }
+ });
+
+ res();
+});
diff --git a/src/api/endpoints/i/authorized_apps.ts b/src/api/endpoints/i/authorized_apps.ts
index 807ca5b1e7..40ce7a68c8 100644
--- a/src/api/endpoints/i/authorized_apps.ts
+++ b/src/api/endpoints/i/authorized_apps.ts
@@ -3,7 +3,7 @@
*/
import $ from 'cafy';
import AccessToken from '../../models/access-token';
-import serialize from '../../serializers/app';
+import { pack } from '../../models/app';
/**
* Get authorized apps of my account
@@ -39,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(tokens.map(async token =>
- await serialize(token.app_id))));
+ await pack(token.app_id))));
});
diff --git a/src/api/endpoints/i/favorites.ts b/src/api/endpoints/i/favorites.ts
index a66eaa7546..eb464cf0f0 100644
--- a/src/api/endpoints/i/favorites.ts
+++ b/src/api/endpoints/i/favorites.ts
@@ -3,7 +3,7 @@
*/
import $ from 'cafy';
import Favorite from '../../models/favorite';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
/**
* Get followers of a user
@@ -39,6 +39,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(favorites.map(async favorite =>
- await serialize(favorite.post)
+ await pack(favorite.post)
)));
});
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 607e0768a4..688039a0dd 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -3,7 +3,8 @@
*/
import $ from 'cafy';
import Notification from '../../models/notification';
-import serialize from '../../serializers/notification';
+import Mute from '../../models/mute';
+import { pack } from '../../models/notification';
import getFriends from '../../common/get-friends';
import read from '../../common/read-notification';
@@ -36,17 +37,27 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
+ const mute = await Mute.find({
+ muter_id: user._id,
+ deleted_at: { $exists: false }
+ });
+
const query = {
- notifiee_id: user._id
+ notifiee_id: user._id,
+ $and: [{
+ notifier_id: {
+ $nin: mute.map(m => m.mutee_id)
+ }
+ }]
} as any;
const sort = {
@@ -54,12 +65,14 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
};
if (following) {
- // ID list of the user $self and other users who the user follows
+ // ID list of the user itself and other users who the user follows
const followingIds = await getFriends(user._id);
- query.notifier_id = {
- $in: followingIds
- };
+ query.$and.push({
+ notifier_id: {
+ $in: followingIds
+ }
+ });
}
if (type) {
@@ -73,9 +86,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -88,7 +101,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(notifications.map(async notification =>
- await serialize(notification))));
+ await pack(notification))));
// Mark as read all
if (notifications.length > 0 && markAsRead) {
diff --git a/src/api/endpoints/i/pin.ts b/src/api/endpoints/i/pin.ts
index a94950d22b..ff546fc2bd 100644
--- a/src/api/endpoints/i/pin.ts
+++ b/src/api/endpoints/i/pin.ts
@@ -4,7 +4,7 @@
import $ from 'cafy';
import User from '../../models/user';
import Post from '../../models/post';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
/**
* Pin post
@@ -35,7 +35,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- const iObj = await serialize(user, user, {
+ const iObj = await pack(user, user, {
detail: true
});
diff --git a/src/api/endpoints/i/signin_history.ts b/src/api/endpoints/i/signin_history.ts
index 1a6e50c7c8..859e81653d 100644
--- a/src/api/endpoints/i/signin_history.ts
+++ b/src/api/endpoints/i/signin_history.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Signin from '../../models/signin';
-import serialize from '../../serializers/signin';
+import Signin, { pack } from '../../models/signin';
/**
* Get signin history of my account
@@ -21,13 +20,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
const query = {
@@ -43,9 +42,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -58,5 +57,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(history.map(async record =>
- await serialize(record))));
+ await pack(record))));
});
diff --git a/src/api/endpoints/i/update.ts b/src/api/endpoints/i/update.ts
index c484c51a96..7bbbf95900 100644
--- a/src/api/endpoints/i/update.ts
+++ b/src/api/endpoints/i/update.ts
@@ -2,9 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import User from '../../models/user';
-import { isValidName, isValidDescription, isValidLocation, isValidBirthday } from '../../models/user';
-import serialize from '../../serializers/user';
+import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../models/user';
import event from '../../event';
import config from '../../../conf';
@@ -65,7 +63,7 @@ module.exports = async (params, user, _, isSecure) => new Promise(async (res, re
});
// Serialize
- const iObj = await serialize(user, user, {
+ const iObj = await pack(user, user, {
detail: true,
includeSecrets: isSecure
});
diff --git a/src/api/endpoints/messaging/history.ts b/src/api/endpoints/messaging/history.ts
index 5f7c9276dd..1683ca7a89 100644
--- a/src/api/endpoints/messaging/history.ts
+++ b/src/api/endpoints/messaging/history.ts
@@ -3,7 +3,8 @@
*/
import $ from 'cafy';
import History from '../../models/messaging-history';
-import serialize from '../../serializers/messaging-message';
+import Mute from '../../models/mute';
+import { pack } from '../../models/messaging-message';
/**
* Show messaging history
@@ -17,10 +18,18 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
+ const mute = await Mute.find({
+ muter_id: user._id,
+ deleted_at: { $exists: false }
+ });
+
// Get history
const history = await History
.find({
- user_id: user._id
+ user_id: user._id,
+ partner: {
+ $nin: mute.map(m => m.mutee_id)
+ }
}, {
limit: limit,
sort: {
@@ -30,5 +39,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(history.map(async h =>
- await serialize(h.message, user))));
+ await pack(h.message, user))));
});
diff --git a/src/api/endpoints/messaging/messages.ts b/src/api/endpoints/messaging/messages.ts
index 7b270924eb..67ba5e9d6d 100644
--- a/src/api/endpoints/messaging/messages.ts
+++ b/src/api/endpoints/messaging/messages.ts
@@ -4,7 +4,7 @@
import $ from 'cafy';
import Message from '../../models/messaging-message';
import User from '../../models/user';
-import serialize from '../../serializers/messaging-message';
+import { pack } from '../../models/messaging-message';
import read from '../../common/read-messaging-message';
/**
@@ -44,13 +44,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
const query = {
@@ -72,9 +72,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -87,7 +87,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(messages.map(async message =>
- await serialize(message, user, {
+ await pack(message, user, {
populateRecipient: false
}))));
diff --git a/src/api/endpoints/messaging/messages/create.ts b/src/api/endpoints/messaging/messages/create.ts
index 3c7689f967..1b8a5f59e6 100644
--- a/src/api/endpoints/messaging/messages/create.ts
+++ b/src/api/endpoints/messaging/messages/create.ts
@@ -6,8 +6,9 @@ import Message from '../../../models/messaging-message';
import { isValidText } from '../../../models/messaging-message';
import History from '../../../models/messaging-history';
import User from '../../../models/user';
+import Mute from '../../../models/mute';
import DriveFile from '../../../models/drive-file';
-import serialize from '../../../serializers/messaging-message';
+import { pack } from '../../../models/messaging-message';
import publishUserStream from '../../../event';
import { publishMessagingStream, publishMessagingIndexStream, pushSw } from '../../../event';
import config from '../../../../conf';
@@ -78,7 +79,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
// Serialize
- const messageObj = await serialize(message);
+ const messageObj = await pack(message);
// Reponse
res(messageObj);
@@ -97,6 +98,17 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
setTimeout(async () => {
const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
if (!freshMessage.is_read) {
+ //#region ただしミュートされているなら発行しない
+ const mute = await Mute.find({
+ muter_id: recipient._id,
+ deleted_at: { $exists: false }
+ });
+ const mutedUserIds = mute.map(m => m.mutee_id.toString());
+ if (mutedUserIds.indexOf(user._id.toString()) != -1) {
+ return;
+ }
+ //#endregion
+
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
pushSw(message.recipient_id, 'unread_messaging_message', messageObj);
}
diff --git a/src/api/endpoints/messaging/unread.ts b/src/api/endpoints/messaging/unread.ts
index 40bc83fe1c..c4326e1d22 100644
--- a/src/api/endpoints/messaging/unread.ts
+++ b/src/api/endpoints/messaging/unread.ts
@@ -2,6 +2,7 @@
* Module dependencies
*/
import Message from '../../models/messaging-message';
+import Mute from '../../models/mute';
/**
* Get count of unread messages
@@ -11,8 +12,17 @@ import Message from '../../models/messaging-message';
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
+ const mute = await Mute.find({
+ muter_id: user._id,
+ deleted_at: { $exists: false }
+ });
+ const mutedUserIds = mute.map(m => m.mutee_id);
+
const count = await Message
.count({
+ user_id: {
+ $nin: mutedUserIds
+ },
recipient_id: user._id,
is_read: false
});
diff --git a/src/api/endpoints/mute/create.ts b/src/api/endpoints/mute/create.ts
new file mode 100644
index 0000000000..f44854ab52
--- /dev/null
+++ b/src/api/endpoints/mute/create.ts
@@ -0,0 +1,61 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+
+/**
+ * Mute a user
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+ const muter = user;
+
+ // Get 'user_id' parameter
+ const [userId, userIdErr] = $(params.user_id).id().$;
+ if (userIdErr) return rej('invalid user_id param');
+
+ // 自分自身
+ if (user._id.equals(userId)) {
+ return rej('mutee is yourself');
+ }
+
+ // Get mutee
+ const mutee = await User.findOne({
+ _id: userId
+ }, {
+ fields: {
+ data: false,
+ profile: false
+ }
+ });
+
+ if (mutee === null) {
+ return rej('user not found');
+ }
+
+ // Check if already muting
+ const exist = await Mute.findOne({
+ muter_id: muter._id,
+ mutee_id: mutee._id,
+ deleted_at: { $exists: false }
+ });
+
+ if (exist !== null) {
+ return rej('already muting');
+ }
+
+ // Create mute
+ await Mute.insert({
+ created_at: new Date(),
+ muter_id: muter._id,
+ mutee_id: mutee._id,
+ });
+
+ // Send response
+ res();
+});
diff --git a/src/api/endpoints/mute/delete.ts b/src/api/endpoints/mute/delete.ts
new file mode 100644
index 0000000000..d6bff3353a
--- /dev/null
+++ b/src/api/endpoints/mute/delete.ts
@@ -0,0 +1,63 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+
+/**
+ * Unmute a user
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+ const muter = user;
+
+ // Get 'user_id' parameter
+ const [userId, userIdErr] = $(params.user_id).id().$;
+ if (userIdErr) return rej('invalid user_id param');
+
+ // Check if the mutee is yourself
+ if (user._id.equals(userId)) {
+ return rej('mutee is yourself');
+ }
+
+ // Get mutee
+ const mutee = await User.findOne({
+ _id: userId
+ }, {
+ fields: {
+ data: false,
+ profile: false
+ }
+ });
+
+ if (mutee === null) {
+ return rej('user not found');
+ }
+
+ // Check not muting
+ const exist = await Mute.findOne({
+ muter_id: muter._id,
+ mutee_id: mutee._id,
+ deleted_at: { $exists: false }
+ });
+
+ if (exist === null) {
+ return rej('already not muting');
+ }
+
+ // Delete mute
+ await Mute.update({
+ _id: exist._id
+ }, {
+ $set: {
+ deleted_at: new Date()
+ }
+ });
+
+ // Send response
+ res();
+});
diff --git a/src/api/endpoints/mute/list.ts b/src/api/endpoints/mute/list.ts
new file mode 100644
index 0000000000..19e3b157e6
--- /dev/null
+++ b/src/api/endpoints/mute/list.ts
@@ -0,0 +1,73 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Mute from '../../models/mute';
+import { pack } from '../../models/user';
+import getFriends from '../../common/get-friends';
+
+/**
+ * Get muted users of a user
+ *
+ * @param {any} params
+ * @param {any} me
+ * @return {Promise<any>}
+ */
+module.exports = (params, me) => new Promise(async (res, rej) => {
+ // Get 'iknow' parameter
+ const [iknow = false, iknowErr] = $(params.iknow).optional.boolean().$;
+ if (iknowErr) return rej('invalid iknow param');
+
+ // Get 'limit' parameter
+ const [limit = 30, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+ if (limitErr) return rej('invalid limit param');
+
+ // Get 'cursor' parameter
+ const [cursor = null, cursorErr] = $(params.cursor).optional.id().$;
+ if (cursorErr) return rej('invalid cursor param');
+
+ // Construct query
+ const query = {
+ muter_id: me._id,
+ deleted_at: { $exists: false }
+ } as any;
+
+ if (iknow) {
+ // Get my friends
+ const myFriends = await getFriends(me._id);
+
+ query.mutee_id = {
+ $in: myFriends
+ };
+ }
+
+ // カーソルが指定されている場合
+ if (cursor) {
+ query._id = {
+ $lt: cursor
+ };
+ }
+
+ // Get mutes
+ const mutes = await Mute
+ .find(query, {
+ limit: limit + 1,
+ sort: { _id: -1 }
+ });
+
+ // 「次のページ」があるかどうか
+ const inStock = mutes.length === limit + 1;
+ if (inStock) {
+ mutes.pop();
+ }
+
+ // Serialize
+ const users = await Promise.all(mutes.map(async m =>
+ await pack(m.mutee_id, me, { detail: true })));
+
+ // Response
+ res({
+ users: users,
+ next: inStock ? mutes[mutes.length - 1]._id : null,
+ });
+});
diff --git a/src/api/endpoints/my/apps.ts b/src/api/endpoints/my/apps.ts
index eb9c758768..b236190506 100644
--- a/src/api/endpoints/my/apps.ts
+++ b/src/api/endpoints/my/apps.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import App from '../../models/app';
-import serialize from '../../serializers/app';
+import App, { pack } from '../../models/app';
/**
* Get my apps
@@ -37,5 +36,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Reply
res(await Promise.all(apps.map(async app =>
- await serialize(app))));
+ await pack(app))));
});
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts
index 9514e78713..845d6b29ce 100644
--- a/src/api/endpoints/notifications/get_unread_count.ts
+++ b/src/api/endpoints/notifications/get_unread_count.ts
@@ -2,6 +2,7 @@
* Module dependencies
*/
import Notification from '../../models/notification';
+import Mute from '../../models/mute';
/**
* Get count of unread notifications
@@ -11,9 +12,18 @@ import Notification from '../../models/notification';
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
+ const mute = await Mute.find({
+ muter_id: user._id,
+ deleted_at: { $exists: false }
+ });
+ const mutedUserIds = mute.map(m => m.mutee_id);
+
const count = await Notification
.count({
notifiee_id: user._id,
+ notifier_id: {
+ $nin: mutedUserIds
+ },
is_read: false
});
diff --git a/src/api/endpoints/posts.ts b/src/api/endpoints/posts.ts
index f6efcc108d..3b29425927 100644
--- a/src/api/endpoints/posts.ts
+++ b/src/api/endpoints/posts.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Post from '../models/post';
-import serialize from '../serializers/post';
+import Post, { pack } from '../models/post';
/**
* Lists all posts
@@ -36,13 +35,13 @@ module.exports = (params) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Construct query
@@ -55,9 +54,9 @@ module.exports = (params) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -85,5 +84,5 @@ module.exports = (params) => new Promise(async (res, rej) => {
});
// Serialize
- res(await Promise.all(posts.map(async post => await serialize(post))));
+ res(await Promise.all(posts.map(async post => await pack(post))));
});
diff --git a/src/api/endpoints/posts/context.ts b/src/api/endpoints/posts/context.ts
index bad59a6bee..5ba3758975 100644
--- a/src/api/endpoints/posts/context.ts
+++ b/src/api/endpoints/posts/context.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import Post, { pack } from '../../models/post';
/**
* Show a context of a post
@@ -60,5 +59,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(context.map(async post =>
- await serialize(post, user))));
+ await pack(post, user))));
});
diff --git a/src/api/endpoints/posts/create.ts b/src/api/endpoints/posts/create.ts
index ae4959dae4..0fa52221f9 100644
--- a/src/api/endpoints/posts/create.ts
+++ b/src/api/endpoints/posts/create.ts
@@ -8,10 +8,11 @@ import { default as Post, IPost, isValidText } from '../../models/post';
import { default as User, IUser } from '../../models/user';
import { default as Channel, IChannel } from '../../models/channel';
import Following from '../../models/following';
+import Mute from '../../models/mute';
import DriveFile from '../../models/drive-file';
import Watching from '../../models/post-watching';
import ChannelWatching from '../../models/channel-watching';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
import notify from '../../common/notify';
import watch from '../../common/watch-post';
import event, { pushSw, publishChannelStream } from '../../event';
@@ -215,14 +216,20 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
poll: poll,
text: text,
user_id: user._id,
- app_id: app ? app._id : null
+ app_id: app ? app._id : null,
+
+ // 以下非正規化データ
+ _reply: reply ? { user_id: reply.user_id } : undefined,
+ _repost: repost ? { user_id: repost.user_id } : undefined,
});
// Serialize
- const postObj = await serialize(post);
+ const postObj = await pack(post);
// Reponse
- res(postObj);
+ res({
+ created_post: postObj
+ });
//#region Post processes
@@ -234,7 +241,7 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
const mentions = [];
- function addMention(mentionee, reason) {
+ async function addMention(mentionee, reason) {
// Reject if already added
if (mentions.some(x => x.equals(mentionee))) return;
@@ -243,8 +250,15 @@ module.exports = (params, user: IUser, app) => new Promise(async (res, rej) => {
// Publish event
if (!user._id.equals(mentionee)) {
- event(mentionee, reason, postObj);
- pushSw(mentionee, reason, postObj);
+ const mentioneeMutes = await Mute.find({
+ muter_id: mentionee,
+ deleted_at: { $exists: false }
+ });
+ const mentioneesMutedUserIds = mentioneeMutes.map(m => m.mutee_id.toString());
+ if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
+ event(mentionee, reason, postObj);
+ pushSw(mentionee, reason, postObj);
+ }
}
}
diff --git a/src/api/endpoints/posts/mentions.ts b/src/api/endpoints/posts/mentions.ts
index 0ebe8be503..7127db0ad1 100644
--- a/src/api/endpoints/posts/mentions.ts
+++ b/src/api/endpoints/posts/mentions.ts
@@ -4,7 +4,7 @@
import $ from 'cafy';
import Post from '../../models/post';
import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
/**
* Get mentions of myself
@@ -27,13 +27,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Construct query
@@ -58,9 +58,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -73,6 +73,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(mentions.map(async mention =>
- await serialize(mention, user)
+ await pack(mention, user)
)));
});
diff --git a/src/api/endpoints/posts/polls/recommendation.ts b/src/api/endpoints/posts/polls/recommendation.ts
index 9c92d6cac4..4a3fa3f55e 100644
--- a/src/api/endpoints/posts/polls/recommendation.ts
+++ b/src/api/endpoints/posts/polls/recommendation.ts
@@ -3,8 +3,7 @@
*/
import $ from 'cafy';
import Vote from '../../../models/poll-vote';
-import Post from '../../../models/post';
-import serialize from '../../../serializers/post';
+import Post, { pack } from '../../../models/post';
/**
* Get recommended polls
@@ -56,5 +55,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(posts.map(async post =>
- await serialize(post, user, { detail: true }))));
+ await pack(post, user, { detail: true }))));
});
diff --git a/src/api/endpoints/posts/reactions.ts b/src/api/endpoints/posts/reactions.ts
index eab5d9b258..feb140ab41 100644
--- a/src/api/endpoints/posts/reactions.ts
+++ b/src/api/endpoints/posts/reactions.ts
@@ -3,8 +3,7 @@
*/
import $ from 'cafy';
import Post from '../../models/post';
-import Reaction from '../../models/post-reaction';
-import serialize from '../../serializers/post-reaction';
+import Reaction, { pack } from '../../models/post-reaction';
/**
* Show reactions of a post
@@ -54,5 +53,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(reactions.map(async reaction =>
- await serialize(reaction, user))));
+ await pack(reaction, user))));
});
diff --git a/src/api/endpoints/posts/reactions/create.ts b/src/api/endpoints/posts/reactions/create.ts
index d537463dfe..0b0e0e294d 100644
--- a/src/api/endpoints/posts/reactions/create.ts
+++ b/src/api/endpoints/posts/reactions/create.ts
@@ -3,13 +3,12 @@
*/
import $ from 'cafy';
import Reaction from '../../../models/post-reaction';
-import Post from '../../../models/post';
+import Post, { pack as packPost } from '../../../models/post';
+import { pack as packUser } from '../../../models/user';
import Watching from '../../../models/post-watching';
import notify from '../../../common/notify';
import watch from '../../../common/watch-post';
import { publishPostStream, pushSw } from '../../../event';
-import serializePost from '../../../serializers/post';
-import serializeUser from '../../../serializers/user';
/**
* React to a post
@@ -90,8 +89,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
});
pushSw(post.user_id, 'reaction', {
- user: await serializeUser(user, post.user_id),
- post: await serializePost(post, post.user_id),
+ user: await packUser(user, post.user_id),
+ post: await packPost(post, post.user_id),
reaction: reaction
});
diff --git a/src/api/endpoints/posts/replies.ts b/src/api/endpoints/posts/replies.ts
index 3fd6a46769..613c4fa24c 100644
--- a/src/api/endpoints/posts/replies.ts
+++ b/src/api/endpoints/posts/replies.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import Post, { pack } from '../../models/post';
/**
* Show a replies of a post
@@ -50,5 +49,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(replies.map(async post =>
- await serialize(post, user))));
+ await pack(post, user))));
});
diff --git a/src/api/endpoints/posts/reposts.ts b/src/api/endpoints/posts/reposts.ts
index b701ff7574..89ab0e3d55 100644
--- a/src/api/endpoints/posts/reposts.ts
+++ b/src/api/endpoints/posts/reposts.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import Post, { pack } from '../../models/post';
/**
* Show a reposts of a post
@@ -25,13 +24,13 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Lookup post
@@ -55,9 +54,9 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -70,5 +69,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(reposts.map(async post =>
- await serialize(post, user))));
+ await pack(post, user))));
});
diff --git a/src/api/endpoints/posts/search.ts b/src/api/endpoints/posts/search.ts
index b434f64342..6e26f55390 100644
--- a/src/api/endpoints/posts/search.ts
+++ b/src/api/endpoints/posts/search.ts
@@ -1,12 +1,13 @@
/**
* Module dependencies
*/
-import * as mongo from 'mongodb';
import $ from 'cafy';
const escapeRegexp = require('escape-regexp');
import Post from '../../models/post';
-import serialize from '../../serializers/post';
-import config from '../../../conf';
+import User from '../../models/user';
+import Mute from '../../models/mute';
+import getFriends from '../../common/get-friends';
+import { pack } from '../../models/post';
/**
* Search a post
@@ -16,104 +17,339 @@ import config from '../../../conf';
* @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
- // Get 'query' parameter
- const [query, queryError] = $(params.query).string().pipe(x => x != '').$;
- if (queryError) return rej('invalid query param');
+ // Get 'text' parameter
+ const [text, textError] = $(params.text).optional.string().$;
+ if (textError) return rej('invalid text param');
+
+ // Get 'include_user_ids' parameter
+ const [includeUserIds = [], includeUserIdsErr] = $(params.include_user_ids).optional.array('id').$;
+ if (includeUserIdsErr) return rej('invalid include_user_ids param');
+
+ // Get 'exclude_user_ids' parameter
+ const [excludeUserIds = [], excludeUserIdsErr] = $(params.exclude_user_ids).optional.array('id').$;
+ if (excludeUserIdsErr) return rej('invalid exclude_user_ids param');
+
+ // Get 'include_user_usernames' parameter
+ const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.include_user_usernames).optional.array('string').$;
+ if (includeUserUsernamesErr) return rej('invalid include_user_usernames param');
+
+ // Get 'exclude_user_usernames' parameter
+ const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.exclude_user_usernames).optional.array('string').$;
+ if (excludeUserUsernamesErr) return rej('invalid exclude_user_usernames param');
+
+ // Get 'following' parameter
+ const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
+ if (followingErr) return rej('invalid following param');
+
+ // Get 'mute' parameter
+ const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$;
+ if (muteErr) return rej('invalid mute param');
+
+ // Get 'reply' parameter
+ const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
+ if (replyErr) return rej('invalid reply param');
+
+ // Get 'repost' parameter
+ const [repost = null, repostErr] = $(params.repost).optional.nullable.boolean().$;
+ if (repostErr) return rej('invalid repost param');
+
+ // Get 'media' parameter
+ const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
+ if (mediaErr) return rej('invalid media param');
+
+ // Get 'poll' parameter
+ const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$;
+ if (pollErr) return rej('invalid poll param');
+
+ // Get 'since_date' parameter
+ const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
+ if (sinceDateErr) throw 'invalid since_date param';
+
+ // Get 'until_date' parameter
+ const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+ if (untilDateErr) throw 'invalid until_date param';
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
- // Get 'max' parameter
- const [max = 10, maxErr] = $(params.max).optional.number().range(1, 30).$;
- if (maxErr) return rej('invalid max param');
+ // Get 'limit' parameter
+ const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
+ if (limitErr) return rej('invalid limit param');
+
+ let includeUsers = includeUserIds;
+ if (includeUserUsernames != null) {
+ const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
+ const _user = await User.findOne({
+ username_lower: username.toLowerCase()
+ });
+ return _user ? _user._id : null;
+ }))).filter(id => id != null);
+ includeUsers = includeUsers.concat(ids);
+ }
- // If Elasticsearch is available, search by $
- // If not, search by MongoDB
- (config.elasticsearch.enable ? byElasticsearch : byNative)
- (res, rej, me, query, offset, max);
+ let excludeUsers = excludeUserIds;
+ if (excludeUserUsernames != null) {
+ const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
+ const _user = await User.findOne({
+ username_lower: username.toLowerCase()
+ });
+ return _user ? _user._id : null;
+ }))).filter(id => id != null);
+ excludeUsers = excludeUsers.concat(ids);
+ }
+
+ search(res, rej, me, text, includeUsers, excludeUsers, following,
+ mute, reply, repost, media, poll, sinceDate, untilDate, offset, limit);
});
-// Search by MongoDB
-async function byNative(res, rej, me, query, offset, max) {
- const escapedQuery = escapeRegexp(query);
+async function search(
+ res, rej, me, text, includeUserIds, excludeUserIds, following,
+ mute, reply, repost, media, poll, sinceDate, untilDate, offset, max) {
- // Search posts
- const posts = await Post
- .find({
- text: new RegExp(escapedQuery)
- }, {
- sort: {
- _id: -1
- },
- limit: max,
- skip: offset
- });
+ let q: any = {
+ $and: []
+ };
- // Serialize
- res(await Promise.all(posts.map(async post =>
- await serialize(post, me))));
-}
+ const push = x => q.$and.push(x);
-// Search by Elasticsearch
-async function byElasticsearch(res, rej, me, query, offset, max) {
- const es = require('../../db/elasticsearch');
+ if (text) {
+ // 完全一致検索
+ if (/"""(.+?)"""/.test(text)) {
+ const x = text.match(/"""(.+?)"""/)[1];
+ push({
+ text: x
+ });
+ } else {
+ push({
+ $and: text.split(' ').map(x => ({
+ // キーワードが-で始まる場合そのキーワードを除外する
+ text: x[0] == '-' ? {
+ $not: new RegExp(escapeRegexp(x.substr(1)))
+ } : new RegExp(escapeRegexp(x))
+ }))
+ });
+ }
+ }
- es.search({
- index: 'misskey',
- type: 'post',
- body: {
- size: max,
- from: offset,
- query: {
- simple_query_string: {
- fields: ['text'],
- query: query,
- default_operator: 'and'
- }
- },
- sort: [
- { _doc: 'desc' }
- ],
- highlight: {
- pre_tags: ['<mark>'],
- post_tags: ['</mark>'],
- encoder: 'html',
- fields: {
- text: {}
- }
+ if (includeUserIds && includeUserIds.length != 0) {
+ push({
+ user_id: {
+ $in: includeUserIds
}
+ });
+ } else if (excludeUserIds && excludeUserIds.length != 0) {
+ push({
+ user_id: {
+ $nin: excludeUserIds
+ }
+ });
+ }
+
+ if (following != null && me != null) {
+ const ids = await getFriends(me._id, false);
+ push({
+ user_id: following ? {
+ $in: ids
+ } : {
+ $nin: ids.concat(me._id)
+ }
+ });
+ }
+
+ if (me != null) {
+ const mutes = await Mute.find({
+ muter_id: me._id,
+ deleted_at: { $exists: false }
+ });
+ const mutedUserIds = mutes.map(m => m.mutee_id);
+
+ switch (mute) {
+ case 'mute_all':
+ push({
+ user_id: {
+ $nin: mutedUserIds
+ },
+ '_reply.user_id': {
+ $nin: mutedUserIds
+ },
+ '_repost.user_id': {
+ $nin: mutedUserIds
+ }
+ });
+ break;
+ case 'mute_related':
+ push({
+ '_reply.user_id': {
+ $nin: mutedUserIds
+ },
+ '_repost.user_id': {
+ $nin: mutedUserIds
+ }
+ });
+ break;
+ case 'mute_direct':
+ push({
+ user_id: {
+ $nin: mutedUserIds
+ }
+ });
+ break;
+ case 'direct_only':
+ push({
+ user_id: {
+ $in: mutedUserIds
+ }
+ });
+ break;
+ case 'related_only':
+ push({
+ $or: [{
+ '_reply.user_id': {
+ $in: mutedUserIds
+ }
+ }, {
+ '_repost.user_id': {
+ $in: mutedUserIds
+ }
+ }]
+ });
+ break;
+ case 'all_only':
+ push({
+ $or: [{
+ user_id: {
+ $in: mutedUserIds
+ }
+ }, {
+ '_reply.user_id': {
+ $in: mutedUserIds
+ }
+ }, {
+ '_repost.user_id': {
+ $in: mutedUserIds
+ }
+ }]
+ });
+ break;
}
- }, async (error, response) => {
- if (error) {
- console.error(error);
- return res(500);
- }
+ }
- if (response.hits.total === 0) {
- return res([]);
+ if (reply != null) {
+ if (reply) {
+ push({
+ reply_id: {
+ $exists: true,
+ $ne: null
+ }
+ });
+ } else {
+ push({
+ $or: [{
+ reply_id: {
+ $exists: false
+ }
+ }, {
+ reply_id: null
+ }]
+ });
}
+ }
- const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
+ if (repost != null) {
+ if (repost) {
+ push({
+ repost_id: {
+ $exists: true,
+ $ne: null
+ }
+ });
+ } else {
+ push({
+ $or: [{
+ repost_id: {
+ $exists: false
+ }
+ }, {
+ repost_id: null
+ }]
+ });
+ }
+ }
- // Fetch found posts
- const posts = await Post
- .find({
- _id: {
- $in: hits
+ if (media != null) {
+ if (media) {
+ push({
+ media_ids: {
+ $exists: true,
+ $ne: null
}
- }, {
- sort: {
- _id: -1
+ });
+ } else {
+ push({
+ $or: [{
+ media_ids: {
+ $exists: false
+ }
+ }, {
+ media_ids: null
+ }]
+ });
+ }
+ }
+
+ if (poll != null) {
+ if (poll) {
+ push({
+ poll: {
+ $exists: true,
+ $ne: null
}
});
+ } else {
+ push({
+ $or: [{
+ poll: {
+ $exists: false
+ }
+ }, {
+ poll: null
+ }]
+ });
+ }
+ }
+
+ if (sinceDate) {
+ push({
+ created_at: {
+ $gt: new Date(sinceDate)
+ }
+ });
+ }
+
+ if (untilDate) {
+ push({
+ created_at: {
+ $lt: new Date(untilDate)
+ }
+ });
+ }
+
+ if (q.$and.length == 0) {
+ q = {};
+ }
- posts.map(post => {
- post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0];
+ // Search posts
+ const posts = await Post
+ .find(q, {
+ sort: {
+ _id: -1
+ },
+ limit: max,
+ skip: offset
});
- // Serialize
- res(await Promise.all(posts.map(async post =>
- await serialize(post, me))));
- });
+ // Serialize
+ res(await Promise.all(posts.map(async post =>
+ await pack(post, me))));
}
diff --git a/src/api/endpoints/posts/show.ts b/src/api/endpoints/posts/show.ts
index 5bfe4f6605..3839490597 100644
--- a/src/api/endpoints/posts/show.ts
+++ b/src/api/endpoints/posts/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import Post, { pack } from '../../models/post';
/**
* Show a post
@@ -27,7 +26,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
}
// Serialize
- res(await serialize(post, user, {
+ res(await pack(post, user, {
detail: true
}));
});
diff --git a/src/api/endpoints/posts/timeline.ts b/src/api/endpoints/posts/timeline.ts
index 0d08b95463..c41cfdb8bd 100644
--- a/src/api/endpoints/posts/timeline.ts
+++ b/src/api/endpoints/posts/timeline.ts
@@ -4,9 +4,10 @@
import $ from 'cafy';
import rap from '@prezzemolo/rap';
import Post from '../../models/post';
+import Mute from '../../models/mute';
import ChannelWatching from '../../models/channel-watching';
import getFriends from '../../common/get-friends';
-import serialize from '../../serializers/post';
+import { pack } from '../../models/post';
/**
* Get timeline of myself
@@ -25,32 +26,40 @@ module.exports = async (params, user, app) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) throw 'invalid since_id param';
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) throw 'invalid max_id param';
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) throw 'invalid until_id param';
// Get 'since_date' parameter
const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
if (sinceDateErr) throw 'invalid since_date param';
- // Get 'max_date' parameter
- const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
- if (maxDateErr) throw 'invalid max_date param';
+ // Get 'until_date' parameter
+ const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+ if (untilDateErr) throw 'invalid until_date param';
- // Check if only one of since_id, max_id, since_date, max_date specified
- if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
- throw 'only one of since_id, max_id, since_date, max_date can be specified';
+ // Check if only one of since_id, until_id, since_date, until_date specified
+ if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ throw 'only one of since_id, until_id, since_date, until_date can be specified';
}
- const { followingIds, watchingChannelIds } = await rap({
+ const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
// ID list of the user itself and other users who the user follows
followingIds: getFriends(user._id),
+
// Watchしているチャンネルを取得
watchingChannelIds: ChannelWatching.find({
user_id: user._id,
// 削除されたドキュメントは除く
deleted_at: { $exists: false }
- }).then(watches => watches.map(w => w.channel_id))
+ }).then(watches => watches.map(w => w.channel_id)),
+
+ // ミュートしているユーザーを取得
+ mutedUserIds: Mute.find({
+ muter_id: user._id,
+ // 削除されたドキュメントは除く
+ deleted_at: { $exists: false }
+ }).then(ms => ms.map(m => m.mutee_id))
});
//#region Construct query
@@ -77,7 +86,17 @@ module.exports = async (params, user, app) => {
channel_id: {
$in: watchingChannelIds
}
- }]
+ }],
+ // mute
+ user_id: {
+ $nin: mutedUserIds
+ },
+ '_reply.user_id': {
+ $nin: mutedUserIds
+ },
+ '_repost.user_id': {
+ $nin: mutedUserIds
+ },
} as any;
if (sinceId) {
@@ -85,18 +104,18 @@ module.exports = async (params, user, app) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
} else if (sinceDate) {
sort._id = 1;
query.created_at = {
$gt: new Date(sinceDate)
};
- } else if (maxDate) {
+ } else if (untilDate) {
query.created_at = {
- $lt: new Date(maxDate)
+ $lt: new Date(untilDate)
};
}
//#endregion
@@ -109,5 +128,5 @@ module.exports = async (params, user, app) => {
});
// Serialize
- return await Promise.all(timeline.map(post => serialize(post, user)));
+ return await Promise.all(timeline.map(post => pack(post, user)));
};
diff --git a/src/api/endpoints/posts/trend.ts b/src/api/endpoints/posts/trend.ts
index 64a195dff1..caded92bf5 100644
--- a/src/api/endpoints/posts/trend.ts
+++ b/src/api/endpoints/posts/trend.ts
@@ -3,8 +3,7 @@
*/
const ms = require('ms');
import $ from 'cafy';
-import Post from '../../models/post';
-import serialize from '../../serializers/post';
+import Post, { pack } from '../../models/post';
/**
* Get trend posts
@@ -76,5 +75,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(posts.map(async post =>
- await serialize(post, user, { detail: true }))));
+ await pack(post, user, { detail: true }))));
});
diff --git a/src/api/endpoints/users.ts b/src/api/endpoints/users.ts
index 134f262fb1..095b9fe40d 100644
--- a/src/api/endpoints/users.ts
+++ b/src/api/endpoints/users.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import User from '../models/user';
-import serialize from '../serializers/user';
+import User, { pack } from '../models/user';
/**
* Lists all users
@@ -21,13 +20,13 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
- // Check if both of since_id and max_id is specified
- if (sinceId && maxId) {
- return rej('cannot set since_id and max_id');
+ // Check if both of since_id and until_id is specified
+ if (sinceId && untilId) {
+ return rej('cannot set since_id and until_id');
}
// Construct query
@@ -40,9 +39,9 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
}
@@ -55,5 +54,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(users.map(async user =>
- await serialize(user, me))));
+ await pack(user, me))));
});
diff --git a/src/api/endpoints/users/followers.ts b/src/api/endpoints/users/followers.ts
index 4905323ba5..b0fb83c683 100644
--- a/src/api/endpoints/users/followers.ts
+++ b/src/api/endpoints/users/followers.ts
@@ -4,7 +4,7 @@
import $ from 'cafy';
import User from '../../models/user';
import Following from '../../models/following';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
import getFriends from '../../common/get-friends';
/**
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
const users = await Promise.all(following.map(async f =>
- await serialize(f.follower_id, me, { detail: true })));
+ await pack(f.follower_id, me, { detail: true })));
// Response
res({
diff --git a/src/api/endpoints/users/following.ts b/src/api/endpoints/users/following.ts
index dc2ff49bbe..8e88431e92 100644
--- a/src/api/endpoints/users/following.ts
+++ b/src/api/endpoints/users/following.ts
@@ -4,7 +4,7 @@
import $ from 'cafy';
import User from '../../models/user';
import Following from '../../models/following';
-import serialize from '../../serializers/user';
+import { pack } from '../../models/user';
import getFriends from '../../common/get-friends';
/**
@@ -82,7 +82,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
const users = await Promise.all(following.map(async f =>
- await serialize(f.followee_id, me, { detail: true })));
+ await pack(f.followee_id, me, { detail: true })));
// Response
res({
diff --git a/src/api/endpoints/users/get_frequently_replied_users.ts b/src/api/endpoints/users/get_frequently_replied_users.ts
index a8add623d4..87f4f77a5b 100644
--- a/src/api/endpoints/users/get_frequently_replied_users.ts
+++ b/src/api/endpoints/users/get_frequently_replied_users.ts
@@ -3,8 +3,7 @@
*/
import $ from 'cafy';
import Post from '../../models/post';
-import User from '../../models/user';
-import serialize from '../../serializers/user';
+import User, { pack } from '../../models/user';
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'user_id' parameter
@@ -91,7 +90,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
- user: await serialize(user, me, { detail: true }),
+ user: await pack(user, me, { detail: true }),
weight: repliedUsers[user] / peak
})));
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts
index fe821cf17a..0c8bceee3d 100644
--- a/src/api/endpoints/users/posts.ts
+++ b/src/api/endpoints/users/posts.ts
@@ -2,9 +2,8 @@
* Module dependencies
*/
import $ from 'cafy';
-import Post from '../../models/post';
+import Post, { pack } from '../../models/post';
import User from '../../models/user';
-import serialize from '../../serializers/post';
/**
* Get posts of a user
@@ -42,21 +41,21 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
const [sinceId, sinceIdErr] = $(params.since_id).optional.id().$;
if (sinceIdErr) return rej('invalid since_id param');
- // Get 'max_id' parameter
- const [maxId, maxIdErr] = $(params.max_id).optional.id().$;
- if (maxIdErr) return rej('invalid max_id param');
+ // Get 'until_id' parameter
+ const [untilId, untilIdErr] = $(params.until_id).optional.id().$;
+ if (untilIdErr) return rej('invalid until_id param');
// Get 'since_date' parameter
const [sinceDate, sinceDateErr] = $(params.since_date).optional.number().$;
if (sinceDateErr) throw 'invalid since_date param';
- // Get 'max_date' parameter
- const [maxDate, maxDateErr] = $(params.max_date).optional.number().$;
- if (maxDateErr) throw 'invalid max_date param';
+ // Get 'until_date' parameter
+ const [untilDate, untilDateErr] = $(params.until_date).optional.number().$;
+ if (untilDateErr) throw 'invalid until_date param';
- // Check if only one of since_id, max_id, since_date, max_date specified
- if ([sinceId, maxId, sinceDate, maxDate].filter(x => x != null).length > 1) {
- throw 'only one of since_id, max_id, since_date, max_date can be specified';
+ // Check if only one of since_id, until_id, since_date, until_date specified
+ if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+ throw 'only one of since_id, until_id, since_date, until_date can be specified';
}
const q = userId !== undefined
@@ -88,18 +87,18 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
query._id = {
$gt: sinceId
};
- } else if (maxId) {
+ } else if (untilId) {
query._id = {
- $lt: maxId
+ $lt: untilId
};
} else if (sinceDate) {
sort._id = 1;
query.created_at = {
$gt: new Date(sinceDate)
};
- } else if (maxDate) {
+ } else if (untilDate) {
query.created_at = {
- $lt: new Date(maxDate)
+ $lt: new Date(untilDate)
};
}
@@ -124,6 +123,6 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(posts.map(async (post) =>
- await serialize(post, me)
+ await pack(post, me)
)));
});
diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts
index 731d68a7b1..736233b340 100644
--- a/src/api/endpoints/users/recommendation.ts
+++ b/src/api/endpoints/users/recommendation.ts
@@ -3,8 +3,7 @@
*/
const ms = require('ms');
import $ from 'cafy';
-import User from '../../models/user';
-import serialize from '../../serializers/user';
+import User, { pack } from '../../models/user';
import getFriends from '../../common/get-friends';
/**
@@ -44,5 +43,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(users.map(async user =>
- await serialize(user, me, { detail: true }))));
+ await pack(user, me, { detail: true }))));
});
diff --git a/src/api/endpoints/users/search.ts b/src/api/endpoints/users/search.ts
index 73a5db47e2..39e2ff9890 100644
--- a/src/api/endpoints/users/search.ts
+++ b/src/api/endpoints/users/search.ts
@@ -3,8 +3,7 @@
*/
import * as mongo from 'mongodb';
import $ from 'cafy';
-import User from '../../models/user';
-import serialize from '../../serializers/user';
+import User, { pack } from '../../models/user';
import config from '../../../conf';
const escapeRegexp = require('escape-regexp');
@@ -52,7 +51,7 @@ async function byNative(res, rej, me, query, offset, max) {
// Serialize
res(await Promise.all(users.map(async user =>
- await serialize(user, me, { detail: true }))));
+ await pack(user, me, { detail: true }))));
}
// Search by Elasticsearch
@@ -94,6 +93,6 @@ async function byElasticsearch(res, rej, me, query, offset, max) {
// Serialize
res(await Promise.all(users.map(async user =>
- await serialize(user, me, { detail: true }))));
+ await pack(user, me, { detail: true }))));
});
}
diff --git a/src/api/endpoints/users/search_by_username.ts b/src/api/endpoints/users/search_by_username.ts
index 7f2f42f0a6..9c5e1905aa 100644
--- a/src/api/endpoints/users/search_by_username.ts
+++ b/src/api/endpoints/users/search_by_username.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import User from '../../models/user';
-import serialize from '../../serializers/user';
+import User, { pack } from '../../models/user';
/**
* Search a user by username
@@ -35,5 +34,5 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
// Serialize
res(await Promise.all(users.map(async user =>
- await serialize(user, me, { detail: true }))));
+ await pack(user, me, { detail: true }))));
});
diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts
index 8e74b0fe3f..7aea59296a 100644
--- a/src/api/endpoints/users/show.ts
+++ b/src/api/endpoints/users/show.ts
@@ -2,8 +2,7 @@
* Module dependencies
*/
import $ from 'cafy';
-import User from '../../models/user';
-import serialize from '../../serializers/user';
+import User, { pack } from '../../models/user';
/**
* Show a user
@@ -41,7 +40,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
}
// Send response
- res(await serialize(user, me, {
+ res(await pack(user, me, {
detail: true
}));
});
diff --git a/src/api/models/app.ts b/src/api/models/app.ts
index 68f2f448b0..34e9867db7 100644
--- a/src/api/models/app.ts
+++ b/src/api/models/app.ts
@@ -1,13 +1,97 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import AccessToken from './access-token';
import db from '../../db/mongodb';
+import config from '../../conf';
-const collection = db.get('apps');
+const App = db.get<IApp>('apps');
+App.createIndex('name_id');
+App.createIndex('name_id_lower');
+App.createIndex('secret');
+export default App;
-(collection as any).createIndex('name_id'); // fuck type definition
-(collection as any).createIndex('name_id_lower'); // fuck type definition
-(collection as any).createIndex('secret'); // fuck type definition
-
-export default collection as any; // fuck type definition
+export type IApp = {
+ _id: mongo.ObjectID;
+ created_at: Date;
+ user_id: mongo.ObjectID;
+ secret: string;
+};
export function isValidNameId(nameId: string): boolean {
return typeof nameId == 'string' && /^[a-zA-Z0-9\-]{3,30}$/.test(nameId);
}
+
+/**
+ * Pack an app for API response
+ *
+ * @param {any} app
+ * @param {any} me?
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+ app: any,
+ me?: any,
+ options?: {
+ includeSecret?: boolean,
+ includeProfileImageIds?: boolean
+ }
+) => new Promise<any>(async (resolve, reject) => {
+ const opts = options || {
+ includeSecret: false,
+ includeProfileImageIds: false
+ };
+
+ let _app: any;
+
+ // Populate the app if 'app' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(app)) {
+ _app = await App.findOne({
+ _id: app
+ });
+ } else if (typeof app === 'string') {
+ _app = await App.findOne({
+ _id: new mongo.ObjectID(app)
+ });
+ } else {
+ _app = deepcopy(app);
+ }
+
+ // Me
+ if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
+ if (typeof me === 'string') {
+ me = new mongo.ObjectID(me);
+ } else {
+ me = me._id;
+ }
+ }
+
+ // Rename _id to id
+ _app.id = _app._id;
+ delete _app._id;
+
+ delete _app.name_id_lower;
+
+ // Visible by only owner
+ if (!opts.includeSecret) {
+ delete _app.secret;
+ }
+
+ _app.icon_url = _app.icon != null
+ ? `${config.drive_url}/${_app.icon}`
+ : `${config.drive_url}/app-default.jpg`;
+
+ if (me) {
+ // 既に連携しているか
+ const exist = await AccessToken.count({
+ app_id: _app.id,
+ user_id: me,
+ }, {
+ limit: 1
+ });
+
+ _app.is_authorized = exist === 1;
+ }
+
+ resolve(_app);
+});
diff --git a/src/api/models/auth-session.ts b/src/api/models/auth-session.ts
index b264a133e9..997ec61c20 100644
--- a/src/api/models/auth-session.ts
+++ b/src/api/models/auth-session.ts
@@ -1,3 +1,45 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
import db from '../../db/mongodb';
+import { pack as packApp } from './app';
-export default db.get('auth_sessions') as any; // fuck type definition
+const AuthSession = db.get('auth_sessions');
+export default AuthSession;
+
+export interface IAuthSession {
+ _id: mongo.ObjectID;
+}
+
+/**
+ * Pack an auth session for API response
+ *
+ * @param {any} session
+ * @param {any} me?
+ * @return {Promise<any>}
+ */
+export const pack = (
+ session: any,
+ me?: any
+) => new Promise<any>(async (resolve, reject) => {
+ let _session: any;
+
+ // TODO: Populate session if it ID
+
+ _session = deepcopy(session);
+
+ // Me
+ if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
+ if (typeof me === 'string') {
+ me = new mongo.ObjectID(me);
+ } else {
+ me = me._id;
+ }
+ }
+
+ delete _session._id;
+
+ // Populate app
+ _session.app = await packApp(_session.app_id, me);
+
+ resolve(_session);
+});
diff --git a/src/api/models/channel.ts b/src/api/models/channel.ts
index c80e84dbc8..815d53593c 100644
--- a/src/api/models/channel.ts
+++ b/src/api/models/channel.ts
@@ -1,9 +1,11 @@
import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { IUser } from './user';
+import Watching from './channel-watching';
import db from '../../db/mongodb';
-const collection = db.get('channels');
-
-export default collection as any; // fuck type definition
+const Channel = db.get<IChannel>('channels');
+export default Channel;
export type IChannel = {
_id: mongo.ObjectID;
@@ -12,3 +14,61 @@ export type IChannel = {
user_id: mongo.ObjectID;
index: number;
};
+
+/**
+ * Pack a channel for API response
+ *
+ * @param channel target
+ * @param me? serializee
+ * @return response
+ */
+export const pack = (
+ channel: string | mongo.ObjectID | IChannel,
+ me?: string | mongo.ObjectID | IUser
+) => new Promise<any>(async (resolve, reject) => {
+
+ let _channel: any;
+
+ // Populate the channel if 'channel' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
+ _channel = await Channel.findOne({
+ _id: channel
+ });
+ } else if (typeof channel === 'string') {
+ _channel = await Channel.findOne({
+ _id: new mongo.ObjectID(channel)
+ });
+ } else {
+ _channel = deepcopy(channel);
+ }
+
+ // Rename _id to id
+ _channel.id = _channel._id;
+ delete _channel._id;
+
+ // Remove needless properties
+ delete _channel.user_id;
+
+ // Me
+ const meId: mongo.ObjectID = me
+ ? mongo.ObjectID.prototype.isPrototypeOf(me)
+ ? me as mongo.ObjectID
+ : typeof me === 'string'
+ ? new mongo.ObjectID(me)
+ : (me as IUser)._id
+ : null;
+
+ if (me) {
+ //#region Watchしているかどうか
+ const watch = await Watching.findOne({
+ user_id: meId,
+ channel_id: _channel.id,
+ deleted_at: { $exists: false }
+ });
+
+ _channel.is_watching = watch !== null;
+ //#endregion
+ }
+
+ resolve(_channel);
+});
diff --git a/src/api/models/drive-file.ts b/src/api/models/drive-file.ts
index 802ee5a5fe..2a46d8dc4d 100644
--- a/src/api/models/drive-file.ts
+++ b/src/api/models/drive-file.ts
@@ -1,9 +1,12 @@
import * as mongodb from 'mongodb';
+import deepcopy = require('deepcopy');
+import { pack as packFolder } from './drive-folder';
+import config from '../../conf';
import monkDb, { nativeDbConn } from '../../db/mongodb';
-const collection = monkDb.get('drive_files.files');
+const DriveFile = monkDb.get<IDriveFile>('drive_files.files');
-export default collection as any; // fuck type definition
+export default DriveFile;
const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
const db = await nativeDbConn();
@@ -15,6 +18,19 @@ const getGridFSBucket = async (): Promise<mongodb.GridFSBucket> => {
export { getGridFSBucket };
+export type IDriveFile = {
+ _id: mongodb.ObjectID;
+ uploadDate: Date;
+ md5: string;
+ filename: string;
+ contentType: string;
+ metadata: {
+ properties: any;
+ user_id: mongodb.ObjectID;
+ folder_id: mongodb.ObjectID;
+ }
+};
+
export function validateFileName(name: string): boolean {
return (
(name.trim().length > 0) &&
@@ -24,3 +40,74 @@ export function validateFileName(name: string): boolean {
(name.indexOf('..') === -1)
);
}
+
+/**
+ * Pack a drive file for API response
+ *
+ * @param {any} file
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+ file: any,
+ options?: {
+ detail: boolean
+ }
+) => new Promise<any>(async (resolve, reject) => {
+ const opts = Object.assign({
+ detail: false
+ }, options);
+
+ let _file: any;
+
+ // Populate the file if 'file' is ID
+ if (mongodb.ObjectID.prototype.isPrototypeOf(file)) {
+ _file = await DriveFile.findOne({
+ _id: file
+ });
+ } else if (typeof file === 'string') {
+ _file = await DriveFile.findOne({
+ _id: new mongodb.ObjectID(file)
+ });
+ } else {
+ _file = deepcopy(file);
+ }
+
+ if (!_file) return reject('invalid file arg.');
+
+ // rendered target
+ let _target: any = {};
+
+ _target.id = _file._id;
+ _target.created_at = _file.uploadDate;
+ _target.name = _file.filename;
+ _target.type = _file.contentType;
+ _target.datasize = _file.length;
+ _target.md5 = _file.md5;
+
+ _target = Object.assign(_target, _file.metadata);
+
+ _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
+
+ if (_target.properties == null) _target.properties = {};
+
+ if (opts.detail) {
+ if (_target.folder_id) {
+ // Populate folder
+ _target.folder = await packFolder(_target.folder_id, {
+ detail: true
+ });
+ }
+
+ /*
+ if (_target.tags) {
+ // Populate tags
+ _target.tags = await _target.tags.map(async (tag: any) =>
+ await serializeDriveTag(tag)
+ );
+ }
+ */
+ }
+
+ resolve(_target);
+});
diff --git a/src/api/models/drive-folder.ts b/src/api/models/drive-folder.ts
index f81ffe855d..54b45049b9 100644
--- a/src/api/models/drive-folder.ts
+++ b/src/api/models/drive-folder.ts
@@ -1,6 +1,18 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
import db from '../../db/mongodb';
+import DriveFile from './drive-file';
-export default db.get('drive_folders') as any; // fuck type definition
+const DriveFolder = db.get<IDriveFolder>('drive_folders');
+export default DriveFolder;
+
+export type IDriveFolder = {
+ _id: mongo.ObjectID;
+ created_at: Date;
+ name: string;
+ user_id: mongo.ObjectID;
+ parent_id: mongo.ObjectID;
+};
export function isValidFolderName(name: string): boolean {
return (
@@ -8,3 +20,58 @@ export function isValidFolderName(name: string): boolean {
(name.length <= 200)
);
}
+
+/**
+ * Pack a drive folder for API response
+ *
+ * @param {any} folder
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+ folder: any,
+ options?: {
+ detail: boolean
+ }
+) => new Promise<any>(async (resolve, reject) => {
+ const opts = Object.assign({
+ detail: false
+ }, options);
+
+ let _folder: any;
+
+ // Populate the folder if 'folder' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(folder)) {
+ _folder = await DriveFolder.findOne({ _id: folder });
+ } else if (typeof folder === 'string') {
+ _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
+ } else {
+ _folder = deepcopy(folder);
+ }
+
+ // Rename _id to id
+ _folder.id = _folder._id;
+ delete _folder._id;
+
+ if (opts.detail) {
+ const childFoldersCount = await DriveFolder.count({
+ parent_id: _folder.id
+ });
+
+ const childFilesCount = await DriveFile.count({
+ 'metadata.folder_id': _folder.id
+ });
+
+ _folder.folders_count = childFoldersCount;
+ _folder.files_count = childFilesCount;
+ }
+
+ if (opts.detail && _folder.parent_id) {
+ // Populate parent folder
+ _folder.parent = await pack(_folder.parent_id, {
+ detail: true
+ });
+ }
+
+ resolve(_folder);
+});
diff --git a/src/api/models/messaging-message.ts b/src/api/models/messaging-message.ts
index 18afa57e44..90cf1cd71c 100644
--- a/src/api/models/messaging-message.ts
+++ b/src/api/models/messaging-message.ts
@@ -1,12 +1,81 @@
import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
+import { pack as packUser } from './user';
+import { pack as packFile } from './drive-file';
import db from '../../db/mongodb';
+import parse from '../common/text';
-export default db.get('messaging_messages') as any; // fuck type definition
+const MessagingMessage = db.get<IMessagingMessage>('messaging_messages');
+export default MessagingMessage;
export interface IMessagingMessage {
_id: mongo.ObjectID;
+ created_at: Date;
+ text: string;
+ user_id: mongo.ObjectID;
+ recipient_id: mongo.ObjectID;
+ is_read: boolean;
}
export function isValidText(text: string): boolean {
return text.length <= 1000 && text.trim() != '';
}
+
+/**
+ * Pack a messaging message for API response
+ *
+ * @param {any} message
+ * @param {any} me?
+ * @param {any} options?
+ * @return {Promise<any>}
+ */
+export const pack = (
+ message: any,
+ me?: any,
+ options?: {
+ populateRecipient: boolean
+ }
+) => new Promise<any>(async (resolve, reject) => {
+ const opts = options || {
+ populateRecipient: true
+ };
+
+ let _message: any;
+
+ // Populate the message if 'message' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(message)) {
+ _message = await MessagingMessage.findOne({
+ _id: message
+ });
+ } else if (typeof message === 'string') {
+ _message = await MessagingMessage.findOne({
+ _id: new mongo.ObjectID(message)
+ });
+ } else {
+ _message = deepcopy(message);
+ }
+
+ // Rename _id to id
+ _message.id = _message._id;
+ delete _message._id;
+
+ // Parse text
+ if (_message.text) {
+ _message.ast = parse(_message.text);
+ }
+
+ // Populate user
+ _message.user = await packUser(_message.user_id, me);
+
+ if (_message.file) {
+ // Populate file
+ _message.file = await packFile(_message.file_id);
+ }
+
+ if (opts.populateRecipient) {
+ // Populate recipient
+ _message.recipient = await packUser(_message.recipient_id, me);
+ }
+
+ resolve(_message);
+});
diff --git a/src/api/models/mute.ts b/src/api/models/mute.ts
new file mode 100644
index 0000000000..16018b82f7
--- /dev/null
+++ b/src/api/models/mute.ts
@@ -0,0 +1,3 @@
+import db from '../../db/mongodb';
+
+export default db.get('mute') as any; // fuck type definition
diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts
index e3dc6c70a3..fa7049d312 100644
--- a/src/api/models/notification.ts
+++ b/src/api/models/notification.ts
@@ -1,8 +1,11 @@
import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
import db from '../../db/mongodb';
-import { IUser } from './user';
+import { IUser, pack as packUser } from './user';
+import { pack as packPost } from './post';
-export default db.get('notifications') as any; // fuck type definition
+const Notification = db.get<INotification>('notifications');
+export default Notification;
export interface INotification {
_id: mongo.ObjectID;
@@ -45,3 +48,60 @@ export interface INotification {
*/
is_read: Boolean;
}
+
+/**
+ * Pack a notification for API response
+ *
+ * @param {any} notification
+ * @return {Promise<any>}
+ */
+export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
+ let _notification: any;
+
+ // Populate the notification if 'notification' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(notification)) {
+ _notification = await Notification.findOne({
+ _id: notification
+ });
+ } else if (typeof notification === 'string') {
+ _notification = await Notification.findOne({
+ _id: new mongo.ObjectID(notification)
+ });
+ } else {
+ _notification = deepcopy(notification);
+ }
+
+ // Rename _id to id
+ _notification.id = _notification._id;
+ delete _notification._id;
+
+ // Rename notifier_id to user_id
+ _notification.user_id = _notification.notifier_id;
+ delete _notification.notifier_id;
+
+ const me = _notification.notifiee_id;
+ delete _notification.notifiee_id;
+
+ // Populate notifier
+ _notification.user = await packUser(_notification.user_id, me);
+
+ switch (_notification.type) {
+ case 'follow':
+ // nope
+ break;
+ case 'mention':
+ case 'reply':
+ case 'repost':
+ case 'quote':
+ case 'reaction':
+ case 'poll_vote':
+ // Populate post
+ _notification.post = await packPost(_notification.post_id, me);
+ break;
+ default:
+ console.error(`Unknown type: ${_notification.type}`);
+ break;
+ }
+
+ resolve(_notification);
+});
diff --git a/src/api/models/post-reaction.ts b/src/api/models/post-reaction.ts
index 282ae5bd21..639a70e006 100644
--- a/src/api/models/post-reaction.ts
+++ b/src/api/models/post-reaction.ts
@@ -1,3 +1,51 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
import db from '../../db/mongodb';
+import Reaction from './post-reaction';
+import { pack as packUser } from './user';
-export default db.get('post_reactions') as any; // fuck type definition
+const PostReaction = db.get<IPostReaction>('post_reactions');
+export default PostReaction;
+
+export interface IPostReaction {
+ _id: mongo.ObjectID;
+ created_at: Date;
+ deleted_at: Date;
+ reaction: string;
+}
+
+/**
+ * Pack a reaction for API response
+ *
+ * @param {any} reaction
+ * @param {any} me?
+ * @return {Promise<any>}
+ */
+export const pack = (
+ reaction: any,
+ me?: any
+) => new Promise<any>(async (resolve, reject) => {
+ let _reaction: any;
+
+ // Populate the reaction if 'reaction' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) {
+ _reaction = await Reaction.findOne({
+ _id: reaction
+ });
+ } else if (typeof reaction === 'string') {
+ _reaction = await Reaction.findOne({
+ _id: new mongo.ObjectID(reaction)
+ });
+ } else {
+ _reaction = deepcopy(reaction);
+ }
+
+ // Rename _id to id
+ _reaction.id = _reaction._id;
+ delete _reaction._id;
+
+ // Populate user
+ _reaction.user = await packUser(_reaction.user_id, me);
+
+ resolve(_reaction);
+});
diff --git a/src/api/models/post.ts b/src/api/models/post.ts
index 7584ce182d..0bbacebf66 100644
--- a/src/api/models/post.ts
+++ b/src/api/models/post.ts
@@ -1,8 +1,18 @@
import * as mongo from 'mongodb';
-
+import deepcopy = require('deepcopy');
+import rap from '@prezzemolo/rap';
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 Reaction from './post-reaction';
+import { pack as packFile } from './drive-file';
+import parse from '../common/text';
+
+const Post = db.get<IPost>('posts');
-export default db.get('posts') as any; // fuck type definition
+export default Post;
export function isValidText(text: string): boolean {
return text.length <= 1000 && text.trim() != '';
@@ -15,8 +25,185 @@ export type IPost = {
media_ids: mongo.ObjectID[];
reply_id: mongo.ObjectID;
repost_id: mongo.ObjectID;
- poll: {}; // todo
+ poll: any; // todo
text: string;
user_id: mongo.ObjectID;
app_id: mongo.ObjectID;
+ category: string;
+ is_category_verified: boolean;
+};
+
+/**
+ * Pack a post for API response
+ *
+ * @param post target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return response
+ */
+export const pack = async (
+ post: string | mongo.ObjectID | IPost,
+ me?: string | mongo.ObjectID | IUser,
+ options?: {
+ detail: boolean
+ }
+) => {
+ const opts = options || {
+ detail: true,
+ };
+
+ // Me
+ const meId: mongo.ObjectID = me
+ ? mongo.ObjectID.prototype.isPrototypeOf(me)
+ ? me as mongo.ObjectID
+ : typeof me === 'string'
+ ? new mongo.ObjectID(me)
+ : (me as IUser)._id
+ : null;
+
+ let _post: any;
+
+ // Populate the post if 'post' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
+ _post = await Post.findOne({
+ _id: post
+ });
+ } else if (typeof post === 'string') {
+ _post = await Post.findOne({
+ _id: new mongo.ObjectID(post)
+ });
+ } else {
+ _post = deepcopy(post);
+ }
+
+ if (!_post) throw 'invalid post arg.';
+
+ const id = _post._id;
+
+ // Rename _id to id
+ _post.id = _post._id;
+ delete _post._id;
+
+ delete _post.mentions;
+
+ // Parse text
+ if (_post.text) {
+ _post.ast = parse(_post.text);
+ }
+
+ // Populate user
+ _post.user = packUser(_post.user_id, meId);
+
+ // Populate app
+ if (_post.app_id) {
+ _post.app = packApp(_post.app_id);
+ }
+
+ // Populate channel
+ if (_post.channel_id) {
+ _post.channel = packChannel(_post.channel_id);
+ }
+
+ // Populate media
+ if (_post.media_ids) {
+ _post.media = Promise.all(_post.media_ids.map(fileId =>
+ packFile(fileId)
+ ));
+ }
+
+ // When requested a detailed post data
+ if (opts.detail) {
+ // Get previous post info
+ _post.prev = (async () => {
+ const prev = await Post.findOne({
+ user_id: _post.user_id,
+ _id: {
+ $lt: id
+ }
+ }, {
+ fields: {
+ _id: true
+ },
+ sort: {
+ _id: -1
+ }
+ });
+ return prev ? prev._id : null;
+ })();
+
+ // Get next post info
+ _post.next = (async () => {
+ const next = await Post.findOne({
+ user_id: _post.user_id,
+ _id: {
+ $gt: id
+ }
+ }, {
+ fields: {
+ _id: true
+ },
+ sort: {
+ _id: 1
+ }
+ });
+ return next ? next._id : null;
+ })();
+
+ if (_post.reply_id) {
+ // Populate reply to post
+ _post.reply = pack(_post.reply_id, meId, {
+ detail: false
+ });
+ }
+
+ if (_post.repost_id) {
+ // Populate repost
+ _post.repost = pack(_post.repost_id, meId, {
+ detail: _post.text == null
+ });
+ }
+
+ // Poll
+ if (meId && _post.poll) {
+ _post.poll = (async (poll) => {
+ const vote = await Vote
+ .findOne({
+ user_id: meId,
+ post_id: id
+ });
+
+ if (vote != null) {
+ const myChoice = poll.choices
+ .filter(c => c.id == vote.choice)[0];
+
+ myChoice.is_voted = true;
+ }
+
+ return poll;
+ })(_post.poll);
+ }
+
+ // Fetch my reaction
+ if (meId) {
+ _post.my_reaction = (async () => {
+ const reaction = await Reaction
+ .findOne({
+ user_id: meId,
+ post_id: id,
+ deleted_at: { $exists: false }
+ });
+
+ if (reaction) {
+ return reaction.reaction;
+ }
+
+ return null;
+ })();
+ }
+ }
+
+ // resolve promises in _post object
+ _post = await rap(_post);
+
+ return _post;
};
diff --git a/src/api/models/signin.ts b/src/api/models/signin.ts
index 385a348f2e..262c8707ed 100644
--- a/src/api/models/signin.ts
+++ b/src/api/models/signin.ts
@@ -1,3 +1,29 @@
+import * as mongo from 'mongodb';
+import deepcopy = require('deepcopy');
import db from '../../db/mongodb';
-export default db.get('signin') as any; // fuck type definition
+const Signin = db.get<ISignin>('signin');
+export default Signin;
+
+export interface ISignin {
+ _id: mongo.ObjectID;
+}
+
+/**
+ * Pack a signin record for API response
+ *
+ * @param {any} record
+ * @return {Promise<any>}
+ */
+export const pack = (
+ record: any
+) => new Promise<any>(async (resolve, reject) => {
+
+ const _record = deepcopy(record);
+
+ // Rename _id to id
+ _record.id = _record._id;
+ delete _record._id;
+
+ resolve(_record);
+});
diff --git a/src/api/models/user.ts b/src/api/models/user.ts
index b2f3af09fa..e92f244dd0 100644
--- a/src/api/models/user.ts
+++ b/src/api/models/user.ts
@@ -1,14 +1,19 @@
import * as mongo from 'mongodb';
-
+import deepcopy = require('deepcopy');
+import rap from '@prezzemolo/rap';
import db from '../../db/mongodb';
-import { IPost } from './post';
+import { IPost, pack as packPost } from './post';
+import Following from './following';
+import Mute from './mute';
+import getFriends from '../common/get-friends';
+import config from '../../conf';
-const collection = db.get('users');
+const User = db.get<IUser>('users');
-(collection as any).createIndex('username'); // fuck type definition
-(collection as any).createIndex('token'); // fuck type definition
+User.createIndex('username');
+User.createIndex('token');
-export default collection as any; // fuck type definition
+export default User;
export function validateUsername(username: string): boolean {
return typeof username == 'string' && /^[a-zA-Z0-9\-]{3,20}$/.test(username);
@@ -37,6 +42,7 @@ export function isValidBirthday(birthday: string): boolean {
export type IUser = {
_id: mongo.ObjectID;
created_at: Date;
+ deleted_at: Date;
email: string;
followers_count: number;
following_count: number;
@@ -72,6 +78,8 @@ export type IUser = {
is_pro: boolean;
is_suspended: boolean;
keywords: string[];
+ two_factor_secret: string;
+ two_factor_enabled: boolean;
};
export function init(user): IUser {
@@ -81,3 +89,182 @@ export function init(user): IUser {
user.pinned_post_id = new mongo.ObjectID(user.pinned_post_id);
return user;
}
+
+/**
+ * Pack a user for API response
+ *
+ * @param user target
+ * @param me? serializee
+ * @param options? serialize options
+ * @return Packed user
+ */
+export const pack = (
+ user: string | mongo.ObjectID | IUser,
+ me?: string | mongo.ObjectID | IUser,
+ options?: {
+ detail?: boolean,
+ includeSecrets?: boolean
+ }
+) => new Promise<any>(async (resolve, reject) => {
+
+ const opts = Object.assign({
+ detail: false,
+ includeSecrets: false
+ }, options);
+
+ let _user: any;
+
+ const fields = opts.detail ? {
+ settings: false
+ } : {
+ settings: false,
+ client_settings: false,
+ profile: false,
+ keywords: false,
+ domains: false
+ };
+
+ // Populate the user if 'user' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
+ _user = await User.findOne({
+ _id: user
+ }, { fields });
+ } else if (typeof user === 'string') {
+ _user = await User.findOne({
+ _id: new mongo.ObjectID(user)
+ }, { fields });
+ } else {
+ _user = deepcopy(user);
+ }
+
+ if (!_user) return reject('invalid user arg.');
+
+ // Me
+ const meId: mongo.ObjectID = me
+ ? mongo.ObjectID.prototype.isPrototypeOf(me)
+ ? me as mongo.ObjectID
+ : typeof me === 'string'
+ ? new mongo.ObjectID(me)
+ : (me as IUser)._id
+ : null;
+
+ // Rename _id to id
+ _user.id = _user._id;
+ delete _user._id;
+
+ // Remove needless properties
+ delete _user.latest_post;
+
+ // Remove private properties
+ delete _user.password;
+ delete _user.token;
+ delete _user.two_factor_temp_secret;
+ delete _user.two_factor_secret;
+ delete _user.username_lower;
+ if (_user.twitter) {
+ delete _user.twitter.access_token;
+ delete _user.twitter.access_token_secret;
+ }
+ delete _user.line;
+
+ // Visible via only the official client
+ if (!opts.includeSecrets) {
+ delete _user.email;
+ delete _user.client_settings;
+ }
+
+ if (!opts.detail) {
+ delete _user.two_factor_enabled;
+ }
+
+ _user.avatar_url = _user.avatar_id != null
+ ? `${config.drive_url}/${_user.avatar_id}`
+ : `${config.drive_url}/default-avatar.jpg`;
+
+ _user.banner_url = _user.banner_id != null
+ ? `${config.drive_url}/${_user.banner_id}`
+ : null;
+
+ if (!meId || !meId.equals(_user.id) || !opts.detail) {
+ delete _user.avatar_id;
+ delete _user.banner_id;
+
+ delete _user.drive_capacity;
+ }
+
+ if (meId && !meId.equals(_user.id)) {
+ // Whether the user is following
+ _user.is_following = (async () => {
+ const follow = await Following.findOne({
+ follower_id: meId,
+ followee_id: _user.id,
+ deleted_at: { $exists: false }
+ });
+ return follow !== null;
+ })();
+
+ // Whether the user is followed
+ _user.is_followed = (async () => {
+ const follow2 = await Following.findOne({
+ follower_id: _user.id,
+ followee_id: meId,
+ deleted_at: { $exists: false }
+ });
+ return follow2 !== null;
+ })();
+
+ // Whether the user is muted
+ _user.is_muted = (async () => {
+ const mute = await Mute.findOne({
+ muter_id: meId,
+ mutee_id: _user.id,
+ deleted_at: { $exists: false }
+ });
+ return mute !== null;
+ })();
+ }
+
+ if (opts.detail) {
+ if (_user.pinned_post_id) {
+ // Populate pinned post
+ _user.pinned_post = packPost(_user.pinned_post_id, meId, {
+ detail: true
+ });
+ }
+
+ if (meId && !meId.equals(_user.id)) {
+ const myFollowingIds = await getFriends(meId);
+
+ // Get following you know count
+ _user.following_you_know_count = Following.count({
+ followee_id: { $in: myFollowingIds },
+ follower_id: _user.id,
+ deleted_at: { $exists: false }
+ });
+
+ // Get followers you know count
+ _user.followers_you_know_count = Following.count({
+ followee_id: _user.id,
+ follower_id: { $in: myFollowingIds },
+ deleted_at: { $exists: false }
+ });
+ }
+ }
+
+ // resolve promises in _user object
+ _user = await rap(_user);
+
+ resolve(_user);
+});
+
+/*
+function img(url) {
+ return {
+ thumbnail: {
+ large: `${url}`,
+ medium: '',
+ small: ''
+ }
+ };
+}
+*/
diff --git a/src/api/private/signin.ts b/src/api/private/signin.ts
index 0ebf8d6aa1..b49d25d99a 100644
--- a/src/api/private/signin.ts
+++ b/src/api/private/signin.ts
@@ -1,16 +1,19 @@
import * as express from 'express';
import * as bcrypt from 'bcryptjs';
+import * as speakeasy from 'speakeasy';
import { default as User, IUser } from '../models/user';
-import Signin from '../models/signin';
-import serialize from '../serializers/signin';
+import Signin, { pack } from '../models/signin';
import event from '../event';
import signin from '../common/signin';
+import config from '../../conf';
export default async (req: express.Request, res: express.Response) => {
+ res.header('Access-Control-Allow-Origin', config.url);
res.header('Access-Control-Allow-Credentials', 'true');
const username = req.body['username'];
const password = req.body['password'];
+ const token = req.body['token'];
if (typeof username != 'string') {
res.sendStatus(400);
@@ -22,6 +25,11 @@ export default async (req: express.Request, res: express.Response) => {
return;
}
+ if (token != null && typeof token != 'string') {
+ res.sendStatus(400);
+ return;
+ }
+
// Fetch user
const user: IUser = await User.findOne({
username_lower: username.toLowerCase()
@@ -43,7 +51,23 @@ export default async (req: express.Request, res: express.Response) => {
const same = await bcrypt.compare(password, user.password);
if (same) {
- signin(res, user, false);
+ if (user.two_factor_enabled) {
+ const verified = (speakeasy as any).totp.verify({
+ secret: user.two_factor_secret,
+ encoding: 'base32',
+ token: token
+ });
+
+ if (verified) {
+ signin(res, user, false);
+ } else {
+ res.status(400).send({
+ error: 'invalid token'
+ });
+ }
+ } else {
+ signin(res, user, false);
+ }
} else {
res.status(400).send({
error: 'incorrect password'
@@ -60,5 +84,5 @@ export default async (req: express.Request, res: express.Response) => {
});
// Publish signin event
- event(user._id, 'signin', await serialize(record));
+ event(user._id, 'signin', await pack(record));
};
diff --git a/src/api/private/signup.ts b/src/api/private/signup.ts
index 466c6a489f..8efdb6db47 100644
--- a/src/api/private/signup.ts
+++ b/src/api/private/signup.ts
@@ -2,9 +2,7 @@ import * as uuid from 'uuid';
import * as express from 'express';
import * as bcrypt from 'bcryptjs';
import recaptcha = require('recaptcha-promise');
-import { default as User, IUser } from '../models/user';
-import { validateUsername, validatePassword } from '../models/user';
-import serialize from '../serializers/user';
+import User, { IUser, validateUsername, validatePassword, pack } from '../models/user';
import generateUserToken from '../common/generate-native-user-token';
import config from '../../conf';
@@ -142,7 +140,7 @@ export default async (req: express.Request, res: express.Response) => {
});
// Response
- res.send(await serialize(account));
+ res.send(await pack(account));
// Create search index
if (config.elasticsearch.enable) {
diff --git a/src/api/serializers/app.ts b/src/api/serializers/app.ts
deleted file mode 100644
index 9d1c46dca4..0000000000
--- a/src/api/serializers/app.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import App from '../models/app';
-import AccessToken from '../models/access-token';
-import config from '../../conf';
-
-/**
- * Serialize an app
- *
- * @param {any} app
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
- app: any,
- me?: any,
- options?: {
- includeSecret?: boolean,
- includeProfileImageIds?: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = options || {
- includeSecret: false,
- includeProfileImageIds: false
- };
-
- let _app: any;
-
- // Populate the app if 'app' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(app)) {
- _app = await App.findOne({
- _id: app
- });
- } else if (typeof app === 'string') {
- _app = await App.findOne({
- _id: new mongo.ObjectID(app)
- });
- } else {
- _app = deepcopy(app);
- }
-
- // Me
- if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
- if (typeof me === 'string') {
- me = new mongo.ObjectID(me);
- } else {
- me = me._id;
- }
- }
-
- // Rename _id to id
- _app.id = _app._id;
- delete _app._id;
-
- delete _app.name_id_lower;
-
- // Visible by only owner
- if (!opts.includeSecret) {
- delete _app.secret;
- }
-
- _app.icon_url = _app.icon != null
- ? `${config.drive_url}/${_app.icon}`
- : `${config.drive_url}/app-default.jpg`;
-
- if (me) {
- // 既に連携しているか
- const exist = await AccessToken.count({
- app_id: _app.id,
- user_id: me,
- }, {
- limit: 1
- });
-
- _app.is_authorized = exist === 1;
- }
-
- resolve(_app);
-});
diff --git a/src/api/serializers/auth-session.ts b/src/api/serializers/auth-session.ts
deleted file mode 100644
index a9acf1243a..0000000000
--- a/src/api/serializers/auth-session.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import serializeApp from './app';
-
-/**
- * Serialize an auth session
- *
- * @param {any} session
- * @param {any} me?
- * @return {Promise<any>}
- */
-export default (
- session: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _session: any;
-
- // TODO: Populate session if it ID
-
- _session = deepcopy(session);
-
- // Me
- if (me && !mongo.ObjectID.prototype.isPrototypeOf(me)) {
- if (typeof me === 'string') {
- me = new mongo.ObjectID(me);
- } else {
- me = me._id;
- }
- }
-
- delete _session._id;
-
- // Populate app
- _session.app = await serializeApp(_session.app_id, me);
-
- resolve(_session);
-});
diff --git a/src/api/serializers/channel.ts b/src/api/serializers/channel.ts
deleted file mode 100644
index 3cba39aa16..0000000000
--- a/src/api/serializers/channel.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { IUser } from '../models/user';
-import { default as Channel, IChannel } from '../models/channel';
-import Watching from '../models/channel-watching';
-
-/**
- * Serialize a channel
- *
- * @param channel target
- * @param me? serializee
- * @return response
- */
-export default (
- channel: string | mongo.ObjectID | IChannel,
- me?: string | mongo.ObjectID | IUser
-) => new Promise<any>(async (resolve, reject) => {
-
- let _channel: any;
-
- // Populate the channel if 'channel' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(channel)) {
- _channel = await Channel.findOne({
- _id: channel
- });
- } else if (typeof channel === 'string') {
- _channel = await Channel.findOne({
- _id: new mongo.ObjectID(channel)
- });
- } else {
- _channel = deepcopy(channel);
- }
-
- // Rename _id to id
- _channel.id = _channel._id;
- delete _channel._id;
-
- // Remove needless properties
- delete _channel.user_id;
-
- // Me
- const meId: mongo.ObjectID = me
- ? mongo.ObjectID.prototype.isPrototypeOf(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- if (me) {
- //#region Watchしているかどうか
- const watch = await Watching.findOne({
- user_id: meId,
- channel_id: _channel.id,
- deleted_at: { $exists: false }
- });
-
- _channel.is_watching = watch !== null;
- //#endregion
- }
-
- resolve(_channel);
-});
diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts
deleted file mode 100644
index dcdaa01fab..0000000000
--- a/src/api/serializers/drive-file.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveFile from '../models/drive-file';
-import serializeDriveFolder from './drive-folder';
-import serializeDriveTag from './drive-tag';
-import deepcopy = require('deepcopy');
-import config from '../../conf';
-
-/**
- * Serialize a drive file
- *
- * @param {any} file
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
- file: any,
- options?: {
- detail: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false
- }, options);
-
- let _file: any;
-
- // Populate the file if 'file' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
- _file = await DriveFile.findOne({
- _id: file
- });
- } else if (typeof file === 'string') {
- _file = await DriveFile.findOne({
- _id: new mongo.ObjectID(file)
- });
- } else {
- _file = deepcopy(file);
- }
-
- if (!_file) return reject('invalid file arg.');
-
- // rendered target
- let _target: any = {};
-
- _target.id = _file._id;
- _target.created_at = _file.uploadDate;
- _target.name = _file.filename;
- _target.type = _file.contentType;
- _target.datasize = _file.length;
- _target.md5 = _file.md5;
-
- _target = Object.assign(_target, _file.metadata);
-
- _target.url = `${config.drive_url}/${_target.id}/${encodeURIComponent(_target.name)}`;
-
- if (opts.detail && _target.folder_id) {
- // Populate folder
- _target.folder = await serializeDriveFolder(_target.folder_id, {
- detail: true
- });
- }
-
- if (opts.detail && _target.tags) {
- // Populate tags
- _target.tags = await _target.tags.map(async (tag: any) =>
- await serializeDriveTag(tag)
- );
- }
-
- resolve(_target);
-});
diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts
deleted file mode 100644
index 6ebf454a28..0000000000
--- a/src/api/serializers/drive-folder.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveFolder from '../models/drive-folder';
-import DriveFile from '../models/drive-file';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a drive folder
- *
- * @param {any} folder
- * @param {any} options?
- * @return {Promise<any>}
- */
-const self = (
- folder: any,
- options?: {
- detail: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = Object.assign({
- detail: false
- }, options);
-
- let _folder: any;
-
- // Populate the folder if 'folder' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(folder)) {
- _folder = await DriveFolder.findOne({ _id: folder });
- } else if (typeof folder === 'string') {
- _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) });
- } else {
- _folder = deepcopy(folder);
- }
-
- // Rename _id to id
- _folder.id = _folder._id;
- delete _folder._id;
-
- if (opts.detail) {
- const childFoldersCount = await DriveFolder.count({
- parent_id: _folder.id
- });
-
- const childFilesCount = await DriveFile.count({
- 'metadata.folder_id': _folder.id
- });
-
- _folder.folders_count = childFoldersCount;
- _folder.files_count = childFilesCount;
- }
-
- if (opts.detail && _folder.parent_id) {
- // Populate parent folder
- _folder.parent = await self(_folder.parent_id, {
- detail: true
- });
- }
-
- resolve(_folder);
-});
-
-export default self;
diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts
deleted file mode 100644
index 2f152381bd..0000000000
--- a/src/api/serializers/drive-tag.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import DriveTag from '../models/drive-tag';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a drive tag
- *
- * @param {any} tag
- * @return {Promise<any>}
- */
-const self = (
- tag: any
-) => new Promise<any>(async (resolve, reject) => {
- let _tag: any;
-
- // Populate the tag if 'tag' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(tag)) {
- _tag = await DriveTag.findOne({ _id: tag });
- } else if (typeof tag === 'string') {
- _tag = await DriveTag.findOne({ _id: new mongo.ObjectID(tag) });
- } else {
- _tag = deepcopy(tag);
- }
-
- // Rename _id to id
- _tag.id = _tag._id;
- delete _tag._id;
-
- resolve(_tag);
-});
-
-export default self;
diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts
deleted file mode 100644
index 4ab95e42a3..0000000000
--- a/src/api/serializers/messaging-message.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import Message from '../models/messaging-message';
-import serializeUser from './user';
-import serializeDriveFile from './drive-file';
-import parse from '../common/text';
-
-/**
- * Serialize a message
- *
- * @param {any} message
- * @param {any} me?
- * @param {any} options?
- * @return {Promise<any>}
- */
-export default (
- message: any,
- me?: any,
- options?: {
- populateRecipient: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
- const opts = options || {
- populateRecipient: true
- };
-
- let _message: any;
-
- // Populate the message if 'message' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(message)) {
- _message = await Message.findOne({
- _id: message
- });
- } else if (typeof message === 'string') {
- _message = await Message.findOne({
- _id: new mongo.ObjectID(message)
- });
- } else {
- _message = deepcopy(message);
- }
-
- // Rename _id to id
- _message.id = _message._id;
- delete _message._id;
-
- // Parse text
- if (_message.text) {
- _message.ast = parse(_message.text);
- }
-
- // Populate user
- _message.user = await serializeUser(_message.user_id, me);
-
- if (_message.file) {
- // Populate file
- _message.file = await serializeDriveFile(_message.file_id);
- }
-
- if (opts.populateRecipient) {
- // Populate recipient
- _message.recipient = await serializeUser(_message.recipient_id, me);
- }
-
- resolve(_message);
-});
diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts
deleted file mode 100644
index ac919dc8b0..0000000000
--- a/src/api/serializers/notification.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import Notification from '../models/notification';
-import serializeUser from './user';
-import serializePost from './post';
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a notification
- *
- * @param {any} notification
- * @return {Promise<any>}
- */
-export default (notification: any) => new Promise<any>(async (resolve, reject) => {
- let _notification: any;
-
- // Populate the notification if 'notification' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(notification)) {
- _notification = await Notification.findOne({
- _id: notification
- });
- } else if (typeof notification === 'string') {
- _notification = await Notification.findOne({
- _id: new mongo.ObjectID(notification)
- });
- } else {
- _notification = deepcopy(notification);
- }
-
- // Rename _id to id
- _notification.id = _notification._id;
- delete _notification._id;
-
- // Rename notifier_id to user_id
- _notification.user_id = _notification.notifier_id;
- delete _notification.notifier_id;
-
- const me = _notification.notifiee_id;
- delete _notification.notifiee_id;
-
- // Populate notifier
- _notification.user = await serializeUser(_notification.user_id, me);
-
- switch (_notification.type) {
- case 'follow':
- // nope
- break;
- case 'mention':
- case 'reply':
- case 'repost':
- case 'quote':
- case 'reaction':
- case 'poll_vote':
- // Populate post
- _notification.post = await serializePost(_notification.post_id, me);
- break;
- default:
- console.error(`Unknown type: ${_notification.type}`);
- break;
- }
-
- resolve(_notification);
-});
diff --git a/src/api/serializers/post-reaction.ts b/src/api/serializers/post-reaction.ts
deleted file mode 100644
index b8807a741c..0000000000
--- a/src/api/serializers/post-reaction.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import Reaction from '../models/post-reaction';
-import serializeUser from './user';
-
-/**
- * Serialize a reaction
- *
- * @param {any} reaction
- * @param {any} me?
- * @return {Promise<any>}
- */
-export default (
- reaction: any,
- me?: any
-) => new Promise<any>(async (resolve, reject) => {
- let _reaction: any;
-
- // Populate the reaction if 'reaction' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(reaction)) {
- _reaction = await Reaction.findOne({
- _id: reaction
- });
- } else if (typeof reaction === 'string') {
- _reaction = await Reaction.findOne({
- _id: new mongo.ObjectID(reaction)
- });
- } else {
- _reaction = deepcopy(reaction);
- }
-
- // Rename _id to id
- _reaction.id = _reaction._id;
- delete _reaction._id;
-
- // Populate user
- _reaction.user = await serializeUser(_reaction.user_id, me);
-
- resolve(_reaction);
-});
diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts
deleted file mode 100644
index 03fd120772..0000000000
--- a/src/api/serializers/post.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { default as Post, IPost } from '../models/post';
-import Reaction from '../models/post-reaction';
-import { IUser } from '../models/user';
-import Vote from '../models/poll-vote';
-import serializeApp from './app';
-import serializeChannel from './channel';
-import serializeUser from './user';
-import serializeDriveFile from './drive-file';
-import parse from '../common/text';
-import rap from '@prezzemolo/rap';
-
-/**
- * Serialize a post
- *
- * @param post target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-const self = async (
- post: string | mongo.ObjectID | IPost,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail: boolean
- }
-) => {
- const opts = options || {
- detail: true,
- };
-
- // Me
- const meId: mongo.ObjectID = me
- ? mongo.ObjectID.prototype.isPrototypeOf(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- let _post: any;
-
- // Populate the post if 'post' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(post)) {
- _post = await Post.findOne({
- _id: post
- });
- } else if (typeof post === 'string') {
- _post = await Post.findOne({
- _id: new mongo.ObjectID(post)
- });
- } else {
- _post = deepcopy(post);
- }
-
- if (!_post) throw 'invalid post arg.';
-
- const id = _post._id;
-
- // Rename _id to id
- _post.id = _post._id;
- delete _post._id;
-
- delete _post.mentions;
-
- // Parse text
- if (_post.text) {
- _post.ast = parse(_post.text);
- }
-
- // Populate user
- _post.user = serializeUser(_post.user_id, meId);
-
- // Populate app
- if (_post.app_id) {
- _post.app = serializeApp(_post.app_id);
- }
-
- // Populate channel
- if (_post.channel_id) {
- _post.channel = serializeChannel(_post.channel_id);
- }
-
- // Populate media
- if (_post.media_ids) {
- _post.media = Promise.all(_post.media_ids.map(fileId =>
- serializeDriveFile(fileId)
- ));
- }
-
- // When requested a detailed post data
- if (opts.detail) {
- // Get previous post info
- _post.prev = (async () => {
- const prev = await Post.findOne({
- user_id: _post.user_id,
- _id: {
- $lt: id
- }
- }, {
- fields: {
- _id: true
- },
- sort: {
- _id: -1
- }
- });
- return prev ? prev._id : null;
- })();
-
- // Get next post info
- _post.next = (async () => {
- const next = await Post.findOne({
- user_id: _post.user_id,
- _id: {
- $gt: id
- }
- }, {
- fields: {
- _id: true
- },
- sort: {
- _id: 1
- }
- });
- return next ? next._id : null;
- })();
-
- if (_post.reply_id) {
- // Populate reply to post
- _post.reply = self(_post.reply_id, meId, {
- detail: false
- });
- }
-
- if (_post.repost_id) {
- // Populate repost
- _post.repost = self(_post.repost_id, meId, {
- detail: _post.text == null
- });
- }
-
- // Poll
- if (meId && _post.poll) {
- _post.poll = (async (poll) => {
- const vote = await Vote
- .findOne({
- user_id: meId,
- post_id: id
- });
-
- if (vote != null) {
- const myChoice = poll.choices
- .filter(c => c.id == vote.choice)[0];
-
- myChoice.is_voted = true;
- }
-
- return poll;
- })(_post.poll);
- }
-
- // Fetch my reaction
- if (meId) {
- _post.my_reaction = (async () => {
- const reaction = await Reaction
- .findOne({
- user_id: meId,
- post_id: id,
- deleted_at: { $exists: false }
- });
-
- if (reaction) {
- return reaction.reaction;
- }
-
- return null;
- })();
- }
- }
-
- // resolve promises in _post object
- _post = await rap(_post);
-
- return _post;
-};
-
-export default self;
diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts
deleted file mode 100644
index 4068067678..0000000000
--- a/src/api/serializers/signin.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Module dependencies
- */
-import deepcopy = require('deepcopy');
-
-/**
- * Serialize a signin record
- *
- * @param {any} record
- * @return {Promise<any>}
- */
-export default (
- record: any
-) => new Promise<any>(async (resolve, reject) => {
-
- const _record = deepcopy(record);
-
- // Rename _id to id
- _record.id = _record._id;
- delete _record._id;
-
- resolve(_record);
-});
diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts
deleted file mode 100644
index 3d84156606..0000000000
--- a/src/api/serializers/user.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-/**
- * Module dependencies
- */
-import * as mongo from 'mongodb';
-import deepcopy = require('deepcopy');
-import { default as User, IUser } from '../models/user';
-import serializePost from './post';
-import Following from '../models/following';
-import getFriends from '../common/get-friends';
-import config from '../../conf';
-import rap from '@prezzemolo/rap';
-
-/**
- * Serialize a user
- *
- * @param user target
- * @param me? serializee
- * @param options? serialize options
- * @return response
- */
-export default (
- user: string | mongo.ObjectID | IUser,
- me?: string | mongo.ObjectID | IUser,
- options?: {
- detail?: boolean,
- includeSecrets?: boolean
- }
-) => new Promise<any>(async (resolve, reject) => {
-
- const opts = Object.assign({
- detail: false,
- includeSecrets: false
- }, options);
-
- let _user: any;
-
- const fields = opts.detail ? {
- settings: false
- } : {
- settings: false,
- client_settings: false,
- profile: false,
- keywords: false,
- domains: false
- };
-
- // Populate the user if 'user' is ID
- if (mongo.ObjectID.prototype.isPrototypeOf(user)) {
- _user = await User.findOne({
- _id: user
- }, { fields });
- } else if (typeof user === 'string') {
- _user = await User.findOne({
- _id: new mongo.ObjectID(user)
- }, { fields });
- } else {
- _user = deepcopy(user);
- }
-
- if (!_user) return reject('invalid user arg.');
-
- // Me
- const meId: mongo.ObjectID = me
- ? mongo.ObjectID.prototype.isPrototypeOf(me)
- ? me as mongo.ObjectID
- : typeof me === 'string'
- ? new mongo.ObjectID(me)
- : (me as IUser)._id
- : null;
-
- // Rename _id to id
- _user.id = _user._id;
- delete _user._id;
-
- // Remove needless properties
- delete _user.latest_post;
-
- // Remove private properties
- delete _user.password;
- delete _user.token;
- delete _user.username_lower;
- if (_user.twitter) {
- delete _user.twitter.access_token;
- delete _user.twitter.access_token_secret;
- }
- delete _user.line;
-
- // Visible via only the official client
- if (!opts.includeSecrets) {
- delete _user.email;
- delete _user.client_settings;
- }
-
- _user.avatar_url = _user.avatar_id != null
- ? `${config.drive_url}/${_user.avatar_id}`
- : `${config.drive_url}/default-avatar.jpg`;
-
- _user.banner_url = _user.banner_id != null
- ? `${config.drive_url}/${_user.banner_id}`
- : null;
-
- if (!meId || !meId.equals(_user.id) || !opts.detail) {
- delete _user.avatar_id;
- delete _user.banner_id;
-
- delete _user.drive_capacity;
- }
-
- if (meId && !meId.equals(_user.id)) {
- // If the user is following
- _user.is_following = (async () => {
- const follow = await Following.findOne({
- follower_id: meId,
- followee_id: _user.id,
- deleted_at: { $exists: false }
- });
- return follow !== null;
- })();
-
- // If the user is followed
- _user.is_followed = (async () => {
- const follow2 = await Following.findOne({
- follower_id: _user.id,
- followee_id: meId,
- deleted_at: { $exists: false }
- });
- return follow2 !== null;
- })();
- }
-
- if (opts.detail) {
- if (_user.pinned_post_id) {
- // Populate pinned post
- _user.pinned_post = serializePost(_user.pinned_post_id, meId, {
- detail: true
- });
- }
-
- if (meId && !meId.equals(_user.id)) {
- const myFollowingIds = await getFriends(meId);
-
- // Get following you know count
- _user.following_you_know_count = Following.count({
- followee_id: { $in: myFollowingIds },
- follower_id: _user.id,
- deleted_at: { $exists: false }
- });
-
- // Get followers you know count
- _user.followers_you_know_count = Following.count({
- followee_id: _user.id,
- follower_id: { $in: myFollowingIds },
- deleted_at: { $exists: false }
- });
- }
- }
-
- // resolve promises in _user object
- _user = await rap(_user);
-
- resolve(_user);
-});
-/*
-function img(url) {
- return {
- thumbnail: {
- large: `${url}`,
- medium: '',
- small: ''
- }
- };
-}
-*/
diff --git a/src/api/server.ts b/src/api/server.ts
index 026357b465..e89d196096 100644
--- a/src/api/server.ts
+++ b/src/api/server.ts
@@ -26,9 +26,7 @@ app.use(bodyParser.json({
}
}
}));
-app.use(cors({
- origin: true
-}));
+app.use(cors());
app.get('/', (req, res) => {
res.send('YEE HAW');
@@ -49,13 +47,6 @@ endpoints.forEach(endpoint =>
app.post('/signup', require('./private/signup').default);
app.post('/signin', require('./private/signin').default);
-app.use((req, res, next) => {
- // req.headers['cookie'] は常に string ですが、型定義の都合上
- // string | string[] になっているので string を明示しています
- res.locals.user = ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1];
- next();
-});
-
require('./service/github')(app);
require('./service/twitter')(app);
diff --git a/src/api/service/twitter.ts b/src/api/service/twitter.ts
index f164cdc458..adcd5ac49b 100644
--- a/src/api/service/twitter.ts
+++ b/src/api/service/twitter.ts
@@ -5,27 +5,51 @@ import * as uuid from 'uuid';
// const Twitter = require('twitter');
import autwh from 'autwh';
import redis from '../../db/redis';
-import User from '../models/user';
-import serialize from '../serializers/user';
+import User, { pack } from '../models/user';
import event from '../event';
import config from '../../conf';
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 compareOrigin(req: express.Request) {
+ function normalizeUrl(url: string) {
+ return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
+ }
+
+ // req.headers['referer'] は常に string ですが、型定義の都合上
+ // string | string[] になっているので string を明示しています
+ const referer = req.headers['referer'] as string;
+
+ return (normalizeUrl(referer) == normalizeUrl(config.url));
+ }
+
app.get('/disconnect/twitter', async (req, res): Promise<any> => {
- if (res.locals.user == null) return res.send('plz signin');
+ 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({
- token: res.locals.user
+ token: userToken
}, {
- $set: {
- twitter: null
- }
- });
+ $set: {
+ twitter: null
+ }
+ });
res.send(`Twitterの連携を解除しました :v:`);
// Publish i updated event
- event(user._id, 'i_updated', await serialize(user, user, {
+ event(user._id, 'i_updated', await pack(user, user, {
detail: true,
includeSecrets: true
}));
@@ -50,9 +74,16 @@ module.exports = (app: express.Application) => {
});
app.get('/connect/twitter', async (req, res): Promise<any> => {
- if (res.locals.user == null) return res.send('plz signin');
+ if (!compareOrigin(req)) {
+ res.status(400).send('invalid origin');
+ return;
+ }
+
+ const userToken = getUserToken(req);
+ if (userToken == null) return res.send('plz signin');
+
const ctx = await twAuth.begin();
- redis.set(res.locals.user, JSON.stringify(ctx));
+ redis.set(userToken, JSON.stringify(ctx));
res.redirect(ctx.url);
});
@@ -77,7 +108,9 @@ module.exports = (app: express.Application) => {
});
app.get('/tw/cb', (req, res): any => {
- if (res.locals.user == null) {
+ const userToken = getUserToken(req);
+
+ if (userToken == null) {
// req.headers['cookie'] は常に string ですが、型定義の都合上
// string | string[] になっているので string を明示しています
const cookies = cookie.parse((req.headers['cookie'] as string || ''));
@@ -86,6 +119,7 @@ module.exports = (app: express.Application) => {
if (sessid == undefined) {
res.status(400).send('invalid session');
+ return;
}
redis.get(sessid, async (_, ctx) => {
@@ -97,16 +131,24 @@ module.exports = (app: express.Application) => {
if (user == null) {
res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`);
+ return;
}
signin(res, user, true);
});
} else {
- redis.get(res.locals.user, async (_, ctx) => {
- const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier);
+ const verifier = req.query.oauth_verifier;
+
+ if (verifier == null) {
+ res.status(400).send('invalid session');
+ return;
+ }
+
+ redis.get(userToken, async (_, ctx) => {
+ const result = await twAuth.done(JSON.parse(ctx), verifier);
const user = await User.findOneAndUpdate({
- token: res.locals.user
+ token: userToken
}, {
$set: {
twitter: {
@@ -121,7 +163,7 @@ module.exports = (app: express.Application) => {
res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`);
// Publish i updated event
- event(user._id, 'i_updated', await serialize(user, user, {
+ event(user._id, 'i_updated', await pack(user, user, {
detail: true,
includeSecrets: true
}));
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index 7c8f3bfec8..10078337c3 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -3,24 +3,53 @@ import * as redis from 'redis';
import * as debug from 'debug';
import User from '../models/user';
-import serializePost from '../serializers/post';
+import Mute from '../models/mute';
+import { pack as packPost } from '../models/post';
import readNotification from '../common/read-notification';
const log = debug('misskey');
-export default function homeStream(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void {
+export default async function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any) {
// Subscribe Home stream channel
subscriber.subscribe(`misskey:user-stream:${user._id}`);
+ const mute = await Mute.find({
+ muter_id: user._id,
+ deleted_at: { $exists: false }
+ });
+ const mutedUserIds = mute.map(m => m.mutee_id.toString());
+
subscriber.on('message', async (channel, data) => {
switch (channel.split(':')[1]) {
case 'user-stream':
- connection.send(data);
+ try {
+ const x = JSON.parse(data);
+
+ if (x.type == 'post') {
+ if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+ return;
+ }
+ if (x.body.reply != null && mutedUserIds.indexOf(x.body.reply.user_id) != -1) {
+ return;
+ }
+ if (x.body.repost != null && mutedUserIds.indexOf(x.body.repost.user_id) != -1) {
+ return;
+ }
+ } else if (x.type == 'notification') {
+ if (mutedUserIds.indexOf(x.body.user_id) != -1) {
+ return;
+ }
+ }
+
+ connection.send(data);
+ } catch (e) {
+ connection.send(data);
+ }
break;
case 'post-stream':
const postId = channel.split(':')[2];
log(`RECEIVED: ${postId} ${data} by @${user.username}`);
- const post = await serializePost(postId, user, {
+ const post = await packPost(postId, user, {
detail: true
});
connection.send(JSON.stringify({
diff --git a/src/common/build/fa.ts b/src/common/build/fa.ts
new file mode 100644
index 0000000000..0c21be9504
--- /dev/null
+++ b/src/common/build/fa.ts
@@ -0,0 +1,57 @@
+/**
+ * Replace fontawesome symbols
+ */
+
+import * as fontawesome from '@fortawesome/fontawesome';
+import * as regular from '@fortawesome/fontawesome-free-regular';
+import * as solid from '@fortawesome/fontawesome-free-solid';
+import * as brands from '@fortawesome/fontawesome-free-brands';
+
+// Add icons
+fontawesome.library.add(regular);
+fontawesome.library.add(solid);
+fontawesome.library.add(brands);
+
+export const pattern = /%fa:(.+?)%/g;
+
+export const replacement = (_, key) => {
+ const args = key.split(' ');
+ let prefix = 'fas';
+ const classes = [];
+ let transform = '';
+ let name;
+
+ args.forEach(arg => {
+ if (arg == 'R' || arg == 'S' || arg == 'B') {
+ prefix =
+ arg == 'R' ? 'far' :
+ arg == 'S' ? 'fas' :
+ arg == 'B' ? 'fab' :
+ '';
+ } else if (arg[0] == '.') {
+ classes.push('fa-' + arg.substr(1));
+ } else if (arg[0] == '-') {
+ transform = arg.substr(1).split('|').join(' ');
+ } else {
+ name = arg;
+ }
+ });
+
+ const icon = fontawesome.icon({ prefix, iconName: name }, {
+ classes: classes
+ });
+
+ if (icon) {
+ icon.transform = fontawesome.parse.transform(transform);
+ return `<i data-fa class="${name}">${icon.html[0]}</i>`;
+ } else {
+ console.warn(`'${name}' not found in fa`);
+ return '';
+ }
+};
+
+export default (src: string) => {
+ return src.replace(pattern, replacement);
+};
+
+export const fa = fontawesome;
diff --git a/src/common/build/i18n.ts b/src/common/build/i18n.ts
new file mode 100644
index 0000000000..1ae22147c4
--- /dev/null
+++ b/src/common/build/i18n.ts
@@ -0,0 +1,50 @@
+/**
+ * Replace i18n texts
+ */
+
+import locale from '../../../locales';
+
+export default class Replacer {
+ private lang: string;
+
+ public pattern = /"%i18n:(.+?)%"|'%i18n:(.+?)%'|%i18n:(.+?)%/g;
+
+ constructor(lang: string) {
+ this.lang = lang;
+
+ this.get = this.get.bind(this);
+ this.replacement = this.replacement.bind(this);
+ }
+
+ private get(key: string) {
+ let text = locale[this.lang];
+
+ // Check the key existance
+ const error = key.split('.').some(k => {
+ if (text.hasOwnProperty(k)) {
+ text = text[k];
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ if (error) {
+ console.warn(`key '${key}' not found in '${this.lang}'`);
+ return key; // Fallback
+ } else {
+ return text;
+ }
+ }
+
+ public replacement(match, a, b, c) {
+ const key = a || b || c;
+ if (match[0] == '"') {
+ return '"' + this.get(key).replace(/"/g, '\\"') + '"';
+ } else if (match[0] == "'") {
+ return '\'' + this.get(key).replace(/'/g, '\\\'') + '\'';
+ } else {
+ return this.get(key);
+ }
+ }
+}
diff --git a/src/common/build/license.ts b/src/common/build/license.ts
new file mode 100644
index 0000000000..e5c264df8a
--- /dev/null
+++ b/src/common/build/license.ts
@@ -0,0 +1,13 @@
+import * as fs from 'fs';
+
+const license = fs.readFileSync(__dirname + '/../../../LICENSE', 'utf-8');
+
+const licenseHtml = license
+ .replace(/\r\n/g, '\n')
+ .replace(/(.)\n(.)/g, '$1 $2')
+ .replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+
+export {
+ license,
+ licenseHtml
+};
diff --git a/src/config.ts b/src/config.ts
index 3ff8007586..3ffefe278b 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -101,7 +101,7 @@ type Mixin = {
secondary_scheme: string;
api_url: string;
auth_url: string;
- about_url: string;
+ docs_url: string;
ch_url: string;
stats_url: string;
status_url: string;
@@ -131,7 +131,7 @@ export default function load() {
mixin.auth_url = `${mixin.scheme}://auth.${mixin.host}`;
mixin.ch_url = `${mixin.scheme}://ch.${mixin.host}`;
mixin.dev_url = `${mixin.scheme}://dev.${mixin.host}`;
- mixin.about_url = `${mixin.scheme}://about.${mixin.host}`;
+ mixin.docs_url = `${mixin.scheme}://docs.${mixin.host}`;
mixin.stats_url = `${mixin.scheme}://stats.${mixin.host}`;
mixin.status_url = `${mixin.scheme}://status.${mixin.host}`;
mixin.drive_url = `${mixin.secondary_scheme}://file.${mixin.secondary_host}`;
diff --git a/src/const.json b/src/const.json
index 924b4dd8b3..d8fe4fe6cd 100644
--- a/src/const.json
+++ b/src/const.json
@@ -1,4 +1,5 @@
{
+ "copyright": "Copyright (c) 2014-2018 syuilo",
"themeColor": "#ff4e45",
"themeColorForeground": "#fff"
}
diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts
index c978e6460f..233f2f3d79 100644
--- a/src/db/mongodb.ts
+++ b/src/db/mongodb.ts
@@ -1,13 +1,16 @@
import config from '../conf';
-const uri = config.mongodb.user && config.mongodb.pass
-? `mongodb://${config.mongodb.user}:${config.mongodb.pass}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
-: `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
+const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
+const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
+
+const uri = u && p
+ ? `mongodb://${u}:${p}@${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`
+ : `mongodb://${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
/**
* monk
*/
-import * as mongo from 'monk';
+import mongo from 'monk';
const db = mongo(uri);
@@ -24,9 +27,9 @@ const nativeDbConn = async (): Promise<mongodb.Db> => {
if (mdb) return mdb;
const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => {
- mongodb.MongoClient.connect(uri, (e, db) => {
+ (mongodb as any).MongoClient.connect(uri, (e, client) => {
if (e) return reject(e);
- resolve(db);
+ resolve(client.db(config.mongodb.db));
});
}))();
diff --git a/src/docs/api/entities/post.pug b/src/docs/api/entities/post.pug
deleted file mode 100644
index 954f172717..0000000000
--- a/src/docs/api/entities/post.pug
+++ /dev/null
@@ -1,149 +0,0 @@
-extend ../../BASE
-
-block title
- | Entity: Post
-
-block content
- h1 Post
- p 投稿を表します。
-
- section
- h2 Properties
- table.entity
- thead: tr
- td Name
- td Type
- td Description
- tbody
- tr.nullable.optional
- td app
- td: a(href='./app', target='_blank') App
- td 投稿したアプリ
- tr.nullable
- td app_id
- td ID
- td 投稿したアプリのID
- tr
- td created_at
- td Date
- td 投稿日時
- tr
- td id
- td ID
- td 投稿ID
- tr.optional
- td is_liked
- td Boolean
- td いいね したかどうか
- tr
- td likes_count
- td Number
- td いいね数
- tr.nullable.optional
- td media_ids
- td ID[]
- td 添付されたメディアのIDの配列
- tr.nullable.optional
- td media
- td: a(href='./drive-file', target='_blank') DriveFile[]
- td 添付されたメディアの配列
- tr
- td replies_count
- td Number
- td 返信数
- tr.optional
- td reply
- td: a(href='./post', target='_blank') Post
- td 返信先の投稿
- tr.nullable
- td reply_id
- td ID
- td 返信先の投稿のID
- tr.optional
- td repost
- td: a(href='./post', target='_blank') Post
- td Repostした投稿
- tr
- td repost_count
- td Number
- td Repostされた数
- tr.nullable
- td repost_id
- td ID
- td Repostした投稿のID
- tr.nullable
- td text
- td String
- td 本文
- tr.optional
- td user
- td: a(href='./user', target='_blank') User
- td 投稿者
- tr
- td user_id
- td ID
- td 投稿者のID
-
- section
- h2 Example
- pre: code.
- {
- "created_at": "2016-12-10T00:28:50.114Z",
- "media_ids": null,
- "reply_id": "584a16b15860fc52320137e3",
- "repost_id": null,
- "text": "小日向美穂だぞ!",
- "user_id": "5848bf7764e572683f4402f8",
- "app_id": null,
- "likes_count": 1,
- "replies_count": 1,
- "id": "584b4c42d8e5186f8f755d0c",
- "user": {
- "birthday": null,
- "created_at": "2016-12-08T02:03:35.332Z",
- "bio": "女が嫌いです、女性は好きです",
- "followers_count": 11,
- "following_count": 11,
- "links": null,
- "location": "",
- "name": "女が嫌い",
- "posts_count": 26,
- "likes_count": 2,
- "liked_count": 20,
- "username": "onnnagakirai",
- "id": "5848bf7764e572683f4402f8",
- "avatar_url": "https://file.himasaku.net/5848c0ec64e572683f4402fc",
- "banner_url": "https://file.himasaku.net/5848c12864e572683f4402fd",
- "is_following": true,
- "is_followed": true
- },
- "reply": {
- "created_at": "2016-12-09T02:28:01.563Z",
- "media_ids": null,
- "reply_id": "5849d35e547e4249be329884",
- "repost_id": null,
- "text": "アイコン小日向美穂?",
- "user_id": "57d01a501fdf2d07be417afe",
- "app_id": null,
- "replies_count": 1,
- "id": "584a16b15860fc52320137e3",
- "user": {
- "birthday": null,
- "created_at": "2016-09-07T13:46:56.605Z",
- "bio": "どうすれば君だけのために生きていけるの",
- "followers_count": 51,
- "following_count": 97,
- "links": null,
- "location": "川崎",
- "name": "きな子",
- "posts_count": 4813,
- "username": "syuilo",
- "likes_count": 3141,
- "liked_count": 750,
- "id": "57d01a501fdf2d07be417afe",
- "avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a",
- "banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5"
- }
- },
- "is_liked": true
- }
diff --git a/src/docs/api/entities/user.pug b/src/docs/api/entities/user.pug
deleted file mode 100644
index a37886bb19..0000000000
--- a/src/docs/api/entities/user.pug
+++ /dev/null
@@ -1,122 +0,0 @@
-extend ../../BASE
-
-block title
- | Entity: User
-
-block content
- h1 User
- p ユーザーを表します。
-
- section
- h2 Properties
- table.entity
- thead: tr
- td Name
- td Type
- td Description
- tbody
- tr.nullable.optional
- td avatar_id
- td ID
- td アバターに設定しているドライブのファイルのID
- tr.nullable
- td avatar_url
- td String
- td アバターURL
- tr.nullable.optional
- td banner_id
- td ID
- td バナーに設定しているドライブのファイルのID
- tr.nullable
- td banner_url
- td String
- td バナーURL
- tr.nullable
- td bio
- td String
- td プロフィール
- tr.nullable
- td birthday
- td String
- td 誕生日(YYYY-MM-DD)
- tr
- td created_at
- td Date
- td アカウント作成日時
- tr.optional
- td drive_capacity
- td Number
- td ドライブの最大容量(byte単位)
- tr
- td followers_count
- td Number
- td フォロワー数
- tr
- td following_count
- td Number
- td フォロー数
- tr
- td id
- td ID
- td ユーザーID
- tr.optional
- td is_bot
- td Boolean
- td botかどうか
- tr.optional
- td is_followed
- td Boolean
- td フォローされているか
- tr.optional
- td is_following
- td Boolean
- td フォローしているか
- tr
- td liked_count
- td Number
- td 投稿にいいねされた数
- tr
- td likes_count
- td Number
- td 投稿にいいねした数
- tr.nullable
- td location
- td String
- td 住処
- tr
- td name
- td String
- td ニックネーム
- tr
- td posts_count
- td Number
- td 投稿数
- tr
- td username
- td String
- td ユーザー名
-
- section
- h2 Example
- pre: code.
- {
- "avatar_id": "583ddc6e64df272771f74c1a",
- "avatar_url": "https://file.himasaku.net/583ddc6e64df272771f74c1a",
- "banner_id": "584bfc82d8e5186f8f755ec5",
- "banner_url": "https://file.himasaku.net/584bfc82d8e5186f8f755ec5",
- "bio": "どうすれば君だけのために生きていけるの",
- "birthday": "1997-12-06",
- "created_at": "2016-09-07T13:46:56.605Z",
- "drive_capacity": 1073741824,
- "email": null,
- "followers_count": 51,
- "following_count": 97,
- "id": "57d01a501fdf2d07be417afe",
- "liked_count": 750,
- "likes_count": 3130,
- "links": null,
- "location": "川崎",
- "name": "きな子",
- "posts_count": 4811,
- "username": "syuilo"
- }
diff --git a/src/docs/api/getting-started.md b/src/docs/api/getting-started.md
deleted file mode 100644
index e13659914e..0000000000
--- a/src/docs/api/getting-started.md
+++ /dev/null
@@ -1,73 +0,0 @@
-Getting Started
-================================================================
-MisskeyはREST APIやStreaming APIを提供しており、プログラムからMisskeyの全ての機能を利用することができます。
-それらのAPIを利用するには、まずAPIを利用したいアカウントのアクセストークンを取得する必要があります:
-
-自分のアクセストークンを取得したい場合
-----------------------------------------------------------------
-自分自身のアクセストークンは、設定 > API で確認できます。
-<p class="tip">
- アカウントを乗っ取られてしまう可能性があるため、トークンは第三者に教えないでください(アプリなどにも入力しないでください)。<br>
- 万が一トークンが漏れたりその可能性がある場合は トークンを再生成できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)
-</p>
-
-他人のアクセストークンを取得する
-----------------------------------------------------------------
-不特定多数のユーザーからAPIを利用したい場合、アプリケーションを作成します。
-アプリケーションを作成すると、ユーザーが連携を許可した時に、そのユーザーのアクセストークンを取得することができます。
-
-アプリケーションを作成してアクセストークンを取得するまでの流れを説明します。
-
-### アプリケーションを作成する
-まずはあなたのアプリケーションを作成しましょう。
- | <a href=#{dev_url} target="_blank">デベロッパーセンター</a>にアクセスし、アプリ > アプリ作成 に進みます。
- br
- | 次に、フォームに必要事項を記入します:
- dl
- dt アプリケーション名
- dd あなたのアプリケーションの名前。
- dt Named ID
- dd アプリを識別する/a-z-/で構成されたID。
- dt アプリの概要
- dd アプリの簡単な説明を入力してください。
- dt コールバックURL
- dd あなたのアプリケーションがWebアプリケーションである場合、ユーザーが後述するフォームで認証を終えた際にリダイレクトするURLを設定できます。
- dt 権限
- dd アプリケーションが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
- p.tip
- | 権限はアプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーはすべて無効になります。
- p
- | アプリケーションを作成すると、作ったアプリの管理ページに進みます。
- br
- | アプリのシークレットキー(App Secret)が表示されていますので、メモしておいてください。
- p.tip
- | アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
-
- section
- h3 ユーザーに認証させる
- p あなたのアプリを使ってもらうには、ユーザーにアカウントへアクセスすることを許可してもらい、Misskeyにそのユーザーのアクセストークンを発行してもらう必要があります。
- p 認証セッションを開始するには、<code>#{api_url}/auth/session/generate</code>へパラメータに<code>app_secret</code>としてApp Secretを含めたリクエストを送信します。
- p
- | そうすると、レスポンスとして認証セッションのトークンや認証フォームのURLが取得できます。
- br
- | この認証フォームのURLをブラウザで表示し、ユーザーにフォームを表示してください。
- section
- h4 あなたのアプリがコールバックURLを設定している場合
- p ユーザーがアプリの連携を許可すると設定しているコールバックURLに<code>token</code>という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
- section
- h4 あなたのアプリがコールバックURLを設定していない場合
- p ユーザーがアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
- p
- | 次に、<code>#{api_url}/auth/session/userkey</code>へ<code>app_secret</code>としてApp Secretを、<code>token</code>としてセッションのトークンをパラメータとして付与したリクエストを送信してください。
- br
- | 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
- p
- | 以降アクセストークンは、<strong>ユーザーのアクセストークン+アプリのシークレットキーをsha256したもの</strong>として扱います。
-
- p アクセストークンを取得できたら、あとは簡単です。REST APIなら、リクエストにアクセストークンを<code>i</code>としてパラメータに含めるだけです。
-
- section
- h2 リクエスト形式
- p <code>application/json</code>を受け付けます。
- p.tip
- | 現在<code>application/x-www-form-urlencoded</code>も受け付けていますが、将来的にこのサポートはされなくなる予定です。
diff --git a/src/docs/api/library.md b/src/docs/api/library.md
deleted file mode 100644
index 71ddbe345d..0000000000
--- a/src/docs/api/library.md
+++ /dev/null
@@ -1,8 +0,0 @@
-ライブラリ
-================================================================
-
-Misskey APIを便利に利用するためのライブラリ一覧です。
-
-.NET
-----------------------------------------------------------------
-* **[Misq (公式)](https://github.com/syuilo/Misq)**
diff --git a/src/docs/index.md b/src/docs/index.md
deleted file mode 100644
index 0846cf27e8..0000000000
--- a/src/docs/index.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Misskeyについて
-================================================================
-
-誰か書いて
diff --git a/src/docs/link-to-twitter.md b/src/docs/link-to-twitter.md
deleted file mode 100644
index 77fb744576..0000000000
--- a/src/docs/link-to-twitter.md
+++ /dev/null
@@ -1,9 +0,0 @@
-Twitterと連携する
-================================================================
-
-設定 -> Twitter から、お使いのMisskeyアカウントとお使いのTwitterアカウントを関連付けることができます。
-アカウントの関連付けを行うと、プロフィールにTwitterアカウントへのリンクが表示されたりなどします。
-
-MisskeyがあなたのTwitterアカウントでツイートしたり誰かをフォローしたりといったことは、
-一切行いませんのでご安心ください。(Misskeyはそのような権限を取得しないので、行おうと思っても行えません)
-Twitterのアプリケーション認証フォームでこの権限の詳細を確認することができます。また、いつでも連携を取り消すことができます。
diff --git a/src/docs/tou.md b/src/docs/tou.md
deleted file mode 100644
index fbf87867b4..0000000000
--- a/src/docs/tou.md
+++ /dev/null
@@ -1,4 +0,0 @@
-利用規約
-================================================================
-
-公序良俗に反する行為はおやめください。
diff --git a/src/file/server.ts b/src/file/server.ts
index 1f8d21b80d..3bda5b14fe 100644
--- a/src/file/server.ts
+++ b/src/file/server.ts
@@ -7,11 +7,15 @@ 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 _gm from 'gm';
import * as stream from 'stream';
import DriveFile, { getGridFSBucket } from '../api/models/drive-file';
+const gm = _gm.subClass({
+ imageMagick: true
+});
+
/**
* Init app
*/
@@ -78,6 +82,8 @@ function thumbnail(data: stream.Readable, type: string, resize: number): ISend {
const stream = g
.compress('jpeg')
.quality(80)
+ .interlace('line')
+ .noProfile() // Remove EXIF
.stream();
return {
@@ -130,6 +136,8 @@ async function sendFileById(req: express.Request, res: express.Response): Promis
}
const fileId = new mongodb.ObjectID(req.params.id);
+
+ // Fetch (drive) file
const file = await DriveFile.findOne({ _id: fileId });
// validate name
diff --git a/src/utils/dependencyInfo.ts b/src/utils/dependencyInfo.ts
index 818fa3136c..89af0d20fb 100644
--- a/src/utils/dependencyInfo.ts
+++ b/src/utils/dependencyInfo.ts
@@ -11,7 +11,7 @@ export default class {
public showAll(): void {
this.show('MongoDB', 'mongo --version', x => x.match(/^MongoDB shell version:? (.*)\r?\n/));
this.show('Redis', 'redis-server --version', x => x.match(/v=([0-9\.]*)/));
- this.show('GraphicsMagick', 'gm -version', x => x.match(/^GraphicsMagick ([0-9\.]*) .*/));
+ this.show('ImageMagick', 'magick -version', x => x.match(/^Version: ImageMagick (.+?)\r?\n/));
}
public show(serviceName: string, command: string, transform: (x: string) => RegExpMatchArray): void {
diff --git a/src/web/app/app.styl b/src/web/app/app.styl
index de66df74d4..22043b8833 100644
--- a/src/web/app/app.styl
+++ b/src/web/app/app.styl
@@ -1,29 +1,4 @@
-json('../../const.json')
-
-@charset 'utf-8'
-
-$theme-color = themeColor
-$theme-color-foreground = themeColorForeground
-
-/*
- ::selection
- background $theme-color
- color #fff
-*/
-
-*
- position relative
- box-sizing border-box
- background-clip padding-box !important
- tap-highlight-color rgba($theme-color, 0.7)
- -webkit-tap-highlight-color rgba($theme-color, 0.7)
-
-html, body
- margin 0
- padding 0
- scroll-behavior smooth
- text-size-adjust 100%
- font-family sans-serif
+@import "../style"
html
&.progress
@@ -96,17 +71,6 @@ body
100%
transform rotate(360deg)
-a
- text-decoration none
- color $theme-color
- cursor pointer
-
- &:hover
- text-decoration underline
-
- *
- cursor pointer
-
code
font-family Consolas, 'Courier New', Courier, Monaco, monospace
diff --git a/src/web/app/base.pug b/src/web/app/base.pug
index 140286a768..d7c7f0aed4 100644
--- a/src/web/app/base.pug
+++ b/src/web/app/base.pug
@@ -24,6 +24,9 @@ html
//- FontAwesome style
style #{facss}
+ //- highlight.js style
+ style #{hljscss}
+
body
noscript: p
| JavaScriptを有効にしてください
diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts
index 2008e6f5ac..bba838f56b 100644
--- a/src/web/app/common/scripts/api.ts
+++ b/src/web/app/common/scripts/api.ts
@@ -40,7 +40,7 @@ export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
} else {
res.json().then(err => {
reject(err.error);
- });
+ }, reject);
}
}).catch(reject);
});
diff --git a/src/web/app/common/scripts/parse-search-query.ts b/src/web/app/common/scripts/parse-search-query.ts
new file mode 100644
index 0000000000..512791ecb0
--- /dev/null
+++ b/src/web/app/common/scripts/parse-search-query.ts
@@ -0,0 +1,53 @@
+export default function(qs: string) {
+ const q = {
+ text: ''
+ };
+
+ qs.split(' ').forEach(x => {
+ if (/^([a-z_]+?):(.+?)$/.test(x)) {
+ const [key, value] = x.split(':');
+ switch (key) {
+ case 'user':
+ q['include_user_usernames'] = value.split(',');
+ break;
+ case 'exclude_user':
+ q['exclude_user_usernames'] = value.split(',');
+ break;
+ case 'follow':
+ q['following'] = value == 'null' ? null : value == 'true';
+ break;
+ case 'reply':
+ q['reply'] = value == 'null' ? null : value == 'true';
+ break;
+ case 'repost':
+ q['repost'] = value == 'null' ? null : value == 'true';
+ break;
+ case 'media':
+ q['media'] = value == 'null' ? null : value == 'true';
+ break;
+ case 'poll':
+ q['poll'] = value == 'null' ? null : value == 'true';
+ break;
+ case 'until':
+ case 'since':
+ // YYYY-MM-DD
+ if (/^[0-9]+\-[0-9]+\-[0-9]+$/) {
+ const [yyyy, mm, dd] = value.split('-');
+ q[`${key}_date`] = (new Date(parseInt(yyyy, 10), parseInt(mm, 10) - 1, parseInt(dd, 10))).getTime();
+ }
+ break;
+ default:
+ q[key] = value;
+ break;
+ }
+ } else {
+ q.text += x + ' ';
+ }
+ });
+
+ if (q.text) {
+ q.text = q.text.trim();
+ }
+
+ return q;
+}
diff --git a/src/web/app/common/tags/authorized-apps.tag b/src/web/app/common/tags/authorized-apps.tag
index 0078a18636..0594032de6 100644
--- a/src/web/app/common/tags/authorized-apps.tag
+++ b/src/web/app/common/tags/authorized-apps.tag
@@ -1,5 +1,7 @@
<mk-authorized-apps>
- <p class="none" if={ !fetching && apps.length == 0 }>%i18n:common.tags.mk-authorized-apps.no-apps%</p>
+ <div class="none ui info" if={ !fetching && apps.length == 0 }>
+ <p>%fa:info-circle%%i18n:common.tags.mk-authorized-apps.no-apps%</p>
+ </div>
<div class="apps" if={ apps.length != 0 }>
<div each={ app in apps }>
<p><b>{ app.name }</b></p>
diff --git a/src/web/app/common/tags/copyright.tag b/src/web/app/common/tags/copyright.tag
deleted file mode 100644
index 9c3f1f648b..0000000000
--- a/src/web/app/common/tags/copyright.tag
+++ /dev/null
@@ -1,7 +0,0 @@
-<mk-copyright>
- <span>(c) syuilo 2014-2017</span>
- <style>
- :scope
- display block
- </style>
-</mk-copyright>
diff --git a/src/web/app/common/tags/index.ts b/src/web/app/common/tags/index.ts
index 2f4e1181d4..df99d93cc5 100644
--- a/src/web/app/common/tags/index.ts
+++ b/src/web/app/common/tags/index.ts
@@ -12,7 +12,6 @@ require('./signin.tag');
require('./signup.tag');
require('./forkit.tag');
require('./introduction.tag');
-require('./copyright.tag');
require('./signin-history.tag');
require('./twitter-setting.tag');
require('./authorized-apps.tag');
diff --git a/src/web/app/common/tags/introduction.tag b/src/web/app/common/tags/introduction.tag
index 3256688d10..28afc6fa46 100644
--- a/src/web/app/common/tags/introduction.tag
+++ b/src/web/app/common/tags/introduction.tag
@@ -3,7 +3,7 @@
<h1>Misskeyとは?</h1>
<p><ruby>Misskey<rt>みすきー</rt></ruby>は、<a href="http://syuilo.com" target="_blank">syuilo</a>が2014年くらいから<a href="https://github.com/syuilo/misskey" target="_blank">オープンソースで</a>開発・運営を行っている、ミニブログベースのSNSです。</p>
<p>無料で誰でも利用でき、広告も掲載していません。</p>
- <p><a href={ _ABOUT_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
+ <p><a href={ _DOCS_URL_ } target="_blank">もっと知りたい方はこちら</a></p>
</article>
<style>
:scope
diff --git a/src/web/app/common/tags/messaging/room.tag b/src/web/app/common/tags/messaging/room.tag
index a149e1de22..7b4d1be569 100644
--- a/src/web/app/common/tags/messaging/room.tag
+++ b/src/web/app/common/tags/messaging/room.tag
@@ -254,7 +254,7 @@
this.api('messaging/messages', {
user_id: this.user.id,
limit: max + 1,
- max_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
+ until_id: this.moreMessagesIsInStock ? this.messages[0].id : undefined
}).then(messages => {
if (messages.length == max + 1) {
this.moreMessagesIsInStock = true;
diff --git a/src/web/app/common/tags/nav-links.tag b/src/web/app/common/tags/nav-links.tag
index 71f0453db0..ea122575aa 100644
--- a/src/web/app/common/tags/nav-links.tag
+++ b/src/web/app/common/tags/nav-links.tag
@@ -1,7 +1,10 @@
<mk-nav-links>
- <a href={ _ABOUT_URL_ }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
+ <a href={ aboutUrl }>%i18n:common.tags.mk-nav-links.about%</a><i>・</i><a href={ _STATS_URL_ }>%i18n:common.tags.mk-nav-links.stats%</a><i>・</i><a href={ _STATUS_URL_ }>%i18n:common.tags.mk-nav-links.status%</a><i>・</i><a href="http://zawazawa.jp/misskey/">%i18n:common.tags.mk-nav-links.wiki%</a><i>・</i><a href="https://github.com/syuilo/misskey/blob/master/DONORS.md">%i18n:common.tags.mk-nav-links.donors%</a><i>・</i><a href="https://github.com/syuilo/misskey">%i18n:common.tags.mk-nav-links.repository%</a><i>・</i><a href={ _DEV_URL_ }>%i18n:common.tags.mk-nav-links.develop%</a><i>・</i><a href="https://twitter.com/misskey_xyz" target="_blank">Follow us on %fa:B twitter%</a>
<style>
:scope
display inline
</style>
+ <script>
+ this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
+ </script>
</mk-nav-links>
diff --git a/src/web/app/common/tags/signin-history.tag b/src/web/app/common/tags/signin-history.tag
index 03afd72326..cdd58c4c67 100644
--- a/src/web/app/common/tags/signin-history.tag
+++ b/src/web/app/common/tags/signin-history.tag
@@ -1,55 +1,11 @@
<mk-signin-history>
<div class="records" if={ history.length != 0 }>
- <div each={ history }>
- <mk-time time={ created_at }/>
- <header>
- <virtual if={ success }>%fa:check%</virtual>
- <virtual if={ !success }>%fa:times%</virtual>
- <span class="ip">{ ip }</span>
- </header>
- <pre><code>{ JSON.stringify(headers, null, ' ') }</code></pre>
- </div>
+ <mk-signin-record each={ rec in history } rec={ rec }/>
</div>
<style>
:scope
display block
- > .records
- > div
- padding 16px 0 0 0
- border-bottom solid 1px #eee
-
- > header
-
- > [data-fa]
- margin-right 8px
-
- &.check
- color #0fda82
-
- &.times
- color #ff3100
-
- > .ip
- display inline-block
- color #444
- background #f8f8f8
-
- > mk-time
- position absolute
- top 16px
- right 0
- color #777
-
- > pre
- overflow auto
- max-height 100px
-
- > code
- white-space pre-wrap
- word-break break-all
- color #4a535a
-
</style>
<script>
this.mixin('i');
@@ -84,3 +40,77 @@
};
</script>
</mk-signin-history>
+
+<mk-signin-record>
+ <header onclick={ toggle }>
+ <virtual if={ rec.success }>%fa:check%</virtual>
+ <virtual if={ !rec.success }>%fa:times%</virtual>
+ <span class="ip">{ rec.ip }</span>
+ <mk-time time={ rec.created_at }/>
+ </header>
+ <pre ref="headers" class="json" show={ show }>{ JSON.stringify(rec.headers, null, 2) }</pre>
+
+ <style>
+ :scope
+ display block
+ border-bottom solid 1px #eee
+
+ > header
+ display flex
+ padding 8px 0
+ line-height 32px
+ cursor pointer
+
+ > [data-fa]
+ margin-right 8px
+ text-align left
+
+ &.check
+ color #0fda82
+
+ &.times
+ color #ff3100
+
+ > .ip
+ display inline-block
+ text-align left
+ padding 8px
+ line-height 16px
+ font-family monospace
+ font-size 14px
+ color #444
+ background #f8f8f8
+ border-radius 4px
+
+ > mk-time
+ margin-left auto
+ text-align right
+ color #777
+
+ > pre
+ overflow auto
+ margin 0 0 16px 0
+ max-height 100px
+ white-space pre-wrap
+ word-break break-all
+ color #4a535a
+
+ </style>
+
+ <script>
+ import hljs from 'highlight.js';
+
+ this.rec = this.opts.rec;
+ this.show = false;
+
+ this.on('mount', () => {
+ hljs.highlightBlock(this.refs.headers);
+ });
+
+ this.toggle = () => {
+ this.update({
+ show: !this.show
+ });
+ };
+ </script>
+</mk-signin-record>
diff --git a/src/web/app/common/tags/signin.tag b/src/web/app/common/tags/signin.tag
index f25d99974b..f5a2be94ed 100644
--- a/src/web/app/common/tags/signin.tag
+++ b/src/web/app/common/tags/signin.tag
@@ -6,6 +6,9 @@
<label class="password">
<input ref="password" type="password" placeholder="%i18n:common.tags.mk-signin.password%" required="required"/>%fa:lock%
</label>
+ <label class="token" if={ user && user.two_factor_enabled }>
+ <input ref="token" type="number" placeholder="%i18n:common.tags.mk-signin.token%" required="required"/>%fa:lock%
+ </label>
<button type="submit" disabled={ signing }>{ signing ? '%i18n:common.tags.mk-signin.signing-in%' : '%i18n:common.tags.mk-signin.signin%' }</button>
</form>
<style>
@@ -39,6 +42,7 @@
input[type=text]
input[type=password]
+ input[type=number]
user-select text
display inline-block
cursor auto
@@ -123,6 +127,10 @@
this.refs.password.focus();
return false;
}
+ if (this.user && this.user.two_factor_enabled && this.refs.token.value == '') {
+ this.refs.token.focus();
+ return false;
+ }
this.update({
signing: true
@@ -130,7 +138,8 @@
this.api('signin', {
username: this.refs.username.value,
- password: this.refs.password.value
+ password: this.refs.password.value,
+ token: this.user && this.user.two_factor_enabled ? this.refs.token.value : undefined
}).then(() => {
location.reload();
}).catch(() => {
diff --git a/src/web/app/common/tags/signup.tag b/src/web/app/common/tags/signup.tag
index 4816fe66db..b488efb927 100644
--- a/src/web/app/common/tags/signup.tag
+++ b/src/web/app/common/tags/signup.tag
@@ -34,7 +34,7 @@
</label>
<label class="agree-tou">
<input name="agree-tou" type="checkbox" autocomplete="off" required="required"/>
- <p><a href="https://github.com/syuilo/misskey/blob/master/src/docs/tou.md" target="_blank">利用規約</a>に同意する</p>
+ <p><a href={ touUrl } target="_blank">利用規約</a>に同意する</p>
</label>
<button onclick={ onsubmit }>%i18n:common.tags.mk-signup.create%</button>
</form>
@@ -182,6 +182,8 @@
this.passwordRetypeState = null;
this.recaptchaed = false;
+ this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/tou`;
+
window.onRecaptchaed = () => {
this.recaptchaed = true;
this.update();
diff --git a/src/web/app/common/tags/twitter-setting.tag b/src/web/app/common/tags/twitter-setting.tag
index 3b70505ba2..4d57cfa55a 100644
--- a/src/web/app/common/tags/twitter-setting.tag
+++ b/src/web/app/common/tags/twitter-setting.tag
@@ -1,5 +1,5 @@
<mk-twitter-setting>
- <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _ABOUT_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
+ <p>%i18n:common.tags.mk-twitter-setting.description%<a href={ _DOCS_URL_ + '/link-to-twitter' } target="_blank">%i18n:common.tags.mk-twitter-setting.detail%</a></p>
<p class="account" if={ I.twitter } title={ 'Twitter ID: ' + I.twitter.user_id }>%i18n:common.tags.mk-twitter-setting.connected-to%: <a href={ 'https://twitter.com/' + I.twitter.screen_name } target="_blank">@{ I.twitter.screen_name }</a></p>
<p>
<a href={ _API_URL_ + '/connect/twitter' } target="_blank" onclick={ connect }>{ I.twitter ? '%i18n:common.tags.mk-twitter-setting.reconnect%' : '%i18n:common.tags.mk-twitter-setting.connect%' }</a>
diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
index 27b63ab2ef..ce68c4f2d1 100644
--- a/src/web/app/desktop/router.ts
+++ b/src/web/app/desktop/router.ts
@@ -16,7 +16,7 @@ export default (mios: MiOS) => {
route('/i/messaging/:user', messaging);
route('/i/mentions', mentions);
route('/post::post', post);
- route('/search::query', search);
+ route('/search', search);
route('/:user', user.bind(null, 'home'));
route('/:user/graphs', user.bind(null, 'graphs'));
route('/:user/:post', post);
@@ -47,7 +47,7 @@ export default (mios: MiOS) => {
function search(ctx) {
const el = document.createElement('mk-search-page');
- el.setAttribute('query', ctx.params.query);
+ el.setAttribute('query', ctx.querystring.substr(2));
mount(el);
}
diff --git a/src/web/app/desktop/style.styl b/src/web/app/desktop/style.styl
index d99e5df2b4..c893e2ed67 100644
--- a/src/web/app/desktop/style.styl
+++ b/src/web/app/desktop/style.styl
@@ -2,6 +2,8 @@
@import "../reset"
@import "../../../../node_modules/cropperjs/dist/cropper.css"
+@import "./ui"
+
*::input-placeholder
color #D8CBC5
@@ -47,66 +49,3 @@ html
#wait
right auto
left 15px
-
-button
- font-family sans-serif
-
- *
- pointer-events none
-
- &.style-normal
- &.style-primary
- display block
- cursor pointer
- padding 0 16px
- margin 0
- min-width 100px
- height 40px
- font-size 1em
- outline none
- border-radius 4px
-
- &:focus
- &:after
- content ""
- pointer-events none
- position absolute
- top -5px
- right -5px
- bottom -5px
- left -5px
- border 2px solid rgba($theme-color, 0.3)
- border-radius 8px
-
- &:disabled
- opacity 0.7
- cursor default
-
- &.style-normal
- color #888
- background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
- border solid 1px #e2e2e2
-
- &:hover
- background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
- border-color #dcdcdc
-
- &:active
- background #ececec
- border-color #dcdcdc
-
- &.style-primary
- color $theme-color-foreground
- background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
- border solid 1px lighten($theme-color, 15%)
-
- &:not(:disabled)
- font-weight bold
-
- &:hover:not(:disabled)
- background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
- border-color $theme-color
-
- &:active:not(:disabled)
- background $theme-color
- border-color $theme-color
diff --git a/src/web/app/desktop/tags/drive/browser.tag b/src/web/app/desktop/tags/drive/browser.tag
index 901daabfd8..a60a46b790 100644
--- a/src/web/app/desktop/tags/drive/browser.tag
+++ b/src/web/app/desktop/tags/drive/browser.tag
@@ -18,14 +18,16 @@
<virtual each={ folder in folders }>
<mk-drive-browser-folder class="folder" folder={ folder }/>
</virtual>
- <div class="padding" each={ folders }></div>
+ <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
+ <div class="padding" each={ Array(10).fill(16) }></div>
<button if={ moreFolders }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
</div>
<div class="files" ref="filesContainer" if={ files.length > 0 }>
<virtual each={ file in files }>
<mk-drive-browser-file class="file" file={ file }/>
</virtual>
- <div class="padding" each={ files }></div>
+ <!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
+ <div class="padding" each={ Array(10).fill(16) }></div>
<button if={ moreFiles } onclick={ fetchMoreFiles }>%i18n:desktop.tags.mk-drive-browser.load-more%</button>
</div>
<div class="empty" if={ files.length == 0 && folders.length == 0 && !fetching }>
diff --git a/src/web/app/desktop/tags/drive/file.tag b/src/web/app/desktop/tags/drive/file.tag
index 0f019d95bf..8b3d36b3f3 100644
--- a/src/web/app/desktop/tags/drive/file.tag
+++ b/src/web/app/desktop/tags/drive/file.tag
@@ -5,7 +5,9 @@
<div class="label" if={ I.banner_id == file.id }><img src="/assets/label.svg"/>
<p>%i18n:desktop.tags.mk-drive-browser-file.banner%</p>
</div>
- <div class="thumbnail"><img src={ file.url + '?thumbnail&size=128' } alt=""/></div>
+ <div class="thumbnail" ref="thumbnail" style="background-color:{ file.properties.average_color ? 'rgb(' + file.properties.average_color.join(',') + ')' : 'transparent' }">
+ <img src={ file.url + '?thumbnail&size=128' } alt="" onload={ onload }/>
+ </div>
<p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
<style>
:scope
@@ -139,6 +141,7 @@
</style>
<script>
+ import anime from 'animejs';
import bytesToSize from '../../../common/scripts/bytes-to-size';
this.mixin('i');
@@ -199,5 +202,16 @@
this.isDragging = false;
this.browser.isDragSource = false;
};
+
+ this.onload = () => {
+ if (this.file.properties.average_color) {
+ anime({
+ targets: this.refs.thumbnail,
+ backgroundColor: `rgba(${this.file.properties.average_color.join(',')}, 0)`,
+ duration: 100,
+ easing: 'linear'
+ });
+ }
+ };
</script>
</mk-drive-browser-file>
diff --git a/src/web/app/desktop/tags/home-widgets/mentions.tag b/src/web/app/desktop/tags/home-widgets/mentions.tag
index a48c7239a1..2687283079 100644
--- a/src/web/app/desktop/tags/home-widgets/mentions.tag
+++ b/src/web/app/desktop/tags/home-widgets/mentions.tag
@@ -101,7 +101,7 @@
});
this.api('posts/mentions', {
following: this.mode == 'following',
- max_id: this.refs.timeline.tail().id
+ until_id: this.refs.timeline.tail().id
}).then(posts => {
this.update({
moreLoading: false
diff --git a/src/web/app/desktop/tags/home-widgets/timeline.tag b/src/web/app/desktop/tags/home-widgets/timeline.tag
index 4c58aa4aa8..9571b09f34 100644
--- a/src/web/app/desktop/tags/home-widgets/timeline.tag
+++ b/src/web/app/desktop/tags/home-widgets/timeline.tag
@@ -86,7 +86,7 @@
});
this.api('posts/timeline', {
- max_date: this.date ? this.date.getTime() : undefined
+ until_date: this.date ? this.date.getTime() : undefined
}).then(posts => {
this.update({
isLoading: false,
@@ -103,7 +103,7 @@
moreLoading: true
});
this.api('posts/timeline', {
- max_id: this.refs.timeline.tail().id
+ until_id: this.refs.timeline.tail().id
}).then(posts => {
this.update({
moreLoading: false
diff --git a/src/web/app/desktop/tags/image-dialog.tag b/src/web/app/desktop/tags/image-dialog.tag
deleted file mode 100644
index 39d16ca139..0000000000
--- a/src/web/app/desktop/tags/image-dialog.tag
+++ /dev/null
@@ -1,61 +0,0 @@
-<mk-image-dialog>
- <div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/>
- <style>
- :scope
- display block
- position fixed
- z-index 2048
- top 0
- left 0
- width 100%
- height 100%
- opacity 0
-
- > .bg
- display block
- position fixed
- z-index 1
- top 0
- left 0
- width 100%
- height 100%
- background rgba(0, 0, 0, 0.7)
-
- > img
- position fixed
- z-index 2
- top 0
- right 0
- bottom 0
- left 0
- max-width 100%
- max-height 100%
- margin auto
- cursor zoom-out
-
- </style>
- <script>
- import anime from 'animejs';
-
- this.image = this.opts.image;
-
- this.on('mount', () => {
- anime({
- targets: this.root,
- opacity: 1,
- duration: 100,
- easing: 'linear'
- });
- });
-
- this.close = () => {
- anime({
- targets: this.root,
- opacity: 0,
- duration: 100,
- easing: 'linear',
- complete: () => this.unmount()
- });
- };
- </script>
-</mk-image-dialog>
diff --git a/src/web/app/desktop/tags/images-viewer.tag b/src/web/app/desktop/tags/images-viewer.tag
deleted file mode 100644
index 44a61cb747..0000000000
--- a/src/web/app/desktop/tags/images-viewer.tag
+++ /dev/null
@@ -1,45 +0,0 @@
-<mk-images-viewer>
- <div class="image" ref="view" onmousemove={ mousemove } style={ 'background-image: url(' + image.url + '?thumbnail' } onclick={ click }><img src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
- <style>
- :scope
- display block
- overflow hidden
- border-radius 4px
-
- > .image
- cursor zoom-in
-
- > img
- display block
- max-height 256px
- max-width 100%
- margin 0 auto
-
- &:hover
- > img
- visibility hidden
-
- &:not(:hover)
- background-image none !important
-
- </style>
- <script>
- this.images = this.opts.images;
- this.image = this.images[0];
-
- this.mousemove = e => {
- const rect = this.refs.view.getBoundingClientRect();
- const mouseX = e.clientX - rect.left;
- const mouseY = e.clientY - rect.top;
- const xp = mouseX / this.refs.view.offsetWidth * 100;
- const yp = mouseY / this.refs.view.offsetHeight * 100;
- this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
- };
-
- this.click = () => {
- riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
- image: this.image
- });
- };
- </script>
-</mk-images-viewer>
diff --git a/src/web/app/desktop/tags/images.tag b/src/web/app/desktop/tags/images.tag
new file mode 100644
index 0000000000..0cd408576f
--- /dev/null
+++ b/src/web/app/desktop/tags/images.tag
@@ -0,0 +1,172 @@
+<mk-images>
+ <virtual each={ image in images }>
+ <mk-images-image image={ image }/>
+ </virtual>
+ <style>
+ :scope
+ display grid
+ grid-gap 4px
+ height 256px
+ </style>
+ <script>
+ this.images = this.opts.images;
+
+ this.on('mount', () => {
+ if (this.images.length == 1) {
+ this.root.style.gridTemplateRows = '1fr';
+
+ this.tags['mk-images-image'].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'].root.style.gridRow = '1 / 2';
+ } else if (this.images.length == 2) {
+ this.root.style.gridTemplateColumns = '1fr 1fr';
+ this.root.style.gridTemplateRows = '1fr';
+
+ this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+ } else if (this.images.length == 3) {
+ this.root.style.gridTemplateColumns = '1fr 0.5fr';
+ this.root.style.gridTemplateRows = '1fr 1fr';
+
+ this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3';
+ this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+ } else if (this.images.length == 4) {
+ this.root.style.gridTemplateColumns = '1fr 1fr';
+ this.root.style.gridTemplateRows = '1fr 1fr';
+
+ this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+ this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3';
+ }
+ });
+ </script>
+</mk-images>
+
+<mk-images-image>
+ <a ref="view"
+ href={ image.url }
+ onmousemove={ mousemove }
+ onmouseleave={ mouseleave }
+ style={ styles }
+ onclick={ click }
+ title={ image.name }></a>
+ <style>
+ :scope
+ display block
+ overflow hidden
+ border-radius 4px
+
+ > a
+ display block
+ cursor zoom-in
+ overflow hidden
+ width 100%
+ height 100%
+ background-position center
+
+ &:not(:hover)
+ background-size cover
+
+ </style>
+ <script>
+ this.image = this.opts.image;
+ this.styles = {
+ 'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+ 'background-image': `url(${this.image.url}?thumbnail&size=512)`
+ };
+
+ this.mousemove = e => {
+ const rect = this.refs.view.getBoundingClientRect();
+ const mouseX = e.clientX - rect.left;
+ const mouseY = e.clientY - rect.top;
+ const xp = mouseX / this.refs.view.offsetWidth * 100;
+ const yp = mouseY / this.refs.view.offsetHeight * 100;
+ this.refs.view.style.backgroundPosition = xp + '% ' + yp + '%';
+ this.refs.view.style.backgroundImage = 'url("' + this.image.url + '?thumbnail")';
+ };
+
+ this.mouseleave = () => {
+ this.refs.view.style.backgroundPosition = '';
+ };
+
+ this.click = ev => {
+ ev.preventDefault();
+ riot.mount(document.body.appendChild(document.createElement('mk-image-dialog')), {
+ image: this.image
+ });
+ return false;
+ };
+ </script>
+</mk-images-image>
+
+<mk-image-dialog>
+ <div class="bg" ref="bg" onclick={ close }></div><img ref="img" src={ image.url } alt={ image.name } title={ image.name } onclick={ close }/>
+ <style>
+ :scope
+ display block
+ position fixed
+ z-index 2048
+ top 0
+ left 0
+ width 100%
+ height 100%
+ opacity 0
+
+ > .bg
+ display block
+ position fixed
+ z-index 1
+ top 0
+ left 0
+ width 100%
+ height 100%
+ background rgba(0, 0, 0, 0.7)
+
+ > img
+ position fixed
+ z-index 2
+ top 0
+ right 0
+ bottom 0
+ left 0
+ max-width 100%
+ max-height 100%
+ margin auto
+ cursor zoom-out
+
+ </style>
+ <script>
+ import anime from 'animejs';
+
+ this.image = this.opts.image;
+
+ this.on('mount', () => {
+ anime({
+ targets: this.root,
+ opacity: 1,
+ duration: 100,
+ easing: 'linear'
+ });
+ });
+
+ this.close = () => {
+ anime({
+ targets: this.root,
+ opacity: 0,
+ duration: 100,
+ easing: 'linear',
+ complete: () => this.unmount()
+ });
+ };
+ </script>
+</mk-image-dialog>
diff --git a/src/web/app/desktop/tags/index.ts b/src/web/app/desktop/tags/index.ts
index 3ec1d108aa..4edda83534 100644
--- a/src/web/app/desktop/tags/index.ts
+++ b/src/web/app/desktop/tags/index.ts
@@ -76,8 +76,7 @@ require('./set-avatar-suggestion.tag');
require('./set-banner-suggestion.tag');
require('./repost-form.tag');
require('./sub-post-content.tag');
-require('./images-viewer.tag');
-require('./image-dialog.tag');
+require('./images.tag');
require('./donation.tag');
require('./users-list.tag');
require('./user-following.tag');
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 3218c00f6a..39862487e9 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -283,7 +283,7 @@
this.api('i/notifications', {
limit: max + 1,
- max_id: this.notifications[this.notifications.length - 1].id
+ until_id: this.notifications[this.notifications.length - 1].id
}).then(notifications => {
if (notifications.length == max + 1) {
this.moreNotifications = true;
diff --git a/src/web/app/desktop/tags/pages/entrance.tag b/src/web/app/desktop/tags/pages/entrance.tag
index 44548e4183..974f49a4fe 100644
--- a/src/web/app/desktop/tags/pages/entrance.tag
+++ b/src/web/app/desktop/tags/pages/entrance.tag
@@ -18,7 +18,7 @@
<footer>
<div>
<mk-nav-links/>
- <mk-copyright/>
+ <p class="c">{ _COPYRIGHT_ }</p>
</div>
</footer>
<!-- ↓ https://github.com/riot/riot/issues/2134 (将来的)-->
@@ -101,7 +101,7 @@
text-align center
border-top solid 1px #fff
- > mk-copyright
+ > .c
margin 0
line-height 64px
font-size 10px
@@ -150,7 +150,7 @@
</mk-entrance>
<mk-entrance-signin>
- <a class="help" href={ _ABOUT_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
+ <a class="help" href={ _DOCS_URL_ + '/help' } title="お困りですか?">%fa:question%</a>
<div class="form">
<h1><img if={ user } src={ user.avatar_url + '?thumbnail&size=32' }/>
<p>{ user ? user.name : 'アカウント' }</p>
diff --git a/src/web/app/desktop/tags/post-detail-sub.tag b/src/web/app/desktop/tags/post-detail-sub.tag
index e22386df91..cccd85c474 100644
--- a/src/web/app/desktop/tags/post-detail-sub.tag
+++ b/src/web/app/desktop/tags/post-detail-sub.tag
@@ -9,7 +9,7 @@
<span class="username">@{ post.user.username }</span>
</div>
<div class="right">
- <a class="time" href={ '/' + this.post.user.username + '/' + this.post.id }>
+ <a class="time" href={ '/' + post.user.username + '/' + post.id }>
<mk-time time={ post.created_at }/>
</a>
</div>
@@ -17,9 +17,7 @@
<div class="body">
<div class="text" ref="text"></div>
<div class="media" if={ post.media }>
- <virtual each={ file in post.media }>
- <img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
- </virtual>
+ <mk-images images={ post.media }/>
</div>
</div>
</div>
@@ -107,11 +105,6 @@
> mk-url-preview
margin-top 8px
- > .media
- > img
- display block
- max-width 100%
-
</style>
<script>
import compile from '../../common/scripts/text-compiler';
diff --git a/src/web/app/desktop/tags/post-detail.tag b/src/web/app/desktop/tags/post-detail.tag
index 37f90a6ffb..47c71a6c12 100644
--- a/src/web/app/desktop/tags/post-detail.tag
+++ b/src/web/app/desktop/tags/post-detail.tag
@@ -37,7 +37,7 @@
<div class="body">
<div class="text" ref="text"></div>
<div class="media" if={ p.media }>
- <virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+ <mk-images images={ p.media }/>
</div>
<mk-poll if={ p.poll } post={ p }/>
</div>
@@ -208,11 +208,6 @@
> mk-url-preview
margin-top 8px
- > .media
- > img
- display block
- max-width 100%
-
> footer
font-size 1.2em
diff --git a/src/web/app/desktop/tags/post-form.tag b/src/web/app/desktop/tags/post-form.tag
index 8e5171c83e..0b4c07906a 100644
--- a/src/web/app/desktop/tags/post-form.tag
+++ b/src/web/app/desktop/tags/post-form.tag
@@ -1,13 +1,12 @@
<mk-post-form ondragover={ ondragover } ondragenter={ ondragenter } ondragleave={ ondragleave } ondrop={ ondrop }>
<div class="content">
<textarea class={ with: (files.length != 0 || poll) } ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ placeholder }></textarea>
- <div class="medias { with: poll }" if={ files.length != 0 }>
- <ul>
- <li each={ files }>
+ <div class="medias { with: poll }" show={ files.length != 0 }>
+ <ul ref="media">
+ <li each={ files } data-id={ id }>
<div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
<img class="remove" onclick={ removeFile } src="/assets/desktop/remove.png" title="%i18n:desktop.tags.mk-post-form.attach-cancel%" alt=""/>
</li>
- <li class="add" if={ files.length < 4 } title="%i18n:desktop.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li>
</ul>
<p class="remain">{ 4 - files.length }/4</p>
</div>
@@ -118,8 +117,9 @@
> li
display block
float left
- margin 4px
+ margin 0
padding 0
+ border solid 4px transparent
cursor move
&:hover > .remove
@@ -140,29 +140,6 @@
height 16px
cursor pointer
- > .add
- display block
- float left
- margin 4px
- padding 0
- border dashed 2px rgba($theme-color, 0.2)
- cursor pointer
-
- &:hover
- border-color rgba($theme-color, 0.3)
-
- > i
- color rgba($theme-color, 0.4)
-
- > i
- display block
- width 60px
- height 60px
- line-height 60px
- text-align center
- font-size 1.2em
- color rgba($theme-color, 0.2)
-
> mk-poll-editor
background lighten($theme-color, 98%)
border solid 1px rgba($theme-color, 0.1)
@@ -306,6 +283,7 @@
</style>
<script>
+ import Sortable from 'sortablejs';
import getKao from '../../common/scripts/get-kao';
import notify from '../scripts/notify';
import Autocomplete from '../scripts/autocomplete';
@@ -365,6 +343,10 @@
this.trigger('change-files', this.files);
this.update();
}
+
+ new Sortable(this.refs.media, {
+ animation: 150
+ });
});
this.on('unmount', () => {
@@ -413,14 +395,17 @@
const data = e.dataTransfer.getData('text');
if (data == null) return false;
- // パース
- // TODO: Validate JSON
- const obj = JSON.parse(data);
+ try {
+ // パース
+ const obj = JSON.parse(data);
+
+ // (ドライブの)ファイルだったら
+ if (obj.type == 'file') {
+ this.files.push(obj.file);
+ this.update();
+ }
+ } catch (e) {
- // (ドライブの)ファイルだったら
- if (obj.type == 'file') {
- this.files.push(obj.file);
- this.update();
}
};
@@ -483,13 +468,19 @@
this.post = e => {
this.wait = true;
- const files = this.files && this.files.length > 0
- ? this.files.map(f => f.id)
- : undefined;
+ const files = [];
+
+ if (this.files.length > 0) {
+ Array.from(this.refs.media.children).forEach(el => {
+ const id = el.getAttribute('data-id');
+ const file = this.files.find(f => f.id == id);
+ files.push(file);
+ });
+ }
this.api('posts/create', {
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
- media_ids: files,
+ media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
reply_id: this.inReplyToPost ? this.inReplyToPost.id : undefined,
repost_id: this.repost ? this.repost.id : undefined,
poll: this.poll ? this.refs.poll.get() : undefined
diff --git a/src/web/app/desktop/tags/search-posts.tag b/src/web/app/desktop/tags/search-posts.tag
index 52f765d1a1..f7ec85a4fe 100644
--- a/src/web/app/desktop/tags/search-posts.tag
+++ b/src/web/app/desktop/tags/search-posts.tag
@@ -33,21 +33,22 @@
</style>
<script>
+ import parse from '../../common/scripts/parse-search-query';
+
this.mixin('api');
this.query = this.opts.query;
this.isLoading = true;
this.isEmpty = false;
this.moreLoading = false;
- this.page = 0;
+ this.limit = 30;
+ this.offset = 0;
this.on('mount', () => {
document.addEventListener('keydown', this.onDocumentKeydown);
window.addEventListener('scroll', this.onScroll);
- this.api('posts/search', {
- query: this.query
- }).then(posts => {
+ this.api('posts/search', parse(this.query)).then(posts => {
this.update({
isLoading: false,
isEmpty: posts.length == 0
@@ -72,16 +73,16 @@
this.more = () => {
if (this.moreLoading || this.isLoading || this.timeline.posts.length == 0) return;
+ this.offset += this.limit;
this.update({
moreLoading: true
});
- this.api('posts/search', {
- query: this.query,
- page: this.page + 1
- }).then(posts => {
+ return this.api('posts/search', Object.assign({}, parse(this.query), {
+ limit: this.limit,
+ offset: this.offset
+ })).then(posts => {
this.update({
- moreLoading: false,
- page: page + 1
+ moreLoading: false
});
this.refs.timeline.prependPosts(posts);
});
diff --git a/src/web/app/desktop/tags/select-file-from-drive-window.tag b/src/web/app/desktop/tags/select-file-from-drive-window.tag
index 17cc607988..c660a2fe90 100644
--- a/src/web/app/desktop/tags/select-file-from-drive-window.tag
+++ b/src/web/app/desktop/tags/select-file-from-drive-window.tag
@@ -33,7 +33,7 @@
height 72px
background lighten($theme-color, 95%)
- .upload
+ > .upload
display inline-block
position absolute
top 8px
@@ -72,8 +72,8 @@
border 2px solid rgba($theme-color, 0.3)
border-radius 8px
- .ok
- .cancel
+ > .ok
+ > .cancel
display block
position absolute
bottom 16px
@@ -102,7 +102,7 @@
opacity 0.7
cursor default
- .ok
+ > .ok
right 16px
color $theme-color-foreground
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
@@ -119,7 +119,7 @@
background $theme-color
border-color $theme-color
- .cancel
+ > .cancel
right 148px
color #888
background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
diff --git a/src/web/app/desktop/tags/settings.tag b/src/web/app/desktop/tags/settings.tag
index 46cd405520..457b7e2276 100644
--- a/src/web/app/desktop/tags/settings.tag
+++ b/src/web/app/desktop/tags/settings.tag
@@ -1,47 +1,35 @@
<mk-settings>
<div class="nav">
- <p class={ active: page == 'account' } onmousedown={ setPage.bind(null, 'account') }>%fa:user .fw%アカウント</p>
+ <p class={ active: page == 'profile' } onmousedown={ setPage.bind(null, 'profile') }>%fa:user .fw%%i18n:desktop.tags.mk-settings.profile%</p>
<p class={ active: page == 'web' } onmousedown={ setPage.bind(null, 'web') }>%fa:desktop .fw%Web</p>
<p class={ active: page == 'notification' } onmousedown={ setPage.bind(null, 'notification') }>%fa:R bell .fw%通知</p>
- <p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%ドライブ</p>
+ <p class={ active: page == 'drive' } onmousedown={ setPage.bind(null, 'drive') }>%fa:cloud .fw%%i18n:desktop.tags.mk-settings.drive%</p>
+ <p class={ active: page == 'mute' } onmousedown={ setPage.bind(null, 'mute') }>%fa:ban .fw%%i18n:desktop.tags.mk-settings.mute%</p>
<p class={ active: page == 'apps' } onmousedown={ setPage.bind(null, 'apps') }>%fa:puzzle-piece .fw%アプリ</p>
<p class={ active: page == 'twitter' } onmousedown={ setPage.bind(null, 'twitter') }>%fa:B twitter .fw%Twitter</p>
- <p class={ active: page == 'signin' } onmousedown={ setPage.bind(null, 'signin') }>%fa:sign-in-alt .fw%ログイン履歴</p>
- <p class={ active: page == 'password' } onmousedown={ setPage.bind(null, 'password') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.password%</p>
+ <p class={ active: page == 'security' } onmousedown={ setPage.bind(null, 'security') }>%fa:unlock-alt .fw%%i18n:desktop.tags.mk-settings.security%</p>
<p class={ active: page == 'api' } onmousedown={ setPage.bind(null, 'api') }>%fa:key .fw%API</p>
+ <p class={ active: page == 'other' } onmousedown={ setPage.bind(null, 'other') }>%fa:cogs .fw%%i18n:desktop.tags.mk-settings.other%</p>
</div>
<div class="pages">
- <section class="account" show={ page == 'account' }>
- <h1>アカウント</h1>
- <label class="avatar">
- <p>アバター</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
- <button class="style-normal" onclick={ avatar }>画像を選択</button>
- </label>
- <label>
- <p>名前</p>
- <input ref="accountName" type="text" value={ I.name }/>
- </label>
- <label>
- <p>場所</p>
- <input ref="accountLocation" type="text" value={ I.profile.location }/>
- </label>
- <label>
- <p>自己紹介</p>
- <textarea ref="accountDescription">{ I.description }</textarea>
- </label>
- <label>
- <p>誕生日</p>
- <input ref="accountBirthday" type="date" value={ I.profile.birthday }/>
- </label>
- <button class="style-primary" onclick={ updateAccount }>保存</button>
+ <section class="profile" show={ page == 'profile' }>
+ <h1>%i18n:desktop.tags.mk-settings.profile%</h1>
+ <mk-profile-setting/>
</section>
<section class="web" show={ page == 'web' }>
<h1>デザイン</h1>
- <a href="/i/customize-home">ホームをカスタマイズ</a>
+ <a href="/i/customize-home" class="ui button">ホームをカスタマイズ</a>
</section>
- <section class="web" show={ page == 'web' }>
+ <section class="drive" show={ page == 'drive' }>
+ <h1>%i18n:desktop.tags.mk-settings.drive%</h1>
+ <mk-drive-setting/>
+ </section>
+
+ <section class="mute" show={ page == 'mute' }>
+ <h1>%i18n:desktop.tags.mk-settings.mute%</h1>
+ <mk-mute-setting/>
</section>
<section class="apps" show={ page == 'apps' }>
@@ -54,20 +42,30 @@
<mk-twitter-setting/>
</section>
- <section class="signin" show={ page == 'signin' }>
- <h1>ログイン履歴</h1>
- <mk-signin-history/>
- </section>
-
- <section class="password" show={ page == 'password' }>
+ <section class="password" show={ page == 'security' }>
<h1>%i18n:desktop.tags.mk-settings.password%</h1>
<mk-password-setting/>
</section>
+ <section class="2fa" show={ page == 'security' }>
+ <h1>%i18n:desktop.tags.mk-settings.2fa%</h1>
+ <mk-2fa-setting/>
+ </section>
+
+ <section class="signin" show={ page == 'security' }>
+ <h1>サインイン履歴</h1>
+ <mk-signin-history/>
+ </section>
+
<section class="api" show={ page == 'api' }>
<h1>API</h1>
<mk-api-info/>
</section>
+
+ <section class="other" show={ page == 'other' }>
+ <h1>%i18n:desktop.tags.mk-settings.license%</h1>
+ %license%
+ </section>
</div>
<style>
:scope
@@ -75,25 +73,6 @@
width 100%
height 100%
- input:not([type])
- input[type='text']
- input[type='password']
- input[type='email']
- input[type='date']
- textarea
- padding 8px
- width 100%
- font-size 16px
- color #55595c
- border solid 1px #dadada
- border-radius 4px
-
- &:hover
- border-color #aeaeae
-
- &:focus
- border-color #aeaeae
-
> .nav
flex 0 0 200px
width 100%
@@ -128,64 +107,63 @@
overflow auto
> section
- padding 32px
+ margin 32px
+ color #4a535a
- // & + section
- // margin-top 16px
-
- h1
+ > h1
display block
- margin 0
+ margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
color #555
border-bottom solid 1px #eee
- label
- display block
- margin 16px 0
-
- &:after
- content ""
- display block
- clear both
-
- > p
- margin 0 0 8px 0
- font-weight bold
- color #373a3c
-
- &.checkbox
- > input
- position absolute
- top 0
- left 0
-
- &:checked + p
- color $theme-color
+ </style>
+ <script>
+ this.page = 'profile';
- > p
- width calc(100% - 32px)
- margin 0 0 0 32px
- font-weight bold
+ this.setPage = page => {
+ this.page = page;
+ };
+ </script>
+</mk-settings>
- &:last-child
- font-weight normal
- color #999
+<mk-profile-setting>
+ <label class="avatar ui from group">
+ <p>%i18n:desktop.tags.mk-profile-setting.avatar%</p><img class="avatar" src={ I.avatar_url + '?thumbnail&size=64' } alt="avatar"/>
+ <button class="ui" onclick={ avatar }>%i18n:desktop.tags.mk-profile-setting.choice-avatar%</button>
+ </label>
+ <label class="ui from group">
+ <p>%i18n:desktop.tags.mk-profile-setting.name%</p>
+ <input ref="accountName" type="text" value={ I.name } class="ui"/>
+ </label>
+ <label class="ui from group">
+ <p>%i18n:desktop.tags.mk-profile-setting.location%</p>
+ <input ref="accountLocation" type="text" value={ I.profile.location } class="ui"/>
+ </label>
+ <label class="ui from group">
+ <p>%i18n:desktop.tags.mk-profile-setting.description%</p>
+ <textarea ref="accountDescription" class="ui">{ I.description }</textarea>
+ </label>
+ <label class="ui from group">
+ <p>%i18n:desktop.tags.mk-profile-setting.birthday%</p>
+ <input ref="accountBirthday" type="date" value={ I.profile.birthday } class="ui"/>
+ </label>
+ <button class="ui primary" onclick={ updateAccount }>%i18n:desktop.tags.mk-profile-setting.save%</button>
+ <style>
+ :scope
+ display block
- &.account
- > .general
- > .avatar
- > img
- display block
- float left
- width 64px
- height 64px
- border-radius 4px
+ > .avatar
+ > img
+ display inline-block
+ vertical-align top
+ width 64px
+ height 64px
+ border-radius 4px
- > button
- float left
- margin-left 8px
+ > button
+ margin-left 8px
</style>
<script>
@@ -195,12 +173,6 @@
this.mixin('i');
this.mixin('api');
- this.page = 'account';
-
- this.setPage = page => {
- this.page = page;
- };
-
this.avatar = () => {
updateAvatar(this.I);
};
@@ -216,21 +188,25 @@
});
};
</script>
-</mk-settings>
+</mk-profile-setting>
<mk-api-info>
- <p>Token:<code>{ I.token }</code></p>
- <p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
- <p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
- <p>万が一このトークンが漏れたりその可能性がある場合は<a class="regenerate" onclick={ regenerateToken }>トークンを再生成</a>できます。(副作用として、ログインしているすべてのデバイスでログアウトが発生します)</p>
+ <p>Token: <code>{ I.token }</code></p>
+ <p>%i18n:desktop.tags.mk-api-info.intro%</p>
+ <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-api-info.caution%</p></div>
+ <p>%i18n:desktop.tags.mk-api-info.regeneration-of-token%</p>
+ <button class="ui" onclick={ regenerateToken }>%i18n:desktop.tags.mk-api-info.regenerate-token%</button>
<style>
:scope
display block
color #4a535a
code
- padding 4px
+ display inline-block
+ padding 4px 6px
+ color #555
background #eee
+ border-radius 2px
</style>
<script>
import passwordDialog from '../scripts/password-dialog';
@@ -239,7 +215,7 @@
this.mixin('api');
this.regenerateToken = () => {
- passwordDialog('%i18n:desktop.tags.mk-api-info.regenerate-token%', password => {
+ passwordDialog('%i18n:desktop.tags.mk-api-info.enter-password%', password => {
this.api('i/regenerate_token', {
password: password
});
@@ -249,7 +225,7 @@
</mk-api-info>
<mk-password-setting>
- <button onclick={ reset }>%i18n:desktop.tags.mk-password-setting.reset%</button>
+ <button onclick={ reset } class="ui primary">%i18n:desktop.tags.mk-password-setting.reset%</button>
<style>
:scope
display block
@@ -285,3 +261,166 @@
};
</script>
</mk-password-setting>
+
+<mk-2fa-setting>
+ <p>%i18n:desktop.tags.mk-2fa-setting.intro%<a href="%i18n:desktop.tags.mk-2fa-setting.url%" target="_blank">%i18n:desktop.tags.mk-2fa-setting.detail%</a></p>
+ <div class="ui info warn"><p>%fa:exclamation-triangle%%i18n:desktop.tags.mk-2fa-setting.caution%</p></div>
+ <p if={ !data && !I.two_factor_enabled }><button onclick={ register } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.register%</button></p>
+ <virtual if={ I.two_factor_enabled }>
+ <p>%i18n:desktop.tags.mk-2fa-setting.already-registered%</p>
+ <button onclick={ unregister } class="ui">%i18n:desktop.tags.mk-2fa-setting.unregister%</button>
+ </virtual>
+ <div if={ data }>
+ <ol>
+ <li>%i18n:desktop.tags.mk-2fa-setting.authenticator% <a href="https://support.google.com/accounts/answer/1066447" target="_blank">%i18n:desktop.tags.mk-2fa-setting.howtoinstall%</a></li>
+ <li>%i18n:desktop.tags.mk-2fa-setting.scan%<br><img src={ data.qr }></li>
+ <li>%i18n:desktop.tags.mk-2fa-setting.done%<br>
+ <input type="number" ref="token" class="ui">
+ <button onclick={ submit } class="ui primary">%i18n:desktop.tags.mk-2fa-setting.submit%</button>
+ </li>
+ </ol>
+ <div class="ui info"><p>%fa:info-circle%%i18n:desktop.tags.mk-2fa-setting.info%</p></div>
+ </div>
+ <style>
+ :scope
+ display block
+ color #4a535a
+
+ </style>
+ <script>
+ import passwordDialog from '../scripts/password-dialog';
+ import notify from '../scripts/notify';
+
+ this.mixin('i');
+ this.mixin('api');
+
+ this.register = () => {
+ passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+ this.api('i/2fa/register', {
+ password: password
+ }).then(data => {
+ this.update({
+ data: data
+ });
+ });
+ });
+ };
+
+ this.unregister = () => {
+ passwordDialog('%i18n:desktop.tags.mk-2fa-setting.enter-password%', password => {
+ this.api('i/2fa/unregister', {
+ password: password
+ }).then(data => {
+ notify('%i18n:desktop.tags.mk-2fa-setting.unregistered%');
+ this.I.two_factor_enabled = false;
+ this.I.update();
+ });
+ });
+ };
+
+ this.submit = () => {
+ this.api('i/2fa/done', {
+ token: this.refs.token.value
+ }).then(() => {
+ notify('%i18n:desktop.tags.mk-2fa-setting.success%');
+ this.I.two_factor_enabled = true;
+ this.I.update();
+ }).catch(() => {
+ notify('%i18n:desktop.tags.mk-2fa-setting.failed%');
+ });
+ };
+ </script>
+</mk-2fa-setting>
+
+<mk-drive-setting>
+ <svg viewBox="0 0 1 1" preserveAspectRatio="none">
+ <circle
+ riot-r={ r }
+ cx="50%" cy="50%"
+ fill="none"
+ stroke-width="0.1"
+ stroke="rgba(0, 0, 0, 0.05)"/>
+ <circle
+ riot-r={ r }
+ cx="50%" cy="50%"
+ riot-stroke-dasharray={ Math.PI * (r * 2) }
+ riot-stroke-dashoffset={ strokeDashoffset }
+ fill="none"
+ stroke-width="0.1"
+ riot-stroke={ color }/>
+ <text x="50%" y="50%" dy="0.05" text-anchor="middle">{ (usageP * 100).toFixed(0) }%</text>
+ </svg>
+
+ <style>
+ :scope
+ display block
+ color #4a535a
+
+ > svg
+ display block
+ height 128px
+
+ > circle
+ transform-origin center
+ transform rotate(-90deg)
+ transition stroke-dashoffset 0.5s ease
+
+ > text
+ font-size 0.15px
+ fill rgba(0, 0, 0, 0.6)
+
+ </style>
+ <script>
+ this.mixin('api');
+
+ this.r = 0.4;
+
+ this.on('mount', () => {
+ this.api('drive').then(info => {
+ const usageP = info.usage / info.capacity;
+ const color = `hsl(${180 - (usageP * 180)}, 80%, 70%)`;
+ const strokeDashoffset = (1 - usageP) * (Math.PI * (this.r * 2));
+
+ this.update({
+ color,
+ strokeDashoffset,
+ usageP,
+ usage: info.usage,
+ capacity: info.capacity
+ });
+ });
+ });
+ </script>
+</mk-drive-setting>
+
+<mk-mute-setting>
+ <div class="none ui info" if={ !fetching && users.length == 0 }>
+ <p>%fa:info-circle%%i18n:desktop.tags.mk-mute-setting.no-users%</p>
+ </div>
+ <div class="users" if={ users.length != 0 }>
+ <div each={ user in users }>
+ <p><b>{ user.name }</b> @{ user.username }</p>
+ </div>
+ </div>
+
+ <style>
+ :scope
+ display block
+
+ </style>
+ <script>
+ this.mixin('api');
+
+ this.apps = [];
+ this.fetching = true;
+
+ this.on('mount', () => {
+ this.api('mute/list').then(x => {
+ this.update({
+ fetching: false,
+ users: x.users
+ });
+ });
+ });
+ </script>
+</mk-mute-setting>
diff --git a/src/web/app/desktop/tags/sub-post-content.tag b/src/web/app/desktop/tags/sub-post-content.tag
index 8989ff1c5b..1a81b545b6 100644
--- a/src/web/app/desktop/tags/sub-post-content.tag
+++ b/src/web/app/desktop/tags/sub-post-content.tag
@@ -8,7 +8,7 @@
</div>
<details if={ post.media }>
<summary>({ post.media.length }つのメディア)</summary>
- <mk-images-viewer images={ post.media }/>
+ <mk-images images={ post.media }/>
</details>
<details if={ post.poll }>
<summary>投票</summary>
diff --git a/src/web/app/desktop/tags/timeline.tag b/src/web/app/desktop/tags/timeline.tag
index 08e658a3c6..ed77a9e608 100644
--- a/src/web/app/desktop/tags/timeline.tag
+++ b/src/web/app/desktop/tags/timeline.tag
@@ -120,7 +120,7 @@
<a class="quote" if={ p.repost != null }>RP:</a>
</div>
<div class="media" if={ p.media }>
- <mk-images-viewer images={ p.media }/>
+ <mk-images images={ p.media }/>
</div>
<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
<div class="repost" if={ p.repost }>%fa:quote-right -flip-h%
@@ -357,11 +357,6 @@
background $theme-color
border-radius 4px
- > .media
- > img
- display block
- max-width 100%
-
> mk-poll
font-size 80%
diff --git a/src/web/app/desktop/tags/ui.tag b/src/web/app/desktop/tags/ui.tag
index 052568062a..3dfdeec01c 100644
--- a/src/web/app/desktop/tags/ui.tag
+++ b/src/web/app/desktop/tags/ui.tag
@@ -146,6 +146,9 @@
color #9eaba8
pointer-events none
+ > *
+ vertical-align middle
+
> input
user-select text
cursor auto
@@ -162,7 +165,7 @@
transition color 0.5s ease, border 0.5s ease
font-family FontAwesome, sans-serif
- &:placeholder-shown
+ &::placeholder
color #9eaba8
&:hover
@@ -177,7 +180,7 @@
this.onsubmit = e => {
e.preventDefault();
- this.page('/search:' + this.refs.q.value);
+ this.page('/search?q=' + encodeURIComponent(this.refs.q.value));
};
</script>
</mk-ui-header-search>
diff --git a/src/web/app/desktop/tags/user-timeline.tag b/src/web/app/desktop/tags/user-timeline.tag
index 2b05f6b5cf..134aeee28c 100644
--- a/src/web/app/desktop/tags/user-timeline.tag
+++ b/src/web/app/desktop/tags/user-timeline.tag
@@ -96,7 +96,7 @@
this.fetch = cb => {
this.api('users/posts', {
user_id: this.user.id,
- max_date: this.date ? this.date.getTime() : undefined,
+ until_date: this.date ? this.date.getTime() : undefined,
with_replies: this.mode == 'with-replies'
}).then(posts => {
this.update({
@@ -116,7 +116,7 @@
this.api('users/posts', {
user_id: this.user.id,
with_replies: this.mode == 'with-replies',
- max_id: this.refs.timeline.tail().id
+ until_id: this.refs.timeline.tail().id
}).then(posts => {
this.update({
moreLoading: false
diff --git a/src/web/app/desktop/tags/user.tag b/src/web/app/desktop/tags/user.tag
index b4db47f9dd..b29d1eaebc 100644
--- a/src/web/app/desktop/tags/user.tag
+++ b/src/web/app/desktop/tags/user.tag
@@ -226,7 +226,9 @@
<mk-user-profile>
<div class="friend-form" if={ SIGNIN && I.id != user.id }>
<mk-big-follow-button user={ user }/>
- <p class="followed" if={ user.is_followed }>フォローされています</p>
+ <p class="followed" if={ user.is_followed }>%i18n:desktop.tags.mk-user.follows-you%</p>
+ <p if={ user.is_muted }>%i18n:desktop.tags.mk-user.muted% <a onclick={ unmute }>%i18n:desktop.tags.mk-user.unmute%</a></p>
+ <p if={ !user.is_muted }><a onclick={ mute }>%i18n:desktop.tags.mk-user.mute%</a></p>
</div>
<div class="description" if={ user.description }>{ user.description }</div>
<div class="birthday" if={ user.profile.birthday }>
@@ -311,6 +313,7 @@
this.age = require('s-age');
this.mixin('i');
+ this.mixin('api');
this.user = this.opts.user;
@@ -325,6 +328,28 @@
user: this.user
});
};
+
+ this.mute = () => {
+ this.api('mute/create', {
+ user_id: this.user.id
+ }).then(() => {
+ this.user.is_muted = true;
+ this.update();
+ }, e => {
+ alert('error');
+ });
+ };
+
+ this.unmute = () => {
+ this.api('mute/delete', {
+ user_id: this.user.id
+ }).then(() => {
+ this.user.is_muted = false;
+ this.update();
+ }, e => {
+ alert('error');
+ });
+ };
</script>
</mk-user-profile>
diff --git a/src/web/app/desktop/ui.styl b/src/web/app/desktop/ui.styl
new file mode 100644
index 0000000000..058271876b
--- /dev/null
+++ b/src/web/app/desktop/ui.styl
@@ -0,0 +1,122 @@
+@import "../../const"
+
+button
+ font-family sans-serif
+
+ *
+ pointer-events none
+
+button.ui
+.button.ui
+ display inline-block
+ cursor pointer
+ padding 0 14px
+ margin 0
+ min-width 100px
+ line-height 38px
+ font-size 14px
+ color #888
+ text-decoration none
+ background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+ border solid 1px #e2e2e2
+ border-radius 4px
+ outline none
+
+ &:focus
+ &:after
+ content ""
+ pointer-events none
+ position absolute
+ top -5px
+ right -5px
+ bottom -5px
+ left -5px
+ border 2px solid rgba($theme-color, 0.3)
+ border-radius 8px
+
+ &:disabled
+ opacity 0.7
+ cursor default
+
+ &:hover
+ background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+ border-color #dcdcdc
+
+ &:active
+ background #ececec
+ border-color #dcdcdc
+
+ &.primary
+ color $theme-color-foreground
+ background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+ border solid 1px lighten($theme-color, 15%)
+
+ &:not(:disabled)
+ font-weight bold
+
+ &:hover:not(:disabled)
+ background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+ border-color $theme-color
+
+ &:active:not(:disabled)
+ background $theme-color
+ border-color $theme-color
+
+input:not([type]).ui
+input[type='text'].ui
+input[type='password'].ui
+input[type='email'].ui
+input[type='date'].ui
+input[type='number'].ui
+textarea.ui
+ display block
+ padding 10px
+ width 100%
+ height 40px
+ font-family sans-serif
+ font-size 16px
+ color #55595c
+ border solid 1px #dadada
+ border-radius 4px
+
+ &:hover
+ border-color #b0b0b0
+
+ &:focus
+ border-color $theme-color
+
+textarea.ui
+ min-width 100%
+ max-width 100%
+ min-height 64px
+
+.ui.info
+ display block
+ margin 1em 0
+ padding 0 1em
+ font-size 90%
+ color rgba(#000, 0.87)
+ background #f8f8f9
+ border solid 1px rgba(34, 36, 38, 0.22)
+ border-radius 4px
+
+ > p
+ opacity 0.8
+
+ > [data-fa]:first-child
+ margin-right 0.25em
+
+ &.warn
+ color #573a08
+ background #FFFAF3
+ border-color #C9BA9B
+
+.ui.from.group
+ display block
+ margin 16px 0
+
+ > p:first-child
+ margin 0 0 6px 0
+ font-size 90%
+ font-weight bold
+ color rgba(#373a3c, 0.9)
diff --git a/src/web/app/mobile/router.ts b/src/web/app/mobile/router.ts
index 0358d10e9e..afb9aa6201 100644
--- a/src/web/app/mobile/router.ts
+++ b/src/web/app/mobile/router.ts
@@ -19,12 +19,11 @@ export default (mios: MiOS) => {
route('/i/settings', settings);
route('/i/settings/profile', settingsProfile);
route('/i/settings/signin-history', settingsSignin);
- route('/i/settings/api', settingsApi);
route('/i/settings/twitter', settingsTwitter);
route('/i/settings/authorized-apps', settingsAuthorizedApps);
route('/post/new', newPost);
route('/post::post', post);
- route('/search::query', search);
+ route('/search', search);
route('/:user', user.bind(null, 'overview'));
route('/:user/graphs', user.bind(null, 'graphs'));
route('/:user/followers', userFollowers);
@@ -74,10 +73,6 @@ export default (mios: MiOS) => {
mount(document.createElement('mk-signin-history-page'));
}
- function settingsApi() {
- mount(document.createElement('mk-api-info-page'));
- }
-
function settingsTwitter() {
mount(document.createElement('mk-twitter-setting-page'));
}
@@ -88,7 +83,7 @@ export default (mios: MiOS) => {
function search(ctx) {
const el = document.createElement('mk-search-page');
- el.setAttribute('query', ctx.params.query);
+ el.setAttribute('query', ctx.querystring.substr(2));
mount(el);
}
diff --git a/src/web/app/mobile/tags/drive.tag b/src/web/app/mobile/tags/drive.tag
index 8350ce07e1..2a3ff23bfa 100644
--- a/src/web/app/mobile/tags/drive.tag
+++ b/src/web/app/mobile/tags/drive.tag
@@ -1,9 +1,9 @@
<mk-drive>
<nav ref="nav">
- <p onclick={ goRoot }>%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</p>
+ <a onclick={ goRoot } href="/i/drive">%fa:cloud%%i18n:mobile.tags.mk-drive.drive%</a>
<virtual each={ folder in hierarchyFolders }>
<span>%fa:angle-right%</span>
- <p onclick={ move }>{ folder.name }</p>
+ <a onclick={ move } href="/i/drive/folder/{ folder.id }">{ folder.name }</a>
</virtual>
<virtual if={ folder != null }>
<span>%fa:angle-right%</span>
@@ -74,9 +74,12 @@
border-bottom solid 1px rgba(0, 0, 0, 0.13)
> p
+ > a
display inline
margin 0
padding 0
+ text-decoration none !important
+ color inherit
&:last-child
font-weight bold
@@ -245,7 +248,9 @@
};
this.move = ev => {
+ ev.preventDefault();
this.cd(ev.item.folder);
+ return false;
};
this.cd = (target, silent = false) => {
@@ -329,7 +334,9 @@
this.prependFile = file => this.addFile(file, true);
this.prependFolder = file => this.addFolder(file, true);
- this.goRoot = () => {
+ this.goRoot = ev => {
+ ev.preventDefault();
+
if (this.folder || this.file) {
this.update({
file: null,
@@ -339,6 +346,8 @@
this.trigger('move-root');
this.fetch();
}
+
+ return false;
};
this.fetch = () => {
@@ -421,7 +430,7 @@
this.api('drive/files', {
folder_id: this.folder ? this.folder.id : null,
limit: max + 1,
- max_id: this.files[this.files.length - 1].id
+ until_id: this.files[this.files.length - 1].id
}).then(files => {
if (files.length == max + 1) {
this.moreFiles = true;
diff --git a/src/web/app/mobile/tags/drive/file-viewer.tag b/src/web/app/mobile/tags/drive/file-viewer.tag
index da895359dc..259873d95c 100644
--- a/src/web/app/mobile/tags/drive/file-viewer.tag
+++ b/src/web/app/mobile/tags/drive/file-viewer.tag
@@ -1,6 +1,11 @@
<mk-drive-file-viewer>
<div class="preview">
- <img if={ kind == 'image' } src={ file.url } alt={ file.name } title={ file.name }>
+ <img if={ kind == 'image' } ref="img"
+ src={ file.url }
+ alt={ file.name }
+ title={ file.name }
+ onload={ onImageLoaded }
+ style="background-color:rgb({ file.properties.average_color.join(',') })">
<virtual if={ kind != 'image' }>%fa:file%</virtual>
<footer if={ kind == 'image' && file.properties && file.properties.width && file.properties.height }>
<span class="size">
@@ -39,6 +44,14 @@
</button>
</div>
</div>
+ <div class="exif" show={ exif }>
+ <div>
+ <p>
+ %fa:camera%%i18n:mobile.tags.mk-drive-file-viewer.exif%
+ </p>
+ <pre ref="exif" class="json">{ exif ? JSON.stringify(exif, null, 2) : '' }</pre>
+ </div>
+ </div>
<div class="hash">
<div>
<p>
@@ -178,12 +191,45 @@
white-space nowrap
overflow auto
font-size 0.8em
+ color #222
+ border solid 1px #dfdfdf
+ border-radius 2px
+ background #f5f5f5
+
+ > .exif
+ padding 14px
+ border-top solid 1px #dfdfdf
+
+ > div
+ max-width 500px
+ margin 0 auto
+
+ > p
+ display block
+ margin 0
+ padding 0
+ color #555
+ font-size 0.9em
+
+ > [data-fa]
+ margin-right 4px
+
+ > pre
+ display block
+ width 100%
+ margin 6px 0 0 0
+ padding 8px
+ height 128px
+ overflow auto
+ font-size 0.9em
border solid 1px #dfdfdf
border-radius 2px
background #f5f5f5
</style>
<script>
+ import EXIF from 'exif-js';
+ import hljs from 'highlight.js';
import bytesToSize from '../../../common/scripts/bytes-to-size';
import gcd from '../../../common/scripts/gcd';
@@ -195,6 +241,17 @@
this.file = this.opts.file;
this.kind = this.file.type.split('/')[0];
+ this.onImageLoaded = () => {
+ const self = this;
+ EXIF.getData(this.refs.img, function() {
+ const allMetaData = EXIF.getAllTags(this);
+ self.update({
+ exif: allMetaData
+ });
+ hljs.highlightBlock(self.refs.exif);
+ });
+ };
+
this.rename = () => {
const name = window.prompt('名前を変更', this.file.name);
if (name == null || name == '' || name == this.file.name) return;
diff --git a/src/web/app/mobile/tags/drive/file.tag b/src/web/app/mobile/tags/drive/file.tag
index 93a8dba7e5..684df7dd08 100644
--- a/src/web/app/mobile/tags/drive/file.tag
+++ b/src/web/app/mobile/tags/drive/file.tag
@@ -1,119 +1,123 @@
-<mk-drive-file onclick={ onclick } data-is-selected={ isSelected }>
- <div class="container">
- <div class="thumbnail" style={ 'background-image: url(' + file.url + '?thumbnail&size=128)' }></div>
- <div class="body">
- <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
- <!--
- if file.tags.length > 0
- ul.tags
- each tag in file.tags
- li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
- -->
- <footer>
- <p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p>
- <p class="separator"></p>
- <p class="data-size">{ bytesToSize(file.datasize) }</p>
- <p class="separator"></p>
- <p class="created-at">
- %fa:R clock%<mk-time time={ file.created_at }/>
- </p>
- </footer>
+<mk-drive-file data-is-selected={ isSelected }>
+ <a onclick={ onclick } href="/i/drive/file/{ file.id }">
+ <div class="container">
+ <div class="thumbnail" style={ thumbnail }></div>
+ <div class="body">
+ <p class="name"><span>{ file.name.lastIndexOf('.') != -1 ? file.name.substr(0, file.name.lastIndexOf('.')) : file.name }</span><span class="ext" if={ file.name.lastIndexOf('.') != -1 }>{ file.name.substr(file.name.lastIndexOf('.')) }</span></p>
+ <!--
+ if file.tags.length > 0
+ ul.tags
+ each tag in file.tags
+ li.tag(style={background: tag.color, color: contrast(tag.color)})= tag.name
+ -->
+ <footer>
+ <p class="type"><mk-file-type-icon type={ file.type }/>{ file.type }</p>
+ <p class="separator"></p>
+ <p class="data-size">{ bytesToSize(file.datasize) }</p>
+ <p class="separator"></p>
+ <p class="created-at">
+ %fa:R clock%<mk-time time={ file.created_at }/>
+ </p>
+ </footer>
+ </div>
</div>
- </div>
+ </a>
<style>
:scope
display block
- &, *
- user-select none
+ > a
+ display block
+ text-decoration none !important
- *
- pointer-events none
+ *
+ user-select none
+ pointer-events none
- > .container
- max-width 500px
- margin 0 auto
- padding 16px
+ > .container
+ max-width 500px
+ margin 0 auto
+ padding 16px
- &:after
- content ""
- display block
- clear both
-
- > .thumbnail
- display block
- float left
- width 64px
- height 64px
- background-size cover
- background-position center center
-
- > .body
- display block
- float left
- width calc(100% - 74px)
- margin-left 10px
-
- > .name
+ &:after
+ content ""
display block
- margin 0
- padding 0
- font-size 0.9em
- font-weight bold
- color #555
- text-overflow ellipsis
- overflow-wrap break-word
+ clear both
- > .ext
- opacity 0.5
-
- > .tags
+ > .thumbnail
display block
- margin 4px 0 0 0
- padding 0
- list-style none
- font-size 0.5em
-
- > .tag
- display inline-block
- margin 0 5px 0 0
- padding 1px 5px
- border-radius 2px
+ float left
+ width 64px
+ height 64px
+ background-size cover
+ background-position center center
- > footer
+ > .body
display block
- margin 4px 0 0 0
- font-size 0.7em
+ float left
+ width calc(100% - 74px)
+ margin-left 10px
- > .separator
- display inline
- margin 0
- padding 0 4px
- color #CDCDCD
-
- > .type
- display inline
+ > .name
+ display block
margin 0
padding 0
- color #9D9D9D
+ font-size 0.9em
+ font-weight bold
+ color #555
+ text-overflow ellipsis
+ overflow-wrap break-word
- > mk-file-type-icon
- margin-right 4px
+ > .ext
+ opacity 0.5
- > .data-size
- display inline
- margin 0
+ > .tags
+ display block
+ margin 4px 0 0 0
padding 0
- color #9D9D9D
+ list-style none
+ font-size 0.5em
- > .created-at
- display inline
- margin 0
- padding 0
- color #BDBDBD
+ > .tag
+ display inline-block
+ margin 0 5px 0 0
+ padding 1px 5px
+ border-radius 2px
+
+ > footer
+ display block
+ margin 4px 0 0 0
+ font-size 0.7em
+
+ > .separator
+ display inline
+ margin 0
+ padding 0 4px
+ color #CDCDCD
+
+ > .type
+ display inline
+ margin 0
+ padding 0
+ color #9D9D9D
- > [data-fa]
- margin-right 2px
+ > mk-file-type-icon
+ margin-right 4px
+
+ > .data-size
+ display inline
+ margin 0
+ padding 0
+ color #9D9D9D
+
+ > .created-at
+ display inline
+ margin 0
+ padding 0
+ color #BDBDBD
+
+ > [data-fa]
+ margin-right 2px
&[data-is-selected]
background $theme-color
@@ -128,14 +132,20 @@
this.browser = this.parent;
this.file = this.opts.file;
+ this.thumbnail = {
+ 'background-color': this.file.properties.average_color ? `rgb(${this.file.properties.average_color.join(',')})` : 'transparent',
+ 'background-image': `url(${this.file.url}?thumbnail&size=128)`
+ };
this.isSelected = this.browser.selectedFiles.some(f => f.id == this.file.id);
this.browser.on('change-selection', selections => {
this.isSelected = selections.some(f => f.id == this.file.id);
});
- this.onclick = () => {
+ this.onclick = ev => {
+ ev.preventDefault();
this.browser.chooseFile(this.file);
+ return false;
};
</script>
</mk-drive-file>
diff --git a/src/web/app/mobile/tags/drive/folder.tag b/src/web/app/mobile/tags/drive/folder.tag
index 196e7e326b..6125e0b254 100644
--- a/src/web/app/mobile/tags/drive/folder.tag
+++ b/src/web/app/mobile/tags/drive/folder.tag
@@ -1,47 +1,53 @@
-<mk-drive-folder onclick={ onclick }>
- <div class="container">
- <p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
- </div>
+<mk-drive-folder>
+ <a onclick={ onclick } href="/i/drive/folder/{ folder.id }">
+ <div class="container">
+ <p class="name">%fa:folder%{ folder.name }</p>%fa:angle-right%
+ </div>
+ </a>
<style>
:scope
display block
- color #777
- &, *
- user-select none
+ > a
+ display block
+ color #777
+ text-decoration none !important
- *
- pointer-events none
+ *
+ user-select none
+ pointer-events none
- > .container
- max-width 500px
- margin 0 auto
- padding 16px
+ > .container
+ max-width 500px
+ margin 0 auto
+ padding 16px
- > .name
- display block
- margin 0
- padding 0
+ > .name
+ display block
+ margin 0
+ padding 0
+
+ > [data-fa]
+ margin-right 6px
> [data-fa]
- margin-right 6px
+ position absolute
+ top 0
+ bottom 0
+ right 20px
- > [data-fa]
- position absolute
- top 0
- bottom 0
- right 8px
- margin auto 0 auto 0
- width 1em
- height 1em
+ > *
+ height 100%
</style>
<script>
this.browser = this.parent;
this.folder = this.opts.folder;
- this.onclick = () => {
+ this.onclick = ev => {
+ ev.preventDefault();
this.browser.cd(this.folder);
+ return false;
};
</script>
</mk-drive-folder>
diff --git a/src/web/app/mobile/tags/home-timeline.tag b/src/web/app/mobile/tags/home-timeline.tag
index e96823fa10..397d2b3980 100644
--- a/src/web/app/mobile/tags/home-timeline.tag
+++ b/src/web/app/mobile/tags/home-timeline.tag
@@ -47,7 +47,7 @@
this.more = () => {
return this.api('posts/timeline', {
- max_id: this.refs.timeline.tail().id
+ until_id: this.refs.timeline.tail().id
});
};
diff --git a/src/web/app/mobile/tags/images-viewer.tag b/src/web/app/mobile/tags/images-viewer.tag
deleted file mode 100644
index 8ef4a50be0..0000000000
--- a/src/web/app/mobile/tags/images-viewer.tag
+++ /dev/null
@@ -1,26 +0,0 @@
-<mk-images-viewer>
- <div class="image" ref="view" onclick={ click }><img ref="img" src={ image.url + '?thumbnail&size=512' } alt={ image.name } title={ image.name }/></div>
- <style>
- :scope
- display block
- overflow hidden
- border-radius 4px
-
- > .image
-
- > img
- display block
- max-height 256px
- max-width 100%
- margin 0 auto
-
- </style>
- <script>
- this.images = this.opts.images;
- this.image = this.images[0];
-
- this.click = () => {
- window.open(this.image.url);
- };
- </script>
-</mk-images-viewer>
diff --git a/src/web/app/mobile/tags/images.tag b/src/web/app/mobile/tags/images.tag
new file mode 100644
index 0000000000..5899364aef
--- /dev/null
+++ b/src/web/app/mobile/tags/images.tag
@@ -0,0 +1,82 @@
+<mk-images>
+ <virtual each={ image in images }>
+ <mk-images-image image={ image }/>
+ </virtual>
+ <style>
+ :scope
+ display grid
+ grid-gap 4px
+ height 256px
+
+ @media (max-width 500px)
+ height 192px
+ </style>
+ <script>
+ this.images = this.opts.images;
+
+ this.on('mount', () => {
+ if (this.images.length == 1) {
+ this.root.style.gridTemplateRows = '1fr';
+
+ this.tags['mk-images-image'].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'].root.style.gridRow = '1 / 2';
+ } else if (this.images.length == 2) {
+ this.root.style.gridTemplateColumns = '1fr 1fr';
+ this.root.style.gridTemplateRows = '1fr';
+
+ this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+ } else if (this.images.length == 3) {
+ this.root.style.gridTemplateColumns = '1fr 0.5fr';
+ this.root.style.gridTemplateRows = '1fr 1fr';
+
+ this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][0].root.style.gridRow = '1 / 3';
+ this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][2].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+ } else if (this.images.length == 4) {
+ this.root.style.gridTemplateColumns = '1fr 1fr';
+ this.root.style.gridTemplateRows = '1fr 1fr';
+
+ this.tags['mk-images-image'][0].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][0].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][1].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][1].root.style.gridRow = '1 / 2';
+ this.tags['mk-images-image'][2].root.style.gridColumn = '1 / 2';
+ this.tags['mk-images-image'][2].root.style.gridRow = '2 / 3';
+ this.tags['mk-images-image'][3].root.style.gridColumn = '2 / 3';
+ this.tags['mk-images-image'][3].root.style.gridRow = '2 / 3';
+ }
+ });
+ </script>
+</mk-images>
+
+<mk-images-image>
+ <a ref="view" href={ image.url } target="_blank" style={ styles } title={ image.name }></a>
+ <style>
+ :scope
+ display block
+ overflow hidden
+ border-radius 4px
+
+ > a
+ display block
+ overflow hidden
+ width 100%
+ height 100%
+ background-position center
+ background-size cover
+
+ </style>
+ <script>
+ this.image = this.opts.image;
+ this.styles = {
+ 'background-color': this.image.properties.average_color ? `rgb(${this.image.properties.average_color.join(',')})` : 'transparent',
+ 'background-image': `url(${this.image.url}?thumbnail&size=512)`
+ };
+ </script>
+</mk-images-image>
diff --git a/src/web/app/mobile/tags/index.ts b/src/web/app/mobile/tags/index.ts
index 19952c20cd..20934cdd8d 100644
--- a/src/web/app/mobile/tags/index.ts
+++ b/src/web/app/mobile/tags/index.ts
@@ -14,7 +14,6 @@ require('./page/search.tag');
require('./page/settings.tag');
require('./page/settings/profile.tag');
require('./page/settings/signin.tag');
-require('./page/settings/api.tag');
require('./page/settings/authorized-apps.tag');
require('./page/settings/twitter.tag');
require('./page/messaging.tag');
@@ -25,7 +24,7 @@ require('./home-timeline.tag');
require('./timeline.tag');
require('./post-preview.tag');
require('./sub-post-content.tag');
-require('./images-viewer.tag');
+require('./images.tag');
require('./drive.tag');
require('./drive-selector.tag');
require('./drive-folder-selector.tag');
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index c3500d1b84..742cc45145 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -146,7 +146,7 @@
this.api('i/notifications', {
limit: max + 1,
- max_id: this.notifications[this.notifications.length - 1].id
+ until_id: this.notifications[this.notifications.length - 1].id
}).then(notifications => {
if (notifications.length == max + 1) {
this.moreNotifications = true;
diff --git a/src/web/app/mobile/tags/page/entrance.tag b/src/web/app/mobile/tags/page/entrance.tag
index 380fb780bc..191874caf9 100644
--- a/src/web/app/mobile/tags/page/entrance.tag
+++ b/src/web/app/mobile/tags/page/entrance.tag
@@ -8,7 +8,7 @@
</div>
</main>
<footer>
- <mk-copyright/>
+ <p class="c">{ _COPYRIGHT_ }</p>
</footer>
<style>
:scope
@@ -34,7 +34,7 @@
margin 16px auto 0 auto
> footer
- > mk-copyright
+ > .c
margin 0
text-align center
line-height 64px
diff --git a/src/web/app/mobile/tags/page/settings.tag b/src/web/app/mobile/tags/page/settings.tag
index 9789782144..9a73b0af3c 100644
--- a/src/web/app/mobile/tags/page/settings.tag
+++ b/src/web/app/mobile/tags/page/settings.tag
@@ -24,7 +24,6 @@
<li><a href="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</a></li>
<li><a href="./settings/twitter">%fa:B twitter%%i18n:mobile.tags.mk-settings-page.twitter-integration%%fa:angle-right%</a></li>
<li><a href="./settings/signin-history">%fa:sign-in-alt%%i18n:mobile.tags.mk-settings-page.signin-history%%fa:angle-right%</a></li>
- <li><a href="./settings/api">%fa:key%%i18n:mobile.tags.mk-settings-page.api%%fa:angle-right%</a></li>
</ul>
<ul>
<li><a onclick={ signout }>%fa:power-off%%i18n:mobile.tags.mk-settings-page.signout%</a></li>
diff --git a/src/web/app/mobile/tags/page/settings/api.tag b/src/web/app/mobile/tags/page/settings/api.tag
deleted file mode 100644
index 8de0e96963..0000000000
--- a/src/web/app/mobile/tags/page/settings/api.tag
+++ /dev/null
@@ -1,36 +0,0 @@
-<mk-api-info-page>
- <mk-ui ref="ui">
- <mk-api-info/>
- </mk-ui>
- <style>
- :scope
- display block
- </style>
- <script>
- import ui from '../../../scripts/ui-event';
-
- this.on('mount', () => {
- document.title = 'Misskey | API';
- ui.trigger('title', '%fa:key%API');
- });
- </script>
-</mk-api-info-page>
-
-<mk-api-info>
- <p>Token:<code>{ I.token }</code></p>
- <p>APIを利用するには、上記のトークンを「i」というキーでパラメータに付加してリクエストします。</p>
- <p>アカウントを乗っ取られてしまう可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。</p>
- <p>万が一このトークンが漏れたりその可能性がある場合はデスクトップ版Misskeyから再生成できます。</p>
- <style>
- :scope
- display block
- color #4a535a
-
- code
- padding 4px
- background #eee
- </style>
- <script>
- this.mixin('i');
- </script>
-</mk-api-info>
diff --git a/src/web/app/mobile/tags/post-detail.tag b/src/web/app/mobile/tags/post-detail.tag
index 9f212a2496..1816d1bf93 100644
--- a/src/web/app/mobile/tags/post-detail.tag
+++ b/src/web/app/mobile/tags/post-detail.tag
@@ -34,7 +34,7 @@
<div class="body">
<div class="text" ref="text"></div>
<div class="media" if={ p.media }>
- <virtual each={ file in p.media }><img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/></virtual>
+ <mk-images images={ p.media }/>
</div>
<mk-poll if={ p.poll } post={ p }/>
</div>
diff --git a/src/web/app/mobile/tags/post-form.tag b/src/web/app/mobile/tags/post-form.tag
index 3ac7296f73..05466a6ec2 100644
--- a/src/web/app/mobile/tags/post-form.tag
+++ b/src/web/app/mobile/tags/post-form.tag
@@ -9,12 +9,11 @@
<div class="form">
<mk-post-preview if={ opts.reply } post={ opts.reply }/>
<textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder={ opts.reply ? '%i18n:mobile.tags.mk-post-form.reply-placeholder%' : '%i18n:mobile.tags.mk-post-form.post-placeholder%' }></textarea>
- <div class="attaches" if={ files.length != 0 }>
+ <div class="attaches" show={ files.length != 0 }>
<ul class="files" ref="attaches">
- <li class="file" each={ files }>
- <div class="img" style="background-image: url({ url + '?thumbnail&size=64' })" title={ name }></div>
+ <li class="file" each={ files } data-id={ id }>
+ <div class="img" style="background-image: url({ url + '?thumbnail&size=128' })" onclick={ removeFile }></div>
</li>
- <li class="add" if={ files.length < 4 } title="%i18n:mobile.tags.mk-post-form.attach-media-from-local%" onclick={ selectFile }>%fa:plus%</li>
</ul>
</div>
<mk-poll-editor if={ poll } ref="poll" ondestroy={ onPollDestroyed }/>
@@ -93,12 +92,9 @@
> .file
display block
float left
- margin 4px
+ margin 0
padding 0
- cursor move
-
- &:hover > .remove
- display block
+ border solid 4px transparent
> .img
width 64px
@@ -106,38 +102,6 @@
background-size cover
background-position center center
- > .remove
- display none
- position absolute
- top -6px
- right -6px
- width 16px
- height 16px
- cursor pointer
-
- > .add
- display block
- float left
- margin 4px
- padding 0
- border dashed 2px rgba($theme-color, 0.2)
- cursor pointer
-
- &:hover
- border-color rgba($theme-color, 0.3)
-
- > [data-fa]
- color rgba($theme-color, 0.4)
-
- > [data-fa]
- display block
- width 60px
- height 60px
- line-height 60px
- text-align center
- font-size 1.2em
- color rgba($theme-color, 0.2)
-
> mk-uploader
margin 8px 0 0 0
padding 8px
@@ -181,6 +145,7 @@
</style>
<script>
+ import Sortable from 'sortablejs';
import getKao from '../../common/scripts/get-kao';
this.mixin('api');
@@ -200,6 +165,10 @@
});
this.refs.text.focus();
+
+ new Sortable(this.refs.attaches, {
+ animation: 150
+ });
});
this.onkeydown = e => {
@@ -247,6 +216,13 @@
this.update();
};
+ this.removeFile = e => {
+ const file = e.item;
+ this.files = this.files.filter(x => x.id != file.id);
+ this.trigger('change-files', this.files);
+ this.update();
+ };
+
this.addPoll = () => {
this.poll = true;
};
@@ -258,15 +234,23 @@
};
this.post = () => {
- this.wait = true;
+ this.update({
+ wait: true
+ });
- const files = this.files && this.files.length > 0
- ? this.files.map(f => f.id)
- : undefined;
+ const files = [];
+
+ if (this.files.length > 0) {
+ Array.from(this.refs.attaches.children).forEach(el => {
+ const id = el.getAttribute('data-id');
+ const file = this.files.find(f => f.id == id);
+ files.push(file);
+ });
+ }
this.api('posts/create', {
text: this.refs.text.value == '' ? undefined : this.refs.text.value,
- media_ids: files,
+ media_ids: this.files.length > 0 ? files.map(f => f.id) : undefined,
reply_id: opts.reply ? opts.reply.id : undefined,
poll: this.poll ? this.refs.poll.get() : undefined
}).then(data => {
diff --git a/src/web/app/mobile/tags/search-posts.tag b/src/web/app/mobile/tags/search-posts.tag
index 967764bc2c..3e3c034f21 100644
--- a/src/web/app/mobile/tags/search-posts.tag
+++ b/src/web/app/mobile/tags/search-posts.tag
@@ -15,30 +15,28 @@
width calc(100% - 32px)
</style>
<script>
+ import parse from '../../common/scripts/parse-search-query';
+
this.mixin('api');
- this.max = 30;
+ this.limit = 30;
this.offset = 0;
this.query = this.opts.query;
- this.withMedia = this.opts.withMedia;
this.init = new Promise((res, rej) => {
- this.api('posts/search', {
- query: this.query
- }).then(posts => {
+ this.api('posts/search', parse(this.query)).then(posts => {
res(posts);
this.trigger('loaded');
});
});
this.more = () => {
- this.offset += this.max;
- return this.api('posts/search', {
- query: this.query,
- max: this.max,
+ this.offset += this.limit;
+ return this.api('posts/search', Object.assign({}, parse(this.query), {
+ limit: this.limit,
offset: this.offset
- });
+ }));
};
</script>
</mk-search-posts>
diff --git a/src/web/app/mobile/tags/sub-post-content.tag b/src/web/app/mobile/tags/sub-post-content.tag
index 9436b6c1d7..adeb84dea0 100644
--- a/src/web/app/mobile/tags/sub-post-content.tag
+++ b/src/web/app/mobile/tags/sub-post-content.tag
@@ -2,7 +2,7 @@
<div class="body"><a class="reply" if={ post.reply_id }>%fa:reply%</a><span ref="text"></span><a class="quote" if={ post.repost_id } href={ '/post:' + post.repost_id }>RP: ...</a></div>
<details if={ post.media }>
<summary>({ post.media.length }個のメディア)</summary>
- <mk-images-viewer images={ post.media }/>
+ <mk-images images={ post.media }/>
</details>
<details if={ post.poll }>
<summary>%i18n:mobile.tags.mk-sub-post-content.poll%</summary>
diff --git a/src/web/app/mobile/tags/timeline.tag b/src/web/app/mobile/tags/timeline.tag
index 19f90a1c11..9e85f97da3 100644
--- a/src/web/app/mobile/tags/timeline.tag
+++ b/src/web/app/mobile/tags/timeline.tag
@@ -172,7 +172,7 @@
<a class="quote" if={ p.repost != null }>RP:</a>
</div>
<div class="media" if={ p.media }>
- <mk-images-viewer images={ p.media }/>
+ <mk-images images={ p.media }/>
</div>
<mk-poll if={ p.poll } post={ p } ref="pollViewer"/>
<span class="app" if={ p.app }>via <b>{ p.app.name }</b></span>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 62e128489a..77ad14530d 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -248,7 +248,7 @@
<li><a href="/i/settings">%fa:cog%%i18n:mobile.tags.mk-ui-nav.settings%%fa:angle-right%</a></li>
</ul>
</div>
- <a href={ _ABOUT_URL_ }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+ <a href={ aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
</div>
<style>
:scope
@@ -359,6 +359,8 @@
this.connection = this.stream.getConnection();
this.connectionId = this.stream.use();
+ this.aboutUrl = `${_DOCS_URL_}/${_LANG_}/about`;
+
this.on('mount', () => {
this.connection.on('read_all_notifications', this.onReadAllNotifications);
this.connection.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
@@ -411,7 +413,7 @@
this.search = () => {
const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
if (query == null || query == '') return;
- this.page('/search:' + query);
+ this.page('/search?q=' + encodeURIComponent(query));
};
</script>
</mk-ui-nav>
diff --git a/src/web/app/mobile/tags/user-timeline.tag b/src/web/app/mobile/tags/user-timeline.tag
index 4dbe719f5a..86ead5971f 100644
--- a/src/web/app/mobile/tags/user-timeline.tag
+++ b/src/web/app/mobile/tags/user-timeline.tag
@@ -26,7 +26,7 @@
return this.api('users/posts', {
user_id: this.user.id,
with_media: this.withMedia,
- max_id: this.refs.timeline.tail().id
+ until_id: this.refs.timeline.tail().id
});
};
</script>
diff --git a/src/web/assets/code-highlight.css b/src/web/assets/code-highlight.css
new file mode 100644
index 0000000000..f0807dc9c3
--- /dev/null
+++ b/src/web/assets/code-highlight.css
@@ -0,0 +1,93 @@
+.hljs {
+ font-family: Consolas, 'Courier New', Courier, Monaco, monospace;
+}
+
+.hljs,
+.hljs-subst {
+ color: #444;
+}
+
+.hljs-comment {
+ color: #888888;
+}
+
+.hljs-keyword {
+ color: #2973b7;
+}
+
+.hljs-number {
+ color: #ae81ff;
+}
+
+.hljs-string {
+ color: #e96900;
+}
+
+.hljs-regexp {
+ color: #e9003f;
+}
+
+.hljs-attribute,
+.hljs-selector-tag,
+.hljs-meta-keyword,
+.hljs-doctag,
+.hljs-name {
+ font-weight: bold;
+}
+
+.hljs-type,
+.hljs-selector-id,
+.hljs-selector-class,
+.hljs-quote,
+.hljs-template-tag,
+.hljs-deletion {
+ color: #880000;
+}
+
+.hljs-title,
+.hljs-section {
+ color: #880000;
+ font-weight: bold;
+}
+
+.hljs-symbol,
+.hljs-variable,
+.hljs-template-variable,
+.hljs-link,
+.hljs-selector-attr,
+.hljs-selector-pseudo {
+ color: #BC6060;
+}
+
+/* Language color: hue: 90; */
+
+.hljs-literal {
+ color: #78A960;
+}
+
+.hljs-built_in,
+.hljs-bullet,
+.hljs-code,
+.hljs-addition {
+ color: #397300;
+}
+
+/* Meta color: hue: 200 */
+
+.hljs-meta {
+ color: #1f7199;
+}
+
+.hljs-meta-string {
+ color: #4d99bf;
+}
+
+/* Misc effects */
+
+.hljs-emphasis {
+ font-style: italic;
+}
+
+.hljs-strong {
+ font-weight: bold;
+}
diff --git a/src/web/assets/recover.html b/src/web/assets/recover.html
index 35afd2adf2..4922b68d35 100644
--- a/src/web/assets/recover.html
+++ b/src/web/assets/recover.html
@@ -6,7 +6,7 @@
<title>Misskeyのリカバリ</title>
<script>
- const yn = window.confirm('キャッシュをクリアしますか?\n\nDo you want to clear caches?');
+ const yn = window.confirm('キャッシュをクリアしますか?(他のタブでMisskeyを開いている状態だと正常にクリアできないので、他のMisskeyのタブをすべて閉じてから行ってください)\n\nDo you want to clear caches?');
if (yn) {
try {
diff --git a/src/web/const.styl b/src/web/const.styl
new file mode 100644
index 0000000000..b6560701d9
--- /dev/null
+++ b/src/web/const.styl
@@ -0,0 +1,4 @@
+json('../const.json')
+
+$theme-color = themeColor
+$theme-color-foreground = themeColorForeground
diff --git a/src/web/docs/about.en.pug b/src/web/docs/about.en.pug
new file mode 100644
index 0000000000..893d9dd6a1
--- /dev/null
+++ b/src/web/docs/about.en.pug
@@ -0,0 +1,3 @@
+h1 About Misskey
+
+p Misskey is a mini blog SNS.
diff --git a/src/web/docs/about.ja.pug b/src/web/docs/about.ja.pug
new file mode 100644
index 0000000000..fec933b0c6
--- /dev/null
+++ b/src/web/docs/about.ja.pug
@@ -0,0 +1,3 @@
+h1 Misskeyについて
+
+p MisskeyはミニブログSNSです。
diff --git a/src/web/docs/api.ja.pug b/src/web/docs/api.ja.pug
new file mode 100644
index 0000000000..2bb08f7f32
--- /dev/null
+++ b/src/web/docs/api.ja.pug
@@ -0,0 +1,103 @@
+h1 Misskey API
+
+p MisskeyはWeb APIを公開しており、様々な操作をプログラム上から行うことができます。
+p APIを自分のアカウントから利用する場合(自分のアカウントのみ操作したい場合)と、アプリケーションから利用する場合(不特定のアカウントを操作したい場合)とで利用手順が異なりますので、それぞれのケースについて説明します。
+
+section
+ h2 自分の所有するアカウントからAPIにアクセスする場合
+ p 「設定 > API」で、APIにアクセスするのに必要なAPIキーを取得してください。
+ p APIにアクセスする際には、リクエストにAPIキーを「i」というパラメータ名で含めます。
+ div.ui.info.warn: p %fa:exclamation-triangle%アカウントを不正利用される可能性があるため、このトークンは第三者に教えないでください(アプリなどにも入力しないでください)。
+ p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
+
+section
+ h2 アプリケーションからAPIにアクセスする場合
+ p
+ | 直接ユーザーのAPIキーをアプリケーションが扱うのは危険なので、
+ | アプリケーションからAPIを利用する際には、アプリケーションとアプリケーションを利用するユーザーが結び付けられた専用のトークン(アクセストークン)をMisskeyに発行してもらい、
+ | そのトークンをリクエストのパラメータに含める必要があります。
+ div.ui.info: p %fa:info-circle%アクセストークンは、ユーザーが自分のアカウントにあなたのアプリケーションがアクセスすることを許可した場合のみ発行されます
+
+ p それでは、アクセストークンを取得するまでの流れを説明します。
+
+ section
+ h3 1.アプリケーションを登録する
+ p まず、あなたのアプリケーションやWebサービス(以後、あなたのアプリと呼びます)をMisskeyに登録します。
+ p
+ a(href=common.config.dev_url, target="_blank") デベロッパーセンター
+ | にアクセスし、「アプリ > アプリ作成」に進みます。
+ | フォームに必要事項を記入し、アプリを作成してください。フォームの記入欄の説明は以下の通りです:
+
+ table
+ thead
+ tr
+ th 名前
+ th 説明
+ tbody
+ tr
+ td アプリケーション名
+ td あなたのアプリの名称。
+ tr
+ td アプリの概要
+ td あなたのアプリの簡単な説明や紹介。
+ tr
+ td コールバックURL
+ td ユーザーが後述する認証フォームで認証を終えた際にリダイレクトするURLを設定できます。あなたのアプリがWebサービスである場合に有用です。
+ tr
+ td 権限
+ td あなたのアプリが要求する権限。ここで要求した機能だけがAPIからアクセスできます。
+
+ p 登録が済むとあなたのアプリのシークレットキーが入手できます。このシークレットキーは後で使用します。
+ div.ui.info.warn: p %fa:exclamation-triangle%アプリに成りすまされる可能性があるため、極力このシークレットキーは公開しないようにしてください。
+
+ section
+ h3 2.ユーザーに認証させる
+ p あなたのアプリを使ってもらうには、ユーザーにアカウントへのアクセスの許可をもらう必要があります。
+ p
+ | 認証セッションを開始するには、#{common.config.api_url}/auth/session/generate へパラメータに app_secret としてシークレットキーを含めたリクエストを送信します。
+ | リクエスト形式はJSONで、メソッドはPOSTです。
+ | レスポンスとして認証セッションのトークンや認証フォームのURLが取得できるので、認証フォームのURLをブラウザで表示し、ユーザーにフォームを提示してください。
+
+ p
+ | あなたのアプリがコールバックURLを設定している場合、
+ | ユーザーがあなたのアプリの連携を許可すると設定しているコールバックURLに token という名前でセッションのトークンが含まれたクエリを付けてリダイレクトします。
+
+ p
+ | あなたのアプリがコールバックURLを設定していない場合、ユーザーがあなたのアプリの連携を許可したことを(何らかの方法で(たとえばボタンを押させるなど))確認出来るようにしてください。
+
+ section
+ h3 3.ユーザーのアクセストークンを取得する
+ p ユーザーが連携を許可したら、#{common.config.api_url}/auth/session/userkey へ次のパラメータを含むリクエストを送信します:
+ table
+ thead
+ tr
+ th 名前
+ th 型
+ th 説明
+ tbody
+ tr
+ td app_secret
+ td string
+ td あなたのアプリのシークレットキー
+ tr
+ td token
+ td string
+ td セッションのトークン
+ p 上手くいけば、認証したユーザーのアクセストークンがレスポンスとして取得できます。おめでとうございます!
+
+ p アクセストークンが取得できたら、「ユーザーのアクセストークン+あなたのアプリのシークレットキーをsha256したもの」を「i」というパラメータでリクエストに含めると、APIにアクセスすることができます。
+
+ p 「i」パラメータの生成方法を擬似コードで表すと次のようになります:
+ pre: code
+ | const i = sha256(accessToken + secretKey);
+
+ p APIの詳しい使用法は「Misskey APIの利用」セクションをご覧ください。
+
+section
+ h2 Misskey APIの利用
+ p APIはすべてリクエストのパラメータ・レスポンスともにJSON形式です。また、すべてのエンドポイントはPOSTメソッドのみ受け付けます。
+ p APIリファレンスもご確認ください。
+
+ section
+ h3 レートリミット
+ p Misskey APIにはレートリミットがあり、短時間のうちに多数のリクエストを送信すると、一定時間APIを利用することができなくなることがあります。
diff --git a/src/web/docs/api/endpoints/posts/create.yaml b/src/web/docs/api/endpoints/posts/create.yaml
new file mode 100644
index 0000000000..5e2307dab4
--- /dev/null
+++ b/src/web/docs/api/endpoints/posts/create.yaml
@@ -0,0 +1,53 @@
+endpoint: "posts/create"
+
+desc:
+ ja: "投稿します。"
+ en: "Compose new post."
+
+params:
+ - name: "text"
+ type: "string"
+ optional: true
+ desc:
+ ja: "投稿の本文"
+ en: "The text of your post"
+ - name: "media_ids"
+ type: "id(DriveFile)[]"
+ optional: true
+ desc:
+ ja: "添付するメディア(1~4つ)"
+ en: "Media you want to attach (1~4)"
+ - name: "reply_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "返信する投稿"
+ en: "The post you want to reply"
+ - name: "repost_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "引用する投稿"
+ en: "The post you want to quote"
+ - name: "poll"
+ type: "object"
+ optional: true
+ desc:
+ ja: "投票"
+ en: "The poll"
+ defName: "poll"
+ def:
+ - name: "choices"
+ type: "string[]"
+ optional: false
+ desc:
+ ja: "投票の選択肢"
+ en: "Choices of a poll"
+
+res:
+ - name: "created_post"
+ type: "entity(Post)"
+ optional: false
+ desc:
+ ja: "作成した投稿"
+ en: "A post that created"
diff --git a/src/web/docs/api/endpoints/posts/timeline.yaml b/src/web/docs/api/endpoints/posts/timeline.yaml
new file mode 100644
index 0000000000..01976b0611
--- /dev/null
+++ b/src/web/docs/api/endpoints/posts/timeline.yaml
@@ -0,0 +1,32 @@
+endpoint: "posts/timeline"
+
+desc:
+ ja: "タイムラインを取得します。"
+ en: "Get your timeline."
+
+params:
+ - name: "limit"
+ type: "number"
+ optional: true
+ desc:
+ ja: "取得する最大の数"
+ - name: "since_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "指定すると、この投稿を基点としてより新しい投稿を取得します"
+ - name: "until_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "指定すると、この投稿を基点としてより古い投稿を取得します"
+ - name: "since_date"
+ type: "number"
+ optional: true
+ desc:
+ ja: "指定した時間を基点としてより新しい投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
+ - name: "until_date"
+ type: "number"
+ optional: true
+ desc:
+ ja: "指定した時間を基点としてより古い投稿を取得します。数値は、1970 年 1 月 1 日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。"
diff --git a/src/web/docs/api/endpoints/style.styl b/src/web/docs/api/endpoints/style.styl
new file mode 100644
index 0000000000..2af9fe9a77
--- /dev/null
+++ b/src/web/docs/api/endpoints/style.styl
@@ -0,0 +1,21 @@
+@import "../style"
+
+#url
+ padding 8px 12px 8px 8px
+ font-family Consolas, 'Courier New', Courier, Monaco, monospace
+ color #fff
+ background #222e40
+ border-radius 4px
+
+ > .method
+ display inline-block
+ margin 0 8px 0 0
+ padding 0 6px
+ color #f4fcff
+ background #17afc7
+ border-radius 4px
+ user-select none
+ pointer-events none
+
+ > .host
+ opacity 0.7
diff --git a/src/web/docs/api/endpoints/view.pug b/src/web/docs/api/endpoints/view.pug
new file mode 100644
index 0000000000..d271a5517a
--- /dev/null
+++ b/src/web/docs/api/endpoints/view.pug
@@ -0,0 +1,32 @@
+extends ../../layout.pug
+include ../mixins
+
+block meta
+ link(rel="stylesheet" href="/assets/api/endpoints/style.css")
+
+block main
+ h1= endpoint
+
+ p#url
+ span.method POST
+ span.host
+ = url.host
+ | /
+ span.path= url.path
+
+ p#desc= desc[lang] || desc['ja']
+
+ section
+ h2 %i18n:docs.api.endpoints.params%
+ +propTable(params)
+
+ if paramDefs
+ each paramDef in paramDefs
+ section(id= paramDef.name)
+ h3= paramDef.name
+ +propTable(paramDef.params)
+
+ if res
+ section
+ h2 %i18n:docs.api.endpoints.res%
+ +propTable(res)
diff --git a/src/web/docs/api/entities/drive-file.yaml b/src/web/docs/api/entities/drive-file.yaml
new file mode 100644
index 0000000000..2ebbb089ab
--- /dev/null
+++ b/src/web/docs/api/entities/drive-file.yaml
@@ -0,0 +1,73 @@
+name: "DriveFile"
+
+desc:
+ ja: "ドライブのファイル。"
+ en: "A file of Drive."
+
+props:
+ - name: "id"
+ type: "id"
+ optional: false
+ desc:
+ ja: "ファイルID"
+ en: "The ID of this file"
+ - name: "created_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "アップロード日時"
+ en: "The upload date of this file"
+ - name: "user_id"
+ type: "id(User)"
+ optional: false
+ desc:
+ ja: "所有者ID"
+ en: "The ID of the owner of this file"
+ - name: "user"
+ type: "entity(User)"
+ optional: true
+ desc:
+ ja: "所有者"
+ en: "The owner of this file"
+ - name: "name"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイル名"
+ en: "The name of this file"
+ - name: "md5"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイルのMD5ハッシュ値"
+ en: "The md5 hash value of this file"
+ - name: "type"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイルの種類"
+ en: "The type of this file"
+ - name: "datasize"
+ type: "number"
+ optional: false
+ desc:
+ ja: "ファイルサイズ(bytes)"
+ en: "The size of this file (bytes)"
+ - name: "url"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ファイルのURL"
+ en: "The URL of this file"
+ - name: "folder_id"
+ type: "id(DriveFolder)"
+ optional: true
+ desc:
+ ja: "フォルダID"
+ en: "The ID of the folder of this file"
+ - name: "folder"
+ type: "entity(DriveFolder)"
+ optional: true
+ desc:
+ ja: "フォルダ"
+ en: "The folder of this file"
diff --git a/src/web/docs/api/entities/post.yaml b/src/web/docs/api/entities/post.yaml
new file mode 100644
index 0000000000..551f3b7c3e
--- /dev/null
+++ b/src/web/docs/api/entities/post.yaml
@@ -0,0 +1,124 @@
+name: "Post"
+
+desc:
+ ja: "投稿。"
+ en: "A post."
+
+props:
+ - name: "id"
+ type: "id"
+ optional: false
+ desc:
+ ja: "投稿ID"
+ en: "The ID of this post"
+ - name: "created_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "投稿日時"
+ en: "The posted date of this post"
+ - name: "text"
+ type: "string"
+ optional: true
+ desc:
+ ja: "投稿の本文"
+ en: "The text of this post"
+ - name: "media_ids"
+ type: "id(DriveFile)[]"
+ optional: true
+ desc:
+ ja: "添付されているメディアのID"
+ en: "The IDs of the attached media"
+ - name: "media"
+ type: "entity(DriveFile)[]"
+ optional: true
+ desc:
+ ja: "添付されているメディア"
+ en: "The attached media"
+ - name: "user_id"
+ type: "id(User)"
+ optional: false
+ desc:
+ ja: "投稿者ID"
+ en: "The ID of author of this post"
+ - name: "user"
+ type: "entity(User)"
+ optional: true
+ desc:
+ ja: "投稿者"
+ en: "The author of this post"
+ - name: "my_reaction"
+ type: "string"
+ optional: true
+ desc:
+ ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
+ en: "The your <a href='/docs/api/reactions'>reaction</a> of this post"
+ - name: "reaction_counts"
+ type: "object"
+ optional: false
+ desc:
+ ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
+ - name: "reply_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "返信した投稿のID"
+ en: "The ID of the replyed post"
+ - name: "reply"
+ type: "entity(Post)"
+ optional: true
+ desc:
+ ja: "返信した投稿"
+ en: "The replyed post"
+ - name: "repost_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "引用した投稿のID"
+ en: "The ID of the quoted post"
+ - name: "repost"
+ type: "entity(Post)"
+ optional: true
+ desc:
+ ja: "引用した投稿"
+ en: "The quoted post"
+ - name: "poll"
+ type: "object"
+ optional: true
+ desc:
+ ja: "投票"
+ en: "The poll"
+ defName: "poll"
+ def:
+ - name: "choices"
+ type: "object[]"
+ optional: false
+ desc:
+ ja: "投票の選択肢"
+ en: "The choices of this poll"
+ defName: "choice"
+ def:
+ - name: "id"
+ type: "number"
+ optional: false
+ desc:
+ ja: "選択肢ID"
+ en: "The ID of this choice"
+ - name: "is_voted"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこの選択肢に投票したかどうか"
+ en: "Whether you voted to this choice"
+ - name: "text"
+ type: "string"
+ optional: false
+ desc:
+ ja: "選択肢本文"
+ en: "The text of this choice"
+ - name: "votes"
+ type: "number"
+ optional: false
+ desc:
+ ja: "この選択肢に投票された数"
+ en: "The number voted for this choice"
diff --git a/src/web/docs/api/entities/style.styl b/src/web/docs/api/entities/style.styl
new file mode 100644
index 0000000000..bddf0f53ab
--- /dev/null
+++ b/src/web/docs/api/entities/style.styl
@@ -0,0 +1 @@
+@import "../style"
diff --git a/src/web/docs/api/entities/user.yaml b/src/web/docs/api/entities/user.yaml
new file mode 100644
index 0000000000..e62ad84db8
--- /dev/null
+++ b/src/web/docs/api/entities/user.yaml
@@ -0,0 +1,153 @@
+name: "User"
+
+desc:
+ ja: "ユーザー。"
+ en: "A user."
+
+props:
+ - name: "id"
+ type: "id"
+ optional: false
+ desc:
+ ja: "ユーザーID"
+ en: "The ID of this user"
+ - name: "created_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "アカウント作成日時"
+ en: "The registered date of this user"
+ - name: "username"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ユーザー名"
+ en: "The username of this user"
+ - name: "description"
+ type: "string"
+ optional: false
+ desc:
+ ja: "アカウントの説明(自己紹介)"
+ en: "The description of this user"
+ - name: "avatar_id"
+ type: "id(DriveFile)"
+ optional: true
+ desc:
+ ja: "アバターのID"
+ en: "The ID of the avatar of this user"
+ - name: "avatar_url"
+ type: "string"
+ optional: false
+ desc:
+ ja: "アバターのURL"
+ en: "The URL of the avatar of this user"
+ - name: "banner_id"
+ type: "id(DriveFile)"
+ optional: true
+ desc:
+ ja: "バナーのID"
+ en: "The ID of the banner of this user"
+ - name: "banner_url"
+ type: "string"
+ optional: false
+ desc:
+ ja: "バナーのURL"
+ en: "The URL of the banner of this user"
+ - name: "followers_count"
+ type: "number"
+ optional: false
+ desc:
+ ja: "フォロワーの数"
+ en: "The number of the followers for this user"
+ - name: "following_count"
+ type: "number"
+ optional: false
+ desc:
+ ja: "フォローしているユーザーの数"
+ en: "The number of the following users for this user"
+ - name: "is_following"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこのユーザーをフォローしているか"
+ - name: "is_followed"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこのユーザーにフォローされているか"
+ - name: "is_muted"
+ type: "boolean"
+ optional: true
+ desc:
+ ja: "自分がこのユーザーをミュートしているか"
+ en: "Whether you muted this user"
+ - name: "last_used_at"
+ type: "date"
+ optional: false
+ desc:
+ ja: "最終利用日時"
+ en: "The last used date of this user"
+ - name: "posts_count"
+ type: "number"
+ optional: false
+ desc:
+ ja: "投稿の数"
+ en: "The number of the posts of this user"
+ - name: "pinned_post"
+ type: "entity(Post)"
+ optional: true
+ desc:
+ ja: "ピン留めされた投稿"
+ en: "The pinned post of this user"
+ - name: "pinned_post_id"
+ type: "id(Post)"
+ optional: true
+ desc:
+ ja: "ピン留めされた投稿のID"
+ en: "The ID of the pinned post of this user"
+ - name: "drive_capacity"
+ type: "number"
+ optional: false
+ desc:
+ ja: "ドライブの容量(bytes)"
+ en: "The capacity of drive of this user (bytes)"
+ - name: "twitter"
+ type: "object"
+ optional: true
+ desc:
+ ja: "連携されているTwitterアカウント情報"
+ en: "The info of the connected twitter account of this user"
+ defName: "twitter"
+ def:
+ - name: "user_id"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ユーザーID"
+ en: "The user ID"
+ - name: "screen_name"
+ type: "string"
+ optional: false
+ desc:
+ ja: "ユーザー名"
+ en: "The screen name of this user"
+ - name: "profile"
+ type: "object"
+ optional: false
+ desc:
+ ja: "プロフィール"
+ en: "The profile of this user"
+ defName: "profile"
+ def:
+ - name: "location"
+ type: "string"
+ optional: true
+ desc:
+ ja: "場所"
+ en: "The location of this user"
+ - name: "birthday"
+ type: "string"
+ optional: true
+ desc:
+ ja: "誕生日 (YYYY-MM-DD)"
+ en: "The birthday of this user (YYYY-MM-DD)"
diff --git a/src/web/docs/api/entities/view.pug b/src/web/docs/api/entities/view.pug
new file mode 100644
index 0000000000..2156463dc7
--- /dev/null
+++ b/src/web/docs/api/entities/view.pug
@@ -0,0 +1,20 @@
+extends ../../layout.pug
+include ../mixins
+
+block meta
+ link(rel="stylesheet" href="/assets/api/entities/style.css")
+
+block main
+ h1= name
+
+ p#desc= desc[lang] || desc['ja']
+
+ section
+ h2 %i18n:docs.api.entities.properties%
+ +propTable(props)
+
+ if propDefs
+ each propDef in propDefs
+ section(id= propDef.name)
+ h3= propDef.name
+ +propTable(propDef.params)
diff --git a/src/web/docs/api/gulpfile.ts b/src/web/docs/api/gulpfile.ts
new file mode 100644
index 0000000000..cd1bf15307
--- /dev/null
+++ b/src/web/docs/api/gulpfile.ts
@@ -0,0 +1,188 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+import * as yaml from 'js-yaml';
+import * as mkdirp from 'mkdirp';
+
+import locales from '../../../../locales';
+import I18nReplacer from '../../../common/build/i18n';
+import fa from '../../../common/build/fa';
+import config from './../../../conf';
+
+import generateVars from '../vars';
+
+const langs = Object.keys(locales);
+
+const kebab = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
+
+const parseParam = param => {
+ const id = param.type.match(/^id\((.+?)\)|^id/);
+ const entity = param.type.match(/^entity\((.+?)\)/);
+ const isObject = /^object/.test(param.type);
+ const isDate = /^date/.test(param.type);
+ const isArray = /\[\]$/.test(param.type);
+ if (id) {
+ param.kind = 'id';
+ param.type = 'string';
+ param.entity = id[1];
+ if (isArray) {
+ param.type += '[]';
+ }
+ }
+ if (entity) {
+ param.kind = 'entity';
+ param.type = 'object';
+ param.entity = entity[1];
+ if (isArray) {
+ param.type += '[]';
+ }
+ }
+ if (isObject) {
+ param.kind = 'object';
+ }
+ if (isDate) {
+ param.kind = 'date';
+ param.type = 'string';
+ if (isArray) {
+ param.type += '[]';
+ }
+ }
+
+ return param;
+};
+
+const sortParams = params => {
+ params.sort((a, b) => {
+ if (a.name < b.name)
+ return -1;
+ if (a.name > b.name)
+ return 1;
+ return 0;
+ });
+ return params;
+};
+
+const extractDefs = params => {
+ let defs = [];
+
+ params.forEach(param => {
+ if (param.def) {
+ defs.push({
+ name: param.defName,
+ params: sortParams(param.def.map(p => parseParam(p)))
+ });
+
+ const childDefs = extractDefs(param.def);
+
+ defs = defs.concat(childDefs);
+ }
+ });
+
+ return sortParams(defs);
+};
+
+gulp.task('doc:api', [
+ 'doc:api:endpoints',
+ 'doc:api:entities'
+]);
+
+gulp.task('doc:api:endpoints', async () => {
+ const commonVars = await generateVars();
+ glob('./src/web/docs/api/endpoints/**/*.yaml', (globErr, files) => {
+ if (globErr) {
+ console.error(globErr);
+ return;
+ }
+ //console.log(files);
+ files.forEach(file => {
+ const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+ const vars = {
+ endpoint: ep.endpoint,
+ url: {
+ host: config.api_url,
+ path: ep.endpoint
+ },
+ desc: ep.desc,
+ params: sortParams(ep.params.map(p => parseParam(p))),
+ paramDefs: extractDefs(ep.params),
+ res: ep.res ? sortParams(ep.res.map(p => parseParam(p))) : null,
+ resDefs: ep.res ? extractDefs(ep.res) : null,
+ };
+ langs.forEach(lang => {
+ pug.renderFile('./src/web/docs/api/endpoints/view.pug', Object.assign({}, vars, {
+ lang,
+ title: ep.endpoint,
+ src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/endpoints/${ep.endpoint}.yaml`,
+ kebab,
+ common: commonVars
+ }), (renderErr, html) => {
+ if (renderErr) {
+ console.error(renderErr);
+ return;
+ }
+ const i18n = new I18nReplacer(lang);
+ html = html.replace(i18n.pattern, i18n.replacement);
+ html = fa(html);
+ const htmlPath = `./built/web/docs/${lang}/api/endpoints/${ep.endpoint}.html`;
+ mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+ if (mkdirErr) {
+ console.error(mkdirErr);
+ return;
+ }
+ fs.writeFileSync(htmlPath, html, 'utf-8');
+ });
+ });
+ });
+ });
+ });
+});
+
+gulp.task('doc:api:entities', async () => {
+ const commonVars = await generateVars();
+ glob('./src/web/docs/api/entities/**/*.yaml', (globErr, files) => {
+ if (globErr) {
+ console.error(globErr);
+ return;
+ }
+ files.forEach(file => {
+ const entity = yaml.safeLoad(fs.readFileSync(file, 'utf-8'));
+ const vars = {
+ name: entity.name,
+ desc: entity.desc,
+ props: sortParams(entity.props.map(p => parseParam(p))),
+ propDefs: extractDefs(entity.props),
+ };
+ langs.forEach(lang => {
+ pug.renderFile('./src/web/docs/api/entities/view.pug', Object.assign({}, vars, {
+ lang,
+ title: entity.name,
+ src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/api/entities/${kebab(entity.name)}.yaml`,
+ kebab,
+ common: commonVars
+ }), (renderErr, html) => {
+ if (renderErr) {
+ console.error(renderErr);
+ return;
+ }
+ const i18n = new I18nReplacer(lang);
+ html = html.replace(i18n.pattern, i18n.replacement);
+ html = fa(html);
+ const htmlPath = `./built/web/docs/${lang}/api/entities/${kebab(entity.name)}.html`;
+ mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+ if (mkdirErr) {
+ console.error(mkdirErr);
+ return;
+ }
+ fs.writeFileSync(htmlPath, html, 'utf-8');
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/web/docs/api/mixins.pug b/src/web/docs/api/mixins.pug
new file mode 100644
index 0000000000..686bf6a2b6
--- /dev/null
+++ b/src/web/docs/api/mixins.pug
@@ -0,0 +1,37 @@
+mixin propTable(props)
+ table.props
+ thead: tr
+ th %i18n:docs.api.props.name%
+ th %i18n:docs.api.props.type%
+ th %i18n:docs.api.props.optional%
+ th %i18n:docs.api.props.description%
+ tbody
+ each prop in props
+ tr
+ td.name= prop.name
+ td.type
+ i= prop.type
+ if prop.kind == 'id'
+ if prop.entity
+ | (
+ a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+ | ID)
+ else
+ | (ID)
+ else if prop.kind == 'entity'
+ | (
+ a(href=`/${lang}/api/entities/${kebab(prop.entity)}`)= prop.entity
+ | )
+ else if prop.kind == 'object'
+ if prop.def
+ | (
+ a(href=`#${prop.defName}`)= prop.defName
+ | )
+ else if prop.kind == 'date'
+ | (Date)
+ td.optional
+ if prop.optional
+ | %i18n:docs.api.props.yes%
+ else
+ | %i18n:docs.api.props.no%
+ td.desc!= prop.desc[lang] || prop.desc['ja']
diff --git a/src/web/docs/api/style.styl b/src/web/docs/api/style.styl
new file mode 100644
index 0000000000..3675a4da6f
--- /dev/null
+++ b/src/web/docs/api/style.styl
@@ -0,0 +1,11 @@
+@import "../style"
+
+table.props
+ .name
+ font-weight bold
+
+ .name
+ .type
+ .optional
+ font-family Consolas, 'Courier New', Courier, Monaco, monospace
+
diff --git a/src/web/docs/gulpfile.ts b/src/web/docs/gulpfile.ts
new file mode 100644
index 0000000000..d5ddda108d
--- /dev/null
+++ b/src/web/docs/gulpfile.ts
@@ -0,0 +1,77 @@
+/**
+ * Gulp tasks
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as glob from 'glob';
+import * as gulp from 'gulp';
+import * as pug from 'pug';
+import * as mkdirp from 'mkdirp';
+import stylus = require('gulp-stylus');
+import cssnano = require('gulp-cssnano');
+
+import I18nReplacer from '../../common/build/i18n';
+import fa from '../../common/build/fa';
+import generateVars from './vars';
+
+require('./api/gulpfile.ts');
+
+gulp.task('doc', [
+ 'doc:docs',
+ 'doc:api',
+ 'doc:styles'
+]);
+
+gulp.task('doc:docs', async () => {
+ const commonVars = await generateVars();
+
+ glob('./src/web/docs/**/*.*.pug', (globErr, files) => {
+ if (globErr) {
+ console.error(globErr);
+ return;
+ }
+ files.forEach(file => {
+ const [, name, lang] = file.match(/docs\/(.+?)\.(.+?)\.pug$/);
+ const vars = {
+ common: commonVars,
+ lang: lang,
+ title: fs.readFileSync(file, 'utf-8').match(/^h1 (.+?)\r?\n/)[1],
+ src: `https://github.com/syuilo/misskey/tree/master/src/web/docs/${name}.${lang}.pug`,
+ };
+ pug.renderFile(file, vars, (renderErr, content) => {
+ if (renderErr) {
+ console.error(renderErr);
+ return;
+ }
+
+ pug.renderFile('./src/web/docs/layout.pug', Object.assign({}, vars, {
+ content
+ }), (renderErr2, html) => {
+ if (renderErr2) {
+ console.error(renderErr2);
+ return;
+ }
+ const i18n = new I18nReplacer(lang);
+ html = html.replace(i18n.pattern, i18n.replacement);
+ html = fa(html);
+ const htmlPath = `./built/web/docs/${lang}/${name}.html`;
+ mkdirp(path.dirname(htmlPath), (mkdirErr) => {
+ if (mkdirErr) {
+ console.error(mkdirErr);
+ return;
+ }
+ fs.writeFileSync(htmlPath, html, 'utf-8');
+ });
+ });
+ });
+ });
+ });
+});
+
+gulp.task('doc:styles', () =>
+ gulp.src('./src/web/docs/**/*.styl')
+ .pipe(stylus())
+ .pipe((cssnano as any)())
+ .pipe(gulp.dest('./built/web/docs/assets/'))
+);
diff --git a/src/web/docs/index.en.pug b/src/web/docs/index.en.pug
new file mode 100644
index 0000000000..1fcc870d3d
--- /dev/null
+++ b/src/web/docs/index.en.pug
@@ -0,0 +1,3 @@
+h1 Misskey Docs
+
+p Welcome to docs of Misskey.
diff --git a/src/web/docs/index.ja.pug b/src/web/docs/index.ja.pug
new file mode 100644
index 0000000000..4a0bf7fa1d
--- /dev/null
+++ b/src/web/docs/index.ja.pug
@@ -0,0 +1,3 @@
+h1 Misskey ドキュメント
+
+p Misskeyのドキュメントへようこそ
diff --git a/src/web/docs/layout.pug b/src/web/docs/layout.pug
new file mode 100644
index 0000000000..9dfd0ab7af
--- /dev/null
+++ b/src/web/docs/layout.pug
@@ -0,0 +1,41 @@
+doctype html
+
+html(lang= lang)
+ head
+ meta(charset="UTF-8")
+ 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")
+ block meta
+
+ //- FontAwesome style
+ style #{common.facss}
+
+ body
+ nav
+ ul
+ each doc in common.docs
+ li: a(href=`/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
+ section
+ h2 API
+ ul
+ li Entities
+ ul
+ each entity in common.entities
+ li: a(href=`/${lang}/api/entities/${common.kebab(entity)}`)= entity
+ li Endpoints
+ ul
+ each endpoint in common.endpoints
+ li: a(href=`/${lang}/api/endpoints/${common.kebab(endpoint)}`)= endpoint
+ main
+ article
+ block main
+ if content
+ | !{content}
+
+ footer
+ p
+ | %i18n:docs.edit-this-page-on-github%
+ a(href=src target="_blank") %i18n:docs.edit-this-page-on-github-link%
+ small= common.copyright
diff --git a/src/web/docs/license.en.pug b/src/web/docs/license.en.pug
new file mode 100644
index 0000000000..45d8b76473
--- /dev/null
+++ b/src/web/docs/license.en.pug
@@ -0,0 +1,17 @@
+h1 License
+
+div!= common.license
+
+details
+ summary Libraries
+
+ section
+ h2 Libraries
+
+ each dependency, name in common.dependencies
+ details
+ summary= name
+
+ section
+ h3= name
+ pre= dependency.licenseText
diff --git a/src/web/docs/license.ja.pug b/src/web/docs/license.ja.pug
new file mode 100644
index 0000000000..7bd9a62941
--- /dev/null
+++ b/src/web/docs/license.ja.pug
@@ -0,0 +1,17 @@
+h1 ライセンス
+
+div!= common.license
+
+details
+ summary ライブラリ
+
+ section
+ h2 ライブラリ
+
+ each dependency, name in common.dependencies
+ details
+ summary= name
+
+ section
+ h3= name
+ pre= dependency.licenseText
diff --git a/src/web/docs/mute.ja.pug b/src/web/docs/mute.ja.pug
new file mode 100644
index 0000000000..5e79af5f8c
--- /dev/null
+++ b/src/web/docs/mute.ja.pug
@@ -0,0 +1,13 @@
+h1 ミュート
+
+p ユーザーページから、そのユーザーをミュートすることができます。
+
+p ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
+ul
+ li タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRepost)
+ li そのユーザーからの通知
+ li メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
+
+p ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。
+
+p 設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。
diff --git a/src/web/docs/search.ja.pug b/src/web/docs/search.ja.pug
new file mode 100644
index 0000000000..f33091ee6b
--- /dev/null
+++ b/src/web/docs/search.ja.pug
@@ -0,0 +1,116 @@
+h1 検索
+
+p 投稿を検索することができます。
+p
+ | キーワードを半角スペースで区切ると、and検索になります。
+ | 例えば、「git コミット」と検索すると、「gitで編集したファイルの特定の行だけコミットする方法がわからない」などがマッチします。
+
+section
+ h2 キーワードの除外
+ p キーワードの前に「-」(ハイフン)をプリフィクスすると、そのキーワードを含まない投稿に限定します。
+ p 例えば、「gitというキーワードを含むが、コミットというキーワードは含まない投稿」を検索したい場合、クエリは以下のようになります:
+ code git -コミット
+
+section
+ h2 完全一致
+ p テキストを「"""」で囲むと、そのテキストと完全に一致する投稿を検索します。
+ p 例えば、「"""にゃーん"""」と検索すると、「にゃーん」という投稿のみがヒットし、「にゃーん…」という投稿はヒットしません。
+
+section
+ h2 オプション
+ p
+ | オプションを使用して、より高度な検索を行えます。
+ | オプションを指定するには、「オプション名:値」という形式でクエリに含めます。
+ p 利用可能なオプション一覧です:
+
+ table
+ thead
+ tr
+ th 名前
+ th 説明
+ tbody
+ tr
+ td user
+ td
+ | 指定されたユーザー名のユーザーの投稿に限定します。
+ | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。
+ br
+ | 例えば、
+ code user:himawari,sakurako
+ | と検索すると「@himawariまたは@sakurakoの投稿」だけに限定します。
+ | (つまりユーザーのホワイトリストです)
+ tr
+ td exclude_user
+ td
+ | 指定されたユーザー名のユーザーの投稿を除外します。
+ | 「,」(カンマ)で区切って、複数ユーザーを指定することもできます。
+ br
+ | 例えば、
+ code exclude_user:akari,chinatsu
+ | と検索すると「@akariまたは@chinatsu以外の投稿」に限定します。
+ | (つまりユーザーのブラックリストです)
+ tr
+ td follow
+ td
+ | true ... フォローしているユーザーに限定。
+ br
+ | false ... フォローしていないユーザーに限定。
+ br
+ | null ... 特に限定しない(デフォルト)
+ tr
+ td mute
+ td
+ | mute_all ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostを除外する(デフォルト)
+ br
+ | mute_related ... ミュートしているユーザーの投稿に対する返信やRepostだけ除外する
+ br
+ | mute_direct ... ミュートしているユーザーの投稿だけ除外する
+ br
+ | disabled ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostも含める
+ br
+ | direct_only ... ミュートしているユーザーの投稿だけに限定
+ br
+ | related_only ... ミュートしているユーザーの投稿に対する返信やRepostだけに限定
+ br
+ | all_only ... ミュートしているユーザーの投稿とその投稿に対する返信やRepostに限定
+ tr
+ td reply
+ td
+ | true ... 返信に限定。
+ br
+ | false ... 返信でない投稿に限定。
+ br
+ | null ... 特に限定しない(デフォルト)
+ tr
+ td repost
+ td
+ | true ... Repostに限定。
+ br
+ | false ... Repostでない投稿に限定。
+ br
+ | null ... 特に限定しない(デフォルト)
+ tr
+ td media
+ td
+ | true ... メディアが添付されている投稿に限定。
+ br
+ | false ... メディアが添付されていない投稿に限定。
+ br
+ | null ... 特に限定しない(デフォルト)
+ tr
+ td poll
+ td
+ | true ... 投票が添付されている投稿に限定。
+ br
+ | false ... 投票が添付されていない投稿に限定。
+ br
+ | null ... 特に限定しない(デフォルト)
+ tr
+ td until
+ td 上限の日時。(YYYY-MM-DD)
+ tr
+ td since
+ td 下限の日時。(YYYY-MM-DD)
+
+ p 例えば、「@syuiloの2017年11月1日から2017年12月31日までの『Misskey』というテキストを含む返信ではない投稿」を検索したい場合、クエリは以下のようになります:
+ code user:syuilo since:2017-11-01 until:2017-12-31 reply:false Misskey
diff --git a/src/web/docs/server.ts b/src/web/docs/server.ts
new file mode 100644
index 0000000000..b2e50457e5
--- /dev/null
+++ b/src/web/docs/server.ts
@@ -0,0 +1,21 @@
+/**
+ * Docs Server
+ */
+
+import * as express from 'express';
+
+/**
+ * Init app
+ */
+const app = express();
+app.disable('x-powered-by');
+
+app.use('/assets', express.static(`${__dirname}/assets`));
+
+/**
+ * Routing
+ */
+app.get(/^\/([a-z_\-\/]+?)$/, (req, res) =>
+ res.sendFile(`${__dirname}/${req.params[0]}.html`));
+
+module.exports = app;
diff --git a/src/web/docs/style.styl b/src/web/docs/style.styl
new file mode 100644
index 0000000000..bc165f8728
--- /dev/null
+++ b/src/web/docs/style.styl
@@ -0,0 +1,120 @@
+@import "../style"
+@import "./ui"
+
+body
+ margin 0
+ color #34495e
+ word-break break-word
+
+main
+ margin 0 0 0 256px
+ padding 64px
+ width 100%
+ max-width 768px
+
+ section
+ margin 32px 0
+
+ h1
+ margin 0 0 24px 0
+ padding 16px 0
+ font-size 1.5em
+ border-bottom solid 2px #eee
+
+ h2
+ margin 0 0 24px 0
+ padding 0 0 16px 0
+ font-size 1.4em
+ border-bottom solid 1px #eee
+
+ h3
+ margin 0
+ padding 0
+ font-size 1.25em
+
+ h4
+ margin 0
+
+ p
+ margin 1em 0
+ line-height 1.6em
+
+ footer
+ margin 32px 0 0 0
+ border-top solid 2px #eee
+
+ > small
+ margin 16px 0 0 0
+ color #aaa
+
+nav
+ display block
+ position fixed
+ z-index 10000
+ top 0
+ left 0
+ width 256px
+ height 100%
+ overflow auto
+ padding 32px
+ background #fff
+ border-right solid 2px #eee
+
+@media (max-width 1025px)
+ main
+ margin 0
+ max-width 100%
+
+ nav
+ position relative
+ width 100%
+ max-height 128px
+ background #f9f9f9
+ border-right none
+
+@media (max-width 768px)
+ main
+ padding 32px
+
+@media (max-width 512px)
+ main
+ padding 16px
+
+table
+ display block
+ width 100%
+ max-width 100%
+ overflow auto
+ border-spacing 0
+ border-collapse collapse
+
+ thead
+ font-weight bold
+ border-bottom solid 2px #eee
+
+ tr
+ th
+ text-align left
+
+ tbody
+ tr
+ &:nth-child(odd)
+ background #fbfbfb
+
+ th, td
+ padding 8px 16px
+ min-width 128px
+
+code
+ display inline-block
+ padding 8px 10px
+ font-family Consolas, 'Courier New', Courier, Monaco, monospace
+ color #295c92
+ background #f2f2f2
+ border-radius 4px
+
+pre
+ overflow auto
+
+ > code
+ display block
diff --git a/src/web/docs/tou.ja.pug b/src/web/docs/tou.ja.pug
new file mode 100644
index 0000000000..7663258f82
--- /dev/null
+++ b/src/web/docs/tou.ja.pug
@@ -0,0 +1,3 @@
+h1 利用規約
+
+p 公序良俗に反する行為はおやめください。
diff --git a/src/web/docs/ui.styl b/src/web/docs/ui.styl
new file mode 100644
index 0000000000..8d5515712f
--- /dev/null
+++ b/src/web/docs/ui.styl
@@ -0,0 +1,19 @@
+.ui.info
+ display block
+ margin 1em 0
+ padding 0 1em
+ font-size 90%
+ color rgba(#000, 0.87)
+ background #f8f8f9
+ border-radius 4px
+ overflow hidden
+
+ > p
+ opacity 0.8
+
+ > [data-fa]:first-child
+ margin-right 0.25em
+
+ &.warn
+ color #573a08
+ background #FFFAF3
diff --git a/src/web/docs/vars.ts b/src/web/docs/vars.ts
new file mode 100644
index 0000000000..6f713f21d0
--- /dev/null
+++ b/src/web/docs/vars.ts
@@ -0,0 +1,64 @@
+import * as fs from 'fs';
+import * as util from 'util';
+import * as glob from 'glob';
+import * as yaml from 'js-yaml';
+import * as licenseChecker from 'license-checker';
+import * as tmp from 'tmp';
+
+import { fa } from '../../common/build/fa';
+import config from '../../conf';
+import { licenseHtml } from '../../common/build/license';
+const constants = require('../../const.json');
+
+export default async function(): Promise<{ [key: string]: any }> {
+ const vars = {} as { [key: string]: any };
+
+ const endpoints = glob.sync('./src/web/docs/api/endpoints/**/*.yaml');
+ vars['endpoints'] = endpoints.map(ep => {
+ const _ep = yaml.safeLoad(fs.readFileSync(ep, 'utf-8'));
+ return _ep.endpoint;
+ });
+
+ const entities = glob.sync('./src/web/docs/api/entities/**/*.yaml');
+ vars['entities'] = entities.map(x => {
+ const _x = yaml.safeLoad(fs.readFileSync(x, 'utf-8'));
+ return _x.name;
+ });
+
+ const docs = glob.sync('./src/web/docs/**/*.*.pug');
+ vars['docs'] = {};
+ docs.forEach(x => {
+ const [, name, lang] = x.match(/docs\/(.+?)\.(.+?)\.pug$/);
+ if (vars['docs'][name] == null) {
+ vars['docs'][name] = {
+ name,
+ title: {}
+ };
+ }
+ vars['docs'][name]['title'][lang] = fs.readFileSync(x, 'utf-8').match(/^h1 (.+?)\r?\n/)[1];
+ });
+
+ vars['kebab'] = string => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase();
+
+ vars['config'] = config;
+
+ vars['copyright'] = constants.copyright;
+
+ vars['facss'] = fa.dom.css();
+
+ vars['license'] = licenseHtml;
+
+ const tmpObj = tmp.fileSync();
+ fs.writeFileSync(tmpObj.name, JSON.stringify({
+ licenseText: ''
+ }), 'utf-8');
+ const dependencies = await util.promisify(licenseChecker.init).bind(licenseChecker)({
+ start: __dirname + '/../../../',
+ customPath: tmpObj.name
+ });
+ tmpObj.removeCallback();
+
+ vars['dependencies'] = dependencies;
+
+ return vars;
+}
diff --git a/src/web/server.ts b/src/web/server.ts
index 1d3687f89e..062d1f197a 100644
--- a/src/web/server.ts
+++ b/src/web/server.ts
@@ -10,6 +10,9 @@ import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as favicon from 'serve-favicon';
import * as compression from 'compression';
+import vhost = require('vhost');
+
+import config from '../conf';
/**
* Init app
@@ -17,6 +20,8 @@ import * as compression from 'compression';
const app = express();
app.disable('x-powered-by');
+app.use(vhost(`docs.${config.host}`, require('./docs/server')));
+
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json({
type: ['application/json', 'text/plain']
diff --git a/src/web/style.styl b/src/web/style.styl
new file mode 100644
index 0000000000..c25fc8fb52
--- /dev/null
+++ b/src/web/style.styl
@@ -0,0 +1,35 @@
+@charset 'utf-8'
+
+@import "./const"
+
+/*
+ ::selection
+ background $theme-color
+ color #fff
+*/
+
+*
+ position relative
+ box-sizing border-box
+ background-clip padding-box !important
+ tap-highlight-color rgba($theme-color, 0.7)
+ -webkit-tap-highlight-color rgba($theme-color, 0.7)
+
+html, body
+ margin 0
+ padding 0
+ scroll-behavior smooth
+ text-size-adjust 100%
+ font-family sans-serif
+
+a
+ text-decoration none
+ color $theme-color
+ cursor pointer
+
+ &:hover
+ text-decoration underline
+
+ *
+ cursor pointer
+