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'); | ||||