Initial commit 🍀
							
								
								
									
										26
									
								
								.ci-files/config.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
maintainer: '@syuilo'
 | 
			
		||||
url: 'https://misskey.xyz'
 | 
			
		||||
secondary_url: 'https://himasaku.net'
 | 
			
		||||
port: 80
 | 
			
		||||
https:
 | 
			
		||||
  enable: false
 | 
			
		||||
  key: null
 | 
			
		||||
  cert: null
 | 
			
		||||
  ca: null
 | 
			
		||||
mongodb:
 | 
			
		||||
  host: localhost
 | 
			
		||||
  port: 27017
 | 
			
		||||
  db: misskey
 | 
			
		||||
  user: syuilo
 | 
			
		||||
  pass: ''
 | 
			
		||||
redis:
 | 
			
		||||
  host: localhost
 | 
			
		||||
  port: 6379
 | 
			
		||||
  pass: ''
 | 
			
		||||
elasticsearch:
 | 
			
		||||
  host: localhost
 | 
			
		||||
  port: 9200
 | 
			
		||||
  pass: ''
 | 
			
		||||
recaptcha:
 | 
			
		||||
  siteKey: hima
 | 
			
		||||
  secretKey: saku
 | 
			
		||||
							
								
								
									
										3
									
								
								.gitattributes
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
*.svg -diff -text
 | 
			
		||||
*.psd -diff -text
 | 
			
		||||
*.ai -diff -text
 | 
			
		||||
							
								
								
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
/.config
 | 
			
		||||
/.vscode
 | 
			
		||||
/node_modules
 | 
			
		||||
/built
 | 
			
		||||
npm-debug.log
 | 
			
		||||
							
								
								
									
										8
									
								
								.travis.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
language: node_js
 | 
			
		||||
node_js:
 | 
			
		||||
  - "7.3.0"
 | 
			
		||||
before_script:
 | 
			
		||||
  - "mkdir -p ./.config && cp ./.ci-files/config.yml ./.config"
 | 
			
		||||
cache:
 | 
			
		||||
  directories:
 | 
			
		||||
    - node_modules
 | 
			
		||||
							
								
								
									
										21
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2014-2016 syuilo
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
in the Software without restriction, including without limitation the rights
 | 
			
		||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
furnished to do so, subject to the following conditions:
 | 
			
		||||
 | 
			
		||||
The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
copies or substantial portions of the Software.
 | 
			
		||||
 | 
			
		||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
SOFTWARE.
 | 
			
		||||
							
								
								
									
										44
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
# Misskey
 | 
			
		||||
 | 
			
		||||
[![][travis-badge]][travis-link]
 | 
			
		||||
[![][dependencies-badge]][dependencies-link]
 | 
			
		||||
[![][mit-badge]][mit]
 | 
			
		||||
 | 
			
		||||
A miniblog-based SNS.
 | 
			
		||||
 | 
			
		||||
## Dependencies
 | 
			
		||||
* Node.js
 | 
			
		||||
* MongoDB
 | 
			
		||||
* Redis
 | 
			
		||||
* GraphicsMagick
 | 
			
		||||
 | 
			
		||||
## Optional dependencies
 | 
			
		||||
* Elasticsearch
 | 
			
		||||
 | 
			
		||||
## Get started
 | 
			
		||||
Misskey requires two domains called the primary domain and the secondary domain.
 | 
			
		||||
 | 
			
		||||
* The primary domain is used to provide main service of Misskey.
 | 
			
		||||
* The secondary domain is used to avoid vulnerabilities such as XSS.
 | 
			
		||||
 | 
			
		||||
**Ensure that the secondary domain is not a subdomain of the primary domain.**
 | 
			
		||||
 | 
			
		||||
## Build
 | 
			
		||||
1. `git clone git://github.com/syuilo/misskey.git`
 | 
			
		||||
2. `cd misskey`
 | 
			
		||||
3. `npm install`
 | 
			
		||||
4. `npm run config`
 | 
			
		||||
5. `npm run build`
 | 
			
		||||
 | 
			
		||||
## Launch
 | 
			
		||||
`npm start`
 | 
			
		||||
 | 
			
		||||
## License
 | 
			
		||||
[MIT](LICENSE)
 | 
			
		||||
 | 
			
		||||
[mit]:                http://opensource.org/licenses/MIT
 | 
			
		||||
[mit-badge]:          https://img.shields.io/badge/license-MIT-444444.svg?style=flat-square
 | 
			
		||||
[travis-link]:        https://travis-ci.org/syuilo/misskey
 | 
			
		||||
[travis-badge]:       http://img.shields.io/travis/syuilo/misskey.svg?style=flat-square
 | 
			
		||||
[dependencies-link]:  https://gemnasium.com/syuilo/misskey
 | 
			
		||||
[dependencies-badge]: https://img.shields.io/gemnasium/syuilo/misskey.svg?style=flat-square
 | 
			
		||||
							
								
								
									
										6
									
								
								elasticsearch/README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
How to create indexes
 | 
			
		||||
=====================
 | 
			
		||||
 | 
			
		||||
``` shell
 | 
			
		||||
curl -XPOST localhost:9200/misskey -d @path/to/mappings.json
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										65
									
								
								elasticsearch/mappings.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
{
 | 
			
		||||
	"settings": {
 | 
			
		||||
		"analysis": {
 | 
			
		||||
			"analyzer": {
 | 
			
		||||
				"bigram": {
 | 
			
		||||
					"tokenizer": "bigram_tokenizer"
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
			"tokenizer": {
 | 
			
		||||
				"bigram_tokenizer": {
 | 
			
		||||
					"type": "nGram",
 | 
			
		||||
					"min_gram": 2,
 | 
			
		||||
					"max_gram": 2,
 | 
			
		||||
					"token_chars": [
 | 
			
		||||
						"letter",
 | 
			
		||||
						"digit"
 | 
			
		||||
					]
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"mappings": {
 | 
			
		||||
		"user": {
 | 
			
		||||
			"properties": {
 | 
			
		||||
				"username": {
 | 
			
		||||
					"type": "string",
 | 
			
		||||
					"index": "analyzed",
 | 
			
		||||
					"analyzer": "bigram"
 | 
			
		||||
				},
 | 
			
		||||
				"name": {
 | 
			
		||||
					"type": "string",
 | 
			
		||||
					"index": "analyzed",
 | 
			
		||||
					"analyzer": "bigram"
 | 
			
		||||
				},
 | 
			
		||||
				"bio": {
 | 
			
		||||
					"type": "string",
 | 
			
		||||
					"index": "analyzed",
 | 
			
		||||
					"analyzer": "kuromoji"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"post": {
 | 
			
		||||
			"properties": {
 | 
			
		||||
				"text": {
 | 
			
		||||
					"type": "string",
 | 
			
		||||
					"index": "analyzed",
 | 
			
		||||
					"analyzer": "kuromoji"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"drive_file": {
 | 
			
		||||
			"properties": {
 | 
			
		||||
				"name": {
 | 
			
		||||
					"type": "string",
 | 
			
		||||
					"index": "analyzed",
 | 
			
		||||
					"analyzer": "kuromoji"
 | 
			
		||||
				},
 | 
			
		||||
				"user": {
 | 
			
		||||
					"type": "string",
 | 
			
		||||
					"index": "not_analyzed"
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								gulpfile.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
eval(require('typescript').transpile(require('fs').readFileSync('./gulpfile.ts').toString()));
 | 
			
		||||
							
								
								
									
										568
									
								
								gulpfile.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,568 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Gulp tasks
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import * as gulp from 'gulp';
 | 
			
		||||
import * as gutil from 'gulp-util';
 | 
			
		||||
import * as babel from 'gulp-babel';
 | 
			
		||||
import * as ts from 'gulp-typescript';
 | 
			
		||||
import * as tslint from 'gulp-tslint';
 | 
			
		||||
import * as glob from 'glob';
 | 
			
		||||
import * as browserify from 'browserify';
 | 
			
		||||
import * as source from 'vinyl-source-stream';
 | 
			
		||||
import * as buffer from 'vinyl-buffer';
 | 
			
		||||
import * as es from 'event-stream';
 | 
			
		||||
const stylus = require('gulp-stylus');
 | 
			
		||||
const cssnano = require('gulp-cssnano');
 | 
			
		||||
import * as uglify from 'gulp-uglify';
 | 
			
		||||
const ls = require('browserify-livescript');
 | 
			
		||||
const aliasify = require('aliasify');
 | 
			
		||||
const riotify = require('riotify');
 | 
			
		||||
const transformify = require('syuilo-transformify');
 | 
			
		||||
const pug = require('gulp-pug');
 | 
			
		||||
const git = require('git-last-commit');
 | 
			
		||||
import * as rimraf from 'rimraf';
 | 
			
		||||
 | 
			
		||||
const env = process.env.NODE_ENV;
 | 
			
		||||
const isProduction = env === 'production';
 | 
			
		||||
const isDebug = !isProduction;
 | 
			
		||||
 | 
			
		||||
import { IConfig } from './src/config';
 | 
			
		||||
const config = eval(require('typescript').transpile(require('fs').readFileSync('./src/config.ts').toString()))
 | 
			
		||||
	('.config/config.yml') as IConfig;
 | 
			
		||||
 | 
			
		||||
const project = ts.createProject('tsconfig.json');
 | 
			
		||||
 | 
			
		||||
gulp.task('build', [
 | 
			
		||||
	'build:js',
 | 
			
		||||
	'build:ts',
 | 
			
		||||
	'build:copy',
 | 
			
		||||
	'build:client'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
gulp.task('rebuild', [
 | 
			
		||||
	'clean',
 | 
			
		||||
	'build'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
gulp.task('build:js', () =>
 | 
			
		||||
	gulp.src(['./src/**/*.js', '!./src/web/**/*.js'])
 | 
			
		||||
		.pipe(babel({
 | 
			
		||||
			presets: ['es2015', 'stage-3']
 | 
			
		||||
		}))
 | 
			
		||||
		.pipe(gulp.dest('./built/'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
gulp.task('build:ts', () =>
 | 
			
		||||
	project
 | 
			
		||||
		.src()
 | 
			
		||||
		.pipe(project())
 | 
			
		||||
		.pipe(babel({
 | 
			
		||||
			presets: ['es2015', 'stage-3']
 | 
			
		||||
		}))
 | 
			
		||||
		.pipe(gulp.dest('./built/'))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
gulp.task('build:copy', () => {
 | 
			
		||||
	gulp.src([
 | 
			
		||||
		'./src/**/resources/**/*',
 | 
			
		||||
		'!./src/web/app/**/resources/**/*'
 | 
			
		||||
	]).pipe(gulp.dest('./built/'));
 | 
			
		||||
	gulp.src([
 | 
			
		||||
		'./src/web/about/**/*'
 | 
			
		||||
	]).pipe(gulp.dest('./built/web/about/'));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
gulp.task('test', ['lint', 'build']);
 | 
			
		||||
 | 
			
		||||
gulp.task('lint', () =>
 | 
			
		||||
	gulp.src('./src/**/*.ts')
 | 
			
		||||
		.pipe(tslint({
 | 
			
		||||
			formatter: 'verbose'
 | 
			
		||||
		}))
 | 
			
		||||
		.pipe(tslint.report())
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
gulp.task('clean', cb =>
 | 
			
		||||
	rimraf('./built', cb)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
gulp.task('cleanall', ['clean'], cb =>
 | 
			
		||||
	rimraf('./node_modules', cb)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
gulp.task('default', ['build']);
 | 
			
		||||
 | 
			
		||||
const aliasifyConfig = {
 | 
			
		||||
	aliases: {
 | 
			
		||||
		'fetch': './node_modules/whatwg-fetch/fetch.js',
 | 
			
		||||
		'page': './node_modules/page/page.js',
 | 
			
		||||
		'NProgress': './node_modules/nprogress/nprogress.js',
 | 
			
		||||
		'velocity': './node_modules/velocity-animate/velocity.js',
 | 
			
		||||
		'chart.js': './node_modules/chart.js/src/chart.js',
 | 
			
		||||
		'textarea-caret-position': './node_modules/textarea-caret/index.js',
 | 
			
		||||
		'misskey-text': './src/common/text/index.js',
 | 
			
		||||
		'strength.js': './node_modules/syuilo-password-strength/strength.js',
 | 
			
		||||
		'cropper': './node_modules/cropperjs/dist/cropper.js',
 | 
			
		||||
		'Sortable': './node_modules/sortablejs/Sortable.js',
 | 
			
		||||
		'fuck-adblock': './node_modules/fuckadblock/fuckadblock.js',
 | 
			
		||||
		'reconnecting-websocket': './node_modules/reconnecting-websocket/dist/index.js'
 | 
			
		||||
	},
 | 
			
		||||
	appliesTo: {
 | 
			
		||||
		'includeExtensions': ['.js', '.ls']
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
gulp.task('build:client', [
 | 
			
		||||
	'build:ts', 'build:js',
 | 
			
		||||
	'build:client:scripts',
 | 
			
		||||
	'build:client:styles',
 | 
			
		||||
	'build:client:pug',
 | 
			
		||||
	'copy:client'
 | 
			
		||||
], () => {
 | 
			
		||||
	gutil.log('ビルドが終了しました。');
 | 
			
		||||
 | 
			
		||||
	if (isDebug) {
 | 
			
		||||
		gutil.log('■ 注意! 開発モードでのビルドです。');
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
gulp.task('build:client:scripts', done => {
 | 
			
		||||
	gutil.log('スクリプトを構築します...');
 | 
			
		||||
 | 
			
		||||
	// Get commit info
 | 
			
		||||
	git.getLastCommit((err, commit) => {
 | 
			
		||||
		glob('./src/web/app/*/script.js', (err, files) => {
 | 
			
		||||
			const tasks = files.map(entry => {
 | 
			
		||||
				let bundle =
 | 
			
		||||
					browserify({
 | 
			
		||||
						entries: [entry]
 | 
			
		||||
					})
 | 
			
		||||
					.transform(ls)
 | 
			
		||||
					.transform(aliasify, aliasifyConfig)
 | 
			
		||||
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
						console.log(file);
 | 
			
		||||
						return source;
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagの{}の''を不要にする (その代わりスタイルの記法は使えなくなるけど)
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
						const html = tag.sections.filter(s => s.name == 'html')[0];
 | 
			
		||||
 | 
			
		||||
						html.lines = html.lines.map(line => {
 | 
			
		||||
							if (line.replace(/\t/g, '')[0] === '|') {
 | 
			
		||||
								return line;
 | 
			
		||||
							} else {
 | 
			
		||||
								return line.replace(/([+=])\s?\{(.+?)\}/g, '$1"{$2}"');
 | 
			
		||||
							}
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						const styles = tag.sections.filter(s => s.name == 'style');
 | 
			
		||||
 | 
			
		||||
						if (styles.length == 0) {
 | 
			
		||||
							return tag.compile();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						styles.forEach(style => {
 | 
			
		||||
							let head = style.lines.shift();
 | 
			
		||||
							head = head.replace(/([+=])\s?\{(.+?)\}/g, '$1"{$2}"');
 | 
			
		||||
							style.lines.unshift(head);
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagの@hogeをref='hoge'にする
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
						const html = tag.sections.filter(s => s.name == 'html')[0];
 | 
			
		||||
 | 
			
		||||
						html.lines = html.lines.map(line => {
 | 
			
		||||
							if (line.indexOf('@') === -1) {
 | 
			
		||||
								return line;
 | 
			
		||||
							} else if (line.replace(/\t/g, '')[0] === '|') {
 | 
			
		||||
								return line;
 | 
			
		||||
							} else {
 | 
			
		||||
								while (line.match(/[^\s']@[a-z-]+/) !== null) {
 | 
			
		||||
									const match = line.match(/@[a-z-]+/);
 | 
			
		||||
									let name = match[0];
 | 
			
		||||
									if (line[line.indexOf(name) + name.length] === '(') {
 | 
			
		||||
										line = line.replace(name + '(', '(ref=\'' + camelCase(name.substr(1)) + '\',');
 | 
			
		||||
									} else {
 | 
			
		||||
										line = line.replace(name, '(ref=\'' + camelCase(name.substr(1)) + '\')');
 | 
			
		||||
									}
 | 
			
		||||
								}
 | 
			
		||||
								return line;
 | 
			
		||||
							}
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
 | 
			
		||||
						function camelCase(str): string {
 | 
			
		||||
							return str.replace(/-([^\s])/g, (match, group1) => {
 | 
			
		||||
								return group1.toUpperCase();
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagのchain-caseをcamelCaseにする
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
						const html = tag.sections.filter(s => s.name == 'html')[0];
 | 
			
		||||
 | 
			
		||||
						html.lines = html.lines.map(line => {
 | 
			
		||||
							(line.match(/\{.+?\}/g) || []).forEach(x => {
 | 
			
		||||
								line = line.replace(x, camelCase(x));
 | 
			
		||||
							});
 | 
			
		||||
							return line;
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
 | 
			
		||||
						function camelCase(str): string {
 | 
			
		||||
							str = str.replace(/([a-z\-]+):/g, (match, group1) => {
 | 
			
		||||
								return group1.replace(/\-/g, '###') + ':';
 | 
			
		||||
							});
 | 
			
		||||
							str = str.replace(/'(.+?)'/g, (match, group1) => {
 | 
			
		||||
								return "'" + group1.replace(/\-/g, '###') + "'";
 | 
			
		||||
							});
 | 
			
		||||
							str = str.replace(/-([^\s0-9])/g, (match, group1) => {
 | 
			
		||||
								return group1.toUpperCase();
 | 
			
		||||
							});
 | 
			
		||||
							str = str.replace(/###/g, '-');
 | 
			
		||||
 | 
			
		||||
							return str;
 | 
			
		||||
						}
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagのstyleの属性
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
 | 
			
		||||
						const styles = tag.sections.filter(s => s.name == 'style');
 | 
			
		||||
 | 
			
		||||
						if (styles.length == 0) {
 | 
			
		||||
							return tag.compile();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						styles.forEach(style => {
 | 
			
		||||
							let head = style.lines.shift();
 | 
			
		||||
							if (style.attr) {
 | 
			
		||||
								style.attr = style.attr + ', type=\'stylus\', scoped';
 | 
			
		||||
							} else {
 | 
			
		||||
								style.attr = 'type=\'stylus\', scoped';
 | 
			
		||||
							}
 | 
			
		||||
							style.lines.unshift(head);
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagのstyleの定数
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
 | 
			
		||||
						const styles = tag.sections.filter(s => s.name == 'style');
 | 
			
		||||
 | 
			
		||||
						if (styles.length == 0) {
 | 
			
		||||
							return tag.compile();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						styles.forEach(style => {
 | 
			
		||||
							const head = style.lines.shift();
 | 
			
		||||
							style.lines.unshift('$theme-color = ' + config.themeColor);
 | 
			
		||||
							style.lines.unshift('$theme-color-foreground = #fff');
 | 
			
		||||
							style.lines.unshift(head);
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagのstyleを暗黙的に:scopeにする
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
 | 
			
		||||
						const styles = tag.sections.filter(s => s.name == 'style');
 | 
			
		||||
 | 
			
		||||
						if (styles.length == 0) {
 | 
			
		||||
							return tag.compile();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						styles.forEach((style, i) => {
 | 
			
		||||
							if (i != 0) {
 | 
			
		||||
								return;
 | 
			
		||||
							}
 | 
			
		||||
							const head = style.lines.shift();
 | 
			
		||||
							style.lines = style.lines.map(line => {
 | 
			
		||||
								return '\t' + line;
 | 
			
		||||
							});
 | 
			
		||||
							style.lines.unshift(':scope');
 | 
			
		||||
							style.lines.unshift(head);
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagのtheme styleのパース
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
 | 
			
		||||
						const styles = tag.sections.filter(s => s.name == 'style');
 | 
			
		||||
 | 
			
		||||
						if (styles.length == 0) {
 | 
			
		||||
							return tag.compile();
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						styles.forEach((style, i) => {
 | 
			
		||||
							if (i == 0) {
 | 
			
		||||
								return;
 | 
			
		||||
							} else if (style.attr.substr(0, 6) != 'theme=') {
 | 
			
		||||
								return;
 | 
			
		||||
							}
 | 
			
		||||
							const head = style.lines.shift();
 | 
			
		||||
							style.lines = style.lines.map(line => {
 | 
			
		||||
								return '\t' + line;
 | 
			
		||||
							});
 | 
			
		||||
							style.lines.unshift(':scope');
 | 
			
		||||
							style.lines = style.lines.map(line => {
 | 
			
		||||
								return '\t' + line;
 | 
			
		||||
							});
 | 
			
		||||
							style.lines.unshift('html[data-' + style.attr.match(/theme='(.+?)'/)[0] + ']');
 | 
			
		||||
							style.lines.unshift(head);
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// tagのstyleおよびscriptのインデントを不要にする
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
						const tag = new Tag(source);
 | 
			
		||||
 | 
			
		||||
						tag.sections = tag.sections.map(section => {
 | 
			
		||||
							if (section.name != 'html') {
 | 
			
		||||
								section.indent++;
 | 
			
		||||
							}
 | 
			
		||||
							return section;
 | 
			
		||||
						});
 | 
			
		||||
 | 
			
		||||
						return tag.compile();
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					// スペースでインデントされてないとエラーが出る
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
						return source.replace(/\t/g, '  ');
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						return source
 | 
			
		||||
							.replace(/VERSION/g, `'${commit ? commit.hash : 'null'}'`)
 | 
			
		||||
							.replace(/CONFIG\.theme-color/g, `'${config.themeColor}'`)
 | 
			
		||||
							.replace(/CONFIG\.themeColor/g, `'${config.themeColor}'`)
 | 
			
		||||
							.replace(/CONFIG\.api\.url/g, `'${config.scheme}://api.${config.host}'`)
 | 
			
		||||
							.replace(/CONFIG\.urls\.about/g, `'${config.scheme}://about.${config.host}'`)
 | 
			
		||||
							.replace(/CONFIG\.urls\.dev/g, `'${config.scheme}://dev.${config.host}'`)
 | 
			
		||||
							.replace(/CONFIG\.url/g, `'${config.url}'`)
 | 
			
		||||
							.replace(/CONFIG\.host/g, `'${config.host}'`)
 | 
			
		||||
							.replace(/CONFIG\.recaptcha\.siteKey/g, `'${config.recaptcha.siteKey}'`)
 | 
			
		||||
							;
 | 
			
		||||
					}))
 | 
			
		||||
 | 
			
		||||
					.transform(riotify, {
 | 
			
		||||
						template: 'pug',
 | 
			
		||||
						type: 'livescript',
 | 
			
		||||
						expr: false,
 | 
			
		||||
						compact: true,
 | 
			
		||||
						parserOptions: {
 | 
			
		||||
							style: {
 | 
			
		||||
								compress: true,
 | 
			
		||||
								rawDefine: config
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					})
 | 
			
		||||
					// Riotが謎の空白を挿入する
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
						return source.replace(/\s<mk\-ellipsis>/g, '<mk-ellipsis>');
 | 
			
		||||
					}))
 | 
			
		||||
					/*
 | 
			
		||||
					// LiveScruptがHTMLクラスのショートカットを変な風に生成するのでそれを修正
 | 
			
		||||
					.transform(transformify((source, file) => {
 | 
			
		||||
						if (file.substr(-4) !== '.tag') return source;
 | 
			
		||||
						return source.replace(/class="\{\(\{(.+?)\}\)\}"/g, 'class="{$1}"');
 | 
			
		||||
					}))*/
 | 
			
		||||
					.bundle()
 | 
			
		||||
					.pipe(source(entry.replace('./src/web/app/', './').replace('.ls', '.js')));
 | 
			
		||||
 | 
			
		||||
				if (isProduction) {
 | 
			
		||||
					bundle = bundle
 | 
			
		||||
						.pipe(buffer())
 | 
			
		||||
						// ↓ https://github.com/mishoo/UglifyJS2/issues/448
 | 
			
		||||
						.pipe(babel({
 | 
			
		||||
							presets: ['es2015']
 | 
			
		||||
						}))
 | 
			
		||||
						.pipe(uglify({
 | 
			
		||||
							compress: true
 | 
			
		||||
						}));
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return bundle
 | 
			
		||||
					.pipe(gulp.dest('./built/web/resources/'));
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			es.merge(tasks).on('end', done);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
gulp.task('build:client:styles', () => {
 | 
			
		||||
	gutil.log('フロントサイドスタイルを構築します...');
 | 
			
		||||
 | 
			
		||||
	return gulp.src('./src/web/app/**/*.styl')
 | 
			
		||||
		.pipe(stylus({
 | 
			
		||||
			'include css': true,
 | 
			
		||||
			compress: true,
 | 
			
		||||
			rawDefine: config
 | 
			
		||||
		}))
 | 
			
		||||
		.pipe(isProduction
 | 
			
		||||
			? cssnano({
 | 
			
		||||
				safe: true // 高度な圧縮は無効にする (一部デザインが不適切になる場合があるため)
 | 
			
		||||
			})
 | 
			
		||||
			: gutil.noop())
 | 
			
		||||
		.pipe(gulp.dest('./built/web/resources/'));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
gulp.task('copy:client', [
 | 
			
		||||
	'build:client:scripts',
 | 
			
		||||
	'build:client:styles'
 | 
			
		||||
], () => {
 | 
			
		||||
	gutil.log('必要なリソースをコピーします...');
 | 
			
		||||
 | 
			
		||||
	return es.merge(
 | 
			
		||||
		gulp.src('./resources/**/*').pipe(gulp.dest('./built/web/resources/')),
 | 
			
		||||
		gulp.src('./src/web/app/desktop/resources/**/*').pipe(gulp.dest('./built/web/resources/desktop/')),
 | 
			
		||||
		gulp.src('./src/web/app/mobile/resources/**/*').pipe(gulp.dest('./built/web/resources/mobile/')),
 | 
			
		||||
		gulp.src('./src/web/app/dev/resources/**/*').pipe(gulp.dest('./built/web/resources/dev/')),
 | 
			
		||||
		gulp.src('./src/web/app/auth/resources/**/*').pipe(gulp.dest('./built/web/resources/auth/'))
 | 
			
		||||
	);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
gulp.task('build:client:pug', [
 | 
			
		||||
	'copy:client',
 | 
			
		||||
	'build:client:scripts',
 | 
			
		||||
	'build:client:styles'
 | 
			
		||||
], () => {
 | 
			
		||||
	gutil.log('Pugをコンパイルします...');
 | 
			
		||||
 | 
			
		||||
	return gulp.src([
 | 
			
		||||
		'./src/web/app/*/view.pug'
 | 
			
		||||
	])
 | 
			
		||||
		.pipe(pug({
 | 
			
		||||
			locals: {
 | 
			
		||||
				themeColor: config.themeColor
 | 
			
		||||
			}
 | 
			
		||||
		}))
 | 
			
		||||
		.pipe(gulp.dest('./built/web/app/'));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Tag {
 | 
			
		||||
	sections: {
 | 
			
		||||
		name: string;
 | 
			
		||||
		attr?: string;
 | 
			
		||||
		indent: number;
 | 
			
		||||
		lines: string[];
 | 
			
		||||
	}[];
 | 
			
		||||
 | 
			
		||||
	constructor(source) {
 | 
			
		||||
		this.sections = [];
 | 
			
		||||
 | 
			
		||||
		source = source
 | 
			
		||||
			.replace(/\r\n/g, '\n')
 | 
			
		||||
			.replace(/\n(\t+?)\n/g, '\n')
 | 
			
		||||
			.replace(/\n+/g, '\n');
 | 
			
		||||
 | 
			
		||||
		const html = {
 | 
			
		||||
			name: 'html',
 | 
			
		||||
			indent: 0,
 | 
			
		||||
			lines: []
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		let flag = false;
 | 
			
		||||
		source.split('\n').forEach((line, i) => {
 | 
			
		||||
			const indent = line.lastIndexOf('\t') + 1;
 | 
			
		||||
			if (i != 0 && indent == 0) {
 | 
			
		||||
				flag = true;
 | 
			
		||||
			}
 | 
			
		||||
			if (!flag) {
 | 
			
		||||
				source = source.replace(/^.*?\n/, '');
 | 
			
		||||
				html.lines.push(i == 0 ? line : line.substr(1));
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		this.sections.push(html);
 | 
			
		||||
 | 
			
		||||
		while (source != '') {
 | 
			
		||||
			const line = source.substr(0, source.indexOf('\n'));
 | 
			
		||||
			const root = line.match(/^\t*([a-z]+)(\.|\()?/)[1];
 | 
			
		||||
			const beginIndent = line.lastIndexOf('\t') + 1;
 | 
			
		||||
			flag = false;
 | 
			
		||||
			const section = {
 | 
			
		||||
				name: root,
 | 
			
		||||
				attr: (line.match(/\((.+?)\)/) || [null, null])[1],
 | 
			
		||||
				indent: beginIndent,
 | 
			
		||||
				lines: []
 | 
			
		||||
			};
 | 
			
		||||
			source.split('\n').forEach((line, i) => {
 | 
			
		||||
				const currentIndent = line.lastIndexOf('\t') + 1;
 | 
			
		||||
				if (i != 0 && (currentIndent == beginIndent || currentIndent == 0)) {
 | 
			
		||||
					flag = true;
 | 
			
		||||
				}
 | 
			
		||||
				if (!flag) {
 | 
			
		||||
					if (i == 0 && line[line.length - 1] == '.') {
 | 
			
		||||
						line = line.substr(0, line.length - 1);
 | 
			
		||||
					}
 | 
			
		||||
					if (i == 0 && line.indexOf('(') != -1) {
 | 
			
		||||
						line = line.substr(0, line.indexOf('('));
 | 
			
		||||
					}
 | 
			
		||||
					source = source.replace(/^.*?\n/, '');
 | 
			
		||||
					section.lines.push(i == 0 ? line.substr(beginIndent) : line.substr(beginIndent + 1));
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
			this.sections.push(section);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	compile(): string {
 | 
			
		||||
		let dist = '';
 | 
			
		||||
		this.sections.forEach((section, j) => {
 | 
			
		||||
			dist += section.lines.map((line, i) => {
 | 
			
		||||
				if (i == 0) {
 | 
			
		||||
					const attr = section.attr != null ? '(' + section.attr + ')' : '';
 | 
			
		||||
					const tail = j != 0 ? '.' : '';
 | 
			
		||||
					return '\t'.repeat(section.indent) + line + attr + tail;
 | 
			
		||||
				} else {
 | 
			
		||||
					return '\t'.repeat(section.indent + 1) + line;
 | 
			
		||||
				}
 | 
			
		||||
			}).join('\n') + '\n';
 | 
			
		||||
		});
 | 
			
		||||
		return dist;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										182
									
								
								init.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,182 @@
 | 
			
		|||
const fs = require('fs');
 | 
			
		||||
const yaml = require('js-yaml');
 | 
			
		||||
const inquirer = require('inquirer');
 | 
			
		||||
 | 
			
		||||
const configDirPath = `${__dirname}/.config`;
 | 
			
		||||
const configPath = `${configDirPath}/config.yml`;
 | 
			
		||||
 | 
			
		||||
const form = [
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'maintainer',
 | 
			
		||||
		message: 'Maintainer name(and email address):'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'url',
 | 
			
		||||
		message: 'PRIMARY URL:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'secondary_url',
 | 
			
		||||
		message: 'SECONDARY URL:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'port',
 | 
			
		||||
		message: 'Listen port:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'confirm',
 | 
			
		||||
		name: 'https',
 | 
			
		||||
		message: 'Use TLS?',
 | 
			
		||||
		default: false
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'https_key',
 | 
			
		||||
		message: 'Path of tls key:',
 | 
			
		||||
		when: ctx => ctx.https
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'https_cert',
 | 
			
		||||
		message: 'Path of tls cert:',
 | 
			
		||||
		when: ctx => ctx.https
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'https_ca',
 | 
			
		||||
		message: 'Path of tls ca:',
 | 
			
		||||
		when: ctx => ctx.https
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'mongo_host',
 | 
			
		||||
		message: 'MongoDB\'s host:',
 | 
			
		||||
		default: 'localhost'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'mongo_port',
 | 
			
		||||
		message: 'MongoDB\'s port:',
 | 
			
		||||
		default: '27017'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'mongo_db',
 | 
			
		||||
		message: 'MongoDB\'s db:',
 | 
			
		||||
		default: 'misskey'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'mongo_user',
 | 
			
		||||
		message: 'MongoDB\'s user:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'password',
 | 
			
		||||
		name: 'mongo_pass',
 | 
			
		||||
		message: 'MongoDB\'s password:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'redis_host',
 | 
			
		||||
		message: 'Redis\'s host:',
 | 
			
		||||
		default: 'localhost'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'redis_port',
 | 
			
		||||
		message: 'Redis\'s port:',
 | 
			
		||||
		default: '6379'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'password',
 | 
			
		||||
		name: 'redis_pass',
 | 
			
		||||
		message: 'Redis\'s password:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'confirm',
 | 
			
		||||
		name: 'elasticsearch',
 | 
			
		||||
		message: 'Use Elasticsearch?',
 | 
			
		||||
		default: false
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'es_host',
 | 
			
		||||
		message: 'Elasticsearch\'s host:',
 | 
			
		||||
		default: 'localhost',
 | 
			
		||||
		when: ctx => ctx.elasticsearch
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'es_port',
 | 
			
		||||
		message: 'Elasticsearch\'s port:',
 | 
			
		||||
		default: '9200',
 | 
			
		||||
		when: ctx => ctx.elasticsearch
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'password',
 | 
			
		||||
		name: 'es_pass',
 | 
			
		||||
		message: 'Elasticsearch\'s password:',
 | 
			
		||||
		when: ctx => ctx.elasticsearch
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'recaptcha_site',
 | 
			
		||||
		message: 'reCAPTCHA\'s site key:'
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
		type: 'input',
 | 
			
		||||
		name: 'recaptcha_secret',
 | 
			
		||||
		message: 'reCAPTCHA\'s secret key:'
 | 
			
		||||
	}
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
inquirer.prompt(form).then(as => {
 | 
			
		||||
	// Mapping answers
 | 
			
		||||
	const conf = {
 | 
			
		||||
		maintainer: as['maintainer'],
 | 
			
		||||
		url: as['url'],
 | 
			
		||||
		secondary_url: as['secondary_url'],
 | 
			
		||||
		port: parseInt(as['port'], 10),
 | 
			
		||||
		https: {
 | 
			
		||||
			enable: as['https'],
 | 
			
		||||
			key: as['https_key'] || null,
 | 
			
		||||
			cert: as['https_cert'] || null,
 | 
			
		||||
			ca: as['https_ca'] || null
 | 
			
		||||
		},
 | 
			
		||||
		mongodb: {
 | 
			
		||||
			host: as['mongo_host'],
 | 
			
		||||
			port: parseInt(as['mongo_port'], 10),
 | 
			
		||||
			db: as['mongo_db'],
 | 
			
		||||
			user: as['mongo_user'],
 | 
			
		||||
			pass: as['mongo_pass']
 | 
			
		||||
		},
 | 
			
		||||
		redis: {
 | 
			
		||||
			host: as['redis_host'],
 | 
			
		||||
			port: parseInt(as['redis_port'], 10),
 | 
			
		||||
			pass: as['redis_pass']
 | 
			
		||||
		},
 | 
			
		||||
		elasticsearch: {
 | 
			
		||||
			enable: as['elasticsearch'],
 | 
			
		||||
			host: as['es_host'] || null,
 | 
			
		||||
			port: parseInt(as['es_port'], 10) || null,
 | 
			
		||||
			pass: as['es_pass'] || null
 | 
			
		||||
		},
 | 
			
		||||
		recaptcha: {
 | 
			
		||||
			siteKey: as['recaptcha_site'],
 | 
			
		||||
			secretKey: as['recaptcha_secret']
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	console.log('Thanks. Writing the configuration to a file...');
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		fs.mkdirSync(configDirPath);
 | 
			
		||||
		fs.writeFileSync(configPath, yaml.dump(conf));
 | 
			
		||||
		console.log('Well done.');
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(e);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										14
									
								
								jsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,14 @@
 | 
			
		|||
{
 | 
			
		||||
	// Please visit https://go.microsoft.com/fwlink/?LinkId=759670 for more information about jsconfig.json
 | 
			
		||||
	"compilerOptions": {
 | 
			
		||||
		"target": "es6",
 | 
			
		||||
		"module": "commonjs",
 | 
			
		||||
		"allowSyntheticDefaultImports": true
 | 
			
		||||
	},
 | 
			
		||||
	"exclude": [
 | 
			
		||||
		"node_modules",
 | 
			
		||||
		"jspm_packages",
 | 
			
		||||
		"tmp",
 | 
			
		||||
		"temp"
 | 
			
		||||
	]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										135
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,135 @@
 | 
			
		|||
{
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "name": "misskey",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "description": "A miniblog-based SNS",
 | 
			
		||||
  "author": "syuilo <i@syuilo.com>",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "repository": "https://github.com/syuilo/misskey.git",
 | 
			
		||||
  "bugs": "https://github.com/syuilo/misskey/issues",
 | 
			
		||||
  "main": "./built/index.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "config": "node ./init.js",
 | 
			
		||||
    "start": "node ./built/index.js",
 | 
			
		||||
    "build": "gulp build",
 | 
			
		||||
    "rebuild": "gulp rebuild",
 | 
			
		||||
    "clean": "gulp clean",
 | 
			
		||||
    "cleanall": "gulp cleanall",
 | 
			
		||||
    "lint": "gulp lint",
 | 
			
		||||
    "test": "gulp test"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/bcrypt": "0.0.30",
 | 
			
		||||
    "@types/body-parser": "0.0.33",
 | 
			
		||||
    "@types/browserify": "12.0.30",
 | 
			
		||||
    "@types/chalk": "0.4.31",
 | 
			
		||||
    "@types/compression": "0.0.33",
 | 
			
		||||
    "@types/cors": "0.0.33",
 | 
			
		||||
    "@types/elasticsearch": "5.0.0",
 | 
			
		||||
    "@types/event-stream": "3.3.30",
 | 
			
		||||
    "@types/express": "4.0.34",
 | 
			
		||||
    "@types/glob": "5.0.30",
 | 
			
		||||
    "@types/gm": "1.17.29",
 | 
			
		||||
    "@types/gulp": "3.8.32",
 | 
			
		||||
    "@types/gulp-babel": "6.1.29",
 | 
			
		||||
    "@types/gulp-tslint": "3.6.30",
 | 
			
		||||
    "@types/gulp-typescript": "0.0.32",
 | 
			
		||||
    "@types/gulp-uglify": "0.0.29",
 | 
			
		||||
    "@types/gulp-util": "3.0.30",
 | 
			
		||||
    "@types/inquirer": "0.0.31",
 | 
			
		||||
    "@types/js-yaml": "3.5.28",
 | 
			
		||||
    "@types/mongodb": "2.1.34",
 | 
			
		||||
    "@types/ms": "0.7.29",
 | 
			
		||||
    "@types/multer": "0.0.32",
 | 
			
		||||
    "@types/ratelimiter": "2.1.28",
 | 
			
		||||
    "@types/redis": "0.12.32",
 | 
			
		||||
    "@types/request": "0.0.33",
 | 
			
		||||
    "@types/rimraf": "0.0.28",
 | 
			
		||||
    "@types/serve-favicon": "2.2.28",
 | 
			
		||||
    "@types/shelljs": "0.3.32",
 | 
			
		||||
    "@types/uuid": "2.0.29",
 | 
			
		||||
    "@types/vinyl-buffer": "0.0.28",
 | 
			
		||||
    "@types/vinyl-source-stream": "0.0.28",
 | 
			
		||||
    "@types/websocket": "0.0.32",
 | 
			
		||||
    "accesses": "1.2.0",
 | 
			
		||||
    "aliasify": "2.1.0",
 | 
			
		||||
    "argv": "0.0.2",
 | 
			
		||||
    "babel-core": "6.20.0",
 | 
			
		||||
    "babel-polyfill": "6.20.0",
 | 
			
		||||
    "babel-preset-es2015": "6.18.0",
 | 
			
		||||
    "babel-preset-stage-3": "6.17.0",
 | 
			
		||||
    "bcrypt": "1.0.1",
 | 
			
		||||
    "body-parser": "1.15.2",
 | 
			
		||||
    "browserify": "13.1.1",
 | 
			
		||||
    "browserify-livescript": "0.2.3",
 | 
			
		||||
    "chalk": "1.1.3",
 | 
			
		||||
    "chart.js": "2.4.0",
 | 
			
		||||
    "compression": "1.6.2",
 | 
			
		||||
    "cors": "2.8.1",
 | 
			
		||||
    "cropperjs": "1.0.0-alpha",
 | 
			
		||||
    "deepcopy": "0.6.3",
 | 
			
		||||
    "del": "2.2.2",
 | 
			
		||||
    "elasticsearch": "12.1.2",
 | 
			
		||||
    "escape-regexp": "0.0.1",
 | 
			
		||||
    "event-stream": "3.3.4",
 | 
			
		||||
    "express": "4.14.0",
 | 
			
		||||
    "file-type": "4.0.0",
 | 
			
		||||
    "fuckadblock": "3.2.1",
 | 
			
		||||
    "git-last-commit": "0.2.0",
 | 
			
		||||
    "glob": "7.1.1",
 | 
			
		||||
    "gm": "1.23.0",
 | 
			
		||||
    "gulp": "3.9.1",
 | 
			
		||||
    "gulp-babel": "6.1.2",
 | 
			
		||||
    "gulp-cssnano": "2.1.2",
 | 
			
		||||
    "gulp-livescript": "3.0.1",
 | 
			
		||||
    "gulp-pug": "3.2.0",
 | 
			
		||||
    "gulp-replace": "0.5.4",
 | 
			
		||||
    "gulp-stylus": "2.6.0",
 | 
			
		||||
    "gulp-tslint": "7.0.1",
 | 
			
		||||
    "gulp-typescript": "3.1.3",
 | 
			
		||||
    "gulp-uglify": "2.0.0",
 | 
			
		||||
    "gulp-util": "3.0.7",
 | 
			
		||||
    "inquirer": "2.0.0",
 | 
			
		||||
    "js-yaml": "3.7.0",
 | 
			
		||||
    "livescript": "1.5.0",
 | 
			
		||||
    "log-cool": "1.1.0",
 | 
			
		||||
    "mime-types": "2.1.13",
 | 
			
		||||
    "mongodb": "2.2.16",
 | 
			
		||||
    "ms": "0.7.2",
 | 
			
		||||
    "multer": "1.2.0",
 | 
			
		||||
    "nprogress": "0.2.0",
 | 
			
		||||
    "page": "1.7.1",
 | 
			
		||||
    "prominence": "0.2.0",
 | 
			
		||||
    "pug": "2.0.0-beta6",
 | 
			
		||||
    "ratelimiter": "2.1.3",
 | 
			
		||||
    "recaptcha-promise": "0.1.2",
 | 
			
		||||
    "reconnecting-websocket": "3.0.3",
 | 
			
		||||
    "redis": "2.6.3",
 | 
			
		||||
    "request": "2.79.0",
 | 
			
		||||
    "rimraf": "2.5.4",
 | 
			
		||||
    "riot": "3.0.5",
 | 
			
		||||
    "riot-compiler": "3.1.1",
 | 
			
		||||
    "riotify": "2.0.0",
 | 
			
		||||
    "rndstr": "1.0.0",
 | 
			
		||||
    "serve-favicon": "2.3.2",
 | 
			
		||||
    "shelljs": "0.7.5",
 | 
			
		||||
    "sortablejs": "1.5.0-rc1",
 | 
			
		||||
    "subdomain": "1.2.0",
 | 
			
		||||
    "summaly": "1.2.7",
 | 
			
		||||
    "syuilo-password-strength": "0.0.1",
 | 
			
		||||
    "syuilo-transformify": "0.1.2",
 | 
			
		||||
    "tcp-port-used": "0.1.2",
 | 
			
		||||
    "textarea-caret": "3.0.2",
 | 
			
		||||
    "tslint": "4.0.2",
 | 
			
		||||
    "typescript": "2.1.4",
 | 
			
		||||
    "uuid": "3.0.1",
 | 
			
		||||
    "velocity-animate": "1.4.0",
 | 
			
		||||
    "vhost": "3.0.2",
 | 
			
		||||
    "vinyl-buffer": "1.0.0",
 | 
			
		||||
    "vinyl-source-stream": "1.1.0",
 | 
			
		||||
    "websocket": "1.0.23",
 | 
			
		||||
    "whatwg-fetch": "2.0.1",
 | 
			
		||||
    "xml2json": "0.10.0",
 | 
			
		||||
    "yargs": "6.5.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								resources/apple-touch-icon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								resources/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 352 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								resources/favicon/128.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								resources/favicon/16.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 323 B  | 
							
								
								
									
										
											BIN
										
									
								
								resources/favicon/256.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								resources/favicon/32.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 532 B  | 
							
								
								
									
										
											BIN
										
									
								
								resources/favicon/64.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 930 B  | 
							
								
								
									
										1794
									
								
								resources/icon.ai
									
										
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								resources/icon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.7 KiB  | 
							
								
								
									
										21
									
								
								resources/icon.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,21 @@
 | 
			
		|||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 16.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 | 
			
		||||
<svg version="1.1" id="Layer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
 | 
			
		||||
<path fill="#EC6B43" d="M128,32c-44.183,0-80,35.817-80,80c0,31.234,16,56,44.002,71.462C111.037,193.973,112,224,128,224
 | 
			
		||||
	s16.964-30.025,36-40.538C192,168,208,143.233,208,112C208,67.817,172.183,32,128,32z M128,132c-11.046,0-20-8.954-20-20
 | 
			
		||||
	s8.954-20,20-20s20,8.954,20,20S139.046,132,128,132z"/>
 | 
			
		||||
<g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 843 B  | 
							
								
								
									
										7
									
								
								resources/logo.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 | 
			
		||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 width="1024px" height="1024px" viewBox="0 0 1024 1024" enable-background="new 0 0 1024 1024" xml:space="preserve">
 | 
			
		||||
<polyline fill="none" stroke="#000" stroke-width="34" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" points="
 | 
			
		||||
	896.5,608.5 800.5,416.5 704.5,608.5 608.5,416.5 512.5,608.5 416.5,416.5 320.5,608.5 224.5,416.5 128.5,608.5 "/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 628 B  | 
							
								
								
									
										55
									
								
								src/api/api-handler.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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 });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										149
									
								
								src/api/common/add-file-to-drive.ts
									
										
									
									
									
										Normal 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()
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										25
									
								
								src/api/common/get-friends.ts
									
										
									
									
									
										Normal 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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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[];
 | 
			
		||||
							
								
								
									
										83
									
								
								src/api/endpoints/aggregation/posts/like.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										76
									
								
								src/api/endpoints/aggregation/posts/likes.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										82
									
								
								src/api/endpoints/aggregation/posts/reply.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										82
									
								
								src/api/endpoints/aggregation/posts/repost.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										77
									
								
								src/api/endpoints/aggregation/users/followers.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										76
									
								
								src/api/endpoints/aggregation/users/following.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										83
									
								
								src/api/endpoints/aggregation/users/like.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										113
									
								
								src/api/endpoints/aggregation/users/post.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										75
									
								
								src/api/endpoints/app/create.js
									
										
									
									
									
										Normal 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));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										40
									
								
								src/api/endpoints/app/name_id/available.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										51
									
								
								src/api/endpoints/app/show.js
									
										
									
									
									
										Normal 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)
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										64
									
								
								src/api/endpoints/auth/accept.js
									
										
									
									
									
										Normal 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();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										51
									
								
								src/api/endpoints/auth/session/generate.js
									
										
									
									
									
										Normal 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}`
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										36
									
								
								src/api/endpoints/auth/session/show.js
									
										
									
									
									
										Normal 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));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										74
									
								
								src/api/endpoints/auth/session/userkey.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
		})
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										33
									
								
								src/api/endpoints/drive.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										82
									
								
								src/api/endpoints/drive/files.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										59
									
								
								src/api/endpoints/drive/files/create.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										48
									
								
								src/api/endpoints/drive/files/find.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										40
									
								
								src/api/endpoints/drive/files/show.js
									
										
									
									
									
										Normal 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));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										89
									
								
								src/api/endpoints/drive/files/update.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										82
									
								
								src/api/endpoints/drive/folders.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										79
									
								
								src/api/endpoints/drive/folders/create.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										46
									
								
								src/api/endpoints/drive/folders/find.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										41
									
								
								src/api/endpoints/drive/folders/show.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										114
									
								
								src/api/endpoints/drive/folders/update.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										85
									
								
								src/api/endpoints/drive/stream.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										86
									
								
								src/api/endpoints/following/create.js
									
										
									
									
									
										Normal 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');
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										83
									
								
								src/api/endpoints/following/delete.js
									
										
									
									
									
										Normal 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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										53
									
								
								src/api/endpoints/i/appdata/get.js
									
										
									
									
									
										Normal 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();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										55
									
								
								src/api/endpoints/i/appdata/set.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										60
									
								
								src/api/endpoints/i/favorites.js
									
										
									
									
									
										Normal 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)
 | 
			
		||||
	)));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										120
									
								
								src/api/endpoints/i/notifications.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										71
									
								
								src/api/endpoints/i/signin_history.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										95
									
								
								src/api/endpoints/i/update.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										48
									
								
								src/api/endpoints/messaging/history.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										139
									
								
								src/api/endpoints/messaging/messages.js
									
										
									
									
									
										Normal 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');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										152
									
								
								src/api/endpoints/messaging/messages/create.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										27
									
								
								src/api/endpoints/messaging/unread.js
									
										
									
									
									
										Normal 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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										59
									
								
								src/api/endpoints/my/apps.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										54
									
								
								src/api/endpoints/notifications/mark_as_read.js
									
										
									
									
									
										Normal 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);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										65
									
								
								src/api/endpoints/posts.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										83
									
								
								src/api/endpoints/posts/context.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										345
									
								
								src/api/endpoints/posts/create.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										56
									
								
								src/api/endpoints/posts/favorites/create.js
									
										
									
									
									
										Normal 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();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										52
									
								
								src/api/endpoints/posts/favorites/delete.js
									
										
									
									
									
										Normal 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();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										77
									
								
								src/api/endpoints/posts/likes.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										93
									
								
								src/api/endpoints/posts/likes/create.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										80
									
								
								src/api/endpoints/posts/likes/delete.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										85
									
								
								src/api/endpoints/posts/mentions.js
									
										
									
									
									
										Normal 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)
 | 
			
		||||
	)));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										73
									
								
								src/api/endpoints/posts/replies.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										85
									
								
								src/api/endpoints/posts/reposts.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										138
									
								
								src/api/endpoints/posts/search.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/api/endpoints/posts/show.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										78
									
								
								src/api/endpoints/posts/timeline.js
									
										
									
									
									
										Normal 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)
 | 
			
		||||
	)));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										41
									
								
								src/api/endpoints/username/available.js
									
										
									
									
									
										Normal 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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										67
									
								
								src/api/endpoints/users.js
									
										
									
									
									
										Normal 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))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										102
									
								
								src/api/endpoints/users/followers.js
									
										
									
									
									
										Normal 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,
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										102
									
								
								src/api/endpoints/users/following.js
									
										
									
									
									
										Normal 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,
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										114
									
								
								src/api/endpoints/users/posts.js
									
										
									
									
									
										Normal 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)
 | 
			
		||||
	)));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										61
									
								
								src/api/endpoints/users/recommendation.js
									
										
									
									
									
										Normal 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 }))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										116
									
								
								src/api/endpoints/users/search.js
									
										
									
									
									
										Normal 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 }))));
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								src/api/endpoints/users/search_by_username.js
									
										
									
									
									
										Normal 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 }))));
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										49
									
								
								src/api/endpoints/users/show.js
									
										
									
									
									
										Normal 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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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
									
								
							
							
						
						| 
						 | 
				
			
			@ -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;
 | 
			
		||||
							
								
								
									
										1
									
								
								src/api/models/appdata.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export default global.db.collection('appdata');
 | 
			
		||||