Initial commit 🍀

This commit is contained in:
syuilo
2016-12-29 07:49:51 +09:00
commit b3f42e62af
405 changed files with 31017 additions and 0 deletions

55
src/api/api-handler.ts Normal file
View File

@ -0,0 +1,55 @@
import * as express from 'express';
import { IEndpoint } from './endpoints';
import authenticate from './authenticate';
import { IAuthContext } from './authenticate';
import _reply from './reply';
import limitter from './limitter';
export default async (endpoint: IEndpoint, req: express.Request, res: express.Response) => {
const reply = _reply.bind(null, res);
let ctx: IAuthContext;
// Authetication
try {
ctx = await authenticate(req);
} catch (e) {
return reply(403, 'AUTHENTICATION_FAILED');
}
if (endpoint.secure && !ctx.isSecure) {
return reply(403, 'ACCESS_DENIED');
}
if (endpoint.shouldBeSignin && ctx.user == null) {
return reply(401, 'PLZ_SIGNIN');
}
if (ctx.app && endpoint.kind) {
if (!ctx.app.permission.some((p: any) => p === endpoint.kind)) {
return reply(403, 'ACCESS_DENIED');
}
}
if (endpoint.shouldBeSignin) {
try {
await limitter(endpoint, ctx); // Rate limit
} catch (e) {
return reply(429);
}
}
let exec = require(`${__dirname}/endpoints/${endpoint.name}`);
if (endpoint.withFile) {
exec = exec.bind(null, req.file);
}
// API invoking
try {
const res = await exec(req.body, ctx.user, ctx.app, ctx.isSecure);
reply(res);
} catch (e) {
reply(400, e);
}
};

61
src/api/authenticate.ts Normal file
View File

@ -0,0 +1,61 @@
import * as express from 'express';
import App from './models/app';
import User from './models/user';
import Userkey from './models/userkey';
export interface IAuthContext {
/**
* App which requested
*/
app: any;
/**
* Authenticated user
*/
user: any;
/**
* Weather if the request is via the (Misskey Web Client or user direct) or not
*/
isSecure: boolean;
}
export default (req: express.Request) =>
new Promise<IAuthContext>(async (resolve, reject) => {
const token = req.body['i'];
if (token) {
const user = await User
.findOne({ token: token });
if (user === null) {
return reject('user not found');
}
return resolve({
app: null,
user: user,
isSecure: true
});
}
const userkey = req.headers['userkey'] || req.body['_userkey'];
if (userkey) {
const userkeyDoc = await Userkey.findOne({
key: userkey
});
if (userkeyDoc === null) {
return reject('invalid userkey');
}
const app = await App
.findOne({ _id: userkeyDoc.app_id });
const user = await User
.findOne({ _id: userkeyDoc.user_id });
return resolve({ app: app, user: user, isSecure: false });
}
return resolve({ app: null, user: null, isSecure: false });
});

View File

@ -0,0 +1,149 @@
import * as mongodb from 'mongodb';
import * as crypto from 'crypto';
import * as gm from 'gm';
const fileType = require('file-type');
const prominence = require('prominence');
import DriveFile from '../models/drive-file';
import DriveFolder from '../models/drive-folder';
import serialize from '../serializers/drive-file';
import event from '../event';
/**
* Add file to drive
*
* @param user User who wish to add file
* @param fileName File name
* @param data Contents
* @param comment Comment
* @param type File type
* @param folderId Folder ID
* @param force If set to true, forcibly upload the file even if there is a file with the same hash.
* @return Object that represents added file
*/
export default (
user: any,
data: Buffer,
name: string = null,
comment: string = null,
folderId: mongodb.ObjectID = null,
force: boolean = false
) => new Promise<any>(async (resolve, reject) => {
// File size
const size = data.byteLength;
// File type
let mime = 'application/octet-stream';
const type = fileType(data);
if (type !== null) {
mime = type.mime;
if (name === null) {
name = `untitled.${type.ext}`;
}
} else {
if (name === null) {
name = 'untitled';
}
}
// Generate hash
const hash = crypto
.createHash('sha256')
.update(data)
.digest('hex') as string;
if (!force) {
// Check if there is a file with the same hash and same data size (to be safe)
const much = await DriveFile.findOne({
user_id: user._id,
hash: hash,
datasize: size
});
if (much !== null) {
resolve(much);
return;
}
}
// Fetch all files to calculate drive usage
const files = await DriveFile
.find({ user_id: user._id }, {
datasize: true,
_id: false
})
.toArray();
// Calculate drive usage (in byte)
const usage = files.map(file => file.datasize).reduce((x, y) => x + y, 0);
// If usage limit exceeded
if (usage + size > user.drive_capacity) {
return reject('no-free-space');
}
// If the folder is specified
let folder: any = null;
if (folderId !== null) {
folder = await DriveFolder
.findOne({
_id: folderId,
user_id: user._id
});
if (folder === null) {
return reject('folder-not-found');
}
}
let properties: any = null;
// If the file is an image
if (/^image\/.*$/.test(mime)) {
// Calculate width and height to save in property
const g = gm(data, name);
const size = await prominence(g).size();
properties = {
width: size.width,
height: size.height
};
}
// Create DriveFile document
const res = await DriveFile.insert({
created_at: new Date(),
user_id: user._id,
folder_id: folder !== null ? folder._id : null,
data: data,
datasize: size,
type: mime,
name: name,
comment: comment,
hash: hash,
properties: properties
});
const file = res.ops[0];
resolve(file);
// Serialize
const fileObj = await serialize(file);
// Publish drive_file_created event
event(user._id, 'drive_file_created', fileObj);
// Register to search database
if (config.elasticsearch.enable) {
const es = require('../../db/elasticsearch');
es.index({
index: 'misskey',
type: 'drive_file',
id: file._id.toString(),
body: {
name: file.name,
user_id: user._id.toString()
}
});
}
});

View File

@ -0,0 +1,25 @@
import * as mongodb from 'mongodb';
import Following from '../models/following';
export default async (me: mongodb.ObjectID, includeMe: boolean = true) => {
// Fetch relation to other users who the I follows
// SELECT followee
const myfollowing = await Following
.find({
follower_id: me,
// 削除されたドキュメントは除く
deleted_at: { $exists: false }
}, {
followee_id: true
})
.toArray();
// ID list of other users who the I follows
const myfollowingIds = myfollowing.map(follow => follow.followee_id);
if (includeMe) {
myfollowingIds.push(me);
}
return myfollowingIds;
};

32
src/api/common/notify.ts Normal file
View File

@ -0,0 +1,32 @@
import * as mongo from 'mongodb';
import Notification from '../models/notification';
import event from '../event';
import serialize from '../serializers/notification';
export default (
notifiee: mongo.ObjectID,
notifier: mongo.ObjectID,
type: string,
content: any
) => new Promise<any>(async (resolve, reject) => {
if (notifiee.equals(notifier)) {
return resolve();
}
// Create notification
const res = await Notification.insert(Object.assign({
created_at: new Date(),
notifiee_id: notifiee,
notifier_id: notifier,
type: type,
is_read: false
}, content));
const notification = res.ops[0];
resolve(notification);
// Publish notification event
event(notifiee, 'notification',
await serialize(notification));
});

101
src/api/endpoints.ts Normal file
View File

@ -0,0 +1,101 @@
const second = 1000;
const minute = 60 * second;
const hour = 60 * minute;
const day = 24 * hour;
export interface IEndpoint {
name: string;
shouldBeSignin: boolean;
limitKey?: string;
limitDuration?: number;
limitMax?: number;
minInterval?: number;
withFile?: boolean;
secure?: boolean;
kind?: string;
}
export default [
{ name: 'meta', shouldBeSignin: false },
{ name: 'username/available', shouldBeSignin: false },
{ name: 'my/apps', shouldBeSignin: true },
{ name: 'app/create', shouldBeSignin: true, limitDuration: day, limitMax: 3 },
{ name: 'app/show', shouldBeSignin: false },
{ name: 'app/name_id/available', shouldBeSignin: false },
{ name: 'auth/session/generate', shouldBeSignin: false },
{ name: 'auth/session/show', shouldBeSignin: false },
{ name: 'auth/session/userkey', shouldBeSignin: false },
{ name: 'auth/accept', shouldBeSignin: true, secure: true },
{ name: 'auth/deny', shouldBeSignin: true, secure: true },
{ name: 'aggregation/users/post', shouldBeSignin: false },
{ name: 'aggregation/users/like', shouldBeSignin: false },
{ name: 'aggregation/users/followers', shouldBeSignin: false },
{ name: 'aggregation/users/following', shouldBeSignin: false },
{ name: 'aggregation/posts/like', shouldBeSignin: false },
{ name: 'aggregation/posts/likes', shouldBeSignin: false },
{ name: 'aggregation/posts/repost', shouldBeSignin: false },
{ name: 'aggregation/posts/reply', shouldBeSignin: false },
{ name: 'i', shouldBeSignin: true },
{ name: 'i/update', shouldBeSignin: true, limitDuration: day, limitMax: 50, kind: 'account-write' },
{ name: 'i/appdata/get', shouldBeSignin: true },
{ name: 'i/appdata/set', shouldBeSignin: true },
{ name: 'i/signin_history', shouldBeSignin: true, kind: 'account-read' },
{ name: 'i/notifications', shouldBeSignin: true, kind: 'notification-read' },
{ name: 'notifications/delete', shouldBeSignin: true, kind: 'notification-write' },
{ name: 'notifications/delete_all', shouldBeSignin: true, kind: 'notification-write' },
{ name: 'notifications/mark_as_read', shouldBeSignin: true, kind: 'notification-write' },
{ name: 'notifications/mark_as_read_all', shouldBeSignin: true, kind: 'notification-write' },
{ name: 'drive', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/stream', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/files', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/files/create', shouldBeSignin: true, limitDuration: hour, limitMax: 100, withFile: true, kind: 'drive-write' },
{ name: 'drive/files/show', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/files/find', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/files/delete', shouldBeSignin: true, kind: 'drive-write' },
{ name: 'drive/files/update', shouldBeSignin: true, kind: 'drive-write' },
{ name: 'drive/folders', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/folders/create', shouldBeSignin: true, limitDuration: hour, limitMax: 50, kind: 'drive-write' },
{ name: 'drive/folders/show', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/folders/find', shouldBeSignin: true, kind: 'drive-read' },
{ name: 'drive/folders/update', shouldBeSignin: true, kind: 'drive-write' },
{ name: 'users', shouldBeSignin: false },
{ name: 'users/show', shouldBeSignin: false },
{ name: 'users/search', shouldBeSignin: false },
{ name: 'users/search_by_username', shouldBeSignin: false },
{ name: 'users/posts', shouldBeSignin: false },
{ name: 'users/following', shouldBeSignin: false },
{ name: 'users/followers', shouldBeSignin: false },
{ name: 'users/recommendation', shouldBeSignin: true, kind: 'account-read' },
{ name: 'following/create', shouldBeSignin: true, limitDuration: hour, limitMax: 100, kind: 'following-write' },
{ name: 'following/delete', shouldBeSignin: true, limitDuration: hour, limitMax: 100, kind: 'following-write' },
{ name: 'posts/show', shouldBeSignin: false },
{ name: 'posts/replies', shouldBeSignin: false },
{ name: 'posts/context', shouldBeSignin: false },
{ name: 'posts/create', shouldBeSignin: true, limitDuration: hour, limitMax: 120, minInterval: 1 * second, kind: 'post-write' },
{ name: 'posts/reposts', shouldBeSignin: false },
{ name: 'posts/search', shouldBeSignin: false },
{ name: 'posts/timeline', shouldBeSignin: true, limitDuration: 10 * minute, limitMax: 100 },
{ name: 'posts/mentions', shouldBeSignin: true, limitDuration: 10 * minute, limitMax: 100 },
{ name: 'posts/likes', shouldBeSignin: true },
{ name: 'posts/likes/create', shouldBeSignin: true, limitDuration: hour, limitMax: 100, kind: 'like-write' },
{ name: 'posts/likes/delete', shouldBeSignin: true, limitDuration: hour, limitMax: 100, kind: 'like-write' },
{ name: 'posts/favorites/create', shouldBeSignin: true, limitDuration: hour, limitMax: 100, kind: 'favorite-write' },
{ name: 'posts/favorites/delete', shouldBeSignin: true, limitDuration: hour, limitMax: 100, kind: 'favorite-write' },
{ name: 'messaging/history', shouldBeSignin: true, kind: 'messaging-read' },
{ name: 'messaging/unread', shouldBeSignin: true, kind: 'messaging-read' },
{ name: 'messaging/messages', shouldBeSignin: true, kind: 'messaging-read' },
{ name: 'messaging/messages/create', shouldBeSignin: true, kind: 'messaging-write' }
] as IEndpoint[];

View File

@ -0,0 +1,83 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../../models/post';
import Like from '../../../models/like';
/**
* Aggregate like of a post
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
const datas = await Like
.aggregate([
{ $match: { post_id: post._id } },
{ $project: {
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
}},
{ $project: {
date: {
year: { $year: '$created_at' },
month: { $month: '$created_at' },
day: { $dayOfMonth: '$created_at' }
}
}},
{ $group: {
_id: '$date',
count: { $sum: 1 }
}}
])
.toArray();
datas.forEach(data => {
data.date = data._id;
delete data._id;
});
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter(d =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data)
} else {
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: 0
})
};
}
res(graph);
});

View File

@ -0,0 +1,76 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../../models/post';
import Like from '../../../models/like';
/**
* Aggregate likes of a post
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
const likes = await Like
.find({
post_id: post._id,
$or: [
{ deleted_at: { $exists: false } },
{ deleted_at: { $gt: startTime } }
]
}, {
_id: false,
post_id: false
}, {
sort: { created_at: -1 }
})
.toArray();
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
day = new Date(day.setMilliseconds(999));
day = new Date(day.setSeconds(59));
day = new Date(day.setMinutes(59));
day = new Date(day.setHours(23));
//day = day.getTime();
const count = likes.filter(l =>
l.created_at < day && (l.deleted_at == null || l.deleted_at > day)
).length;
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: count
});
}
res(graph);
});

View File

@ -0,0 +1,82 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../../models/post';
/**
* Aggregate reply of a post
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
const datas = await Post
.aggregate([
{ $match: { reply_to: post._id } },
{ $project: {
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
}},
{ $project: {
date: {
year: { $year: '$created_at' },
month: { $month: '$created_at' },
day: { $dayOfMonth: '$created_at' }
}
}},
{ $group: {
_id: '$date',
count: { $sum: 1 }
}}
])
.toArray();
datas.forEach(data => {
data.date = data._id;
delete data._id;
});
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter(d =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data)
} else {
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: 0
})
};
}
res(graph);
});

View File

@ -0,0 +1,82 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../../models/post';
/**
* Aggregate repost of a post
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
const datas = await Post
.aggregate([
{ $match: { repost_id: post._id } },
{ $project: {
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
}},
{ $project: {
date: {
year: { $year: '$created_at' },
month: { $month: '$created_at' },
day: { $dayOfMonth: '$created_at' }
}
}},
{ $group: {
_id: '$date',
count: { $sum: 1 }
}}
])
.toArray();
datas.forEach(data => {
data.date = data._id;
delete data._id;
});
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter(d =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data)
} else {
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: 0
})
};
}
res(graph);
});

View File

@ -0,0 +1,77 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../../models/user';
import Following from '../../../models/following';
/**
* Aggregate followers of a user
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
const following = await Following
.find({
followee_id: user._id,
$or: [
{ deleted_at: { $exists: false } },
{ deleted_at: { $gt: startTime } }
]
}, {
_id: false,
follower_id: false,
followee_id: false
}, {
sort: { created_at: -1 }
})
.toArray();
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
day = new Date(day.setMilliseconds(999));
day = new Date(day.setSeconds(59));
day = new Date(day.setMinutes(59));
day = new Date(day.setHours(23));
// day = day.getTime();
const count = following.filter(f =>
f.created_at < day && (f.deleted_at == null || f.deleted_at > day)
).length;
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: count
});
}
res(graph);
});

View File

@ -0,0 +1,76 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../../models/user';
import Following from '../../../models/following';
/**
* Aggregate following of a user
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
const startTime = new Date(new Date().setMonth(new Date().getMonth() - 1));
const following = await Following
.find({
follower_id: user._id,
$or: [
{ deleted_at: { $exists: false } },
{ deleted_at: { $gt: startTime } }
]
}, {
_id: false,
follower_id: false,
followee_id: false
}, {
sort: { created_at: -1 }
})
.toArray();
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
day = new Date(day.setMilliseconds(999));
day = new Date(day.setSeconds(59));
day = new Date(day.setMinutes(59));
day = new Date(day.setHours(23));
const count = following.filter(f =>
f.created_at < day && (f.deleted_at == null || f.deleted_at > day)
).length;
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: count
});
}
res(graph);
});

View File

@ -0,0 +1,83 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../../models/user';
import Like from '../../../models/like';
/**
* Aggregate like of a user
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
const datas = await Like
.aggregate([
{ $match: { user_id: user._id } },
{ $project: {
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
}},
{ $project: {
date: {
year: { $year: '$created_at' },
month: { $month: '$created_at' },
day: { $dayOfMonth: '$created_at' }
}
}},
{ $group: {
_id: '$date',
count: { $sum: 1 }
}}
])
.toArray();
datas.forEach(data => {
data.date = data._id;
delete data._id;
});
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter(d =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data)
} else {
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
count: 0
})
};
}
res(graph);
});

View File

@ -0,0 +1,113 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../../models/user';
import Post from '../../../models/post';
/**
* Aggregate post of a user
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
const datas = await Post
.aggregate([
{ $match: { user_id: user._id } },
{ $project: {
repost_id: '$repost_id',
reply_to_id: '$reply_to_id',
created_at: { $add: ['$created_at', 9 * 60 * 60 * 1000] } // Convert into JST
}},
{ $project: {
date: {
year: { $year: '$created_at' },
month: { $month: '$created_at' },
day: { $dayOfMonth: '$created_at' }
},
type: {
$cond: {
if: { $ne: ['$repost_id', null] },
then: 'repost',
else: {
$cond: {
if: { $ne: ['$reply_to_id', null] },
then: 'reply',
else: 'post'
}
}
}
}}
},
{ $group: { _id: {
date: '$date',
type: '$type'
}, count: { $sum: 1 } } },
{ $group: {
_id: '$_id.date',
data: { $addToSet: {
type: '$_id.type',
count: '$count'
}}
} }
])
.toArray();
datas.forEach(data => {
data.date = data._id;
delete data._id;
data.posts = (data.data.filter(x => x.type == 'post')[0] || { count: 0 }).count;
data.reposts = (data.data.filter(x => x.type == 'repost')[0] || { count: 0 }).count;
data.replies = (data.data.filter(x => x.type == 'reply')[0] || { count: 0 }).count;
delete data.data;
});
const graph = [];
for (let i = 0; i < 30; i++) {
let day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter(d =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data)
} else {
graph.push({
date: {
year: day.getFullYear(),
month: day.getMonth() + 1, // In JavaScript, month is zero-based.
day: day.getDate()
},
posts: 0,
reposts: 0,
replies: 0
})
};
}
res(graph);
});

View File

@ -0,0 +1,75 @@
'use strict';
/**
* Module dependencies
*/
import rndstr from 'rndstr';
import App from '../../models/app';
import serialize from '../../serializers/app';
/**
* Create an app
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = async (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'name_id' parameter
const nameId = params.name_id;
if (nameId == null || nameId == '') {
return rej('name_id is required');
}
// Validate name_id
if (!/^[a-zA-Z0-9\-]{3,30}$/.test(nameId)) {
return rej('invalid name_id');
}
// Get 'name' parameter
const name = params.name;
if (name == null || name == '') {
return rej('name is required');
}
// Get 'description' parameter
const description = params.description;
if (description == null || description == '') {
return rej('description is required');
}
// Get 'permission' parameter
const permission = params.permission;
if (permission == null || permission == '') {
return rej('permission is required');
}
// Get 'callback_url' parameter
let callback = params.callback_url;
if (callback === '') {
callback = null;
}
// Generate secret
const secret = rndstr('a-zA-Z0-9', 32);
// Create account
const inserted = await App.insert({
created_at: new Date(),
user_id: user._id,
name: name,
name_id: nameId,
name_id_lower: nameId.toLowerCase(),
description: description,
permission: permission.split(','),
callback_url: callback,
secret: secret
});
const app = inserted.ops[0];
// Response
res(await serialize(app));
});

View File

@ -0,0 +1,40 @@
'use strict';
/**
* Module dependencies
*/
import App from '../../../models/app';
/**
* Check available name_id of app
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = async (params) =>
new Promise(async (res, rej) =>
{
// Get 'name_id' parameter
const nameId = params.name_id;
if (nameId == null || nameId == '') {
return rej('name_id is required');
}
// Validate name_id
if (!/^[a-zA-Z0-9\-]{3,30}$/.test(nameId)) {
return rej('invalid name_id');
}
// Get exist
const exist = await App
.count({
name_id_lower: nameId.toLowerCase()
}, {
limit: 1
});
// Reply
res({
available: exist === 0
});
});

View File

@ -0,0 +1,51 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import App from '../../models/app';
import serialize from '../../serializers/app';
/**
* Show an app
*
* @param {Object} params
* @param {Object} user
* @param {Object} _
* @param {Object} isSecure
* @return {Promise<object>}
*/
module.exports = (params, user, _, isSecure) =>
new Promise(async (res, rej) =>
{
// Get 'app_id' parameter
let appId = params.app_id;
if (appId == null || appId == '') {
appId = null;
}
// Get 'name_id' parameter
let nameId = params.name_id;
if (nameId == null || nameId == '') {
nameId = null;
}
if (appId === null && nameId === null) {
return rej('app_id or name_id is required');
}
// Lookup app
const app = appId !== null
? await App.findOne({ _id: new mongo.ObjectID(appId) })
: await App.findOne({ name_id_lower: nameId.toLowerCase() });
if (app === null) {
return rej('app not found');
}
// Send response
res(await serialize(app, user, {
includeSecret: isSecure && app.user_id.equals(user._id)
}));
});

View File

@ -0,0 +1,64 @@
'use strict';
/**
* Module dependencies
*/
import rndstr from 'rndstr';
import AuthSess from '../../models/auth-session';
import Userkey from '../../models/userkey';
/**
* Accept
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'token' parameter
const token = params.token;
if (token == null) {
return rej('token is required');
}
// Fetch token
const session = await AuthSess
.findOne({ token: token });
if (session === null) {
return rej('session not found');
}
// Generate userkey
const key = rndstr('a-zA-Z0-9', 32);
// Fetch exist userkey
const exist = await Userkey.findOne({
app_id: session.app_id,
user_id: user._id,
});
if (exist === null) {
// Insert userkey doc
await Userkey.insert({
created_at: new Date(),
app_id: session.app_id,
user_id: user._id,
key: key
});
}
// Update session
await AuthSess.updateOne({
_id: session._id
}, {
$set: {
user_id: user._id
}
});
// Response
res();
});

View File

@ -0,0 +1,51 @@
'use strict';
/**
* Module dependencies
*/
import * as uuid from 'uuid';
import App from '../../../models/app';
import AuthSess from '../../../models/auth-session';
/**
* Generate a session
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'app_secret' parameter
const appSecret = params.app_secret;
if (appSecret == null) {
return rej('app_secret is required');
}
// Lookup app
const app = await App.findOne({
secret: appSecret
});
if (app == null) {
return rej('app not found');
}
// Generate token
const token = uuid.v4();
// Create session token document
const inserted = await AuthSess.insert({
created_at: new Date(),
app_id: app._id,
token: token
});
const doc = inserted.ops[0];
// Response
res({
token: doc.token,
url: `${config.auth_url}/${doc.token}`
});
});

View File

@ -0,0 +1,36 @@
'use strict';
/**
* Module dependencies
*/
import AuthSess from '../../../models/auth-session';
import serialize from '../../../serializers/auth-session';
/**
* Show a session
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'token' parameter
const token = params.token;
if (token == null) {
return rej('token is required');
}
// Lookup session
const session = await AuthSess.findOne({
token: token
});
if (session == null) {
return rej('session not found');
}
// Response
res(await serialize(session, user));
});

View File

@ -0,0 +1,74 @@
'use strict';
/**
* Module dependencies
*/
import App from '../../../models/app';
import AuthSess from '../../../models/auth-session';
import Userkey from '../../../models/userkey';
import serialize from '../../../serializers/user';
/**
* Generate a session
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'app_secret' parameter
const appSecret = params.app_secret;
if (appSecret == null) {
return rej('app_secret is required');
}
// Lookup app
const app = await App.findOne({
secret: appSecret
});
if (app == null) {
return rej('app not found');
}
// Get 'token' parameter
const token = params.token;
if (token == null) {
return rej('token is required');
}
// Fetch token
const session = await AuthSess
.findOne({
token: token,
app_id: app._id
});
if (session === null) {
return rej('session not found');
}
if (session.user_id == null) {
return rej('this session is not allowed yet');
}
// Lookup userkey
const userkey = await Userkey.findOne({
app_id: app._id,
user_id: session.user_id
});
// Delete session
AuthSess.deleteOne({
_id: session._id
});
// Response
res({
userkey: userkey.key,
user: await serialize(session.user_id, null, {
detail: true
})
});
});

View File

@ -0,0 +1,33 @@
'use strict';
/**
* Module dependencies
*/
import DriveFile from './models/drive-file';
/**
* Get drive information
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Fetch all files to calculate drive usage
const files = await DriveFile
.find({ user_id: user._id }, {
datasize: true,
_id: false
})
.toArray();
// Calculate drive usage (in byte)
const usage = files.map(file => file.datasize).reduce((x, y) => x + y, 0);
res({
capacity: user.drive_capacity,
usage: usage
});
});

View File

@ -0,0 +1,82 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFile from '../../models/drive-file';
import serialize from '../../serializers/drive-file';
/**
* Get drive files
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @return {Promise<object>}
*/
module.exports = (params, user, app) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Get 'folder_id' parameter
let folder = params.folder_id;
if (folder === undefined || folder === null || folder === 'null') {
folder = null;
} else {
folder = new mongo.ObjectID(folder);
}
// Construct query
const sort = {
_id: -1
};
const query = {
user_id: user._id,
folder_id: folder
};
if (since !== null) {
sort._id = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const files = await DriveFile
.find(query, {
data: false
}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(files.map(async file =>
await serialize(file))));
});

View File

@ -0,0 +1,59 @@
'use strict';
/**
* Module dependencies
*/
import * as fs from 'fs';
import * as mongo from 'mongodb';
import File from '../../../models/drive-file';
import { validateFileName } from '../../../models/drive-file';
import User from '../../../models/user';
import serialize from '../../../serializers/drive-file';
import create from '../../../common/add-file-to-drive';
/**
* Create a file
*
* @param {Object} file
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (file, params, user) =>
new Promise(async (res, rej) =>
{
const buffer = fs.readFileSync(file.path);
fs.unlink(file.path);
// Get 'name' parameter
let name = file.originalname;
if (name !== undefined && name !== null) {
name = name.trim();
if (name.length === 0) {
name = null;
} else if (name === 'blob') {
name = null;
} else if (!validateFileName(name)) {
return rej('invalid name');
}
} else {
name = null;
}
// Get 'folder_id' parameter
let folder = params.folder_id;
if (folder === undefined || folder === null || folder === 'null') {
folder = null;
} else {
folder = new mongo.ObjectID(folder);
}
// Create file
const driveFile = await create(user, buffer, name, null, folder);
// Serialize
const fileObj = await serialize(driveFile);
// Response
res(fileObj);
});

View File

@ -0,0 +1,48 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFile from '../../../models/drive-file';
import serialize from '../../../serializers/drive-file';
/**
* Find a file(s)
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'name' parameter
const name = params.name;
if (name === undefined || name === null) {
return rej('name is required');
}
// Get 'folder_id' parameter
let folder = params.folder_id;
if (folder === undefined || folder === null || folder === 'null') {
folder = null;
} else {
folder = new mongo.ObjectID(folder);
}
// Issue query
const files = await DriveFile
.find({
name: name,
user_id: user._id,
folder_id: folder
}, {
data: false
})
.toArray();
// Serialize
res(await Promise.all(files.map(async file =>
await serialize(file))));
});

View File

@ -0,0 +1,40 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFile from '../../../models/drive-file';
import serialize from '../../../serializers/drive-file';
/**
* Show a file
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'file_id' parameter
const fileId = params.file_id;
if (fileId === undefined || fileId === null) {
return rej('file_id is required');
}
const file = await DriveFile
.findOne({
_id: new mongo.ObjectID(fileId),
user_id: user._id
}, {
data: false
});
if (file === null) {
return rej('file-not-found');
}
// Serialize
res(await serialize(file));
});

View File

@ -0,0 +1,89 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
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 event from '../../../event';
/**
* Update a file
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'file_id' parameter
const fileId = params.file_id;
if (fileId === undefined || fileId === null) {
return rej('file_id is required');
}
const file = await DriveFile
.findOne({
_id: new mongo.ObjectID(fileId),
user_id: user._id
}, {
data: false
});
if (file === null) {
return rej('file-not-found');
}
// Get 'name' parameter
let name = params.name;
if (name) {
name = name.trim();
if (validateFileName(name)) {
file.name = name;
} else {
return rej('invalid file name');
}
}
// Get 'folder_id' parameter
let folderId = params.folder_id;
if (folderId !== undefined && folderId !== 'null') {
folderId = new mongo.ObjectID(folderId);
}
let folder = null;
if (folderId !== undefined && folderId !== null) {
if (folderId === 'null') {
file.folder_id = null;
} else {
folder = await DriveFolder
.findOne({
_id: folderId,
user_id: user._id
});
if (folder === null) {
return reject('folder-not-found');
}
file.folder_id = folder._id;
}
}
DriveFile.updateOne({ _id: file._id }, {
$set: file
});
// Serialize
const fileObj = await serialize(file);
// Response
res(fileObj);
// Publish drive_file_updated event
event(user._id, 'drive_file_updated', fileObj);
});

View File

@ -0,0 +1,82 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFolder from '../../models/drive-folder';
import serialize from '../../serializers/drive-folder';
/**
* Get drive folders
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @return {Promise<object>}
*/
module.exports = (params, user, app) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Get 'folder_id' parameter
let folder = params.folder_id;
if (folder === undefined || folder === null || folder === 'null') {
folder = null;
} else {
folder = new mongo.ObjectID(folder);
}
// Construct query
const sort = {
created_at: -1
};
const query = {
user_id: user._id,
parent_id: folder
};
if (since !== null) {
sort.created_at = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const folders = await DriveFolder
.find(query, {
data: false
}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(folders.map(async folder =>
await serialize(folder))));
});

View File

@ -0,0 +1,79 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFolder from '../../../models/drive-folder';
import { isValidFolderName } from '../../../models/drive-folder';
import serialize from '../../../serializers/drive-folder';
import event from '../../../event';
/**
* Create drive folder
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'name' parameter
let name = params.name;
if (name !== undefined && name !== null) {
name = name.trim();
if (name.length === 0) {
name = null;
} else if (!isValidFolderName(name)) {
return rej('invalid name');
}
} else {
name = null;
}
if (name == null) {
name = '無題のフォルダー';
}
// Get 'folder_id' parameter
let parentId = params.folder_id;
if (parentId === undefined || parentId === null) {
parentId = null;
} else {
parentId = new mongo.ObjectID(parentId);
}
// If the parent folder is specified
let parent = null;
if (parentId !== null) {
parent = await DriveFolder
.findOne({
_id: parentId,
user_id: user._id
});
if (parent === null) {
return reject('parent-not-found');
}
}
// Create folder
const inserted = await DriveFolder.insert({
created_at: new Date(),
name: name,
parent_id: parent !== null ? parent._id : null,
user_id: user._id
});
const folder = inserted.ops[0];
// Serialize
const folderObj = await serialize(folder);
// Response
res(folderObj);
// Publish drive_folder_created event
event(user._id, 'drive_folder_created', folderObj);
});

View File

@ -0,0 +1,46 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFolder from '../../../models/drive-folder';
import serialize from '../../../serializers/drive-folder';
/**
* Find a folder(s)
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'name' parameter
const name = params.name;
if (name === undefined || name === null) {
return rej('name is required');
}
// Get 'parent_id' parameter
let parentId = params.parent_id;
if (parentId === undefined || parentId === null || parentId === 'null') {
parentId = null;
} else {
parentId = new mongo.ObjectID(parentId);
}
// Issue query
const folders = await DriveFolder
.find({
name: name,
user_id: user._id,
parent_id: parentId
})
.toArray();
// Serialize
res(await Promise.all(folders.map(async folder =>
await serialize(folder))));
});

View File

@ -0,0 +1,41 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFolder from '../../../models/drive-folder';
import serialize from '../../../serializers/drive-folder';
/**
* Show a folder
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'folder_id' parameter
const folderId = params.folder_id;
if (folderId === undefined || folderId === null) {
return rej('folder_id is required');
}
// Get folder
const folder = await DriveFolder
.findOne({
_id: new mongo.ObjectID(folderId),
user_id: user._id
});
if (folder === null) {
return rej('folder-not-found');
}
// Serialize
res(await serialize(folder, {
includeParent: true
}));
});

View File

@ -0,0 +1,114 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFolder from '../../../models/drive-folder';
import { isValidFolderName } from '../../../models/drive-folder';
import serialize from '../../../serializers/drive-file';
import event from '../../../event';
/**
* Update a folder
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'folder_id' parameter
const folderId = params.folder_id;
if (folderId === undefined || folderId === null) {
return rej('folder_id is required');
}
// Fetch folder
const folder = await DriveFolder
.findOne({
_id: new mongo.ObjectID(folderId),
user_id: user._id
});
if (folder === null) {
return rej('folder-not-found');
}
// Get 'name' parameter
let name = params.name;
if (name) {
name = name.trim();
if (isValidFolderName(name)) {
folder.name = name;
} else {
return rej('invalid folder name');
}
}
// Get 'parent_id' parameter
let parentId = params.parent_id;
if (parentId !== undefined && parentId !== 'null') {
parentId = new mongo.ObjectID(parentId);
}
let parent = null;
if (parentId !== undefined && parentId !== null) {
if (parentId === 'null') {
folder.parent_id = null;
} else {
// Get parent folder
parent = await DriveFolder
.findOne({
_id: parentId,
user_id: user._id
});
if (parent === null) {
return rej('parent-folder-not-found');
}
// Check if the circular reference will be occured
async function checkCircle(folderId) {
// Fetch folder
const folder2 = await DriveFolder.findOne({
_id: folderId
}, {
_id: true,
parent_id: true
});
if (folder2._id.equals(folder._id)) {
return true;
} else if (folder2.parent_id) {
return await checkCircle(folder2.parent_id);
} else {
return false;
}
}
if (parent.parent_id !== null) {
if (await checkCircle(parent.parent_id)) {
return rej('detected-circular-definition');
}
}
folder.parent_id = parent._id;
}
}
// Update
DriveFolder.updateOne({ _id: folder._id }, {
$set: folder
});
// Serialize
const folderObj = await serialize(folder);
// Response
res(folderObj);
// Publish drive_folder_updated event
event(user._id, 'drive_folder_updated', folderObj);
});

View File

@ -0,0 +1,85 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFile from '../../models/drive-file';
import serialize from '../../serializers/drive-file';
/**
* Get drive stream
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Get 'type' parameter
let type = params.type;
if (type === undefined || type === null) {
type = null;
} else if (!/^[a-zA-Z\/\-\*]+$/.test(type)) {
return rej('invalid type format');
} else {
type = new RegExp(`^${type.replace(/\*/g, '.+?')}$`);
}
// Construct query
const sort = {
created_at: -1
};
const query = {
user_id: user._id
};
if (since !== null) {
sort.created_at = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
if (type !== null) {
query.type = type;
}
// Issue query
const files = await DriveFile
.find(query, {
data: false
}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(files.map(async file =>
await serialize(file))));
});

View File

@ -0,0 +1,86 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User 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
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
const follower = user;
// Get 'user_id' parameter
let userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// 自分自身
if (user._id.equals(userId)) {
return rej('followee is yourself');
}
// Get followee
const followee = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (followee === null) {
return rej('user not found');
}
// Check arleady following
const exist = await Following.findOne({
follower_id: follower._id,
followee_id: followee._id,
deleted_at: { $exists: false }
});
if (exist !== null) {
return rej('already following');
}
// Create following
await Following.insert({
created_at: new Date(),
follower_id: follower._id,
followee_id: followee._id
});
// Send response
res();
// Increment following count
User.updateOne({ _id: follower._id }, {
$inc: {
following_count: 1
}
});
// Increment followers count
User.updateOne({ _id: followee._id }, {
$inc: {
followers_count: 1
}
});
// Publish follow event
event(follower._id, 'follow', await serializeUser(followee, follower));
event(followee._id, 'followed', await serializeUser(follower, followee));
// Notify
notify(followee._id, follower._id, 'follow');
});

View File

@ -0,0 +1,83 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import Following from '../../models/following';
import event from '../../event';
import serializeUser from '../../serializers/user';
/**
* Unfollow a user
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
const follower = user;
// Get 'user_id' parameter
let userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Check if the followee is yourself
if (user._id.equals(userId)) {
return rej('followee is yourself');
}
// Get followee
const followee = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (followee === null) {
return rej('user not found');
}
// Check not following
const exist = await Following.findOne({
follower_id: follower._id,
followee_id: followee._id,
deleted_at: { $exists: false }
});
if (exist === null) {
return rej('already not following');
}
// Delete following
await Following.updateOne({
_id: exist._id
}, {
$set: {
deleted_at: new Date()
}
});
// Send response
res();
// Decrement following count
User.updateOne({ _id: follower._id }, {
$inc: {
following_count: -1
}
});
// Decrement followers count
User.updateOne({ _id: followee._id }, {
$inc: {
followers_count: -1
}
});
// Publish follow event
event(follower._id, 'unfollow', await serializeUser(followee, follower));
});

25
src/api/endpoints/i.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
/**
* Module dependencies
*/
import serialize from '../serializers/user';
/**
* Show myself
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @param {Boolean} isSecure
* @return {Promise<object>}
*/
module.exports = (params, user, _, isSecure) =>
new Promise(async (res, rej) =>
{
// Serialize
res(await serialize(user, user, {
detail: true,
includeSecrets: isSecure
}));
});

View File

@ -0,0 +1,53 @@
'use strict';
/**
* Module dependencies
*/
import Appdata from '../../../models/appdata';
/**
* Get app data
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @param {Boolean} isSecure
* @return {Promise<object>}
*/
module.exports = (params, user, app, isSecure) =>
new Promise(async (res, rej) =>
{
// Get 'key' parameter
let key = params.key;
if (key === undefined) {
key = null;
}
if (isSecure) {
if (!user.data) {
return res();
}
if (key !== null) {
const data = {};
data[key] = user.data[key];
res(data);
} else {
res(user.data);
}
} else {
const select = {};
if (key !== null) {
select['data.' + key] = true;
}
const appdata = await Appdata.findOne({
app_id: app._id,
user_id: user._id
}, select);
if (appdata) {
res(appdata.data);
} else {
res();
}
}
});

View File

@ -0,0 +1,55 @@
'use strict';
/**
* Module dependencies
*/
import Appdata from '../../../models/appdata';
import User from '../../../models/user';
/**
* Set app data
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @param {Boolean} isSecure
* @return {Promise<object>}
*/
module.exports = (params, user, app, isSecure) =>
new Promise(async (res, rej) =>
{
const data = params.data;
if (data == null) {
return rej('data is required');
}
if (isSecure) {
const set = {
$set: {
data: Object.assign(user.data || {}, JSON.parse(data))
}
};
await User.updateOne({ _id: user._id }, set);
res(204);
} else {
const appdata = await Appdata.findOne({
app_id: app._id,
user_id: user._id
});
const set = {
$set: {
data: Object.assign((appdata || {}).data || {}, JSON.parse(data))
}
};
await Appdata.updateOne({
app_id: app._id,
user_id: user._id
}, Object.assign({
app_id: app._id,
user_id: user._id
}, set), {
upsert: true
});
res(204);
}
});

View File

@ -0,0 +1,60 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Favorite from '../../models/favorite';
import serialize from '../../serializers/post';
/**
* Get followers of a user
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// Get 'sort' parameter
let sort = params.sort || 'desc';
// Get favorites
const favorites = await Favorites
.find({
user_id: user._id
}, {}, {
limit: limit,
skip: offset,
sort: {
_id: sort == 'asc' ? 1 : -1
}
})
.toArray();
// Serialize
res(await Promise.all(favorites.map(async favorite =>
await serialize(favorite.post)
)));
});

View File

@ -0,0 +1,120 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Notification from '../../models/notification';
import serialize from '../../serializers/notification';
import getFriends from '../../common/get-friends';
/**
* Get notifications
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'following' parameter
const following = params.following === 'true';
// Get 'mark_as_read' parameter
let markAsRead = params.mark_as_read;
if (markAsRead == null) {
markAsRead = true;
} else {
markAsRead = markAsRead === 'true';
}
// Get 'type' parameter
let type = params.type;
if (type !== undefined && type !== null) {
type = type.split(',').map(x => x.trim());
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
const query = {
notifiee_id: user._id
};
const sort = {
_id: -1
};
if (following) {
// ID list of the user itself and other users who the user follows
const followingIds = await getFriends(user._id);
query.notifier_id = {
$in: followingIds
};
}
if (type) {
query.type = {
$in: type
};
}
if (since !== null) {
sort._id = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const notifications = await Notification
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(notifications.map(async notification =>
await serialize(notification))));
// Mark as read all
if (notifications.length > 0 && markAsRead) {
const ids = notifications
.filter(x => x.is_read == false)
.map(x => x._id);
// Update documents
await Notification.update({
_id: { $in: ids }
}, {
$set: { is_read: true }
}, {
multi: true
});
}
});

View File

@ -0,0 +1,71 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Signin from '../../models/signin';
import serialize from '../../serializers/signin';
/**
* Get signin history of my account
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
const query = {
user_id: user._id
};
const sort = {
_id: -1
};
if (since !== null) {
sort._id = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const history = await Signin
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(history.map(async record =>
await serialize(record))));
});

View File

@ -0,0 +1,95 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import serialize from '../../serializers/user';
import event from '../../event';
/**
* Update myself
*
* @param {Object} params
* @param {Object} user
* @param {Object} _
* @param {boolean} isSecure
* @return {Promise<object>}
*/
module.exports = async (params, user, _, isSecure) =>
new Promise(async (res, rej) =>
{
// Get 'name' parameter
const name = params.name;
if (name !== undefined && name !== null) {
if (name.length > 50) {
return rej('too long name');
}
user.name = name;
}
// Get 'location' parameter
const location = params.location;
if (location !== undefined && location !== null) {
if (location.length > 50) {
return rej('too long location');
}
user.location = location;
}
// Get 'bio' parameter
const bio = params.bio;
if (bio !== undefined && bio !== null) {
if (bio.length > 500) {
return rej('too long bio');
}
user.bio = bio;
}
// Get 'avatar_id' parameter
const avatar = params.avatar_id;
if (avatar !== undefined && avatar !== null) {
user.avatar_id = new mongo.ObjectID(avatar);
}
// Get 'banner_id' parameter
const banner = params.banner_id;
if (banner !== undefined && banner !== null) {
user.banner_id = new mongo.ObjectID(banner);
}
await User.updateOne({ _id: user._id }, {
$set: user
});
// Serialize
const iObj = await serialize(user, user, {
detail: true,
includeSecrets: isSecure
})
// Send response
res(iObj);
// Publish i updated event
event(user._id, 'i_updated', iObj);
// Update search index
if (config.elasticsearch.enable) {
const es = require('../../../db/elasticsearch');
es.index({
index: 'misskey',
type: 'user',
id: user._id.toString(),
body: {
name: user.name,
bio: user.bio
}
});
}
});

View File

@ -0,0 +1,48 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import History from '../../models/messaging-history';
import serialize from '../../serializers/messaging-message';
/**
* Show messaging history
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get history
const history = await History
.find({
user_id: user._id
}, {}, {
limit: limit,
sort: {
updated_at: -1
}
})
.toArray();
// Serialize
res(await Promise.all(history.map(async h =>
await serialize(h.message, user))));
});

View File

@ -0,0 +1,139 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Message from '../../models/messaging-message';
import User from '../../models/user';
import serialize from '../../serializers/messaging-message';
import publishUserStream from '../../event';
import { publishMessagingStream } from '../../event';
/**
* Get messages
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
let recipient = params.user_id;
if (recipient !== undefined && recipient !== null) {
recipient = await User.findOne({
_id: new mongo.ObjectID(recipient)
});
if (recipient === null) {
return rej('user not found');
}
} else {
return rej('user_id is required');
}
// Get 'mark_as_read' parameter
let markAsRead = params.mark_as_read;
if (markAsRead == null) {
markAsRead = true;
} else {
markAsRead = markAsRead === 'true';
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
const query = {
$or: [{
user_id: user._id,
recipient_id: recipient._id
}, {
user_id: recipient._id,
recipient_id: user._id
}]
};
const sort = {
created_at: -1
};
if (since !== null) {
sort.created_at = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const messages = await Message
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(messages.map(async message =>
await serialize(message, user, {
populateRecipient: false
}))));
if (messages.length === 0) {
return;
}
// Mark as read all
if (markAsRead) {
const ids = messages
.filter(m => m.is_read == false)
.filter(m => m.recipient_id.equals(user._id))
.map(m => m._id);
// Update documents
await Message.update({
_id: { $in: ids }
}, {
$set: { is_read: true }
}, {
multi: true
});
// Publish event
publishMessagingStream(recipient._id, user._id, 'read', ids.map(id => id.toString()));
const count = await Message
.count({
recipient_id: user._id,
is_read: false
});
if (count == 0) {
// 全ての(いままで未読だった)メッセージを(これで)読みましたよというイベントを発行
publishUserStream(user._id, 'read_all_messaging_messages');
}
}
});

View File

@ -0,0 +1,152 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Message from '../../../models/messaging-message';
import History from '../../../models/messaging-history';
import User from '../../../models/user';
import DriveFile from '../../../models/drive-file';
import serialize from '../../../serializers/messaging-message';
import publishUserStream from '../../../event';
import { publishMessagingStream } from '../../../event';
/**
* 最大文字数
*/
const maxTextLength = 500;
/**
* Create a message
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
let recipient = params.user_id;
if (recipient !== undefined && recipient !== null) {
recipient = await User.findOne({
_id: new mongo.ObjectID(recipient)
});
if (recipient === null) {
return rej('user not found');
}
} else {
return rej('user_id is required');
}
// Get 'text' parameter
let text = params.text;
if (text !== undefined && text !== null) {
text = text.trim();
if (text.length === 0) {
text = null;
} else if (text.length > maxTextLength) {
return rej('too long text');
}
} else {
text = null;
}
// Get 'file_id' parameter
let file = params.file_id;
if (file !== undefined && file !== null) {
file = await DriveFile.findOne({
_id: new mongo.ObjectID(file),
user_id: user._id
}, {
data: false
});
if (file === null) {
return rej('file not found');
}
} else {
file = null;
}
// テキストが無いかつ添付ファイルも無かったらエラー
if (text === null && file === null) {
return rej('text or file is required');
}
// メッセージを作成
const inserted = await Message.insert({
created_at: new Date(),
file_id: file ? file._id : undefined,
recipient_id: recipient._id,
text: text ? text : undefined,
user_id: user._id,
is_read: false
});
const message = inserted.ops[0];
// Serialize
const messageObj = await serialize(message);
// Reponse
res(messageObj);
// 自分のストリーム
publishMessagingStream(message.user_id, message.recipient_id, 'message', messageObj);
publishUserStream(message.user_id, 'messaging_message', messageObj);
// 相手のストリーム
publishMessagingStream(message.recipient_id, message.user_id, 'message', messageObj);
publishUserStream(message.recipient_id, 'messaging_message', messageObj);
// 5秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する
setTimeout(async () => {
const freshMessage = await Message.findOne({ _id: message._id }, { is_read: true });
if (!freshMessage.is_read) {
publishUserStream(message.recipient_id, 'unread_messaging_message', messageObj);
}
}, 5000);
// Register to search database
if (message.text && config.elasticsearch.enable) {
const es = require('../../../db/elasticsearch');
es.index({
index: 'misskey',
type: 'messaging_message',
id: message._id.toString(),
body: {
text: message.text
}
});
}
// 履歴作成(自分)
History.updateOne({
user_id: user._id,
partner: recipient._id
}, {
updated_at: new Date(),
user_id: user._id,
partner: recipient._id,
message: message._id
}, {
upsert: true
});
// 履歴作成(相手)
History.updateOne({
user_id: recipient._id,
partner: user._id
}, {
updated_at: new Date(),
user_id: recipient._id,
partner: user._id,
message: message._id
}, {
upsert: true
});
});

View File

@ -0,0 +1,27 @@
'use strict';
/**
* Module dependencies
*/
import Message from '../../models/messaging-message';
/**
* Get count of unread messages
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
const count = await Message
.count({
recipient_id: user._id,
is_read: false
});
res({
count: count
});
});

24
src/api/endpoints/meta.js Normal file
View File

@ -0,0 +1,24 @@
'use strict';
/**
* Module dependencies
*/
import Git from 'nodegit';
/**
* Show core info
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
const repository = await Git.Repository.open(__dirname + '/../../');
res({
maintainer: config.maintainer,
commit: (await repository.getHeadCommit()).sha(),
secure: config.https.enable
});
});

View File

@ -0,0 +1,59 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import App from '../../models/app';
import serialize from '../../serializers/app';
/**
* Get my apps
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
const query = {
user_id: user._id
};
// Execute query
const apps = await App
.find(query, {}, {
limit: limit,
skip: offset,
sort: {
created_at: -1
}
})
.toArray();
// Reply
res(await Promise.all(apps.map(async app =>
await serialize(app))));
});

View File

@ -0,0 +1,54 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Notification from '../../../models/notification';
import serialize from '../../../serializers/notification';
import event from '../../../event';
/**
* Mark as read a notification
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
const notificationId = params.notification;
if (notificationId === undefined || notificationId === null) {
return rej('notification is required');
}
// Get notifcation
const notification = await Notification
.findOne({
_id: new mongo.ObjectID(notificationId),
i: user._id
});
if (notification === null) {
return rej('notification-not-found');
}
// Update
notification.is_read = true;
Notification.updateOne({ _id: notification._id }, {
$set: {
is_read: true
}
});
// Response
res();
// Serialize
const notificationObj = await serialize(notification);
// Publish read_notification event
event(user._id, 'read_notification', notificationObj);
});

View File

@ -0,0 +1,65 @@
'use strict';
/**
* Module dependencies
*/
import Post from '../models/post';
import serialize from '../serializers/post';
/**
* Lists all posts
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = (params) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Construct query
const sort = {
created_at: -1
};
const query = {};
if (since !== null) {
sort.created_at = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const posts = await Post
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(posts.map(async post => await serialize(post))));
});

View File

@ -0,0 +1,83 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import serialize from '../../serializers/post';
/**
* Show a context of a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found', 'POST_NOT_FOUND');
}
const context = [];
let i = 0;
async function get(id) {
i++;
const p = await Post.findOne({ _id: id });
if (i > offset) {
context.push(p);
}
if (context.length == limit) {
return;
}
if (p.reply_to_id) {
await get(p.reply_to_id);
}
}
if (post.reply_to_id) {
await get(post.reply_to_id);
}
// Serialize
res(await Promise.all(context.map(async post =>
await serialize(post, user))));
});

View File

@ -0,0 +1,345 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import parse from '../../../common/text';
import Post from '../../models/post';
import User from '../../models/user';
import Following from '../../models/following';
import DriveFile from '../../models/drive-file';
import serialize from '../../serializers/post';
import createFile from '../../common/add-file-to-drive';
import notify from '../../common/notify';
import event from '../../event';
/**
* 最大文字数
*/
const maxTextLength = 300;
/**
* 添付できるファイルの数
*/
const maxMediaCount = 4;
/**
* Create a post
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @return {Promise<object>}
*/
module.exports = (params, user, app) =>
new Promise(async (res, rej) =>
{
// Get 'text' parameter
let text = params.text;
if (text !== undefined && text !== null) {
text = text.trim();
if (text.length == 0) {
text = null;
} else if (text.length > maxTextLength) {
return rej('too long text');
}
} else {
text = null;
}
// Get 'media_ids' parameter
let media = params.media_ids;
let files = [];
if (media !== undefined && media !== null) {
media = media.split(',');
if (media.length > maxMediaCount) {
return rej('too many media');
}
// Drop duplicates
media = media.filter((x, i, s) => s.indexOf(x) == i);
// Fetch files
// forEach だと途中でエラーなどがあっても return できないので
// 敢えて for を使っています。
for (let i = 0; i < media.length; i++) {
const image = media[i];
// Fetch file
// SELECT _id
const entity = await DriveFile.findOne({
_id: new mongo.ObjectID(image),
user_id: user._id
}, {
_id: true
});
if (entity === null) {
return rej('file not found');
} else {
files.push(entity);
}
}
} else {
files = null;
}
// Get 'repost_id' parameter
let repost = params.repost_id;
if (repost !== undefined && repost !== null) {
// Fetch repost to post
repost = await Post.findOne({
_id: new mongo.ObjectID(repost)
});
if (repost == null) {
return rej('repostee is not found');
} else if (repost.repost_id && !repost.text && !repost.media_ids) {
return rej('cannot repost to repost');
}
// Fetch recently post
const latestPost = await Post.findOne({
user_id: user._id
}, {}, {
sort: {
_id: -1
}
});
// 直近と同じRepost対象かつ引用じゃなかったらエラー
if (latestPost &&
latestPost.repost_id &&
latestPost.repost_id.equals(repost._id) &&
text === null && files === null) {
return rej('二重Repostです(NEED TRANSLATE)');
}
// 直近がRepost対象かつ引用じゃなかったらエラー
if (latestPost &&
latestPost._id.equals(repost._id) &&
text === null && files === null) {
return rej('二重Repostです(NEED TRANSLATE)');
}
} else {
repost = null;
}
// Get 'reply_to_id' parameter
let replyTo = params.reply_to_id;
if (replyTo !== undefined && replyTo !== null) {
replyTo = await Post.findOne({
_id: new mongo.ObjectID(replyTo)
});
if (replyTo === null) {
return rej('reply to post is not found');
}
// 返信対象が引用でないRepostだったらエラー
if (replyTo.repost_id && !replyTo.text && !replyTo.media_ids) {
return rej('cannot reply to repost');
}
} else {
replyTo = null;
}
// テキストが無いかつ添付ファイルが無いかつRepostも無かったらエラー
if (text === null && files === null && repost === null) {
return rej('text, media_ids or repost_id is required');
}
// 投稿を作成
const inserted = await Post.insert({
created_at: new Date(),
media_ids: media ? files.map(file => file._id) : undefined,
reply_to_id: replyTo ? replyTo._id : undefined,
repost_id: repost ? repost._id : undefined,
text: text,
user_id: user._id,
app_id: app ? app._id : null
});
const post = inserted.ops[0];
// Serialize
const postObj = await serialize(post);
// Reponse
res(postObj);
//--------------------------------
// Post processes
let mentions = [];
function addMention(mentionee, type) {
// Reject if already added
if (mentions.some(x => x.equals(mentionee))) return;
// Add mention
mentions.push(mentionee);
// Publish event
if (!user._id.equals(mentionee)) {
event(mentionee, type, postObj);
}
}
// Publish event to myself's stream
event(user._id, 'post', postObj);
// Fetch all followers
const followers = await Following
.find({
followee_id: user._id,
// 削除されたドキュメントは除く
deleted_at: { $exists: false }
}, {
follower_id: true,
_id: false
})
.toArray();
// Publish event to followers stream
followers.forEach(following =>
event(following.follower_id, 'post', postObj));
// Increment my posts count
User.updateOne({ _id: user._id }, {
$inc: {
posts_count: 1
}
});
// If has in reply to post
if (replyTo) {
// Increment replies count
Post.updateOne({ _id: replyTo._id }, {
$inc: {
replies_count: 1
}
});
// 自分自身へのリプライでない限りは通知を作成
notify(replyTo.user_id, user._id, 'reply', {
post_id: post._id
});
// Add mention
addMention(replyTo.user_id, 'reply');
}
// If it is repost
if (repost) {
// Notify
const type = text ? 'quote' : 'repost';
notify(repost.user_id, user._id, type, {
post_id: post._id
});
// If it is quote repost
if (text) {
// Add mention
addMention(repost.user_id, 'quote');
} else {
// Publish event
if (!user._id.equals(repost.user_id)) {
event(repost.user_id, 'repost', postObj);
}
}
// 今までで同じ投稿をRepostしているか
const existRepost = await Post.findOne({
user_id: user._id,
repost_id: repost._id,
_id: {
$ne: post._id
}
});
if (!existRepost) {
// Update repostee status
Post.updateOne({ _id: repost._id }, {
$inc: {
repost_count: 1
}
});
}
}
// If has text content
if (text) {
// Analyze
const tokens = parse(text);
// Extract a hashtags
const hashtags = tokens
.filter(t => t.type == 'hashtag')
.map(t => t.hashtag)
// Drop dupulicates
.filter((v, i, s) => s.indexOf(v) == i);
// ハッシュタグをデータベースに登録
//registerHashtags(user, hashtags);
// Extract an '@' mentions
const atMentions = tokens
.filter(t => t.type == 'mention')
.map(m => m.username)
// Drop dupulicates
.filter((v, i, s) => s.indexOf(v) == i);
// Resolve all mentions
await Promise.all(atMentions.map(async (mention) => {
// Fetch mentioned user
// SELECT _id
const mentionee = await User
.findOne({
username_lower: mention.toLowerCase()
}, { _id: true });
// When mentioned user not found
if (mentionee == null) return;
// 既に言及されたユーザーに対する返信や引用repostの場合も無視
if (replyTo && replyTo.user_id.equals(mentionee._id)) return;
if (repost && repost.user_id.equals(mentionee._id)) return;
// Add mention
addMention(mentionee._id, 'mention');
// Create notification
notify(mentionee._id, user._id, 'mention', {
post_id: post._id
});
return;
}));
}
// Register to search database
if (text && config.elasticsearch.enable) {
const es = require('../../../db/elasticsearch');
es.index({
index: 'misskey',
type: 'post',
id: post._id.toString(),
body: {
text: post.text
}
});
}
// Append mentions data
if (mentions.length > 0) {
Post.updateOne({ _id: post._id }, {
$set: {
mentions: mentions
}
});
}
});

View File

@ -0,0 +1,56 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Favorite from '../../models/favorite';
import Post from '../../models/post';
/**
* Favorite a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
let postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get favoritee
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
// Check arleady favorited
const exist = await Favorite.findOne({
post_id: post._id,
user_id: user._id
});
if (exist !== null) {
return rej('already favorited');
}
// Create favorite
const inserted = await Favorite.insert({
created_at: new Date(),
post_id: post._id,
user_id: user._id
});
const favorite = inserted.ops[0];
// Send response
res();
});

View File

@ -0,0 +1,52 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Favorite from '../../models/favorite';
import Post from '../../models/post';
/**
* Unfavorite a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
let postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get favoritee
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
// Check arleady favorited
const exist = await Favorite.findOne({
post_id: post._id,
user_id: user._id
});
if (exist === null) {
return rej('already not favorited');
}
// Delete favorite
await Favorite.deleteOne({
_id: exist._id
});
// Send response
res();
});

View File

@ -0,0 +1,77 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import Like from '../../models/like';
import serialize from '../../serializers/user';
/**
* Show a likes of a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// Get 'sort' parameter
let sort = params.sort || 'desc';
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
// Issue query
const likes = await Like
.find({
post_id: post._id,
deleted_at: { $exists: false }
}, {}, {
limit: limit,
skip: offset,
sort: {
_id: sort == 'asc' ? 1 : -1
}
})
.toArray();
// Serialize
res(await Promise.all(likes.map(async like =>
await serialize(like.user_id, user))));
});

View File

@ -0,0 +1,93 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Like from '../../../models/like';
import Post from '../../../models/post';
import User from '../../../models/user';
import notify from '../../../common/notify';
import event from '../../../event';
import serializeUser from '../../../serializers/user';
import serializePost from '../../../serializers/post';
/**
* Like a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
let postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get likee
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
// Myself
if (post.user_id.equals(user._id)) {
return rej('-need-translate-');
}
// Check arleady liked
const exist = await Like.findOne({
post_id: post._id,
user_id: user._id,
deleted_at: { $exists: false }
});
if (exist !== null) {
return rej('already liked');
}
// Create like
const inserted = await Like.insert({
created_at: new Date(),
post_id: post._id,
user_id: user._id
});
const like = inserted.ops[0];
// Send response
res();
// Increment likes count
Post.updateOne({ _id: post._id }, {
$inc: {
likes_count: 1
}
});
// Increment user likes count
User.updateOne({ _id: user._id }, {
$inc: {
likes_count: 1
}
});
// Increment user liked count
User.updateOne({ _id: post.user_id }, {
$inc: {
liked_count: 1
}
});
// Notify
notify(post.user_id, user._id, 'like', {
post_id: post._id
});
});

View File

@ -0,0 +1,80 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Like from '../../../models/like';
import Post from '../../../models/post';
import User from '../../../models/user';
// import event from '../../../event';
/**
* Unlike a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
let postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get likee
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
// Check arleady liked
const exist = await Like.findOne({
post_id: post._id,
user_id: user._id,
deleted_at: { $exists: false }
});
if (exist === null) {
return rej('already not liked');
}
// Delete like
await Like.updateOne({
_id: exist._id
}, {
$set: {
deleted_at: new Date()
}
});
// Send response
res();
// Decrement likes count
Post.updateOne({ _id: post._id }, {
$inc: {
likes_count: -1
}
});
// Decrement user likes count
User.updateOne({ _id: user._id }, {
$inc: {
likes_count: -1
}
});
// Decrement user liked count
User.updateOne({ _id: post.user_id }, {
$inc: {
liked_count: -1
}
});
});

View File

@ -0,0 +1,85 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import getFriends from '../../common/get-friends';
import serialize from '../../serializers/post';
/**
* Get mentions of myself
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'following' parameter
const following = params.following === 'true';
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Construct query
const query = {
mentions: user._id
};
const sort = {
_id: -1
};
if (following) {
const followingIds = await getFriends(user._id);
query.user_id = {
$in: followingIds
};
}
if (since) {
sort._id = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const mentions = await Post
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(mentions.map(async mention =>
await serialize(mention, user)
)));
});

View File

@ -0,0 +1,73 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import serialize from '../../serializers/post';
/**
* Show a replies of a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// Get 'sort' parameter
let sort = params.sort || 'desc';
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found', 'POST_NOT_FOUND');
}
// Issue query
const replies = await Post
.find({ reply_to_id: post._id }, {}, {
limit: limit,
skip: offset,
sort: {
_id: sort == 'asc' ? 1 : -1
}
})
.toArray();
// Serialize
res(await Promise.all(replies.map(async post =>
await serialize(post, user))));
});

View File

@ -0,0 +1,85 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import serialize from '../../serializers/post';
/**
* Show a reposts of a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Lookup post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found', 'POST_NOT_FOUND');
}
// Construct query
const sort = {
created_at: -1
};
const query = {
repost_id: post._id
};
if (since !== null) {
sort.created_at = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const reposts = await Post
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(reposts.map(async post =>
await serialize(post, user))));
});

View File

@ -0,0 +1,138 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import serialize from '../../serializers/post';
const escapeRegexp = require('escape-regexp');
/**
* Search a post
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'query' parameter
let query = params.query;
if (query === undefined || query === null || query.trim() === '') {
return rej('query is required');
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// Get 'max' parameter
let max = params.max;
if (max !== undefined && max !== null) {
max = parseInt(max, 10);
// From 1 to 30
if (!(1 <= max && max <= 30)) {
return rej('invalid max range');
}
} else {
max = 10;
}
// If Elasticsearch is available, search by it
// If not, search by MongoDB
(config.elasticsearch.enable ? byElasticsearch : byNative)
(res, rej, me, query, offset, max);
});
// Search by MongoDB
async function byNative(res, rej, me, query, offset, max) {
const escapedQuery = escapeRegexp(query);
// Search posts
const posts = await Post
.find({
text: new RegExp(escapedQuery)
}, {
sort: {
_id: -1
},
limit: max,
skip: offset
})
.toArray();
// Serialize
res(await Promise.all(posts.map(async post =>
await serialize(post, me))));
}
// Search by Elasticsearch
async function byElasticsearch(res, rej, me, query, offset, max) {
const es = require('../../db/elasticsearch');
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: {}
}
}
}
}, async (error, response) => {
if (error) {
console.error(error);
return res(500);
}
if (response.hits.total === 0) {
return res([]);
}
const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
// Fetxh found posts
const posts = await Post
.find({
_id: {
$in: hits
}
}, {}, {
sort: {
_id: -1
}
})
.toArray();
posts.map(post => {
post._highlight = response.hits.hits.filter(hit => post._id.equals(hit._id))[0].highlight.text[0];
});
// Serialize
res(await Promise.all(posts.map(async post =>
await serialize(post, me))));
});
}

View File

@ -0,0 +1,40 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import serialize from '../../serializers/post';
/**
* Show a post
*
* @param {Object} params
* @param {Object} user
* @return {Promise<object>}
*/
module.exports = (params, user) =>
new Promise(async (res, rej) =>
{
// Get 'post_id' parameter
const postId = params.post_id;
if (postId === undefined || postId === null) {
return rej('post_id is required');
}
// Get post
const post = await Post.findOne({
_id: new mongo.ObjectID(postId)
});
if (post === null) {
return rej('post not found');
}
// Serialize
res(await serialize(post, user, {
serializeReplyTo: true,
includeIsLiked: true
}));
});

View File

@ -0,0 +1,78 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import getFriends from '../../common/get-friends';
import serialize from '../../serializers/post';
/**
* Get timeline of myself
*
* @param {Object} params
* @param {Object} user
* @param {Object} app
* @return {Promise<object>}
*/
module.exports = (params, user, app) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// ID list of the user itself and other users who the user follows
const followingIds = await getFriends(user._id);
// Construct query
const sort = {
_id: -1
};
const query = {
user_id: {
$in: followingIds
}
};
if (since !== null) {
sort._id = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const timeline = await Post
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(timeline.map(async post =>
await serialize(post, user)
)));
});

View File

@ -0,0 +1,41 @@
'use strict';
/**
* Module dependencies
*/
import User from '../../models/user';
import { validateUsername } from '../../models/user';
/**
* Check available username
*
* @param {Object} params
* @return {Promise<object>}
*/
module.exports = async (params) =>
new Promise(async (res, rej) =>
{
// Get 'username' parameter
const username = params.username;
if (username == null || username == '') {
return rej('username-is-required');
}
// Validate username
if (!validateUsername(username)) {
return rej('invalid-username');
}
// Get exist
const exist = await User
.count({
username_lower: username.toLowerCase()
}, {
limit: 1
});
// Reply
res({
available: exist === 0
});
});

View File

@ -0,0 +1,67 @@
'use strict';
/**
* Module dependencies
*/
import User from '../models/user';
import serialize from '../serializers/user';
/**
* Lists all users
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Construct query
const sort = {
created_at: -1
};
const query = {};
if (since !== null) {
sort.created_at = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
// Issue query
const users = await User
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(users.map(async user =>
await serialize(user, me))));
});

View File

@ -0,0 +1,102 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import Following from '../../models/following';
import serialize from '../../serializers/user';
import getFriends from '../../common/get-friends';
/**
* Get followers of a user
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Get 'iknow' parameter
const iknow = params.iknow === 'true';
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'cursor' parameter
const cursor = params.cursor || null;
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
// Construct query
const query = {
followee_id: user._id,
deleted_at: { $exists: false }
};
// ログインしていてかつ iknow フラグがあるとき
if (me && iknow) {
// Get my friends
const myFriends = await getFriends(me._id);
query.follower_id = {
$in: myFriends
};
}
// カーソルが指定されている場合
if (cursor) {
query._id = {
$lt: new mongo.ObjectID(cursor)
};
}
// Get followers
const following = await Following
.find(query, {}, {
limit: limit + 1,
sort: { _id: -1 }
})
.toArray();
// 「次のページ」があるかどうか
const inStock = following.length === limit + 1;
if (inStock) {
following.pop();
}
// Serialize
const users = await Promise.all(following.map(async f =>
await serialize(f.follower_id, me, { detail: true })));
// Response
res({
users: users,
next: inStock ? following[following.length - 1]._id : null,
});
});

View File

@ -0,0 +1,102 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import Following from '../../models/following';
import serialize from '../../serializers/user';
import getFriends from '../../common/get-friends';
/**
* Get following users of a user
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Get 'iknow' parameter
const iknow = params.iknow === 'true';
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'cursor' parameter
const cursor = params.cursor || null;
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
// Construct query
const query = {
follower_id: user._id,
deleted_at: { $exists: false }
};
// ログインしていてかつ iknow フラグがあるとき
if (me && iknow) {
// Get my friends
const myFriends = await getFriends(me._id);
query.followee_id = {
$in: myFriends
};
}
// カーソルが指定されている場合
if (cursor) {
query._id = {
$lt: new mongo.ObjectID(cursor)
};
}
// Get followers
const following = await Following
.find(query, {}, {
limit: limit + 1,
sort: { _id: -1 }
})
.toArray();
// 「次のページ」があるかどうか
const inStock = following.length === limit + 1;
if (inStock) {
following.pop();
}
// Serialize
const users = await Promise.all(following.map(async f =>
await serialize(f.followee_id, me, { detail: true })));
// Response
res({
users: users,
next: inStock ? following[following.length - 1]._id : null,
});
});

View File

@ -0,0 +1,114 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Post from '../../models/post';
import User from '../../models/user';
import serialize from '../../serializers/post';
/**
* Get posts of a user
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
const userId = params.user_id;
if (userId === undefined || userId === null) {
return rej('user_id is required');
}
// Get 'with_replies' parameter
let withReplies = params.with_replies;
if (withReplies !== undefined && withReplies !== null && withReplies === 'true') {
withReplies = true;
} else {
withReplies = false;
}
// Get 'with_media' parameter
let withMedia = params.with_media;
if (withMedia !== undefined && withMedia !== null && withMedia === 'true') {
withMedia = true;
} else {
withMedia = false;
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
const since = params.since_id || null;
const max = params.max_id || null;
// Check if both of since_id and max_id is specified
if (since !== null && max !== null) {
return rej('cannot set since_id and max_id');
}
// Lookup user
const user = await User.findOne({
_id: new mongo.ObjectID(userId)
});
if (user === null) {
return rej('user not found');
}
// Construct query
const sort = {
_id: -1
};
const query = {
user_id: user._id
};
if (since !== null) {
sort._id = 1;
query._id = {
$gt: new mongo.ObjectID(since)
};
} else if (max !== null) {
query._id = {
$lt: new mongo.ObjectID(max)
};
}
if (!withReplies) {
query.reply_to_id = null;
}
if (withMedia) {
query.media_ids = {
$exists: true,
$ne: null
};
}
// Issue query
const posts = await Post
.find(query, {}, {
limit: limit,
sort: sort
})
.toArray();
// Serialize
res(await Promise.all(posts.map(async (post) =>
await serialize(post, me)
)));
});

View File

@ -0,0 +1,61 @@
'use strict';
/**
* Module dependencies
*/
import User from '../../models/user';
import serialize from '../../serializers/user';
import getFriends from '../../common/get-friends';
/**
* Get recommended users
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// ID list of the user itself and other users who the user follows
const followingIds = await getFriends(me._id);
const users = await User
.find({
_id: {
$nin: followingIds
}
}, {}, {
limit: limit,
skip: offset,
sort: {
followers_count: -1
}
})
.toArray();
// Serialize
res(await Promise.all(users.map(async user =>
await serialize(user, me, { detail: true }))));
});

View File

@ -0,0 +1,116 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import serialize from '../../serializers/user';
const escapeRegexp = require('escape-regexp');
/**
* Search a user
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'query' parameter
let query = params.query;
if (query === undefined || query === null || query.trim() === '') {
return rej('query is required');
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
// Get 'max' parameter
let max = params.max;
if (max !== undefined && max !== null) {
max = parseInt(max, 10);
// From 1 to 30
if (!(1 <= max && max <= 30)) {
return rej('invalid max range');
}
} else {
max = 10;
}
// If Elasticsearch is available, search by it
// If not, search by MongoDB
(config.elasticsearch.enable ? byElasticsearch : byNative)
(res, rej, me, query, offset, max);
});
// Search by MongoDB
async function byNative(res, rej, me, query, offset, max) {
const escapedQuery = escapeRegexp(query);
// Search users
const users = await User
.find({
$or: [{
username_lower: new RegExp(escapedQuery.toLowerCase())
}, {
name: new RegExp(escapedQuery)
}]
})
.toArray();
// Serialize
res(await Promise.all(users.map(async user =>
await serialize(user, me, { detail: true }))));
}
// Search by Elasticsearch
async function byElasticsearch(res, rej, me, query, offset, max) {
const es = require('../../db/elasticsearch');
es.search({
index: 'misskey',
type: 'user',
body: {
size: max,
from: offset,
query: {
simple_query_string: {
fields: ['username', 'name', 'bio'],
query: query,
default_operator: 'and'
}
}
}
}, async (error, response) => {
if (error) {
console.error(error);
return res(500);
}
if (response.hits.total === 0) {
return res([]);
}
const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id));
const users = await User
.find({
_id: {
$in: hits
}
})
.toArray();
// Serialize
res(await Promise.all(users.map(async user =>
await serialize(user, me, { detail: true }))));
});
}

View File

@ -0,0 +1,65 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import serialize from '../../serializers/user';
/**
* Search a user by username
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'query' parameter
let query = params.query;
if (query === undefined || query === null || query.trim() === '') {
return rej('query is required');
}
query = query.trim();
if (!/^[a-zA-Z0-9-]+$/.test(query)) {
return rej('invalid query');
}
// Get 'limit' parameter
let limit = params.limit;
if (limit !== undefined && limit !== null) {
limit = parseInt(limit, 10);
// From 1 to 100
if (!(1 <= limit && limit <= 100)) {
return rej('invalid limit range');
}
} else {
limit = 10;
}
// Get 'offset' parameter
let offset = params.offset;
if (offset !== undefined && offset !== null) {
offset = parseInt(offset, 10);
} else {
offset = 0;
}
const users = await User
.find({
username_lower: new RegExp(query.toLowerCase())
}, {
limit: limit,
skip: offset
})
.toArray();
// Serialize
res(await Promise.all(users.map(async user =>
await serialize(user, me, { detail: true }))));
});

View File

@ -0,0 +1,49 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import User from '../../models/user';
import serialize from '../../serializers/user';
/**
* Show a user
*
* @param {Object} params
* @param {Object} me
* @return {Promise<object>}
*/
module.exports = (params, me) =>
new Promise(async (res, rej) =>
{
// Get 'user_id' parameter
let userId = params.user_id;
if (userId === undefined || userId === null || userId === '') {
userId = null;
}
// Get 'username' parameter
let username = params.username;
if (username === undefined || username === null || username === '') {
username = null;
}
if (userId === null && username === null) {
return rej('user_id or username is required');
}
// Lookup user
const user = userId !== null
? await User.findOne({ _id: new mongo.ObjectID(userId) })
: await User.findOne({ username_lower: username.toLowerCase() });
if (user === null) {
return rej('user not found');
}
// Send response
res(await serialize(user, me, {
detail: true
}));
});

36
src/api/event.ts Normal file
View File

@ -0,0 +1,36 @@
import * as mongo from 'mongodb';
import * as redis from 'redis';
type ID = string | mongo.ObjectID;
class MisskeyEvent {
private redisClient: redis.RedisClient;
constructor() {
// Connect to Redis
this.redisClient = redis.createClient(
config.redis.port, config.redis.host);
}
private publish(channel: string, type: string, value?: Object): void {
const message = value == null ?
{ type: type } :
{ type: type, body: value };
this.redisClient.publish(`misskey:${channel}`, JSON.stringify(message));
}
public publishUserStream(userId: ID, type: string, value?: Object): void {
this.publish(`user-stream:${userId}`, type, typeof value === 'undefined' ? null : value);
}
public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: Object): void {
this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value);
}
}
const ev = new MisskeyEvent();
export default ev.publishUserStream.bind(ev);
export const publishMessagingStream = ev.publishMessagingStream.bind(ev);

69
src/api/limitter.ts Normal file
View File

@ -0,0 +1,69 @@
import * as Limiter from 'ratelimiter';
import limiterDB from '../db/redis';
import { IEndpoint } from './endpoints';
import { IAuthContext } from './authenticate';
export default (endpoint: IEndpoint, ctx: IAuthContext) => new Promise((ok, reject) => {
const limitKey = endpoint.hasOwnProperty('limitKey')
? endpoint.limitKey
: endpoint.name;
const hasMinInterval =
endpoint.hasOwnProperty('minInterval');
const hasRateLimit =
endpoint.hasOwnProperty('limitDuration') &&
endpoint.hasOwnProperty('limitMax');
if (hasMinInterval) {
min();
} else if (hasRateLimit) {
max();
} else {
ok();
}
// Short-term limit
function min(): void {
const minIntervalLimiter = new Limiter({
id: `${ctx.user._id}:${limitKey}:min`,
duration: endpoint.minInterval,
max: 1,
db: limiterDB
});
minIntervalLimiter.get((limitErr, limit) => {
if (limitErr) {
reject('ERR');
} else if (limit.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL');
} else {
if (hasRateLimit) {
max();
} else {
ok();
}
}
});
}
// Long term limit
function max(): void {
const limiter = new Limiter({
id: `${ctx.user._id}:${limitKey}`,
duration: endpoint.limitDuration,
max: endpoint.limitMax,
db: limiterDB
});
limiter.get((limitErr, limit) => {
if (limitErr) {
reject('ERR');
} else if (limit.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED');
} else {
ok();
}
});
}
});

7
src/api/models/app.ts Normal file
View File

@ -0,0 +1,7 @@
const collection = global.db.collection('apps');
collection.createIndex('name_id');
collection.createIndex('name_id_lower');
collection.createIndex('secret');
export default collection;

View File

@ -0,0 +1 @@
export default global.db.collection('appdata');

View File

@ -0,0 +1 @@
export default global.db.collection('auth_sessions');

View File

@ -0,0 +1,11 @@
export default global.db.collection('drive_files');
export function validateFileName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200) &&
(name.indexOf('\\') === -1) &&
(name.indexOf('/') === -1) &&
(name.indexOf('..') === -1)
);
}

View File

@ -0,0 +1,8 @@
export default global.db.collection('drive_folders');
export function isValidFolderName(name: string): boolean {
return (
(name.trim().length > 0) &&
(name.length <= 200)
);
}

View File

@ -0,0 +1 @@
export default global.db.collection('drive_tags');

View File

@ -0,0 +1 @@
export default global.db.collection('favorites');

View File

@ -0,0 +1 @@
export default global.db.collection('following');

1
src/api/models/like.ts Normal file
View File

@ -0,0 +1 @@
export default global.db.collection('likes');

View File

@ -0,0 +1 @@
export default global.db.collection('messaging_histories');

View File

@ -0,0 +1 @@
export default global.db.collection('messaging_messages');

View File

@ -0,0 +1 @@
export default global.db.collection('notifications');

1
src/api/models/post.ts Normal file
View File

@ -0,0 +1 @@
export default global.db.collection('posts');

1
src/api/models/signin.ts Normal file
View File

@ -0,0 +1 @@
export default global.db.collection('signin');

10
src/api/models/user.ts Normal file
View File

@ -0,0 +1,10 @@
const collection = global.db.collection('users');
collection.createIndex('username');
collection.createIndex('token');
export default collection;
export function validateUsername(username: string): boolean {
return /^[a-zA-Z0-9\-]{3,20}$/.test(username);
}

View File

@ -0,0 +1,5 @@
const collection = global.db.collection('userkeys');
collection.createIndex('key');
export default collection;

57
src/api/private/signin.ts Normal file
View File

@ -0,0 +1,57 @@
import * as express from 'express';
import * as bcrypt from 'bcrypt';
import User from '../models/user';
import Signin from '../models/signin';
import serialize from '../serializers/signin';
import event from '../event';
export default async (req: express.Request, res: express.Response) => {
res.header('Access-Control-Allow-Credentials', 'true');
const username = req.body['username'];
const password = req.body['password'];
// Fetch user
const user = await User.findOne({
username_lower: username.toLowerCase()
});
if (user === null) {
res.status(404).send('user not found');
return;
}
// Compare password
const same = await bcrypt.compare(password, user.password);
if (same) {
const expires = 1000 * 60 * 60 * 24 * 365; // One Year
res.cookie('i', user.token, {
path: '/',
domain: `.${config.host}`,
secure: config.url.substr(0, 5) === 'https',
httpOnly: false,
expires: new Date(Date.now() + expires),
maxAge: expires
});
res.sendStatus(204);
} else {
res.status(400).send('incorrect password');
}
// Append signin history
const inserted = await Signin.insert({
created_at: new Date(),
user_id: user._id,
ip: req.ip,
headers: req.headers,
success: same
});
const record = inserted.ops[0];
// Publish signin event
event(user._id, 'signin', await serialize(record));
};

94
src/api/private/signup.ts Normal file
View File

@ -0,0 +1,94 @@
import * as express from 'express';
import * as bcrypt from 'bcrypt';
import rndstr from 'rndstr';
const recaptcha = require('recaptcha-promise');
import User from '../models/user';
import { validateUsername } from '../models/user';
import serialize from '../serializers/user';
recaptcha.init({
secret_key: config.recaptcha.secretKey
});
export default async (req: express.Request, res: express.Response) => {
// Verify recaptcha
const success = await recaptcha(req.body['g-recaptcha-response']);
if (!success) {
res.status(400).send('recaptcha-failed');
return;
}
const username = req.body['username'];
const password = req.body['password'];
const name = '名無し';
// Validate username
if (!validateUsername(username)) {
res.sendStatus(400);
return;
}
// Fetch exist user that same username
const usernameExist = await User
.count({
username_lower: username.toLowerCase()
}, {
limit: 1
});
// Check username already used
if (usernameExist !== 0) {
res.sendStatus(400);
return;
}
// Generate hash of password
const salt = bcrypt.genSaltSync(14);
const hash = bcrypt.hashSync(password, salt);
// Generate secret
const secret = rndstr('a-zA-Z0-9', 32);
// Create account
const inserted = await User.insert({
token: secret,
avatar_id: null,
banner_id: null,
birthday: null,
created_at: new Date(),
bio: null,
email: null,
followers_count: 0,
following_count: 0,
links: null,
location: null,
name: name,
password: hash,
posts_count: 0,
likes_count: 0,
liked_count: 0,
drive_capacity: 1073741824, // 1GB
username: username,
username_lower: username.toLowerCase()
});
const account = inserted.ops[0];
// Response
res.send(await serialize(account));
// Create search index
if (config.elasticsearch.enable) {
const es = require('../../db/elasticsearch');
es.index({
index: 'misskey',
type: 'user',
id: account._id.toString(),
body: {
username: username
}
});
}
};

13
src/api/reply.ts Normal file
View File

@ -0,0 +1,13 @@
import * as express from 'express';
export default (res: express.Response, x?: any, y?: any) => {
if (x === undefined) {
res.sendStatus(204);
} else if (typeof x === 'number') {
res.status(x).send({
error: x === 500 ? 'INTERNAL_ERROR' : y
});
} else {
res.send(x);
}
};

View File

@ -0,0 +1,85 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
const deepcopy = require('deepcopy');
import App from '../models/app';
import User from '../models/user';
import Userkey from '../models/userkey';
/**
* Serialize an app
*
* @param {Object} app
* @param {Object} me?
* @param {Object} options?
* @return {Promise<Object>}
*/
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 User.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 Userkey.count({
app_id: _app.id,
user_id: me,
}, {
limit: 1
});
_app.is_authorized = exist === 1;
}
resolve(_app);
});

View File

@ -0,0 +1,42 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
const deepcopy = require('deepcopy');
import serializeApp from './app';
/**
* Serialize an auth session
*
* @param {Object} session
* @param {Object} me?
* @return {Promise<Object>}
*/
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);
});

View File

@ -0,0 +1,63 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFile from '../models/drive-file';
import serializeDriveTag from './drive-tag';
const deepcopy = require('deepcopy');
/**
* Serialize a drive file
*
* @param {Object} file
* @param {Object} options?
* @return {Promise<Object>}
*/
export default (
file: any,
options?: {
includeTags: boolean
}
) => new Promise<Object>(async (resolve, reject) => {
const opts = options || {
includeTags: true
};
let _file: any;
// Populate the file if 'file' is ID
if (mongo.ObjectID.prototype.isPrototypeOf(file)) {
_file = await DriveFile.findOne({
_id: file
}, {
data: false
});
} else if (typeof file === 'string') {
_file = await DriveFile.findOne({
_id: new mongo.ObjectID(file)
}, {
data: false
});
} else {
_file = deepcopy(file);
}
// Rename _id to id
_file.id = _file._id;
delete _file._id;
delete _file.data;
_file.url = `${config.drive_url}/${_file.id}/${encodeURIComponent(_file.name)}`;
if (opts.includeTags && _file.tags) {
// Populate tags
_file.tags = await _file.tags.map(async (tag: any) =>
await serializeDriveTag(tag)
);
}
resolve(_file);
});

View File

@ -0,0 +1,52 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveFolder from '../models/drive-folder';
const deepcopy = require('deepcopy');
/**
* Serialize a drive folder
*
* @param {Object} folder
* @param {Object} options?
* @return {Promise<Object>}
*/
const self = (
folder: any,
options?: {
includeParent: boolean
}
) => new Promise<Object>(async (resolve, reject) => {
const opts = options || {
includeParent: false
};
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.includeParent && _folder.parent_id) {
// Populate parent folder
_folder.parent = await self(_folder.parent_id, {
includeParent: true
});
}
resolve(_folder);
});
export default self;

View File

@ -0,0 +1,37 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import DriveTag from '../models/drive-tag';
const deepcopy = require('deepcopy');
/**
* Serialize a drive tag
*
* @param {Object} tag
* @return {Promise<Object>}
*/
const self = (
tag: any
) => new Promise<Object>(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;

View File

@ -0,0 +1,64 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Message from '../models/messaging-message';
import serializeUser from './user';
import serializeDriveFile from './drive-file';
const deepcopy = require('deepcopy');
/**
* Serialize a message
*
* @param {Object} message
* @param {Object} me?
* @param {Object} options?
* @return {Promise<Object>}
*/
export default (
message: any,
me: any,
options?: {
populateRecipient: boolean
}
) => new Promise<Object>(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;
// 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);
});

View File

@ -0,0 +1,66 @@
'use strict';
/**
* Module dependencies
*/
import * as mongo from 'mongodb';
import Notification from '../models/notification';
import serializeUser from './user';
import serializePost from './post';
const deepcopy = require('deepcopy');
/**
* Serialize a notification
*
* @param {Object} notification
* @return {Promise<Object>}
*/
export default (notification: any) => new Promise<Object>(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 'like':
// Populate post
_notification.post = await serializePost(_notification.post_id, me);
break;
default:
console.error(`Unknown type: ${_notification.type}`);
break;
}
resolve(_notification);
});

Some files were not shown because too many files have changed in this diff Show More