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