Use PostgreSQL instead of MongoDB (#4572)
* wip * Update note.ts * Update timeline.ts * Update core.ts * wip * Update generate-visibility-query.ts * wip * wip * wip * wip * wip * Update global-timeline.ts * wip * wip * wip * Update vote.ts * wip * wip * Update create.ts * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update files.ts * wip * wip * Update CONTRIBUTING.md * wip * wip * wip * wip * wip * wip * wip * wip * Update read-notification.ts * wip * wip * wip * wip * wip * wip * wip * Update cancel.ts * wip * wip * wip * Update show.ts * wip * wip * Update gen-id.ts * Update create.ts * Update id.ts * wip * wip * wip * wip * wip * wip * wip * Docker: Update files about Docker (#4599) * Docker: Use cache if files used by `yarn install` was not updated This patch reduces the number of times to installing node_modules. For example, `yarn install` step will be skipped when only ".config/default.yml" is updated. * Docker: Migrate MongoDB to Postgresql Misskey uses Postgresql as a database instead of Mongodb since version 11. * Docker: Uncomment about data persistence This patch will save a lot of databases. * wip * wip * wip * Update activitypub.ts * wip * wip * wip * Update logs.ts * wip * Update drive-file.ts * Update register.ts * wip * wip * Update mentions.ts * wip * wip * wip * Update recommendation.ts * wip * Update index.ts * wip * Update recommendation.ts * Doc: Update docker.ja.md and docker.en.md (#1) (#4608) Update how to set up misskey. * wip * ✌️ * wip * Update note.ts * Update postgre.ts * wip * wip * wip * wip * Update add-file.ts * wip * wip * wip * Clean up * Update logs.ts * wip * 🍕 * wip * Ad notes * wip * Update api-visibility.ts * Update note.ts * Update add-file.ts * tests * tests * Update postgre.ts * Update utils.ts * wip * wip * Refactor * wip * Refactor * wip * wip * Update show-users.ts * Update update-instance.ts * wip * Update feed.ts * Update outbox.ts * Update outbox.ts * Update user.ts * wip * Update list.ts * Update update-hashtag.ts * wip * Update update-hashtag.ts * Refactor * Update update.ts * wip * wip * ✌️ * clean up * docs * Update push.ts * wip * Update api.ts * wip * ✌️ * Update make-pagination-query.ts * ✌️ * Delete hashtags.ts * Update instances.ts * Update instances.ts * Update create.ts * Update search.ts * Update reversi-game.ts * Update signup.ts * Update user.ts * id * Update example.yml * 🎨 * objectid * fix * reversi * reversi * Fix bug of chart engine * Add test of chart engine * Improve test * Better testing * Improve chart engine * Refactor * Add test of chart engine * Refactor * Add chart test * Fix bug * コミットし忘れ * Refactoring * ✌️ * Add tests * Add test * Extarct note tests * Refactor * 存在しないユーザーにメンションできなくなっていた問題を修正 * Fix bug * Update update-meta.ts * Fix bug * Update mention.vue * Fix bug * Update meta.ts * Update CONTRIBUTING.md * Fix bug * Fix bug * Fix bug * Clean up * Clean up * Update notification.ts * Clean up * Add mute tests * Add test * Refactor * Add test * Fix test * Refactor * Refactor * Add tests * Update utils.ts * Update utils.ts * Fix test * Update package.json * Update update.ts * Update manifest.ts * Fix bug * Fix bug * Add test * 🎨 * Update endpoint permissions * Updaye permisison * Update person.ts #4299 * データベースと同期しないように * Fix bug * Fix bug * Update reversi-game.ts * Use a feature of Node v11.7.0 to extract a public key (#4644) * wip * wip * ✌️ * Refactoring #1540 * test * test * test * test * test * test * test * Fix bug * Fix test * 🍣 * wip * #4471 * Add test for #4335 * Refactor * Fix test * Add tests * 🕓 * Fix bug * Add test * Add test * rename * Fix bug
This commit is contained in:
		
							parent
							
								
									13caf37991
								
							
						
					
					
						commit
						f0a29721c9
					
				
					 592 changed files with 13463 additions and 14147 deletions
				
			
		
							
								
								
									
										5
									
								
								.config/docker_example.env
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.config/docker_example.env
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| # db settings | ||||
| POSTGRES_PASSWORD="example-misskey-pass" | ||||
| POSTGRES_USER="example-misskey-user" | ||||
| POSTGRES_DB="misskey" | ||||
| 
 | ||||
|  | @ -1,8 +1,16 @@ | |||
| #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||||
| # Misskey configuration | ||||
| #━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||||
| 
 | ||||
| #   ┌─────┐ | ||||
| #───┘ URL └───────────────────────────────────────────────────── | ||||
| 
 | ||||
| # Final accessible URL seen by a user. | ||||
| url: https://example.tld/ | ||||
| 
 | ||||
| #   ┌───────────────────────┐ | ||||
| #───┘ Port and TLS settings └─────────────────────────────────── | ||||
| 
 | ||||
| ### Port and TLS settings ###################################### | ||||
| # | ||||
| # Misskey supports two deployment options for public. | ||||
| # | ||||
|  | @ -34,24 +42,47 @@ url: https://example.tld/ | |||
| 
 | ||||
| # To use option 2, uncomment below lines. | ||||
| #port: 443 | ||||
| # | ||||
| 
 | ||||
| #https: | ||||
| #  # path for certification | ||||
| #  key: /etc/letsencrypt/live/example.tld/privkey.pem | ||||
| #  cert: /etc/letsencrypt/live/example.tld/fullchain.pem | ||||
| 
 | ||||
| ################################################################ | ||||
| #   ┌──────────────────────────┐ | ||||
| #───┘ PostgreSQL configuration └──────────────────────────────── | ||||
| 
 | ||||
| 
 | ||||
| mongodb: | ||||
| db: | ||||
|   host: localhost | ||||
|   port: 27017 | ||||
|   port: 5432 | ||||
| 
 | ||||
|   # Database name | ||||
|   db: misskey | ||||
| 
 | ||||
|   # Auth | ||||
|   user: example-misskey-user | ||||
|   pass: example-misskey-pass | ||||
| 
 | ||||
| #   ┌─────────────────────┐ | ||||
| #───┘ Redis configuration └───────────────────────────────────── | ||||
| 
 | ||||
| #redis: | ||||
| #  host: localhost | ||||
| #  port: 6379 | ||||
| #  pass: example-pass | ||||
| 
 | ||||
| #   ┌─────────────────────────────┐ | ||||
| #───┘ Elasticsearch configuration └───────────────────────────── | ||||
| 
 | ||||
| #elasticsearch: | ||||
| #  host: localhost | ||||
| #  port: 9200 | ||||
| #  pass: null | ||||
| 
 | ||||
| #   ┌────────────────────────────────────┐ | ||||
| #───┘ File storage (Drive) configuration └────────────────────── | ||||
| 
 | ||||
| drive: | ||||
|   storage: 'db' | ||||
|   storage: 'fs' | ||||
| 
 | ||||
|   # OR | ||||
| 
 | ||||
|  | @ -88,26 +119,44 @@ drive: | |||
|   #   accessKey: XXX | ||||
|   #   secretKey: YYY | ||||
| 
 | ||||
| #   ┌───────────────┐ | ||||
| #───┘ ID generation └─────────────────────────────────────────── | ||||
| 
 | ||||
| # You can select the ID generation method. | ||||
| # You don't usually need to change this setting, but you can | ||||
| # change it according to your preferences. | ||||
| 
 | ||||
| # Available methods: | ||||
| # aid1 ... Use AID for ID generation (with random 1 char) | ||||
| # aid2 ... Use AID for ID generation (with random 2 chars) | ||||
| # aid3 ... Use AID for ID generation (with random 3 chars) | ||||
| # aid4 ... Use AID for ID generation (with random 4 chars) | ||||
| # ulid ... Use ulid for ID generation | ||||
| # objectid ... This is left for backward compatibility. | ||||
| 
 | ||||
| # AID(n) is the original ID generation method. | ||||
| # The trailing n represents the number of random characters that | ||||
| # will be suffixed. | ||||
| # The larger n is the safer. If n is small, the possibility of | ||||
| # collision at the same time increases, but there are also | ||||
| # advantages such as shortening of the URL. | ||||
| 
 | ||||
| # ULID: Universally Unique Lexicographically Sortable Identifier. | ||||
| # for more details: https://github.com/ulid/spec | ||||
| # * Normally, AID should be sufficient. | ||||
| 
 | ||||
| # ObjectID is the method used in previous versions of Misskey. | ||||
| # * Choose this if you are migrating from a previous Misskey. | ||||
| 
 | ||||
| id: 'aid2' | ||||
| 
 | ||||
| #   ┌─────────────────────┐ | ||||
| #───┘ Other configuration └───────────────────────────────────── | ||||
| 
 | ||||
| # If enabled: | ||||
| #  The first account created is automatically marked as Admin. | ||||
| autoAdmin: true | ||||
| 
 | ||||
| # | ||||
| # Below settings are optional | ||||
| # | ||||
| 
 | ||||
| # Redis | ||||
| #redis: | ||||
| #  host: localhost | ||||
| #  port: 6379 | ||||
| #  pass: example-pass | ||||
| 
 | ||||
| # Elasticsearch | ||||
| #elasticsearch: | ||||
| #  host: localhost | ||||
| #  port: 9200 | ||||
| #  pass: null | ||||
| 
 | ||||
| # Whether disable HSTS | ||||
| #disableHsts: true | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,13 +0,0 @@ | |||
| var user = { | ||||
| 	user: 'example-misskey-user', | ||||
| 	pwd: 'example-misskey-pass', | ||||
| 	roles: [ | ||||
| 	    { | ||||
| 		    role: 'readWrite', | ||||
| 		    db: 'misskey' | ||||
| 	    } | ||||
| 	] | ||||
| }; | ||||
| 
 | ||||
| db.createUser(user); | ||||
| 
 | ||||
							
								
								
									
										6
									
								
								.dockerignore
									
										
									
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										6
									
								
								.dockerignore
									
										
									
									
									
										
										
										Executable file → Normal file
									
								
							|  | @ -5,8 +5,8 @@ | |||
| .vscode | ||||
| Dockerfile | ||||
| build/ | ||||
| db/ | ||||
| docker-compose.yml | ||||
| node_modules/ | ||||
| mongo/ | ||||
| redis/ | ||||
| elasticsearch/ | ||||
| node_modules/ | ||||
| redis/ | ||||
|  |  | |||
							
								
								
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -8,14 +8,15 @@ | |||
| built | ||||
| /data | ||||
| /.cache-loader | ||||
| /db | ||||
| /elasticsearch | ||||
| npm-debug.log | ||||
| *.pem | ||||
| run.bat | ||||
| api-docs.json | ||||
| *.log | ||||
| /redis | ||||
| /mongo | ||||
| /elasticsearch | ||||
| *.code-workspace | ||||
| yarn.lock | ||||
| .DS_Store | ||||
| /files | ||||
|  |  | |||
|  | @ -75,3 +75,61 @@ src ... Source code | |||
| test ... Test code | ||||
| 
 | ||||
| ``` | ||||
| 
 | ||||
| ## Notes | ||||
| ### placeholder | ||||
| SQLをクエリビルダで組み立てる際、使用するプレースホルダは重複してはならない | ||||
| 例えば | ||||
| ``` ts | ||||
| query.andWhere(new Brackets(qb => { | ||||
| 	for (const type of ps.fileType) { | ||||
| 		qb.orWhere(`:type = ANY(note.attachedFileTypes)`, { type: type }); | ||||
| 	} | ||||
| })); | ||||
| ``` | ||||
| と書くと、ループ中で`type`というプレースホルダが複数回使われてしまいおかしくなる | ||||
| だから次のようにする必要がある | ||||
| ```ts | ||||
| query.andWhere(new Brackets(qb => { | ||||
| 	for (const type of ps.fileType) { | ||||
| 		const i = ps.fileType.indexOf(type); | ||||
| 		qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); | ||||
| 	} | ||||
| })); | ||||
| ``` | ||||
| 
 | ||||
| ### `null` in SQL | ||||
| SQLを発行する際、パラメータが`null`になる可能性のある場合はSQL文を出し分けなければならない | ||||
| 例えば | ||||
| ``` ts | ||||
| query.where('file.folderId = :folderId', { folderId: ps.folderId }); | ||||
| ``` | ||||
| という処理で、`ps.folderId`が`null`だと結果的に`file.folderId = null`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない | ||||
| だから次のようにする必要がある | ||||
| ``` ts | ||||
| if (ps.folderId) { | ||||
| 	query.where('file.folderId = :folderId', { folderId: ps.folderId }); | ||||
| } else { | ||||
| 	query.where('file.folderId IS NULL'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### `[]` in SQL | ||||
| SQLを発行する際、`IN`のパラメータが`[]`(空の配列)になる可能性のある場合はSQL文を出し分けなければならない | ||||
| 例えば | ||||
| ``` ts | ||||
| const users = await Users.find({ | ||||
| 	id: In(userIds) | ||||
| }); | ||||
| ``` | ||||
| という処理で、`userIds`が`[]`だと結果的に`user.id IN ()`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない | ||||
| だから次のようにする必要がある | ||||
| ``` ts | ||||
| const users = userIds.length > 0 ? await Users.find({ | ||||
| 	id: In(userIds) | ||||
| }) : []; | ||||
| ``` | ||||
| 
 | ||||
| ### `undefined`にご用心 | ||||
| MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。 | ||||
| MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください | ||||
|  |  | |||
|  | @ -23,8 +23,9 @@ RUN apk add --no-cache \ | |||
|     zlib-dev | ||||
| RUN npm i -g yarn | ||||
| 
 | ||||
| COPY . ./ | ||||
| COPY package.json ./ | ||||
| RUN yarn install | ||||
| COPY . ./ | ||||
| RUN yarn build | ||||
| 
 | ||||
| FROM base AS runner | ||||
|  |  | |||
|  | @ -1,9 +0,0 @@ | |||
| { | ||||
| 	'targets': [ | ||||
| 		{ | ||||
| 			'target_name': 'crypto_key', | ||||
| 			'sources': ['src/crypto_key.cc'], | ||||
| 			'include_dirs': ['<!(node -e "require(\'nan\')")'] | ||||
| 		} | ||||
| 	] | ||||
| } | ||||
|  | @ -1,57 +0,0 @@ | |||
| // for Node.js interpret
 | ||||
| 
 | ||||
| const chalk = require('chalk'); | ||||
| const sequential = require('promise-sequential'); | ||||
| 
 | ||||
| const { default: User } = require('../../built/models/user'); | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| async function main() { | ||||
| 	const promiseGens = []; | ||||
| 
 | ||||
| 	const count = await DriveFile.count({}); | ||||
| 
 | ||||
| 	let prev; | ||||
| 
 | ||||
| 	for (let i = 0; i < count; i++) { | ||||
| 		promiseGens.push(() => { | ||||
| 			const promise = new Promise(async (res, rej) => { | ||||
| 				const file = await DriveFile.findOne(prev ? { | ||||
| 					_id: { $gt: prev._id } | ||||
| 				} : {}, { | ||||
| 					sort: { | ||||
| 						_id: 1 | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				prev = file; | ||||
| 
 | ||||
| 				const user = await User.findOne({ _id: file.metadata.userId }); | ||||
| 
 | ||||
| 				DriveFile.update({ | ||||
| 					_id: file._id | ||||
| 				}, { | ||||
| 					$set: { | ||||
| 						'metadata._user': { | ||||
| 							host: user.host | ||||
| 						} | ||||
| 					} | ||||
| 				}).then(() => { | ||||
| 					res([i, file]); | ||||
| 				}).catch(rej); | ||||
| 			}); | ||||
| 
 | ||||
| 			promise.then(([i, file]) => { | ||||
| 				console.log(chalk`{gray ${i}} {green done: {bold ${file._id}} ${file.filename}}`); | ||||
| 			}); | ||||
| 
 | ||||
| 			return promise; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	return await sequential(promiseGens); | ||||
| } | ||||
| 
 | ||||
| main().then(() => { | ||||
| 	console.log('ALL DONE'); | ||||
| }).catch(console.error); | ||||
|  | @ -1,71 +0,0 @@ | |||
| // for Node.js interpret
 | ||||
| 
 | ||||
| const chalk = require('chalk'); | ||||
| const sequential = require('promise-sequential'); | ||||
| 
 | ||||
| const { default: User } = require('../../built/models/user'); | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| async function main() { | ||||
| 	const promiseGens = []; | ||||
| 
 | ||||
| 	const count = await User.count({}); | ||||
| 
 | ||||
| 	let prev; | ||||
| 
 | ||||
| 	for (let i = 0; i < count; i++) { | ||||
| 		promiseGens.push(() => { | ||||
| 			const promise = new Promise(async (res, rej) => { | ||||
| 				const user = await User.findOne(prev ? { | ||||
| 					_id: { $gt: prev._id } | ||||
| 				} : {}, { | ||||
| 					sort: { | ||||
| 						_id: 1 | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				prev = user; | ||||
| 
 | ||||
| 				const set = {}; | ||||
| 
 | ||||
| 				if (user.avatarId != null) { | ||||
| 					const file = await DriveFile.findOne({ _id: user.avatarId }); | ||||
| 
 | ||||
| 					if (file && file.metadata.properties.avgColor) { | ||||
| 						set.avatarColor = file.metadata.properties.avgColor; | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				if (user.bannerId != null) { | ||||
| 					const file = await DriveFile.findOne({ _id: user.bannerId }); | ||||
| 
 | ||||
| 					if (file && file.metadata.properties.avgColor) { | ||||
| 						set.bannerColor = file.metadata.properties.avgColor; | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				if (Object.keys(set).length === 0) return res([i, user]); | ||||
| 
 | ||||
| 				User.update({ | ||||
| 					_id: user._id | ||||
| 				}, { | ||||
| 					$set: set | ||||
| 				}).then(() => { | ||||
| 					res([i, user]); | ||||
| 				}).catch(rej); | ||||
| 			}); | ||||
| 
 | ||||
| 			promise.then(([i, user]) => { | ||||
| 				console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`); | ||||
| 			}); | ||||
| 
 | ||||
| 			return promise; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	return await sequential(promiseGens); | ||||
| } | ||||
| 
 | ||||
| main().then(() => { | ||||
| 	console.log('ALL DONE'); | ||||
| }).catch(console.error); | ||||
|  | @ -1,9 +0,0 @@ | |||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| DriveFile.update({}, { | ||||
| 	$rename: { | ||||
| 		'metadata.isMetaOnly': 'metadata.withoutChunks' | ||||
| 	} | ||||
| }, { | ||||
| 	multi: true | ||||
| }); | ||||
|  | @ -1,134 +0,0 @@ | |||
| const { default: Stats } = require('../../built/models/stats'); | ||||
| const { default: User } = require('../../built/models/user'); | ||||
| const { default: Note } = require('../../built/models/note'); | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| const now = new Date(); | ||||
| const y = now.getFullYear(); | ||||
| const m = now.getMonth(); | ||||
| const d = now.getDate(); | ||||
| const today = new Date(y, m, d); | ||||
| 
 | ||||
| async function main() { | ||||
| 	const localUsersCount = await User.count({ | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	const remoteUsersCount = await User.count({ | ||||
| 		host: { $ne: null } | ||||
| 	}); | ||||
| 
 | ||||
| 	const localNotesCount = await Note.count({ | ||||
| 		'_user.host': null | ||||
| 	}); | ||||
| 
 | ||||
| 	const remoteNotesCount = await Note.count({ | ||||
| 		'_user.host': { $ne: null } | ||||
| 	}); | ||||
| 
 | ||||
| 	const localDriveFilesCount = await DriveFile.count({ | ||||
| 		'metadata._user.host': null | ||||
| 	}); | ||||
| 
 | ||||
| 	const remoteDriveFilesCount = await DriveFile.count({ | ||||
| 		'metadata._user.host': { $ne: null } | ||||
| 	}); | ||||
| 
 | ||||
| 	const localDriveFilesSize = await DriveFile | ||||
| 		.aggregate([{ | ||||
| 			$match: { | ||||
| 				'metadata._user.host': null, | ||||
| 				'metadata.deletedAt': { $exists: false } | ||||
| 			} | ||||
| 		}, { | ||||
| 			$project: { | ||||
| 				length: true | ||||
| 			} | ||||
| 		}, { | ||||
| 			$group: { | ||||
| 				_id: null, | ||||
| 				usage: { $sum: '$length' } | ||||
| 			} | ||||
| 		}]) | ||||
| 		.then(aggregates => { | ||||
| 			if (aggregates.length > 0) { | ||||
| 				return aggregates[0].usage; | ||||
| 			} | ||||
| 			return 0; | ||||
| 		}); | ||||
| 
 | ||||
| 	const remoteDriveFilesSize = await DriveFile | ||||
| 		.aggregate([{ | ||||
| 			$match: { | ||||
| 				'metadata._user.host': { $ne: null }, | ||||
| 				'metadata.deletedAt': { $exists: false } | ||||
| 			} | ||||
| 		}, { | ||||
| 			$project: { | ||||
| 				length: true | ||||
| 			} | ||||
| 		}, { | ||||
| 			$group: { | ||||
| 				_id: null, | ||||
| 				usage: { $sum: '$length' } | ||||
| 			} | ||||
| 		}]) | ||||
| 		.then(aggregates => { | ||||
| 			if (aggregates.length > 0) { | ||||
| 				return aggregates[0].usage; | ||||
| 			} | ||||
| 			return 0; | ||||
| 		}); | ||||
| 
 | ||||
| 	await Stats.insert({ | ||||
| 		date: today, | ||||
| 		users: { | ||||
| 			local: { | ||||
| 				total: localUsersCount, | ||||
| 				diff: 0 | ||||
| 			}, | ||||
| 			remote: { | ||||
| 				total: remoteUsersCount, | ||||
| 				diff: 0 | ||||
| 			} | ||||
| 		}, | ||||
| 		notes: { | ||||
| 			local: { | ||||
| 				total: localNotesCount, | ||||
| 				diff: 0, | ||||
| 				diffs: { | ||||
| 					normal: 0, | ||||
| 					reply: 0, | ||||
| 					renote: 0 | ||||
| 				} | ||||
| 			}, | ||||
| 			remote: { | ||||
| 				total: remoteNotesCount, | ||||
| 				diff: 0, | ||||
| 				diffs: { | ||||
| 					normal: 0, | ||||
| 					reply: 0, | ||||
| 					renote: 0 | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		drive: { | ||||
| 			local: { | ||||
| 				totalCount: localDriveFilesCount, | ||||
| 				totalSize: localDriveFilesSize, | ||||
| 				diffCount: 0, | ||||
| 				diffSize: 0 | ||||
| 			}, | ||||
| 			remote: { | ||||
| 				totalCount: remoteDriveFilesCount, | ||||
| 				totalSize: remoteDriveFilesSize, | ||||
| 				diffCount: 0, | ||||
| 				diffSize: 0 | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	console.log('done'); | ||||
| } | ||||
| 
 | ||||
| main(); | ||||
|  | @ -1,144 +0,0 @@ | |||
| const { default: Stats } = require('../../built/models/stats'); | ||||
| const { default: User } = require('../../built/models/user'); | ||||
| const { default: Note } = require('../../built/models/note'); | ||||
| const { default: DriveFile } = require('../../built/models/drive-file'); | ||||
| 
 | ||||
| const now = new Date(); | ||||
| const y = now.getFullYear(); | ||||
| const m = now.getMonth(); | ||||
| const d = now.getDate(); | ||||
| const h = now.getHours(); | ||||
| const date = new Date(y, m, d, h); | ||||
| 
 | ||||
| async function main() { | ||||
| 	await Stats.update({}, { | ||||
| 		$set: { | ||||
| 			span: 'day' | ||||
| 		} | ||||
| 	}, { | ||||
| 		multi: true | ||||
| 	}); | ||||
| 
 | ||||
| 	const localUsersCount = await User.count({ | ||||
| 		host: null | ||||
| 	}); | ||||
| 
 | ||||
| 	const remoteUsersCount = await User.count({ | ||||
| 		host: { $ne: null } | ||||
| 	}); | ||||
| 
 | ||||
| 	const localNotesCount = await Note.count({ | ||||
| 		'_user.host': null | ||||
| 	}); | ||||
| 
 | ||||
| 	const remoteNotesCount = await Note.count({ | ||||
| 		'_user.host': { $ne: null } | ||||
| 	}); | ||||
| 
 | ||||
| 	const localDriveFilesCount = await DriveFile.count({ | ||||
| 		'metadata._user.host': null | ||||
| 	}); | ||||
| 
 | ||||
| 	const remoteDriveFilesCount = await DriveFile.count({ | ||||
| 		'metadata._user.host': { $ne: null } | ||||
| 	}); | ||||
| 
 | ||||
| 	const localDriveFilesSize = await DriveFile | ||||
| 		.aggregate([{ | ||||
| 			$match: { | ||||
| 				'metadata._user.host': null, | ||||
| 				'metadata.deletedAt': { $exists: false } | ||||
| 			} | ||||
| 		}, { | ||||
| 			$project: { | ||||
| 				length: true | ||||
| 			} | ||||
| 		}, { | ||||
| 			$group: { | ||||
| 				_id: null, | ||||
| 				usage: { $sum: '$length' } | ||||
| 			} | ||||
| 		}]) | ||||
| 		.then(aggregates => { | ||||
| 			if (aggregates.length > 0) { | ||||
| 				return aggregates[0].usage; | ||||
| 			} | ||||
| 			return 0; | ||||
| 		}); | ||||
| 
 | ||||
| 	const remoteDriveFilesSize = await DriveFile | ||||
| 		.aggregate([{ | ||||
| 			$match: { | ||||
| 				'metadata._user.host': { $ne: null }, | ||||
| 				'metadata.deletedAt': { $exists: false } | ||||
| 			} | ||||
| 		}, { | ||||
| 			$project: { | ||||
| 				length: true | ||||
| 			} | ||||
| 		}, { | ||||
| 			$group: { | ||||
| 				_id: null, | ||||
| 				usage: { $sum: '$length' } | ||||
| 			} | ||||
| 		}]) | ||||
| 		.then(aggregates => { | ||||
| 			if (aggregates.length > 0) { | ||||
| 				return aggregates[0].usage; | ||||
| 			} | ||||
| 			return 0; | ||||
| 		}); | ||||
| 
 | ||||
| 	await Stats.insert({ | ||||
| 		date: date, | ||||
| 		span: 'hour', | ||||
| 		users: { | ||||
| 			local: { | ||||
| 				total: localUsersCount, | ||||
| 				diff: 0 | ||||
| 			}, | ||||
| 			remote: { | ||||
| 				total: remoteUsersCount, | ||||
| 				diff: 0 | ||||
| 			} | ||||
| 		}, | ||||
| 		notes: { | ||||
| 			local: { | ||||
| 				total: localNotesCount, | ||||
| 				diff: 0, | ||||
| 				diffs: { | ||||
| 					normal: 0, | ||||
| 					reply: 0, | ||||
| 					renote: 0 | ||||
| 				} | ||||
| 			}, | ||||
| 			remote: { | ||||
| 				total: remoteNotesCount, | ||||
| 				diff: 0, | ||||
| 				diffs: { | ||||
| 					normal: 0, | ||||
| 					reply: 0, | ||||
| 					renote: 0 | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		drive: { | ||||
| 			local: { | ||||
| 				totalCount: localDriveFilesCount, | ||||
| 				totalSize: localDriveFilesSize, | ||||
| 				diffCount: 0, | ||||
| 				diffSize: 0 | ||||
| 			}, | ||||
| 			remote: { | ||||
| 				totalCount: remoteDriveFilesCount, | ||||
| 				totalSize: remoteDriveFilesSize, | ||||
| 				diffCount: 0, | ||||
| 				diffSize: 0 | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	console.log('done'); | ||||
| } | ||||
| 
 | ||||
| main(); | ||||
|  | @ -5,7 +5,7 @@ services: | |||
|     build: . | ||||
|     restart: always | ||||
|     links: | ||||
|       - mongo | ||||
|       - db | ||||
| #      - redis | ||||
| #      - es | ||||
|     ports: | ||||
|  | @ -19,21 +19,18 @@ services: | |||
| #    image: redis:4.0-alpine | ||||
| #    networks: | ||||
| #      - internal_network | ||||
| ### Uncomment to enable Redis persistance | ||||
| ##    volumes: | ||||
| ##      - ./redis:/data | ||||
| #    volumes: | ||||
| #      - ./redis:/data | ||||
| 
 | ||||
|   mongo: | ||||
|   db: | ||||
|     restart: always | ||||
|     image: mongo:4.1 | ||||
|     image: postgres:11.2-alpine | ||||
|     networks: | ||||
|       - internal_network | ||||
|     environment: | ||||
|       MONGO_INITDB_DATABASE: "misskey" | ||||
|     env_file: | ||||
|       - .config/docker.env | ||||
|     volumes: | ||||
|       - ./.config/mongo_initdb.js:/docker-entrypoint-initdb.d/mongo_initdb.js:ro | ||||
| ### Uncomment to enable MongoDB persistance | ||||
| #      - ./mongo:/data | ||||
|       - ./db:/var/lib/postgresql/data | ||||
| 
 | ||||
| #  es: | ||||
| #    restart: always | ||||
|  | @ -42,9 +39,8 @@ services: | |||
| #      - "ES_JAVA_OPTS=-Xms512m -Xmx512m" | ||||
| #    networks: | ||||
| #      - internal_network | ||||
| #### Uncomment to enable ES persistence | ||||
| ##    volumes: | ||||
| ##      - ./elasticsearch:/usr/share/elasticsearch/data | ||||
| #    volumes: | ||||
| #      - ./elasticsearch:/usr/share/elasticsearch/data | ||||
| 
 | ||||
| networks: | ||||
|   internal_network: | ||||
|  |  | |||
|  | @ -1,22 +0,0 @@ | |||
| Comment faire une sauvegarde de votre Misskey ? | ||||
| ========================== | ||||
| 
 | ||||
| Assurez-vous d'avoir installé **mongodb-tools**. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Dans votre terminal : | ||||
| ``` shell | ||||
| $ mongodump --archive=db-backup -u <VotreNomdUtilisateur> -p <VotreMotDePasse> | ||||
| ``` | ||||
| 
 | ||||
| Pour plus de détails, merci de consulter [la documentation de mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/). | ||||
| 
 | ||||
| Restauration | ||||
| ------- | ||||
| 
 | ||||
| ``` shell | ||||
| $ mongorestore --archive=db-backup | ||||
| ``` | ||||
| 
 | ||||
| Pour plus de détails, merci de consulter [la documentation de mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/). | ||||
|  | @ -1,22 +0,0 @@ | |||
| How to backup your Misskey | ||||
| ========================== | ||||
| 
 | ||||
| Make sure **mongodb-tools** installed. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| In your shell: | ||||
| ``` shell | ||||
| $ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword> | ||||
| ``` | ||||
| 
 | ||||
| For details, please see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/). | ||||
| 
 | ||||
| Restore | ||||
| ------- | ||||
| 
 | ||||
| ``` shell | ||||
| $ mongorestore --archive=db-backup | ||||
| ``` | ||||
| 
 | ||||
| For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/). | ||||
|  | @ -15,9 +15,37 @@ This guide describes how to install and setup Misskey with Docker. | |||
| 
 | ||||
| *2.* Configure Misskey | ||||
| ---------------------------------------------------------------- | ||||
| 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. | ||||
| 2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copy the `.config/mongo_initdb_example.js` and rename it to `mongo_initdb.js`. | ||||
| 3. Edit `default.yml` and `mongo_initdb.js`. | ||||
| 
 | ||||
| Create configuration files with following: | ||||
| 
 | ||||
| ```bash | ||||
| cd .config | ||||
| cp example.yml default.yml | ||||
| cp docker_example.env docker.env | ||||
| ``` | ||||
| 
 | ||||
| ### `default.yml` | ||||
| 
 | ||||
| Edit this file the same as non-Docker environment.   | ||||
| However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`.   | ||||
| The following is default hostname: | ||||
| 
 | ||||
| | Service       | Hostname | | ||||
| |---------------|----------| | ||||
| | Postgresql    | `db`     | | ||||
| | Redis         | `redis`  | | ||||
| | Elasticsearch | `es`     | | ||||
| 
 | ||||
| ### `docker.env` | ||||
| 
 | ||||
| Configure Postgresql in this file.   | ||||
| The minimum required settings are: | ||||
| 
 | ||||
| | name                | Description   | | ||||
| |---------------------|---------------| | ||||
| | `POSTGRES_PASSWORD` | Password      | | ||||
| | `POSTGRES_USER`     | Username      | | ||||
| | `POSTGRES_DB`       | Database name | | ||||
| 
 | ||||
| *3.* Configure Docker | ||||
| ---------------------------------------------------------------- | ||||
|  |  | |||
|  | @ -13,11 +13,39 @@ Dockerを使ったMisskey構築方法 | |||
| 2. `cd misskey` misskeyディレクトリに移動 | ||||
| 3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 | ||||
| 
 | ||||
| *2.* 設定ファイルを作成する | ||||
| *2.* 設定ファイルの作成と編集 | ||||
| ---------------------------------------------------------------- | ||||
| 1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする | ||||
| 2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` `.config/mongo_initdb_example.js`をコピーし名前を`mongo_initdb.js`にする | ||||
| 3. `default.yml`と`mongo_initdb.js`を編集する | ||||
| 
 | ||||
| 下記コマンドで設定ファイルを作成してください。 | ||||
| 
 | ||||
| ```bash | ||||
| cd .config | ||||
| cp example.yml default.yml | ||||
| cp docker_example.env docker.env | ||||
| ``` | ||||
| 
 | ||||
| ### `default.yml`の編集 | ||||
| 
 | ||||
| 非Docker環境と同じ様に編集してください。   | ||||
| ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。   | ||||
| 標準設定では次の通りです。 | ||||
| 
 | ||||
| | サービス       | ホスト名 | | ||||
| |---------------|---------| | ||||
| | Postgresql    |`db`     | | ||||
| | Redis         |`redis`  | | ||||
| | Elasticsearch |`es`     | | ||||
| 
 | ||||
| ### `docker.env`の編集 | ||||
| 
 | ||||
| このファイルはPostgresqlの設定を記述します。   | ||||
| 最低限記述する必要がある設定は次の通りです。 | ||||
| 
 | ||||
| | 設定                 | 内容         | | ||||
| |---------------------|--------------| | ||||
| | `POSTGRES_PASSWORD` | パスワード    | | ||||
| | `POSTGRES_USER`     | ユーザー名    | | ||||
| | `POSTGRES_DB`       | データベース名 | | ||||
| 
 | ||||
| *3.* Dockerの設定 | ||||
| ---------------------------------------------------------------- | ||||
|  |  | |||
|  | @ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey | |||
| Please install and setup these softwares: | ||||
| 
 | ||||
| #### Dependencies :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** >= 10.0.0 | ||||
| * **[MongoDB](https://www.mongodb.com/)** >= 3.6 | ||||
| * **[Node.js](https://nodejs.org/en/)** >= 11.7.0 | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** >= 10 | ||||
| 
 | ||||
| ##### Optional | ||||
| * [Redis](https://redis.io/) | ||||
|  | @ -31,13 +31,9 @@ Please install and setup these softwares: | |||
| * [Elasticsearch](https://www.elastic.co/) - required to enable the search feature | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* Setup MongoDB | ||||
| *3.* Setup PostgreSQL | ||||
| ---------------------------------------------------------------- | ||||
| As root: | ||||
| 1. `mongo` Go to the mongo shell | ||||
| 2. `use misskey` Use the misskey database | ||||
| 3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Create the misskey user. | ||||
| 4. `exit` You're done! | ||||
| :) | ||||
| 
 | ||||
| *4.* Install Misskey | ||||
| ---------------------------------------------------------------- | ||||
|  | @ -68,7 +64,13 @@ If you're still encountering errors about some modules, use node-gyp: | |||
| 3. `node-gyp build` | ||||
| 4. `NODE_ENV=production npm run build` | ||||
| 
 | ||||
| *7.* That is it. | ||||
| *7.* Init DB | ||||
| ---------------------------------------------------------------- | ||||
| ``` shell | ||||
| npm run init | ||||
| ``` | ||||
| 
 | ||||
| *8.* That is it. | ||||
| ---------------------------------------------------------------- | ||||
| Well done! Now, you have an environment that run to Misskey. | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey | |||
| Installez les paquets suivants : | ||||
| 
 | ||||
| #### Dépendences :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** >= 10.0.0 | ||||
| * **[MongoDB](https://www.mongodb.com/)** >= 3.6 | ||||
| * **[Node.js](https://nodejs.org/en/)** >= 11.7.0 | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** >= 10 | ||||
| 
 | ||||
| ##### Optionnels | ||||
| * [Redis](https://redis.io/) | ||||
|  | @ -31,13 +31,9 @@ Installez les paquets suivants : | |||
| * [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* Paramètrage de MongoDB | ||||
| *3.* Paramètrage de PostgreSQL | ||||
| ---------------------------------------------------------------- | ||||
| En root : | ||||
| 1. `mongo` Ouvrez le shell mongo | ||||
| 2. `use misskey` Utilisez la base de données misskey | ||||
| 3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Créez l'utilisateur misskey. | ||||
| 4. `exit` Vous avez terminé ! | ||||
| :) | ||||
| 
 | ||||
| *4.* Installation de Misskey | ||||
| ---------------------------------------------------------------- | ||||
|  |  | |||
|  | @ -22,8 +22,8 @@ adduser --disabled-password --disabled-login misskey | |||
| これらのソフトウェアをインストール・設定してください: | ||||
| 
 | ||||
| #### 依存関係 :package: | ||||
| * **[Node.js](https://nodejs.org/en/)** (10.0.0以上) | ||||
| * **[MongoDB](https://www.mongodb.com/)** (3.6以上) | ||||
| * **[Node.js](https://nodejs.org/en/)** (11.7.0以上) | ||||
| * **[PostgreSQL](https://www.postgresql.org/)** (10以上) | ||||
| 
 | ||||
| ##### オプション | ||||
| * [Redis](https://redis.io/) | ||||
|  | @ -38,13 +38,9 @@ adduser --disabled-password --disabled-login misskey | |||
| 	* 検索機能を有効にするためにはインストールが必要です。 | ||||
| * [FFmpeg](https://www.ffmpeg.org/) | ||||
| 
 | ||||
| *3.* MongoDBの設定 | ||||
| *3.* PostgreSQLの設定 | ||||
| ---------------------------------------------------------------- | ||||
| ルートで: | ||||
| 1. `mongo` mongoシェルを起動 | ||||
| 2. `use misskey` misskeyデータベースを使用 | ||||
| 3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` misskeyユーザーを作成 | ||||
| 4. `exit` mongoシェルを終了 | ||||
| :) | ||||
| 
 | ||||
| *4.* Misskeyのインストール | ||||
| ---------------------------------------------------------------- | ||||
|  | @ -74,7 +70,13 @@ Debianをお使いであれば、`build-essential`パッケージをインスト | |||
| 3. `node-gyp build` | ||||
| 4. `NODE_ENV=production npm run build` | ||||
| 
 | ||||
| *7.* 以上です! | ||||
| *7.* データベースを初期化 | ||||
| ---------------------------------------------------------------- | ||||
| ``` shell | ||||
| npm run init | ||||
| ``` | ||||
| 
 | ||||
| *8.* 以上です! | ||||
| ---------------------------------------------------------------- | ||||
| お疲れ様でした。これでMisskeyを動かす準備は整いました。 | ||||
| 
 | ||||
|  |  | |||
|  | @ -49,7 +49,6 @@ gulp.task('build:copy:views', () => | |||
| 
 | ||||
| gulp.task('build:copy', gulp.parallel('build:copy:views', () => | ||||
| 	gulp.src([ | ||||
| 		'./build/Release/crypto_key.node', | ||||
| 		'./src/const.json', | ||||
| 		'./src/server/web/views/**/*', | ||||
| 		'./src/**/assets/**/*', | ||||
|  |  | |||
							
								
								
									
										2
									
								
								index.js
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								index.js
									
										
									
									
									
								
							|  | @ -1 +1 @@ | |||
| require('./built'); | ||||
| require('./built').default(); | ||||
|  |  | |||
|  | @ -1238,11 +1238,6 @@ admin/views/instance.vue: | |||
|   save: "保存" | ||||
|   saved: "保存しました" | ||||
|   user-recommendation-config: "おすすめユーザー" | ||||
|   enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする" | ||||
|   external-user-recommendation-engine: "エンジン" | ||||
|   external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" | ||||
|   external-user-recommendation-timeout: "タイムアウト" | ||||
|   external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)" | ||||
|   email-config: "メールサーバーの設定" | ||||
|   email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。" | ||||
|   enable-email: "メール配信を有効にする" | ||||
|  |  | |||
							
								
								
									
										28
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										28
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,8 +1,8 @@ | |||
| { | ||||
| 	"name": "misskey", | ||||
| 	"author": "syuilo <i@syuilo.com>", | ||||
| 	"version": "10.99.0", | ||||
| 	"codename": "nighthike", | ||||
| 	"version": "11.0.0", | ||||
| 	"codename": "daybreak", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "https://github.com/syuilo/misskey.git" | ||||
|  | @ -11,6 +11,7 @@ | |||
| 	"private": true, | ||||
| 	"scripts": { | ||||
| 		"start": "node ./index.js", | ||||
| 		"init": "node ./built/init.js", | ||||
| 		"debug": "DEBUG=misskey:* node ./index.js", | ||||
| 		"build": "webpack && gulp build", | ||||
| 		"webpack": "webpack", | ||||
|  | @ -62,10 +63,9 @@ | |||
| 		"@types/koa-send": "4.1.1", | ||||
| 		"@types/koa-views": "2.0.3", | ||||
| 		"@types/koa__cors": "2.2.3", | ||||
| 		"@types/lolex": "3.1.1", | ||||
| 		"@types/minio": "7.0.1", | ||||
| 		"@types/mkdirp": "0.5.2", | ||||
| 		"@types/mocha": "5.2.5", | ||||
| 		"@types/mongodb": "3.1.20", | ||||
| 		"@types/mocha": "5.2.6", | ||||
| 		"@types/node": "11.10.4", | ||||
| 		"@types/nodemailer": "4.6.6", | ||||
| 		"@types/nprogress": "0.0.29", | ||||
|  | @ -107,6 +107,7 @@ | |||
| 		"chai": "4.2.0", | ||||
| 		"chai-http": "4.2.1", | ||||
| 		"chalk": "2.4.2", | ||||
| 		"cli-highlight": "2.1.0", | ||||
| 		"commander": "2.20.0", | ||||
| 		"content-disposition": "0.5.3", | ||||
| 		"crc-32": "1.2.0", | ||||
|  | @ -114,12 +115,10 @@ | |||
| 		"cssnano": "4.1.10", | ||||
| 		"dateformat": "3.0.3", | ||||
| 		"deep-equal": "1.0.1", | ||||
| 		"deepcopy": "0.6.3", | ||||
| 		"diskusage": "1.0.0", | ||||
| 		"double-ended-queue": "2.1.0-0", | ||||
| 		"elasticsearch": "15.4.1", | ||||
| 		"emojilib": "2.4.0", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"eslint": "5.15.1", | ||||
| 		"eslint-plugin-vue": "5.2.2", | ||||
| 		"eventemitter3": "3.1.0", | ||||
|  | @ -163,23 +162,22 @@ | |||
| 		"koa-views": "6.2.0", | ||||
| 		"langmap": "0.0.16", | ||||
| 		"loader-utils": "1.2.3", | ||||
| 		"lolex": "3.1.0", | ||||
| 		"lookup-dns-cache": "2.1.0", | ||||
| 		"minio": "7.0.5", | ||||
| 		"mkdirp": "0.5.1", | ||||
| 		"mocha": "5.2.0", | ||||
| 		"mocha": "6.0.2", | ||||
| 		"moji": "0.5.1", | ||||
| 		"moment": "2.24.0", | ||||
| 		"mongodb": "3.2.2", | ||||
| 		"monk": "6.0.6", | ||||
| 		"ms": "2.1.1", | ||||
| 		"nan": "2.12.1", | ||||
| 		"nested-property": "0.0.7", | ||||
| 		"node-fetch": "2.3.0", | ||||
| 		"nodemailer": "5.1.1", | ||||
| 		"nprogress": "0.2.0", | ||||
| 		"object-assign-deep": "0.4.0", | ||||
| 		"os-utils": "0.0.14", | ||||
| 		"parse5": "5.1.0", | ||||
| 		"parsimmon": "1.12.0", | ||||
| 		"pg": "7.9.0", | ||||
| 		"portscanner": "2.2.0", | ||||
| 		"postcss-loader": "3.0.0", | ||||
| 		"prismjs": "1.16.0", | ||||
|  | @ -195,10 +193,12 @@ | |||
| 		"recaptcha-promise": "0.1.3", | ||||
| 		"reconnecting-websocket": "4.1.10", | ||||
| 		"redis": "2.8.0", | ||||
| 		"reflect-metadata": "0.1.13", | ||||
| 		"rename": "1.0.4", | ||||
| 		"request": "2.88.0", | ||||
| 		"request-promise-native": "1.0.7", | ||||
| 		"request-stats": "3.0.0", | ||||
| 		"require-all": "3.0.0", | ||||
| 		"rimraf": "2.6.3", | ||||
| 		"rndstr": "1.0.0", | ||||
| 		"s-age": "1.1.2", | ||||
|  | @ -219,12 +219,14 @@ | |||
| 		"tinycolor2": "1.4.1", | ||||
| 		"tmp": "0.0.33", | ||||
| 		"ts-loader": "5.3.3", | ||||
| 		"ts-node": "8.0.3", | ||||
| 		"ts-node": "7.0.1", | ||||
| 		"tslint": "5.13.1", | ||||
| 		"tslint-sonarts": "1.9.0", | ||||
| 		"typeorm": "0.2.16-rc.1", | ||||
| 		"typescript": "3.3.3333", | ||||
| 		"typescript-eslint-parser": "22.0.0", | ||||
| 		"uglify-es": "3.3.9", | ||||
| 		"ulid": "2.3.0", | ||||
| 		"url-loader": "1.1.2", | ||||
| 		"uuid": "3.3.2", | ||||
| 		"v-animate-css": "0.0.3", | ||||
|  |  | |||
							
								
								
									
										19
									
								
								src/@types/deepcopy.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								src/@types/deepcopy.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,19 +0,0 @@ | |||
| declare module 'deepcopy' { | ||||
| 	type DeepcopyCustomizerValueType = 'Object'; | ||||
| 
 | ||||
| 	type DeepcopyCustomizer<T> = ( | ||||
| 		value: T, | ||||
| 		valueType: DeepcopyCustomizerValueType) => T; | ||||
| 
 | ||||
| 	interface IDeepcopyOptions<T> { | ||||
| 		customizer: DeepcopyCustomizer<T>; | ||||
| 	} | ||||
| 
 | ||||
| 	function deepcopy<T>( | ||||
| 		value: T, | ||||
| 		options?: IDeepcopyOptions<T> | DeepcopyCustomizer<T>): T; | ||||
| 
 | ||||
| 	namespace deepcopy {} // Hack
 | ||||
| 
 | ||||
| 	export = deepcopy; | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/@types/escape-regexp.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								src/@types/escape-regexp.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,7 +0,0 @@ | |||
| declare module 'escape-regexp' { | ||||
| 	function escapeRegExp(str: string): string; | ||||
| 
 | ||||
| 	namespace escapeRegExp {} // Hack
 | ||||
| 
 | ||||
| 	export = escapeRegExp; | ||||
| } | ||||
|  | @ -15,5 +15,8 @@ program | |||
| 	.parse(process.argv); | ||||
| 
 | ||||
| if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true; | ||||
| if (process.env.NODE_ENV === 'test') program.disableClustering = true; | ||||
| if (process.env.NODE_ENV === 'test') program.quiet = true; | ||||
| if (process.env.NODE_ENV === 'test') program.noDaemons = true; | ||||
| 
 | ||||
| export { program }; | ||||
|  |  | |||
							
								
								
									
										77
									
								
								src/boot/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/boot/index.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| import * as cluster from 'cluster'; | ||||
| import chalk from 'chalk'; | ||||
| import Xev from 'xev'; | ||||
| 
 | ||||
| import Logger from '../services/logger'; | ||||
| import { program } from '../argv'; | ||||
| 
 | ||||
| // for typeorm
 | ||||
| import 'reflect-metadata'; | ||||
| import { masterMain } from './master'; | ||||
| import { workerMain } from './worker'; | ||||
| 
 | ||||
| const logger = new Logger('core', 'cyan'); | ||||
| const clusterLogger = logger.createSubLogger('cluster', 'orange', false); | ||||
| const ev = new Xev(); | ||||
| 
 | ||||
| /** | ||||
|  * Init process | ||||
|  */ | ||||
| export default async function() { | ||||
| 	process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; | ||||
| 
 | ||||
| 	if (cluster.isMaster || program.disableClustering) { | ||||
| 		await masterMain(); | ||||
| 
 | ||||
| 		if (cluster.isMaster) { | ||||
| 			ev.mount(); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if (cluster.isWorker || program.disableClustering) { | ||||
| 		await workerMain(); | ||||
| 	} | ||||
| 
 | ||||
| 	// ユニットテスト時にMisskeyが子プロセスで起動された時のため
 | ||||
| 	// それ以外のときは process.send は使えないので弾く
 | ||||
| 	if (process.send) { | ||||
| 		process.send('ok'); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| //#region Events
 | ||||
| 
 | ||||
| // Listen new workers
 | ||||
| cluster.on('fork', worker => { | ||||
| 	clusterLogger.debug(`Process forked: [${worker.id}]`); | ||||
| }); | ||||
| 
 | ||||
| // Listen online workers
 | ||||
| cluster.on('online', worker => { | ||||
| 	clusterLogger.debug(`Process is now online: [${worker.id}]`); | ||||
| }); | ||||
| 
 | ||||
| // Listen for dying workers
 | ||||
| cluster.on('exit', worker => { | ||||
| 	// Replace the dead worker,
 | ||||
| 	// we're not sentimental
 | ||||
| 	clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); | ||||
| 	cluster.fork(); | ||||
| }); | ||||
| 
 | ||||
| // Display detail of unhandled promise rejection
 | ||||
| if (!program.quiet) { | ||||
| 	process.on('unhandledRejection', console.dir); | ||||
| } | ||||
| 
 | ||||
| // Display detail of uncaught exception
 | ||||
| process.on('uncaughtException', err => { | ||||
| 	logger.error(err); | ||||
| }); | ||||
| 
 | ||||
| // Dying away...
 | ||||
| process.on('exit', code => { | ||||
| 	logger.info(`The process is going to exit with code ${code}`); | ||||
| }); | ||||
| 
 | ||||
| //#endregion
 | ||||
							
								
								
									
										176
									
								
								src/boot/master.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/boot/master.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,176 @@ | |||
| import * as os from 'os'; | ||||
| import * as cluster from 'cluster'; | ||||
| import chalk from 'chalk'; | ||||
| import * as portscanner from 'portscanner'; | ||||
| import * as isRoot from 'is-root'; | ||||
| 
 | ||||
| import Logger from '../services/logger'; | ||||
| import loadConfig from '../config/load'; | ||||
| import { Config } from '../config/types'; | ||||
| import { lessThan } from '../prelude/array'; | ||||
| import * as pkg from '../../package.json'; | ||||
| import { program } from '../argv'; | ||||
| import { showMachineInfo } from '../misc/show-machine-info'; | ||||
| import { initDb } from '../db/postgre'; | ||||
| 
 | ||||
| const logger = new Logger('core', 'cyan'); | ||||
| const bootLogger = logger.createSubLogger('boot', 'magenta', false); | ||||
| 
 | ||||
| function greet() { | ||||
| 	if (!program.quiet) { | ||||
| 		//#region Misskey logo
 | ||||
| 		const v = `v${pkg.version}`; | ||||
| 		console.log('  _____ _         _           '); | ||||
| 		console.log(' |     |_|___ ___| |_ ___ _ _ '); | ||||
| 		console.log(' | | | | |_ -|_ -| \'_| -_| | |'); | ||||
| 		console.log(' |_|_|_|_|___|___|_,_|___|_  |'); | ||||
| 		console.log(' ' + chalk.gray(v) + ('                        |___|\n'.substr(v.length))); | ||||
| 		//#endregion
 | ||||
| 
 | ||||
| 		console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.'); | ||||
| 		console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo')); | ||||
| 
 | ||||
| 		console.log(''); | ||||
| 		console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`); | ||||
| 	} | ||||
| 
 | ||||
| 	bootLogger.info('Welcome to Misskey!'); | ||||
| 	bootLogger.info(`Misskey v${pkg.version}`, null, true); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Init master process | ||||
|  */ | ||||
| export async function masterMain() { | ||||
| 	greet(); | ||||
| 
 | ||||
| 	let config: Config; | ||||
| 
 | ||||
| 	try { | ||||
| 		// initialize app
 | ||||
| 		config = await init(); | ||||
| 
 | ||||
| 		if (config.port == null) { | ||||
| 			bootLogger.error('The port is not configured. Please configure port.', null, true); | ||||
| 			process.exit(1); | ||||
| 		} | ||||
| 
 | ||||
| 		if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { | ||||
| 			bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); | ||||
| 			process.exit(1); | ||||
| 		} | ||||
| 
 | ||||
| 		if (!await isPortAvailable(config.port)) { | ||||
| 			bootLogger.error(`Port ${config.port} is already in use`, null, true); | ||||
| 			process.exit(1); | ||||
| 		} | ||||
| 	} catch (e) { | ||||
| 		bootLogger.error('Fatal error occurred during initialization', null, true); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| 
 | ||||
| 	bootLogger.succ('Misskey initialized'); | ||||
| 
 | ||||
| 	if (!program.disableClustering) { | ||||
| 		await spawnWorkers(config.clusterLimit); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!program.noDaemons) { | ||||
| 		require('../daemons/server-stats').default(); | ||||
| 		require('../daemons/notes-stats').default(); | ||||
| 		require('../daemons/queue-stats').default(); | ||||
| 	} | ||||
| 
 | ||||
| 	bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); | ||||
| } | ||||
| 
 | ||||
| const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); | ||||
| const requiredNodejsVersion = [11, 7, 0]; | ||||
| const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion); | ||||
| 
 | ||||
| function isWellKnownPort(port: number): boolean { | ||||
| 	return port < 1024; | ||||
| } | ||||
| 
 | ||||
| async function isPortAvailable(port: number): Promise<boolean> { | ||||
| 	return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; | ||||
| } | ||||
| 
 | ||||
| function showEnvironment(): void { | ||||
| 	const env = process.env.NODE_ENV; | ||||
| 	const logger = bootLogger.createSubLogger('env'); | ||||
| 	logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); | ||||
| 
 | ||||
| 	if (env !== 'production') { | ||||
| 		logger.warn('The environment is not in production mode.'); | ||||
| 		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); | ||||
| 	} | ||||
| 
 | ||||
| 	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Init app | ||||
|  */ | ||||
| async function init(): Promise<Config> { | ||||
| 	showEnvironment(); | ||||
| 
 | ||||
| 	const nodejsLogger = bootLogger.createSubLogger('nodejs'); | ||||
| 
 | ||||
| 	nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`); | ||||
| 
 | ||||
| 	if (!satisfyNodejsVersion) { | ||||
| 		nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| 
 | ||||
| 	await showMachineInfo(bootLogger); | ||||
| 
 | ||||
| 	const configLogger = bootLogger.createSubLogger('config'); | ||||
| 	let config; | ||||
| 
 | ||||
| 	try { | ||||
| 		config = loadConfig(); | ||||
| 	} catch (exception) { | ||||
| 		if (typeof exception === 'string') { | ||||
| 			configLogger.error(exception); | ||||
| 			process.exit(1); | ||||
| 		} | ||||
| 		if (exception.code === 'ENOENT') { | ||||
| 			configLogger.error('Configuration file not found', null, true); | ||||
| 			process.exit(1); | ||||
| 		} | ||||
| 		throw exception; | ||||
| 	} | ||||
| 
 | ||||
| 	configLogger.succ('Loaded'); | ||||
| 
 | ||||
| 	// Try to connect to DB
 | ||||
| 	try { | ||||
| 		bootLogger.info('Connecting database...'); | ||||
| 		await initDb(); | ||||
| 	} catch (e) { | ||||
| 		bootLogger.error('Cannot connect to database', null, true); | ||||
| 		bootLogger.error(e); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| 
 | ||||
| 	return config; | ||||
| } | ||||
| 
 | ||||
| async function spawnWorkers(limit: number = Infinity) { | ||||
| 	const workers = Math.min(limit, os.cpus().length); | ||||
| 	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); | ||||
| 	await Promise.all([...Array(workers)].map(spawnWorker)); | ||||
| 	bootLogger.succ('All workers started'); | ||||
| } | ||||
| 
 | ||||
| function spawnWorker(): Promise<void> { | ||||
| 	return new Promise(res => { | ||||
| 		const worker = cluster.fork(); | ||||
| 		worker.on('message', message => { | ||||
| 			if (message !== 'ready') return; | ||||
| 			res(); | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/boot/worker.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/boot/worker.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import * as cluster from 'cluster'; | ||||
| import { initDb } from '../db/postgre'; | ||||
| 
 | ||||
| /** | ||||
|  * Init worker process | ||||
|  */ | ||||
| export async function workerMain() { | ||||
| 	await initDb(); | ||||
| 
 | ||||
| 	// start server
 | ||||
| 	await require('../server').default(); | ||||
| 
 | ||||
| 	// start job queue
 | ||||
| 	require('../queue').default(); | ||||
| 
 | ||||
| 	if (cluster.isWorker) { | ||||
| 		// Send a 'ready' message to parent process
 | ||||
| 		process.send('ready'); | ||||
| 	} | ||||
| } | ||||
|  | @ -48,7 +48,7 @@ | |||
| 							<div> | ||||
| 								<div> | ||||
| 									<span style="margin-right:16px;">{{ file.type }}</span> | ||||
| 									<span>{{ file.datasize | bytes }}</span> | ||||
| 									<span>{{ file.size | bytes }}</span> | ||||
| 								</div> | ||||
| 								<div><mk-time :time="file.createdAt" mode="detail"/></div> | ||||
| 							</div> | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	<ui-card> | ||||
| 		<template #title>{{ $t('hided-tags') }}</template> | ||||
| 		<section> | ||||
| 			<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea> | ||||
| 			<textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea> | ||||
| 			<ui-button @click="save">{{ $t('save') }}</ui-button> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
|  | @ -18,18 +18,18 @@ export default Vue.extend({ | |||
| 	i18n: i18n('admin/views/hashtags.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			hidedTags: '', | ||||
| 			hiddenTags: '', | ||||
| 		}; | ||||
| 	}, | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.hidedTags = meta.hidedTags.join('\n'); | ||||
| 			this.hiddenTags = meta.hiddenTags.join('\n'); | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		save() { | ||||
| 			this.$root.api('admin/update-meta', { | ||||
| 				hidedTags: this.hidedTags.split('\n') | ||||
| 				hiddenTags: this.hiddenTags.split('\n') | ||||
| 			}).then(() => { | ||||
| 				//this.$root.os.apis.dialog({ text: `Saved` }); | ||||
| 			}).catch(e => { | ||||
|  |  | |||
|  | @ -77,12 +77,6 @@ | |||
| 			<header>summaly Proxy</header> | ||||
| 			<ui-input v-model="summalyProxy">URL</ui-input> | ||||
| 		</section> | ||||
| 		<section> | ||||
| 			<header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header> | ||||
| 			<ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch> | ||||
| 			<ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<template #desc>{{ $t('external-user-recommendation-engine-desc') }}</template></ui-input> | ||||
| 			<ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<template #suffix>ms</template><template #desc>{{ $t('external-user-recommendation-timeout-desc') }}</template></ui-input> | ||||
| 		</section> | ||||
| 		<section> | ||||
| 			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> | ||||
| 		</section> | ||||
|  | @ -184,9 +178,6 @@ export default Vue.extend({ | |||
| 			discordClientSecret: null, | ||||
| 			proxyAccount: null, | ||||
| 			inviteCode: null, | ||||
| 			enableExternalUserRecommendation: false, | ||||
| 			externalUserRecommendationEngine: null, | ||||
| 			externalUserRecommendationTimeout: null, | ||||
| 			summalyProxy: null, | ||||
| 			enableEmail: false, | ||||
| 			email: null, | ||||
|  | @ -205,8 +196,8 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	created() { | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.maintainerName = meta.maintainer.name; | ||||
| 			this.maintainerEmail = meta.maintainer.email; | ||||
| 			this.maintainerName = meta.maintainerName; | ||||
| 			this.maintainerEmail = meta.maintainerEmail; | ||||
| 			this.disableRegistration = meta.disableRegistration; | ||||
| 			this.disableLocalTimeline = meta.disableLocalTimeline; | ||||
| 			this.disableGlobalTimeline = meta.disableGlobalTimeline; | ||||
|  | @ -236,9 +227,6 @@ export default Vue.extend({ | |||
| 			this.enableDiscordIntegration = meta.enableDiscordIntegration; | ||||
| 			this.discordClientId = meta.discordClientId; | ||||
| 			this.discordClientSecret = meta.discordClientSecret; | ||||
| 			this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation; | ||||
| 			this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine; | ||||
| 			this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout; | ||||
| 			this.summalyProxy = meta.summalyProxy; | ||||
| 			this.enableEmail = meta.enableEmail; | ||||
| 			this.email = meta.email; | ||||
|  | @ -299,9 +287,6 @@ export default Vue.extend({ | |||
| 				enableDiscordIntegration: this.enableDiscordIntegration, | ||||
| 				discordClientId: this.discordClientId, | ||||
| 				discordClientSecret: this.discordClientSecret, | ||||
| 				enableExternalUserRecommendation: this.enableExternalUserRecommendation, | ||||
| 				externalUserRecommendationEngine: this.externalUserRecommendationEngine, | ||||
| 				externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10), | ||||
| 				summalyProxy: this.summalyProxy, | ||||
| 				enableEmail: this.enableEmail, | ||||
| 				email: this.email, | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
| 			</ui-horizon-group> | ||||
| 
 | ||||
| 			<div class="nqjzuvev"> | ||||
| 				<code v-for="log in logs" :key="log._id" :class="log.level"> | ||||
| 				<code v-for="log in logs" :key="log.id" :class="log.level"> | ||||
| 					<details> | ||||
| 						<summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> | ||||
| 						<vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> | ||||
|  |  | |||
|  | @ -165,7 +165,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		/** 処理対象ユーザーの情報を更新する */ | ||||
| 		async refreshUser() { | ||||
| 			this.$root.api('admin/show-user', { userId: this.user._id }).then(info => { | ||||
| 			this.$root.api('admin/show-user', { userId: this.user.id }).then(info => { | ||||
| 				this.user = info; | ||||
| 			}); | ||||
| 		}, | ||||
|  | @ -173,7 +173,7 @@ export default Vue.extend({ | |||
| 		async resetPassword() { | ||||
| 			if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; | ||||
| 
 | ||||
| 			this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => { | ||||
| 			this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('password-updated', { password: res.password }) | ||||
|  | @ -187,7 +187,7 @@ export default Vue.extend({ | |||
| 			this.verifying = true; | ||||
| 
 | ||||
| 			const process = async () => { | ||||
| 				await this.$root.api('admin/verify-user', { userId: this.user._id }); | ||||
| 				await this.$root.api('admin/verify-user', { userId: this.user.id }); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('verified') | ||||
|  | @ -212,7 +212,7 @@ export default Vue.extend({ | |||
| 			this.unverifying = true; | ||||
| 
 | ||||
| 			const process = async () => { | ||||
| 				await this.$root.api('admin/unverify-user', { userId: this.user._id }); | ||||
| 				await this.$root.api('admin/unverify-user', { userId: this.user.id }); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('unverified') | ||||
|  | @ -233,7 +233,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		async silenceUser() { | ||||
| 			const process = async () => { | ||||
| 				await this.$root.api('admin/silence-user', { userId: this.user._id }); | ||||
| 				await this.$root.api('admin/silence-user', { userId: this.user.id }); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					splash: true | ||||
|  | @ -252,7 +252,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		async unsilenceUser() { | ||||
| 			const process = async () => { | ||||
| 				await this.$root.api('admin/unsilence-user', { userId: this.user._id }); | ||||
| 				await this.$root.api('admin/unsilence-user', { userId: this.user.id }); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					splash: true | ||||
|  | @ -275,7 +275,7 @@ export default Vue.extend({ | |||
| 			this.suspending = true; | ||||
| 
 | ||||
| 			const process = async () => { | ||||
| 				await this.$root.api('admin/suspend-user', { userId: this.user._id }); | ||||
| 				await this.$root.api('admin/suspend-user', { userId: this.user.id }); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('suspended') | ||||
|  | @ -300,7 +300,7 @@ export default Vue.extend({ | |||
| 			this.unsuspending = true; | ||||
| 
 | ||||
| 			const process = async () => { | ||||
| 				await this.$root.api('admin/unsuspend-user', { userId: this.user._id }); | ||||
| 				await this.$root.api('admin/unsuspend-user', { userId: this.user.id }); | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('unsuspended') | ||||
|  | @ -320,7 +320,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		async updateRemoteUser() { | ||||
| 			this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => { | ||||
| 			this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('remote-user-updated') | ||||
|  |  | |||
|  | @ -14,15 +14,15 @@ | |||
| 			<h2>{{ $t('permission-ask') }}</h2> | ||||
| 			<ul> | ||||
| 				<template v-for="p in app.permission"> | ||||
| 					<li v-if="p == 'account-read'">{{ $t('account-read') }}</li> | ||||
| 					<li v-if="p == 'account-write'">{{ $t('account-write') }}</li> | ||||
| 					<li v-if="p == 'note-write'">{{ $t('note-write') }}</li> | ||||
| 					<li v-if="p == 'read:account'">{{ $t('read:account') }}</li> | ||||
| 					<li v-if="p == 'write:account'">{{ $t('write:account') }}</li> | ||||
| 					<li v-if="p == 'write:notes'">{{ $t('write:notes') }}</li> | ||||
| 					<li v-if="p == 'like-write'">{{ $t('like-write') }}</li> | ||||
| 					<li v-if="p == 'following-write'">{{ $t('following-write') }}</li> | ||||
| 					<li v-if="p == 'drive-read'">{{ $t('drive-read') }}</li> | ||||
| 					<li v-if="p == 'drive-write'">{{ $t('drive-write') }}</li> | ||||
| 					<li v-if="p == 'notification-read'">{{ $t('notification-read') }}</li> | ||||
| 					<li v-if="p == 'notification-write'">{{ $t('notification-write') }}</li> | ||||
| 					<li v-if="p == 'write:following'">{{ $t('write:following') }}</li> | ||||
| 					<li v-if="p == 'read:drive'">{{ $t('read:drive') }}</li> | ||||
| 					<li v-if="p == 'write:drive'">{{ $t('write:drive') }}</li> | ||||
| 					<li v-if="p == 'read:notifications'">{{ $t('read:notifications') }}</li> | ||||
| 					<li v-if="p == 'write:notifications'">{{ $t('write:notifications') }}</li> | ||||
| 				</template> | ||||
| 			</ul> | ||||
| 		</section> | ||||
|  |  | |||
|  | @ -45,15 +45,9 @@ export default function <T extends object>(data: { | |||
| 			this.$watch('props', () => { | ||||
| 				this.mergeProps(); | ||||
| 			}); | ||||
| 
 | ||||
| 			this.bakeProps(); | ||||
| 		}, | ||||
| 
 | ||||
| 		methods: { | ||||
| 			bakeProps() { | ||||
| 				this.bakedOldProps = JSON.stringify(this.props); | ||||
| 			}, | ||||
| 
 | ||||
| 			mergeProps() { | ||||
| 				if (data.props) { | ||||
| 					const defaultProps = data.props(); | ||||
|  | @ -65,17 +59,10 @@ export default function <T extends object>(data: { | |||
| 			}, | ||||
| 
 | ||||
| 			save() { | ||||
| 				if (this.bakedOldProps == JSON.stringify(this.props)) return; | ||||
| 
 | ||||
| 				this.bakeProps(); | ||||
| 
 | ||||
| 				if (this.platform == 'deck') { | ||||
| 					this.$store.commit('device/updateDeckColumn', this.column); | ||||
| 				} else { | ||||
| 					this.$root.api('i/update_widget', { | ||||
| 						id: this.id, | ||||
| 						data: this.props | ||||
| 					}); | ||||
| 					this.$store.commit('device/updateWidget', this.widget); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -70,8 +70,8 @@ export default (opts: Opts = {}) => ({ | |||
| 		}, | ||||
| 
 | ||||
| 		reactionsCount(): number { | ||||
| 			return this.appearNote.reactionCounts | ||||
| 				? sum(Object.values(this.appearNote.reactionCounts)) | ||||
| 			return this.appearNote.reactions | ||||
| 				? sum(Object.values(this.appearNote.reactions)) | ||||
| 				: 0; | ||||
| 		}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -87,16 +87,16 @@ export default prop => ({ | |||
| 				case 'reacted': { | ||||
| 					const reaction = body.reaction; | ||||
| 
 | ||||
| 					if (this.$_ns_target.reactionCounts == null) { | ||||
| 						Vue.set(this.$_ns_target, 'reactionCounts', {}); | ||||
| 					if (this.$_ns_target.reactions == null) { | ||||
| 						Vue.set(this.$_ns_target, 'reactions', {}); | ||||
| 					} | ||||
| 
 | ||||
| 					if (this.$_ns_target.reactionCounts[reaction] == null) { | ||||
| 						Vue.set(this.$_ns_target.reactionCounts, reaction, 0); | ||||
| 					if (this.$_ns_target.reactions[reaction] == null) { | ||||
| 						Vue.set(this.$_ns_target.reactions, reaction, 0); | ||||
| 					} | ||||
| 
 | ||||
| 					// Increment the count
 | ||||
| 					this.$_ns_target.reactionCounts[reaction]++; | ||||
| 					this.$_ns_target.reactions[reaction]++; | ||||
| 
 | ||||
| 					if (body.userId == this.$store.state.i.id) { | ||||
| 						Vue.set(this.$_ns_target, 'myReaction', reaction); | ||||
|  | @ -107,16 +107,16 @@ export default prop => ({ | |||
| 				case 'unreacted': { | ||||
| 					const reaction = body.reaction; | ||||
| 
 | ||||
| 					if (this.$_ns_target.reactionCounts == null) { | ||||
| 					if (this.$_ns_target.reactions == null) { | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 					if (this.$_ns_target.reactionCounts[reaction] == null) { | ||||
| 					if (this.$_ns_target.reactions[reaction] == null) { | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 					// Decrement the count
 | ||||
| 					if (this.$_ns_target.reactionCounts[reaction] > 0) this.$_ns_target.reactionCounts[reaction]--; | ||||
| 					if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--; | ||||
| 
 | ||||
| 					if (body.userId == this.$store.state.i.id) { | ||||
| 						Vue.set(this.$_ns_target, 'myReaction', null); | ||||
|  | @ -125,9 +125,11 @@ export default prop => ({ | |||
| 				} | ||||
| 
 | ||||
| 				case 'pollVoted': { | ||||
| 					if (body.userId == this.$store.state.i.id) return; | ||||
| 					const choice = body.choice; | ||||
| 					this.$_ns_target.poll.choices.find(c => c.id === choice).votes++; | ||||
| 					this.$_ns_target.poll.choices[choice].votes++; | ||||
| 					if (body.userId == this.$store.state.i.id) { | ||||
| 						Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true); | ||||
| 					} | ||||
| 					break; | ||||
| 				} | ||||
| 
 | ||||
|  |  | |||
|  | @ -55,10 +55,11 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 		icon(): any { | ||||
| 			return { | ||||
| 				backgroundColor: this.lightmode | ||||
| 					? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` | ||||
| 					: this.user.avatarColor && this.user.avatarColor.length == 3 | ||||
| 						? `rgb(${this.user.avatarColor.join(',')})` | ||||
| 				backgroundColor: this.user.avatarColor ? this.lightmode | ||||
| 					? this.user.avatarColor | ||||
| 					: this.user.avatarColor.startsWith('rgb(') | ||||
| 						? this.user.avatarColor | ||||
| 						: null | ||||
| 					: null, | ||||
| 				backgroundImage: this.lightmode ? null : `url(${this.url})`, | ||||
| 				borderRadius: this.$store.state.settings.circleIcons ? '100%' : null | ||||
|  | @ -67,7 +68,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.user.avatarColor) { | ||||
| 			this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`; | ||||
| 			this.$el.style.color = this.user.avatarColor; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -24,11 +24,11 @@ | |||
| 
 | ||||
| 	<div class="board"> | ||||
| 		<div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> | ||||
| 			<span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> | ||||
| 			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> | ||||
| 		</div> | ||||
| 		<div class="flex"> | ||||
| 			<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> | ||||
| 				<div v-for="i in game.settings.map.length">{{ i }}</div> | ||||
| 				<div v-for="i in game.map.length">{{ i }}</div> | ||||
| 			</div> | ||||
| 			<div class="cells" :style="cellsStyle"> | ||||
| 				<div v-for="(stone, i) in o.board" | ||||
|  | @ -46,11 +46,11 @@ | |||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> | ||||
| 				<div v-for="i in game.settings.map.length">{{ i }}</div> | ||||
| 				<div v-for="i in game.map.length">{{ i }}</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> | ||||
| 			<span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> | ||||
| 			<span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
|  | @ -71,9 +71,9 @@ | |||
| 	</div> | ||||
| 
 | ||||
| 	<div class="info"> | ||||
| 		<p v-if="game.settings.isLlotheo">{{ $t('is-llotheo') }}</p> | ||||
| 		<p v-if="game.settings.loopedBoard">{{ $t('looped-map') }}</p> | ||||
| 		<p v-if="game.settings.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> | ||||
| 		<p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p> | ||||
| 		<p v-if="game.loopedBoard">{{ $t('looped-map') }}</p> | ||||
| 		<p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
|  | @ -160,8 +160,8 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		cellsStyle(): any { | ||||
| 			return { | ||||
| 				'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`, | ||||
| 				'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)` | ||||
| 				'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`, | ||||
| 				'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)` | ||||
| 			}; | ||||
| 		} | ||||
| 	}, | ||||
|  | @ -169,10 +169,10 @@ export default Vue.extend({ | |||
| 	watch: { | ||||
| 		logPos(v) { | ||||
| 			if (!this.game.isEnded) return; | ||||
| 			this.o = new Reversi(this.game.settings.map, { | ||||
| 				isLlotheo: this.game.settings.isLlotheo, | ||||
| 				canPutEverywhere: this.game.settings.canPutEverywhere, | ||||
| 				loopedBoard: this.game.settings.loopedBoard | ||||
| 			this.o = new Reversi(this.game.map, { | ||||
| 				isLlotheo: this.game.isLlotheo, | ||||
| 				canPutEverywhere: this.game.canPutEverywhere, | ||||
| 				loopedBoard: this.game.loopedBoard | ||||
| 			}); | ||||
| 			for (const log of this.logs.slice(0, v)) { | ||||
| 				this.o.put(log.color, log.pos); | ||||
|  | @ -184,10 +184,10 @@ export default Vue.extend({ | |||
| 	created() { | ||||
| 		this.game = this.initGame; | ||||
| 
 | ||||
| 		this.o = new Reversi(this.game.settings.map, { | ||||
| 			isLlotheo: this.game.settings.isLlotheo, | ||||
| 			canPutEverywhere: this.game.settings.canPutEverywhere, | ||||
| 			loopedBoard: this.game.settings.loopedBoard | ||||
| 		this.o = new Reversi(this.game.map, { | ||||
| 			isLlotheo: this.game.isLlotheo, | ||||
| 			canPutEverywhere: this.game.canPutEverywhere, | ||||
| 			loopedBoard: this.game.loopedBoard | ||||
| 		}); | ||||
| 
 | ||||
| 		for (const log of this.game.logs) { | ||||
|  | @ -286,10 +286,10 @@ export default Vue.extend({ | |||
| 		onRescue(game) { | ||||
| 			this.game = game; | ||||
| 
 | ||||
| 			this.o = new Reversi(this.game.settings.map, { | ||||
| 				isLlotheo: this.game.settings.isLlotheo, | ||||
| 				canPutEverywhere: this.game.settings.canPutEverywhere, | ||||
| 				loopedBoard: this.game.settings.loopedBoard | ||||
| 			this.o = new Reversi(this.game.map, { | ||||
| 				isLlotheo: this.game.isLlotheo, | ||||
| 				canPutEverywhere: this.game.canPutEverywhere, | ||||
| 				loopedBoard: this.game.loopedBoard | ||||
| 			}); | ||||
| 
 | ||||
| 			for (const log of this.game.logs) { | ||||
|  |  | |||
|  | @ -17,9 +17,9 @@ | |||
| 			</header> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class="random" v-if="game.settings.map == null"><fa icon="dice"/></div> | ||||
| 				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> | ||||
| 					<div v-for="(x, i) in game.settings.map.join('')" | ||||
| 				<div class="random" v-if="game.map == null"><fa icon="dice"/></div> | ||||
| 				<div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> | ||||
| 					<div v-for="(x, i) in game.map.join('')" | ||||
| 							:data-none="x == ' '" | ||||
| 							@click="onPixelClick(i, x)"> | ||||
| 						<fa v-if="x == 'b'" :icon="fasCircle"/> | ||||
|  | @ -35,9 +35,9 @@ | |||
| 			</header> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio> | ||||
| 				<form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> | ||||
| 				<form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> | ||||
| 				<form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio> | ||||
| 				<form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> | ||||
| 				<form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
|  | @ -47,9 +47,9 @@ | |||
| 			</header> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">{{ $t('is-llotheo') }}</ui-switch> | ||||
| 				<ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">{{ $t('looped-map') }}</ui-switch> | ||||
| 				<ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">{{ $t('can-put-everywhere') }}</ui-switch> | ||||
| 				<ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch> | ||||
| 				<ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch> | ||||
| 				<ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
|  | @ -159,8 +159,8 @@ export default Vue.extend({ | |||
| 		this.connection.on('initForm', this.onInitForm); | ||||
| 		this.connection.on('message', this.onMessage); | ||||
| 
 | ||||
| 		if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1; | ||||
| 		if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2; | ||||
| 		if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1; | ||||
| 		if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2; | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeDestroy() { | ||||
|  | @ -189,18 +189,19 @@ export default Vue.extend({ | |||
| 			this.$forceUpdate(); | ||||
| 		}, | ||||
| 
 | ||||
| 		updateSettings() { | ||||
| 		updateSettings(key: string) { | ||||
| 			this.connection.send('updateSettings', { | ||||
| 				settings: this.game.settings | ||||
| 				key: key, | ||||
| 				value: this.game[key] | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		onUpdateSettings(settings) { | ||||
| 			this.game.settings = settings; | ||||
| 			if (this.game.settings.map == null) { | ||||
| 		onUpdateSettings({ key, value }) { | ||||
| 			this.game[key] = value; | ||||
| 			if (this.game.map == null) { | ||||
| 				this.mapName = null; | ||||
| 			} else { | ||||
| 				const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join('')); | ||||
| 				const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join('')); | ||||
| 				this.mapName = found ? found.name : '-Custom-'; | ||||
| 			} | ||||
| 		}, | ||||
|  | @ -224,27 +225,27 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		onMapChange() { | ||||
| 			if (this.mapName == null) { | ||||
| 				this.game.settings.map = null; | ||||
| 				this.game.map = null; | ||||
| 			} else { | ||||
| 				this.game.settings.map = Object.values(maps).find(x => x.name == this.mapName).data; | ||||
| 				this.game.map = Object.values(maps).find(x => x.name == this.mapName).data; | ||||
| 			} | ||||
| 			this.$forceUpdate(); | ||||
| 			this.updateSettings(); | ||||
| 		}, | ||||
| 
 | ||||
| 		onPixelClick(pos, pixel) { | ||||
| 			const x = pos % this.game.settings.map[0].length; | ||||
| 			const y = Math.floor(pos / this.game.settings.map[0].length); | ||||
| 			const x = pos % this.game.map[0].length; | ||||
| 			const y = Math.floor(pos / this.game.map[0].length); | ||||
| 			const newPixel = | ||||
| 				pixel == ' ' ? '-' : | ||||
| 				pixel == '-' ? 'b' : | ||||
| 				pixel == 'b' ? 'w' : | ||||
| 				' '; | ||||
| 			const line = this.game.settings.map[y].split(''); | ||||
| 			const line = this.game.map[y].split(''); | ||||
| 			line[x] = newPixel; | ||||
| 			this.$set(this.game.settings.map, y, line.join('')); | ||||
| 			this.$set(this.game.map, y, line.join('')); | ||||
| 			this.$forceUpdate(); | ||||
| 			this.updateSettings(); | ||||
| 			this.updateSettings('map'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -106,7 +106,7 @@ export default Vue.extend({ | |||
| 		async nav(game, actualNav = true) { | ||||
| 			if (this.selfNav) { | ||||
| 				// 受け取ったゲーム情報が省略されたものなら完全な情報を取得する | ||||
| 				if (game != null && (game.settings == null || game.settings.map == null)) { | ||||
| 				if (game != null && game.map == null) { | ||||
| 					game = await this.$root.api('games/reversi/games/show', { | ||||
| 						gameId: game.id | ||||
| 					}); | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta"> | ||||
| 	<div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div> | ||||
| 
 | ||||
| 	<h1>{{ meta.name }}</h1> | ||||
| 	<h1>{{ meta.name || 'Misskey' }}</h1> | ||||
| 	<p v-html="meta.description || this.$t('@.about')"></p> | ||||
| 	<router-link to="/">{{ $t('start') }}</router-link> | ||||
| </div> | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	computed: { | ||||
| 		canonical(): string { | ||||
| 			return `@${this.username}@${toUnicode(this.host)}`; | ||||
| 			return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`; | ||||
| 		}, | ||||
| 		isMe(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.canonical.toLowerCase() === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase(); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div class="mk-poll" :data-done="closed || isVoted"> | ||||
| 	<ul> | ||||
| 		<li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> | ||||
| 		<li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> | ||||
| 			<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> | ||||
| 			<span> | ||||
| 				<template v-if="choice.isVoted"><fa icon="check"/></template> | ||||
|  | @ -82,12 +82,6 @@ export default Vue.extend({ | |||
| 				noteId: this.note.id, | ||||
| 				choice: id | ||||
| 			}).then(() => { | ||||
| 				for (const c of this.poll.choices) { | ||||
| 					if (c.id == id) { | ||||
| 						c.votes++; | ||||
| 						Vue.set(c, 'isVoted', true); | ||||
| 					} | ||||
| 				} | ||||
| 				if (!this.showResult) this.showResult = !this.poll.multiple; | ||||
| 			}); | ||||
| 		} | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	computed: { | ||||
| 		reactions(): any { | ||||
| 			return this.note.reactionCounts; | ||||
| 			return this.note.reactions; | ||||
| 		}, | ||||
| 		isMe(): boolean { | ||||
| 			return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| <ui-card> | ||||
| 	<template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template> | ||||
| 	<section> | ||||
| 		<ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch"> | ||||
| 		<ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch"> | ||||
| 			{{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template> | ||||
| 		</ui-switch> | ||||
| 		<section> | ||||
|  |  | |||
|  | @ -158,14 +158,14 @@ export default Vue.extend({ | |||
| 
 | ||||
| 	computed: { | ||||
| 		alwaysMarkNsfw: { | ||||
| 			get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, | ||||
| 			get() { return this.$store.state.i.alwaysMarkNsfw; }, | ||||
| 			set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); } | ||||
| 		}, | ||||
| 
 | ||||
| 		bannerStyle(): any { | ||||
| 			if (this.$store.state.i.bannerUrl == null) return {}; | ||||
| 			return { | ||||
| 				backgroundColor: this.$store.state.i.bannerColor && this.$store.state.i.bannerColor.length == 3 ? `rgb(${ this.$store.state.i.bannerColor.join(',') })` : null, | ||||
| 				backgroundColor: this.$store.state.i.bannerColor ? this.$store.state.i.bannerColor : null, | ||||
| 				backgroundImage: `url(${ this.$store.state.i.bannerUrl })` | ||||
| 			}; | ||||
| 		}, | ||||
|  | @ -178,10 +178,10 @@ export default Vue.extend({ | |||
| 		this.email = this.$store.state.i.email; | ||||
| 		this.name = this.$store.state.i.name; | ||||
| 		this.username = this.$store.state.i.username; | ||||
| 		this.location = this.$store.state.i.profile.location; | ||||
| 		this.location = this.$store.state.i.location; | ||||
| 		this.description = this.$store.state.i.description; | ||||
| 		this.lang = this.$store.state.i.lang; | ||||
| 		this.birthday = this.$store.state.i.profile.birthday; | ||||
| 		this.birthday = this.$store.state.i.birthday; | ||||
| 		this.avatarId = this.$store.state.i.avatarId; | ||||
| 		this.bannerId = this.$store.state.i.bannerId; | ||||
| 		this.isCat = this.$store.state.i.isCat; | ||||
|  |  | |||
|  | @ -130,20 +130,6 @@ import * as tinycolor from 'tinycolor2'; | |||
| import * as JSON5 from 'json5'; | ||||
| import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; | ||||
| 
 | ||||
| // 後方互換性のため | ||||
| function convertOldThemedefinition(t) { | ||||
| 	const t2 = { | ||||
| 		id: t.meta.id, | ||||
| 		name: t.meta.name, | ||||
| 		author: t.meta.author, | ||||
| 		base: t.meta.base, | ||||
| 		vars: t.meta.vars, | ||||
| 		props: t | ||||
| 	}; | ||||
| 	delete t2.props.meta; | ||||
| 	return t2; | ||||
| } | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/theme.vue'), | ||||
| 	components: { | ||||
|  | @ -231,20 +217,6 @@ export default Vue.extend({ | |||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	beforeCreate() { | ||||
| 		// migrate old theme definitions | ||||
| 		// 後方互換性のため | ||||
| 		this.$store.commit('device/set', { | ||||
| 			key: 'themes', value: this.$store.state.device.themes.map(t => { | ||||
| 				if (t.id == null) { | ||||
| 					return convertOldThemedefinition(t); | ||||
| 				} else { | ||||
| 					return t; | ||||
| 				} | ||||
| 			}) | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		install(code) { | ||||
| 			let theme; | ||||
|  | @ -259,11 +231,6 @@ export default Vue.extend({ | |||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			// 後方互換性のため | ||||
| 			if (theme.id == null && theme.meta != null) { | ||||
| 				theme = convertOldThemedefinition(theme); | ||||
| 			} | ||||
| 
 | ||||
| 			if (theme.id == null) { | ||||
| 				this.$root.dialog({ | ||||
| 					type: 'error', | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 		<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> | ||||
| 			<span>{{ $t('invitation-code') }}</span> | ||||
| 			<template #prefix><fa icon="id-card-alt"/></template> | ||||
| 			<template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainer.email)"></template> | ||||
| 			<template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template> | ||||
| 		</ui-input> | ||||
| 		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> | ||||
| 			<span>{{ $t('username') }}</span> | ||||
|  |  | |||
|  | @ -4,9 +4,9 @@ | |||
| 	<p class="empty" v-else-if="stats.length == 0"><fa icon="exclamation-circle"/>{{ $t('empty') }}</p> | ||||
| 	<!-- トランジションを有効にするとなぜかメモリリークする --> | ||||
| 	<transition-group v-else tag="div" name="chart"> | ||||
| 		<div v-for="stat in stats" :key="stat.tag"> | ||||
| 		<div v-for="stat in stats" :key="stat.name"> | ||||
| 			<div class="tag"> | ||||
| 				<router-link :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</router-link> | ||||
| 				<router-link :to="`/tags/${ encodeURIComponent(stat.name) }`" :title="stat.name">#{{ stat.name }}</router-link> | ||||
| 				<p>{{ $t('count').replace('{}', stat.usersCount) }}</p> | ||||
| 			</div> | ||||
| 			<x-chart class="chart" :src="stat.chart"/> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <template> | ||||
| <div class="cudqjmnl"> | ||||
| 	<ui-card> | ||||
| 		<template #title><fa :icon="faList"/> {{ list.title }}</template> | ||||
| 		<template #title><fa :icon="faList"/> {{ list.name }}</template> | ||||
| 
 | ||||
| 		<section> | ||||
| 			<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> | ||||
|  | @ -75,7 +75,7 @@ export default Vue.extend({ | |||
| 			this.$root.dialog({ | ||||
| 				title: this.$t('rename'), | ||||
| 				input: { | ||||
| 					default: this.list.title | ||||
| 					default: this.list.name | ||||
| 				} | ||||
| 			}).then(({ canceled, result: title }) => { | ||||
| 				if (canceled) return; | ||||
|  | @ -89,7 +89,7 @@ export default Vue.extend({ | |||
| 		del() { | ||||
| 			this.$root.dialog({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$t('delete-are-you-sure').replace('$1', this.list.title), | ||||
| 				text: this.$t('delete-are-you-sure').replace('$1', this.list.name), | ||||
| 				showCancelButton: true | ||||
| 			}).then(({ canceled }) => { | ||||
| 				if (canceled) return; | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ export default Vue.extend({ | |||
| 				title: t, | ||||
| 				select: { | ||||
| 					items: lists.map(list => ({ | ||||
| 						value: list.id, text: list.title | ||||
| 						value: list.id, text: list.name | ||||
| 					})) | ||||
| 				}, | ||||
| 				showCancelButton: true | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| <x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'social'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
| <x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/> | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ export default Vue.extend({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			connection: null, | ||||
| 			makePromise: cursor => this.$root.api('notes/search_by_tag', { | ||||
| 			makePromise: cursor => this.$root.api('notes/search-by-tag', { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: cursor ? cursor : undefined, | ||||
| 				withFiles: this.mediaOnly, | ||||
|  |  | |||
|  | @ -62,7 +62,7 @@ | |||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> | ||||
| 	<div class="notification pollVote" v-if="notification.type == 'pollVote'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	<template #header> | ||||
| 		<fa v-if="column.type == 'home'" icon="home"/> | ||||
| 		<fa v-if="column.type == 'local'" :icon="['far', 'comments']"/> | ||||
| 		<fa v-if="column.type == 'hybrid'" icon="share-alt"/> | ||||
| 		<fa v-if="column.type == 'social'" icon="share-alt"/> | ||||
| 		<fa v-if="column.type == 'global'" icon="globe"/> | ||||
| 		<fa v-if="column.type == 'list'" icon="list"/> | ||||
| 		<fa v-if="column.type == 'hashtag'" icon="hashtag"/> | ||||
|  | @ -80,9 +80,9 @@ export default Vue.extend({ | |||
| 			switch (this.column.type) { | ||||
| 				case 'home': return this.$t('@deck.home'); | ||||
| 				case 'local': return this.$t('@deck.local'); | ||||
| 				case 'hybrid': return this.$t('@deck.hybrid'); | ||||
| 				case 'social': return this.$t('@deck.social'); | ||||
| 				case 'global': return this.$t('@deck.global'); | ||||
| 				case 'list': return this.column.list.title; | ||||
| 				case 'list': return this.column.list.name; | ||||
| 				case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ export default Vue.extend({ | |||
| 			switch (this.src) { | ||||
| 				case 'home': return this.$root.stream.useSharedConnection('homeTimeline'); | ||||
| 				case 'local': return this.$root.stream.useSharedConnection('localTimeline'); | ||||
| 				case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline'); | ||||
| 				case 'social': return this.$root.stream.useSharedConnection('socialTimeline'); | ||||
| 				case 'global': return this.$root.stream.useSharedConnection('globalTimeline'); | ||||
| 			} | ||||
| 		}, | ||||
|  | @ -60,7 +60,7 @@ export default Vue.extend({ | |||
| 			switch (this.src) { | ||||
| 				case 'home': return 'notes/timeline'; | ||||
| 				case 'local': return 'notes/local-timeline'; | ||||
| 				case 'hybrid': return 'notes/hybrid-timeline'; | ||||
| 				case 'social': return 'notes/social-timeline'; | ||||
| 				case 'global': return 'notes/global-timeline'; | ||||
| 			} | ||||
| 		}, | ||||
|  | @ -107,7 +107,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && ( | ||||
| 				meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) || | ||||
| 				meta.disableLocalTimeline && ['local', 'social'].includes(this.src) || | ||||
| 				meta.disableGlobalTimeline && ['global'].includes(this.src)); | ||||
| 		}); | ||||
| 	}, | ||||
|  |  | |||
|  | @ -106,16 +106,6 @@ export default Vue.extend({ | |||
| 				value: deck | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		// 互換性のため | ||||
| 		if (this.$store.state.device.deck != null && this.$store.state.device.deck.layout == null) { | ||||
| 			this.$store.commit('device/set', { | ||||
| 				key: 'deck', | ||||
| 				value: Object.assign({}, this.$store.state.device.deck, { | ||||
| 					layout: this.$store.state.device.deck.columns.map(c => [c.id]) | ||||
| 				}) | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	mounted() { | ||||
|  | @ -155,11 +145,11 @@ export default Vue.extend({ | |||
| 					} | ||||
| 				}, { | ||||
| 					icon: 'share-alt', | ||||
| 					text: this.$t('@deck.hybrid'), | ||||
| 					text: this.$t('@deck.social'), | ||||
| 					action: () => { | ||||
| 						this.$store.commit('device/addDeckColumn', { | ||||
| 							id: uuid(), | ||||
| 							type: 'hybrid' | ||||
| 							type: 'social' | ||||
| 						}); | ||||
| 					} | ||||
| 				}, { | ||||
|  | @ -199,7 +189,7 @@ export default Vue.extend({ | |||
| 							title: this.$t('@deck.select-list'), | ||||
| 							select: { | ||||
| 								items: lists.map(list => ({ | ||||
| 									value: list.id, text: list.title | ||||
| 									value: list.id, text: list.name | ||||
| 								})) | ||||
| 							}, | ||||
| 							showCancelButton: true | ||||
|  | @ -312,7 +302,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 		isTlColumn(id) { | ||||
| 			const column = this.columns.find(c => c.id === id); | ||||
| 			return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type); | ||||
| 			return ['home', 'local', 'social', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| 	<ui-container :show-header="false" v-if="meta && stats"> | ||||
| 		<div class="kpdsmpnk" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> | ||||
| 			<div> | ||||
| 				<router-link to="/explore" class="title">{{ $t('explore', { host: meta.name }) }}</router-link> | ||||
| 				<router-link to="/explore" class="title">{{ $t('explore', { host: meta.name || 'Misskey' }) }}</router-link> | ||||
| 				<span>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</span> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | @ -13,8 +13,8 @@ | |||
| 		<template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template> | ||||
| 
 | ||||
| 		<div class="vxjfqztj"> | ||||
| 			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link> | ||||
| 			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link> | ||||
| 			<router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.name}`" :key="'local:' + tag.name" class="local">{{ tag.name }}</router-link> | ||||
| 			<router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.name}`" :key="'remote:' + tag.name">{{ tag.name }}</router-link> | ||||
| 		</div> | ||||
| 	</ui-container> | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,20 +9,30 @@ import Vue from 'vue'; | |||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import i18n from '../../../i18n'; | ||||
| 
 | ||||
| const fetchLimit = 30; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(''), | ||||
| 	i18n: i18n(), | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			makePromise: cursor => this.$root.api('users/followers', { | ||||
| 				...parseAcct(this.$route.params.user), | ||||
| 				limit: 30, | ||||
| 				cursor: cursor ? cursor : undefined | ||||
| 			}).then(x => { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: cursor ? cursor : undefined, | ||||
| 			}).then(followings => { | ||||
| 				if (followings.length == fetchLimit + 1) { | ||||
| 					followings.pop(); | ||||
| 					return { | ||||
| 					users: x.users, | ||||
| 					cursor: x.next | ||||
| 						users: followings.map(following => following.follower), | ||||
| 						cursor: followings[followings.length - 1].id | ||||
| 					}; | ||||
| 				} else { | ||||
| 					return { | ||||
| 						users: followings.map(following => following.follower), | ||||
| 						cursor: null | ||||
| 					}; | ||||
| 				} | ||||
| 			}), | ||||
| 		}; | ||||
| 	}, | ||||
|  |  | |||
|  | @ -7,19 +7,32 @@ | |||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import parseAcct from '../../../../../misc/acct/parse'; | ||||
| import i18n from '../../../i18n'; | ||||
| 
 | ||||
| const fetchLimit = 30; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			makePromise: cursor => this.$root.api('users/following', { | ||||
| 				...parseAcct(this.$route.params.user), | ||||
| 				limit: 30, | ||||
| 				cursor: cursor ? cursor : undefined | ||||
| 			}).then(x => { | ||||
| 				limit: fetchLimit + 1, | ||||
| 				untilId: cursor ? cursor : undefined, | ||||
| 			}).then(followings => { | ||||
| 				if (followings.length == fetchLimit + 1) { | ||||
| 					followings.pop(); | ||||
| 					return { | ||||
| 					users: x.users, | ||||
| 					cursor: x.next | ||||
| 						users: followings.map(following => following.followee), | ||||
| 						cursor: followings[followings.length - 1].id | ||||
| 					}; | ||||
| 				} else { | ||||
| 					return { | ||||
| 						users: followings.map(following => following.followee), | ||||
| 						cursor: null | ||||
| 					}; | ||||
| 				} | ||||
| 			}), | ||||
| 		}; | ||||
| 	}, | ||||
|  |  | |||
|  | @ -42,7 +42,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$root.getMeta().then(meta => { | ||||
| 			this.name = meta.name; | ||||
| 			this.name = meta.name || 'Misskey'; | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <div class="info"> | ||||
| 	<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||
| 	<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> | ||||
| 	<p>Machine: {{ meta.machine }}</p> | ||||
| 	<p>Node: {{ meta.node }}</p> | ||||
| 	<p>Version: {{ meta.version }} </p> | ||||
|  |  | |||
|  | @ -60,7 +60,7 @@ export default Vue.extend({ | |||
| 			return this.browser.selectedFiles.some(f => f.id == this.file.id); | ||||
| 		}, | ||||
| 		title(): string { | ||||
| 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`; | ||||
| 			return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`; | ||||
| 		} | ||||
| 	}, | ||||
| 	methods: { | ||||
|  |  | |||
|  | @ -54,11 +54,11 @@ | |||
| 				</button> | ||||
| 				<button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')"> | ||||
| 					<fa icon="plus"/> | ||||
| 					<p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p> | ||||
| 					<p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> | ||||
| 				</button> | ||||
| 				<button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> | ||||
| 					<fa icon="minus"/> | ||||
| 					<p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p> | ||||
| 					<p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> | ||||
| 				</button> | ||||
| 				<button @click="menu()" ref="menuButton" class="button"> | ||||
| 					<fa icon="ellipsis-h"/> | ||||
|  |  | |||
|  | @ -110,7 +110,7 @@ | |||
| 						</div> | ||||
| 					</template> | ||||
| 
 | ||||
| 					<template v-if="notification.type == 'poll_vote'"> | ||||
| 					<template v-if="notification.type == 'pollVote'"> | ||||
| 						<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 						<div class="text"> | ||||
| 							<p><fa icon="chart-pie"/><a :href="notification.user | userPage" v-user-preview="notification.user.id"> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <mk-window ref="window" width="450px" height="500px" @closed="destroyDom"> | ||||
| 	<template #header><fa icon="list"/> {{ list.title }}</template> | ||||
| 	<template #header><fa icon="list"/> {{ list.name }}</template> | ||||
| 
 | ||||
| 	<x-editor :list="list"/> | ||||
| </mk-window> | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 
 | ||||
| 	<div class="xkxvokkjlptzyewouewmceqcxhpgzprp"> | ||||
| 		<button class="ui" @click="add">{{ $t('create-list') }}</button> | ||||
| 		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a> | ||||
| 		<a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a> | ||||
| 	</div> | ||||
| </mk-window> | ||||
| </template> | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ export default Vue.extend({ | |||
| 	computed: { | ||||
| 		home(): any[] { | ||||
| 			if (this.$store.getters.isSignedIn) { | ||||
| 				return this.$store.state.settings.home || []; | ||||
| 				return this.$store.state.device.home || []; | ||||
| 			} else { | ||||
| 				return [{ | ||||
| 					name: 'instance', | ||||
|  | @ -182,12 +182,8 @@ export default Vue.extend({ | |||
| 			} | ||||
| 			//#endregion | ||||
| 
 | ||||
| 			if (this.$store.state.settings.home == null) { | ||||
| 				this.$root.api('i/update_home', { | ||||
| 					home: _defaultDesktopHomeWidgets | ||||
| 				}).then(() => { | ||||
| 					this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); | ||||
| 				}); | ||||
| 			if (this.$store.state.device.home == null) { | ||||
| 				this.$store.commit('device/setHome', _defaultDesktopHomeWidgets); | ||||
| 			} | ||||
| 		} | ||||
| 	}, | ||||
|  | @ -226,7 +222,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		addWidget() { | ||||
| 			this.$store.dispatch('settings/addHomeWidget', { | ||||
| 			this.$store.commit('device/addHomeWidget', { | ||||
| 				name: this.widgetAdderSelected, | ||||
| 				id: uuid(), | ||||
| 				place: 'left', | ||||
|  | @ -237,12 +233,9 @@ export default Vue.extend({ | |||
| 		saveHome() { | ||||
| 			const left = this.widgets.left; | ||||
| 			const right = this.widgets.right; | ||||
| 			this.$store.commit('settings/setHome', left.concat(right)); | ||||
| 			this.$store.commit('device/setHome', left.concat(right)); | ||||
| 			for (const w of left) w.place = 'left'; | ||||
| 			for (const w of right) w.place = 'right'; | ||||
| 			this.$root.api('i/update_home', { | ||||
| 				home: this.home | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		done() { | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ export default Vue.extend({ | |||
| 	i18n: i18n('desktop/views/pages/tag.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			makePromise: cursor => this.$root.api('notes/search_by_tag', { | ||||
| 			makePromise: cursor => this.$root.api('notes/search-by-tag', { | ||||
| 				limit: limit + 1, | ||||
| 				offset: cursor ? cursor : undefined, | ||||
| 				tag: this.$route.params.tag | ||||
|  |  | |||
|  | @ -58,7 +58,7 @@ export default Vue.extend({ | |||
| 		}; | ||||
| 
 | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.endpoint = 'notes/search_by_tag'; | ||||
| 			this.endpoint = 'notes/search-by-tag'; | ||||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
|  | @ -77,9 +77,9 @@ export default Vue.extend({ | |||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('localTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); | ||||
| 		} else if (this.src == 'social') { | ||||
| 			this.endpoint = 'notes/social-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('socialTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
|  |  | |||
|  | @ -6,10 +6,10 @@ | |||
| 			<header class="zahtxcqi"> | ||||
| 				<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> | ||||
| 				<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> | ||||
| 				<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> | ||||
| 				<span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span> | ||||
| 				<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> | ||||
| 				<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> | ||||
| 				<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> | ||||
| 				<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span> | ||||
| 				<div class="buttons"> | ||||
| 					<button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> | ||||
| 					<button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> | ||||
|  | @ -78,7 +78,7 @@ export default Vue.extend({ | |||
| 			) && this.src === 'global') this.src = 'local'; | ||||
| 			if (!( | ||||
| 				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin | ||||
| 			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; | ||||
| 			) && ['local', 'social'].includes(this.src)) this.src = 'home'; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (this.$store.state.device.tl) { | ||||
|  | @ -89,7 +89,7 @@ export default Vue.extend({ | |||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
| 			this.src = 'social'; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -143,7 +143,7 @@ export default Vue.extend({ | |||
| 
 | ||||
| 			menu = menu.concat(lists.map(list => ({ | ||||
| 				icon: 'list', | ||||
| 				text: list.title, | ||||
| 				text: list.name, | ||||
| 				action: () => { | ||||
| 					this.list = list; | ||||
| 					this.src = 'list'; | ||||
|  |  | |||
|  | @ -36,8 +36,8 @@ | |||
| 			</dl> | ||||
| 		</div> | ||||
| 		<div class="info"> | ||||
| 			<span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span> | ||||
| 			<span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> | ||||
| 			<span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span> | ||||
| 			<span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> | ||||
| 		</div> | ||||
| 		<div class="status"> | ||||
| 			<router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link> | ||||
|  | @ -71,7 +71,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		age(): number { | ||||
| 			return age(this.user.profile.birthday); | ||||
| 			return age(this.user.birthday); | ||||
| 		} | ||||
| 	}, | ||||
| 	mounted() { | ||||
|  |  | |||
|  | @ -13,8 +13,8 @@ | |||
| 		<div class="body"> | ||||
| 			<div class="main block"> | ||||
| 				<div> | ||||
| 					<h1 v-if="name != 'Misskey'">{{ name }}</h1> | ||||
| 					<h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1> | ||||
| 					<h1 v-if="name != null">{{ name }}</h1> | ||||
| 					<h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1> | ||||
| 
 | ||||
| 					<div class="info"> | ||||
| 						<span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span> | ||||
|  | @ -87,7 +87,7 @@ | |||
| 					<div> | ||||
| 						<div v-if="meta" class="body"> | ||||
| 							<p>Version: <b>{{ meta.version }}</b></p> | ||||
| 							<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||
| 							<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -162,7 +162,7 @@ export default Vue.extend({ | |||
| 			banner: null, | ||||
| 			copyright, | ||||
| 			host: toUnicode(host), | ||||
| 			name: 'Misskey', | ||||
| 			name: null, | ||||
| 			description: '', | ||||
| 			announcements: [], | ||||
| 			photos: [] | ||||
|  |  | |||
|  | @ -15,15 +15,21 @@ | |||
| 				<b-form-group :description="$t('description')"> | ||||
| 					<b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert> | ||||
| 					<b-form-checkbox-group v-model="permission" stacked> | ||||
| 						<b-form-checkbox value="account-read">{{ $t('account-read') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="account-write">{{ $t('account-write') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="note-write">{{ $t('note-write') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="reaction-write">{{ $t('reaction-write') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="following-write">{{ $t('following-write') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="drive-read">{{ $t('drive-read') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="drive-write">{{ $t('drive-write') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="notification-read">{{ $t('notification-read') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="notification-write">{{ $t('notification-write') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox> | ||||
| 						<b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox> | ||||
| 					</b-form-checkbox-group> | ||||
| 				</b-form-group> | ||||
| 			</b-card> | ||||
|  |  | |||
|  | @ -278,21 +278,6 @@ export default class MiOS extends EventEmitter { | |||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('homeUpdated', x => { | ||||
| 				this.store.commit('settings/setHome', x); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('mobileHomeUpdated', x => { | ||||
| 				this.store.commit('settings/setMobileHome', x); | ||||
| 			}); | ||||
| 
 | ||||
| 			main.on('widgetUpdated', x => { | ||||
| 				this.store.commit('settings/updateWidget', { | ||||
| 					id: x.id, | ||||
| 					data: x.data | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			// トークンが再生成されたとき
 | ||||
| 			// このままではMisskeyが利用できないので強制的にサインアウトさせる
 | ||||
| 			main.on('myTokenRegenerated', () => { | ||||
|  |  | |||
|  | @ -22,7 +22,7 @@ | |||
| 		<div> | ||||
| 			<span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span> | ||||
| 			<span class="separator"></span> | ||||
| 			<span class="data-size">{{ file.datasize | bytes }}</span> | ||||
| 			<span class="data-size">{{ file.size | bytes }}</span> | ||||
| 			<span class="separator"></span> | ||||
| 			<span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> | ||||
| 			<template v-if="file.isSensitive"> | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
| 			<footer> | ||||
| 				<span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> | ||||
| 				<span class="separator"></span> | ||||
| 				<span class="data-size">{{ file.datasize | bytes }}</span> | ||||
| 				<span class="data-size">{{ file.size | bytes }}</span> | ||||
| 				<span class="separator"></span> | ||||
| 				<span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> | ||||
| 				<template v-if="file.isSensitive"> | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ | |||
| 		</div> | ||||
| 	</template> | ||||
| 
 | ||||
| 	<template v-if="notification.type == 'poll_vote'"> | ||||
| 	<template v-if="notification.type == 'pollVote'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div class="text"> | ||||
| 			<p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p> | ||||
|  |  | |||
|  | @ -54,7 +54,7 @@ | |||
| 		</div> | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> | ||||
| 	<div class="notification pollVote" v-if="notification.type == 'pollVote'"> | ||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | ||||
| 		<div> | ||||
| 			<header> | ||||
|  |  | |||
|  | @ -59,7 +59,7 @@ export default Vue.extend({ | |||
| 		}; | ||||
| 
 | ||||
| 		if (this.src == 'tag') { | ||||
| 			this.endpoint = 'notes/search_by_tag'; | ||||
| 			this.endpoint = 'notes/search-by-tag'; | ||||
| 			this.query = { | ||||
| 				query: this.tagTl.query | ||||
| 			}; | ||||
|  | @ -78,9 +78,9 @@ export default Vue.extend({ | |||
| 			this.endpoint = 'notes/local-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('localTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'hybrid') { | ||||
| 			this.endpoint = 'notes/hybrid-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('hybridTimeline'); | ||||
| 		} else if (this.src == 'social') { | ||||
| 			this.endpoint = 'notes/social-timeline'; | ||||
| 			this.connection = this.$root.stream.useSharedConnection('socialTimeline'); | ||||
| 			this.connection.on('note', prepend); | ||||
| 		} else if (this.src == 'global') { | ||||
| 			this.endpoint = 'notes/global-timeline'; | ||||
|  |  | |||
|  | @ -5,11 +5,11 @@ | |||
| 			<span :class="$style.title"> | ||||
| 				<span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span> | ||||
| 				<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span> | ||||
| 				<span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span> | ||||
| 				<span v-if="src == 'social'"><fa icon="share-alt"/>{{ $t('social') }}</span> | ||||
| 				<span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span> | ||||
| 				<span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span> | ||||
| 				<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span> | ||||
| 				<span v-if="src == 'list'"><fa icon="list"/>{{ list.title }}</span> | ||||
| 				<span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span> | ||||
| 				<span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span> | ||||
| 			</span> | ||||
| 			<span style="margin-left:8px"> | ||||
|  | @ -32,7 +32,7 @@ | |||
| 				<div> | ||||
| 					<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span> | ||||
| 					<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span> | ||||
| 					<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> | ||||
| 					<span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span> | ||||
| 					<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> | ||||
| 					<div class="hr"></div> | ||||
| 					<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span> | ||||
|  | @ -50,7 +50,7 @@ | |||
| 		<div class="tl"> | ||||
| 			<x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> | ||||
| 			<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> | ||||
| 			<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> | ||||
| 			<x-tl v-if="src == 'social'" ref="tl" key="social" src="social"/> | ||||
| 			<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> | ||||
| 			<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/> | ||||
| 			<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/> | ||||
|  | @ -120,7 +120,7 @@ export default Vue.extend({ | |||
| 			) && this.src === 'global') this.src = 'local'; | ||||
| 			if (!( | ||||
| 				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin | ||||
| 			) && ['local', 'hybrid'].includes(this.src)) this.src = 'home'; | ||||
| 			) && ['local', 'social'].includes(this.src)) this.src = 'home'; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (this.$store.state.device.tl) { | ||||
|  | @ -131,7 +131,7 @@ export default Vue.extend({ | |||
| 				this.tagTl = this.$store.state.device.tl.arg; | ||||
| 			} | ||||
| 		} else if (this.$store.state.i.followingCount == 0) { | ||||
| 			this.src = 'hybrid'; | ||||
| 			this.src = 'social'; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ export default Vue.extend({ | |||
| 	i18n: i18n('mobile/views/pages/tag.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			makePromise: cursor => this.$root.api('notes/search_by_tag', { | ||||
| 			makePromise: cursor => this.$root.api('notes/search-by-tag', { | ||||
| 				limit: limit + 1, | ||||
| 				offset: cursor ? cursor : undefined, | ||||
| 				tag: this.$route.params.tag | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <template> | ||||
| <mk-ui> | ||||
| 	<template #header v-if="!fetching"><fa icon="list"/>{{ list.title }}</template> | ||||
| 	<template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template> | ||||
| 
 | ||||
| 	<main v-if="!fetching"> | ||||
| 		<x-editor :list="list"/> | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
| 
 | ||||
| 	<main> | ||||
| 		<ul> | ||||
| 			<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.title }}</router-link></li> | ||||
| 			<li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li> | ||||
| 		</ul> | ||||
| 	</main> | ||||
| </mk-ui> | ||||
|  |  | |||
|  | @ -36,11 +36,11 @@ | |||
| 					</dl> | ||||
| 				</div> | ||||
| 				<div class="info"> | ||||
| 					<p class="location" v-if="user.host === null && user.profile.location"> | ||||
| 						<fa icon="map-marker"/>{{ user.profile.location }} | ||||
| 					<p class="location" v-if="user.host === null && user.location"> | ||||
| 						<fa icon="map-marker"/>{{ user.location }} | ||||
| 					</p> | ||||
| 					<p class="birthday" v-if="user.host === null && user.profile.birthday"> | ||||
| 						<fa icon="birthday-cake"/>{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }}) | ||||
| 					<p class="birthday" v-if="user.host === null && user.birthday"> | ||||
| 						<fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }}) | ||||
| 					</p> | ||||
| 				</div> | ||||
| 				<div class="status"> | ||||
|  | @ -104,7 +104,7 @@ export default Vue.extend({ | |||
| 	}, | ||||
| 	computed: { | ||||
| 		age(): number { | ||||
| 			return age(this.user.profile.birthday); | ||||
| 			return age(this.user.birthday); | ||||
| 		}, | ||||
| 		avator(): string { | ||||
| 			return this.$store.state.device.disableShowingAnimatedImages | ||||
|  |  | |||
|  | @ -3,10 +3,10 @@ | |||
| 	<div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> | ||||
| 
 | ||||
| 	<div> | ||||
| 		<img svg-inline src="../../../../assets/title.svg" :alt="name"> | ||||
| 		<img svg-inline src="../../../../assets/title.svg" alt="Misskey"> | ||||
| 		<p class="host">{{ host }}</p> | ||||
| 		<div class="about"> | ||||
| 			<h2>{{ name }}</h2> | ||||
| 			<h2>{{ name || 'Misskey' }}</h2> | ||||
| 			<p v-html="description || this.$t('@.about')"></p> | ||||
| 			<router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link> | ||||
| 		</div> | ||||
|  | @ -62,7 +62,7 @@ | |||
| 		</article> | ||||
| 		<div class="info" v-if="meta"> | ||||
| 			<p>Version: <b>{{ meta.version }}</b></p> | ||||
| 			<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||
| 			<p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> | ||||
| 		</div> | ||||
| 		<footer> | ||||
| 			<small>{{ copyright }}</small> | ||||
|  | @ -87,7 +87,7 @@ export default Vue.extend({ | |||
| 			stats: null, | ||||
| 			banner: null, | ||||
| 			host: toUnicode(host), | ||||
| 			name: 'Misskey', | ||||
| 			name: null, | ||||
| 			description: '', | ||||
| 			photos: [], | ||||
| 			announcements: [] | ||||
|  |  | |||
|  | @ -119,7 +119,7 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		addWidget() { | ||||
| 			this.$store.dispatch('settings/addMobileHomeWidget', { | ||||
| 			this.$store.commit('settings/addMobileHomeWidget', { | ||||
| 				name: this.widgetAdderSelected, | ||||
| 				id: uuid(), | ||||
| 				data: {} | ||||
|  | @ -127,14 +127,11 @@ export default Vue.extend({ | |||
| 		}, | ||||
| 
 | ||||
| 		removeWidget(widget) { | ||||
| 			this.$store.dispatch('settings/removeMobileHomeWidget', widget); | ||||
| 			this.$store.commit('settings/removeMobileHomeWidget', widget); | ||||
| 		}, | ||||
| 
 | ||||
| 		saveHome() { | ||||
| 			this.$store.commit('settings/setMobileHome', this.widgets); | ||||
| 			this.$root.api('i/update_mobile_home', { | ||||
| 				home: this.widgets | ||||
| 			}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
|  |  | |||
|  | @ -7,8 +7,6 @@ import { erase } from '../../prelude/array'; | |||
| import getNoteSummary from '../../misc/get-note-summary'; | ||||
| 
 | ||||
| const defaultSettings = { | ||||
| 	home: null, | ||||
| 	mobileHome: [], | ||||
| 	keepCw: false, | ||||
| 	tagTimelines: [], | ||||
| 	fetchOnScroll: true, | ||||
|  | @ -41,6 +39,8 @@ const defaultSettings = { | |||
| }; | ||||
| 
 | ||||
| const defaultDeviceSettings = { | ||||
| 	home: null, | ||||
| 	mobileHome: [], | ||||
| 	deck: null, | ||||
| 	deckMode: false, | ||||
| 	deckColumnAlign: 'center', | ||||
|  | @ -120,7 +120,7 @@ export default (os: MiOS) => new Vuex.Store({ | |||
| 	actions: { | ||||
| 		login(ctx, i) { | ||||
| 			ctx.commit('updateI', i); | ||||
| 			ctx.dispatch('settings/merge', i.clientSettings); | ||||
| 			ctx.dispatch('settings/merge', i.clientData); | ||||
| 		}, | ||||
| 
 | ||||
| 		logout(ctx) { | ||||
|  | @ -134,8 +134,8 @@ export default (os: MiOS) => new Vuex.Store({ | |||
| 				ctx.commit('updateIKeyValue', { key, value }); | ||||
| 			} | ||||
| 
 | ||||
| 			if (me.clientSettings) { | ||||
| 				ctx.dispatch('settings/merge', me.clientSettings); | ||||
| 			if (me.clientData) { | ||||
| 				ctx.dispatch('settings/merge', me.clientData); | ||||
| 			} | ||||
| 		}, | ||||
| 	}, | ||||
|  | @ -162,6 +162,48 @@ export default (os: MiOS) => new Vuex.Store({ | |||
| 					state.visibility = visibility; | ||||
| 				}, | ||||
| 
 | ||||
| 				setHome(state, data) { | ||||
| 					state.home = data; | ||||
| 				}, | ||||
| 
 | ||||
| 				addHomeWidget(state, widget) { | ||||
| 					state.home.unshift(widget); | ||||
| 				}, | ||||
| 
 | ||||
| 				setMobileHome(state, data) { | ||||
| 					state.mobileHome = data; | ||||
| 				}, | ||||
| 
 | ||||
| 				updateWidget(state, x) { | ||||
| 					let w; | ||||
| 
 | ||||
| 					//#region Desktop home
 | ||||
| 					if (state.home) { | ||||
| 						w = state.home.find(w => w.id == x.id); | ||||
| 						if (w) { | ||||
| 							w.data = x.data; | ||||
| 						} | ||||
| 					} | ||||
| 					//#endregion
 | ||||
| 
 | ||||
| 					//#region Mobile home
 | ||||
| 					if (state.mobileHome) { | ||||
| 						w = state.mobileHome.find(w => w.id == x.id); | ||||
| 						if (w) { | ||||
| 							w.data = x.data; | ||||
| 						} | ||||
| 					} | ||||
| 					//#endregion
 | ||||
| 				}, | ||||
| 
 | ||||
| 				addMobileHomeWidget(state, widget) { | ||||
| 					state.mobileHome.unshift(widget); | ||||
| 				}, | ||||
| 
 | ||||
| 				removeMobileHomeWidget(state, widget) { | ||||
| 					state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); | ||||
| 				}, | ||||
| 
 | ||||
| 				addDeckColumn(state, column) { | ||||
| 					if (column.name == undefined) column.name = null; | ||||
| 					state.deck.columns.push(column); | ||||
|  | @ -301,48 +343,6 @@ export default (os: MiOS) => new Vuex.Store({ | |||
| 				set(state, x: { key: string; value: any }) { | ||||
| 					nestedProperty.set(state, x.key, x.value); | ||||
| 				}, | ||||
| 
 | ||||
| 				setHome(state, data) { | ||||
| 					state.home = data; | ||||
| 				}, | ||||
| 
 | ||||
| 				addHomeWidget(state, widget) { | ||||
| 					state.home.unshift(widget); | ||||
| 				}, | ||||
| 
 | ||||
| 				setMobileHome(state, data) { | ||||
| 					state.mobileHome = data; | ||||
| 				}, | ||||
| 
 | ||||
| 				updateWidget(state, x) { | ||||
| 					let w; | ||||
| 
 | ||||
| 					//#region Desktop home
 | ||||
| 					if (state.home) { | ||||
| 						w = state.home.find(w => w.id == x.id); | ||||
| 						if (w) { | ||||
| 							w.data = x.data; | ||||
| 						} | ||||
| 					} | ||||
| 					//#endregion
 | ||||
| 
 | ||||
| 					//#region Mobile home
 | ||||
| 					if (state.mobileHome) { | ||||
| 						w = state.mobileHome.find(w => w.id == x.id); | ||||
| 						if (w) { | ||||
| 							w.data = x.data; | ||||
| 						} | ||||
| 					} | ||||
| 					//#endregion
 | ||||
| 				}, | ||||
| 
 | ||||
| 				addMobileHomeWidget(state, widget) { | ||||
| 					state.mobileHome.unshift(widget); | ||||
| 				}, | ||||
| 
 | ||||
| 				removeMobileHomeWidget(state, widget) { | ||||
| 					state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); | ||||
| 				}, | ||||
| 			}, | ||||
| 
 | ||||
| 			actions: { | ||||
|  | @ -363,30 +363,6 @@ export default (os: MiOS) => new Vuex.Store({ | |||
| 						}); | ||||
| 					} | ||||
| 				}, | ||||
| 
 | ||||
| 				addHomeWidget(ctx, widget) { | ||||
| 					ctx.commit('addHomeWidget', widget); | ||||
| 
 | ||||
| 					os.api('i/update_home', { | ||||
| 						home: ctx.state.home | ||||
| 					}); | ||||
| 				}, | ||||
| 
 | ||||
| 				addMobileHomeWidget(ctx, widget) { | ||||
| 					ctx.commit('addMobileHomeWidget', widget); | ||||
| 
 | ||||
| 					os.api('i/update_mobile_home', { | ||||
| 						home: ctx.state.mobileHome | ||||
| 					}); | ||||
| 				}, | ||||
| 
 | ||||
| 				removeMobileHomeWidget(ctx, widget) { | ||||
| 					ctx.commit('removeMobileHomeWidget', widget); | ||||
| 
 | ||||
| 					os.api('i/update_mobile_home', { | ||||
| 						home: ctx.state.mobileHome.filter(w => w.id != widget.id) | ||||
| 					}); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ export type Source = { | |||
| 	port: number; | ||||
| 	https?: { [x: string]: string }; | ||||
| 	disableHsts?: boolean; | ||||
| 	mongodb: { | ||||
| 	db: { | ||||
| 		host: string; | ||||
| 		port: number; | ||||
| 		db: string; | ||||
|  | @ -42,6 +42,8 @@ export type Source = { | |||
| 	accesslog?: string; | ||||
| 
 | ||||
| 	clusterLimit?: number; | ||||
| 
 | ||||
| 	id: string; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
|  | @ -1,111 +0,0 @@ | |||
| #include <nan.h> | ||||
| #include <openssl/bio.h> | ||||
| #include <openssl/buffer.h> | ||||
| #include <openssl/crypto.h> | ||||
| #include <openssl/pem.h> | ||||
| #include <openssl/rsa.h> | ||||
| #include <openssl/x509.h> | ||||
| 
 | ||||
| NAN_METHOD(extractPublic) | ||||
| { | ||||
| 	const auto sourceString = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked(); | ||||
| 	if (!sourceString->IsOneByte()) { | ||||
| 		Nan::ThrowError("Malformed character found"); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	size_t sourceLength = sourceString->Length(); | ||||
| 	const auto sourceBuf = new char[sourceLength]; | ||||
| 
 | ||||
| 	Nan::DecodeWrite(sourceBuf, sourceLength, sourceString); | ||||
| 
 | ||||
| 	const auto source = BIO_new_mem_buf(sourceBuf, sourceLength); | ||||
| 	if (source == nullptr) { | ||||
| 		Nan::ThrowError("Memory allocation failed"); | ||||
| 		delete[] sourceBuf; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr); | ||||
| 
 | ||||
| 	BIO_free(source); | ||||
| 	delete[] sourceBuf; | ||||
| 
 | ||||
| 	if (rsa == nullptr) { | ||||
| 		Nan::ThrowError("Decode failed"); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const auto destination = BIO_new(BIO_s_mem()); | ||||
| 	if (destination == nullptr) { | ||||
| 		Nan::ThrowError("Memory allocation failed"); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const auto result = PEM_write_bio_RSAPublicKey(destination, rsa); | ||||
| 
 | ||||
| 	RSA_free(rsa); | ||||
| 
 | ||||
| 	if (result != 1) { | ||||
| 		Nan::ThrowError("Public key extraction failed"); | ||||
| 		BIO_free(destination); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	char *pem; | ||||
| 	const auto pemLength = BIO_get_mem_data(destination, &pem); | ||||
| 
 | ||||
| 	info.GetReturnValue().Set(Nan::Encode(pem, pemLength)); | ||||
| 	BIO_free(destination); | ||||
| } | ||||
| 
 | ||||
| NAN_METHOD(generate) | ||||
| { | ||||
| 	const auto exponent = BN_new(); | ||||
| 	const auto mem = BIO_new(BIO_s_mem()); | ||||
| 	const auto rsa = RSA_new(); | ||||
| 	char *data; | ||||
| 	long result; | ||||
| 
 | ||||
| 	if (exponent == nullptr || mem == nullptr || rsa == nullptr) { | ||||
| 		Nan::ThrowError("Memory allocation failed"); | ||||
| 		goto done; | ||||
| 	} | ||||
| 
 | ||||
| 	result = BN_set_word(exponent, 65537); | ||||
| 	if (result != 1) { | ||||
| 		Nan::ThrowError("Exponent setting failed"); | ||||
| 		goto done; | ||||
| 	} | ||||
| 
 | ||||
| 	result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr); | ||||
| 	if (result != 1) { | ||||
| 		Nan::ThrowError("Key generation failed"); | ||||
| 		goto done; | ||||
| 	} | ||||
| 
 | ||||
| 	result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL); | ||||
| 	if (result != 1) { | ||||
| 		Nan::ThrowError("Key export failed"); | ||||
| 		goto done; | ||||
| 	} | ||||
| 
 | ||||
| 	result = BIO_get_mem_data(mem, &data); | ||||
| 	info.GetReturnValue().Set(Nan::Encode(data, result)); | ||||
| 
 | ||||
| done: | ||||
| 	RSA_free(rsa); | ||||
| 	BIO_free(mem); | ||||
| 	BN_free(exponent); | ||||
| } | ||||
| 
 | ||||
| NAN_MODULE_INIT(InitAll) | ||||
| { | ||||
| 	Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(), | ||||
| 		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked()); | ||||
| 
 | ||||
| 	Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(), | ||||
| 		Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked()); | ||||
| } | ||||
| 
 | ||||
| NODE_MODULE(crypto_key, InitAll); | ||||
							
								
								
									
										2
									
								
								src/crypto_key.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/crypto_key.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -1,2 +0,0 @@ | |||
| export function extractPublic(keypair: string): string; | ||||
| export function generate(): string; | ||||
|  | @ -1,17 +1,18 @@ | |||
| import Note from '../models/note'; | ||||
| import { MoreThanOrEqual, getRepository } from 'typeorm'; | ||||
| import { Note } from '../models/entities/note'; | ||||
| import { initDb } from '../db/postgre'; | ||||
| 
 | ||||
| const interval = 5000; | ||||
| 
 | ||||
| initDb().then(() => { | ||||
| 	const Notes = getRepository(Note); | ||||
| 
 | ||||
| 	async function tick() { | ||||
| 	const [all, local] = await Promise.all([Note.count({ | ||||
| 		createdAt: { | ||||
| 			$gte: new Date(Date.now() - interval) | ||||
| 		} | ||||
| 	}), Note.count({ | ||||
| 		createdAt: { | ||||
| 			$gte: new Date(Date.now() - interval) | ||||
| 		}, | ||||
| 		'_user.host': null | ||||
| 		const [all, local] = await Promise.all([Notes.count({ | ||||
| 			createdAt: MoreThanOrEqual(new Date(Date.now() - interval)) | ||||
| 		}), Notes.count({ | ||||
| 			createdAt: MoreThanOrEqual(new Date(Date.now() - interval)), | ||||
| 			userHost: null | ||||
| 		})]); | ||||
| 
 | ||||
| 		const stats = { | ||||
|  | @ -24,3 +25,4 @@ async function tick() { | |||
| 	tick(); | ||||
| 
 | ||||
| 	setInterval(tick, interval); | ||||
| }); | ||||
|  |  | |||
|  | @ -1,39 +0,0 @@ | |||
| import config from '../config'; | ||||
| 
 | ||||
| const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; | ||||
| const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; | ||||
| 
 | ||||
| const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; | ||||
| 
 | ||||
| /** | ||||
|  * monk | ||||
|  */ | ||||
| import mongo from 'monk'; | ||||
| 
 | ||||
| const db = mongo(uri); | ||||
| 
 | ||||
| export default db; | ||||
| 
 | ||||
| /** | ||||
|  * MongoDB native module (officialy) | ||||
|  */ | ||||
| import * as mongodb from 'mongodb'; | ||||
| 
 | ||||
| let mdb: mongodb.Db; | ||||
| 
 | ||||
| const nativeDbConn = async (): Promise<mongodb.Db> => { | ||||
| 	if (mdb) return mdb; | ||||
| 
 | ||||
| 	const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => { | ||||
| 		mongodb.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => { | ||||
| 			if (e) return reject(e); | ||||
| 			resolve(client.db(config.mongodb.db)); | ||||
| 		}); | ||||
| 	}))(); | ||||
| 
 | ||||
| 	mdb = db; | ||||
| 
 | ||||
| 	return db; | ||||
| }; | ||||
| 
 | ||||
| export { nativeDbConn }; | ||||
							
								
								
									
										137
									
								
								src/db/postgre.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/db/postgre.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,137 @@ | |||
| import { createConnection, Logger, getConnection } from 'typeorm'; | ||||
| import config from '../config'; | ||||
| import { entities as charts } from '../services/chart/entities'; | ||||
| import { dbLogger } from './logger'; | ||||
| import * as highlight from 'cli-highlight'; | ||||
| 
 | ||||
| import { Log } from '../models/entities/log'; | ||||
| import { User } from '../models/entities/user'; | ||||
| import { DriveFile } from '../models/entities/drive-file'; | ||||
| import { DriveFolder } from '../models/entities/drive-folder'; | ||||
| import { AccessToken } from '../models/entities/access-token'; | ||||
| import { App } from '../models/entities/app'; | ||||
| import { PollVote } from '../models/entities/poll-vote'; | ||||
| import { Note } from '../models/entities/note'; | ||||
| import { NoteReaction } from '../models/entities/note-reaction'; | ||||
| import { NoteWatching } from '../models/entities/note-watching'; | ||||
| import { NoteUnread } from '../models/entities/note-unread'; | ||||
| import { Notification } from '../models/entities/notification'; | ||||
| import { Meta } from '../models/entities/meta'; | ||||
| import { Following } from '../models/entities/following'; | ||||
| import { Instance } from '../models/entities/instance'; | ||||
| import { Muting } from '../models/entities/muting'; | ||||
| import { SwSubscription } from '../models/entities/sw-subscription'; | ||||
| import { Blocking } from '../models/entities/blocking'; | ||||
| import { UserList } from '../models/entities/user-list'; | ||||
| import { UserListJoining } from '../models/entities/user-list-joining'; | ||||
| import { Hashtag } from '../models/entities/hashtag'; | ||||
| import { NoteFavorite } from '../models/entities/note-favorite'; | ||||
| import { AbuseUserReport } from '../models/entities/abuse-user-report'; | ||||
| import { RegistrationTicket } from '../models/entities/registration-tickets'; | ||||
| import { MessagingMessage } from '../models/entities/messaging-message'; | ||||
| import { Signin } from '../models/entities/signin'; | ||||
| import { AuthSession } from '../models/entities/auth-session'; | ||||
| import { FollowRequest } from '../models/entities/follow-request'; | ||||
| import { Emoji } from '../models/entities/emoji'; | ||||
| import { ReversiGame } from '../models/entities/games/reversi/game'; | ||||
| import { ReversiMatching } from '../models/entities/games/reversi/matching'; | ||||
| import { UserNotePining } from '../models/entities/user-note-pinings'; | ||||
| import { UserServiceLinking } from '../models/entities/user-service-linking'; | ||||
| import { Poll } from '../models/entities/poll'; | ||||
| import { UserKeypair } from '../models/entities/user-keypair'; | ||||
| import { UserPublickey } from '../models/entities/user-publickey'; | ||||
| 
 | ||||
| const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); | ||||
| 
 | ||||
| class MyCustomLogger implements Logger { | ||||
| 	private highlight(sql: string) { | ||||
| 		return highlight.highlight(sql, { | ||||
| 			language: 'sql', ignoreIllegals: true, | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	public logQuery(query: string, parameters?: any[]) { | ||||
| 		sqlLogger.info(this.highlight(query)); | ||||
| 	} | ||||
| 
 | ||||
| 	public logQueryError(error: string, query: string, parameters?: any[]) { | ||||
| 		sqlLogger.error(this.highlight(query)); | ||||
| 	} | ||||
| 
 | ||||
| 	public logQuerySlow(time: number, query: string, parameters?: any[]) { | ||||
| 		sqlLogger.warn(this.highlight(query)); | ||||
| 	} | ||||
| 
 | ||||
| 	public logSchemaBuild(message: string) { | ||||
| 		sqlLogger.info(message); | ||||
| 	} | ||||
| 
 | ||||
| 	public log(message: string) { | ||||
| 		sqlLogger.info(message); | ||||
| 	} | ||||
| 
 | ||||
| 	public logMigration(message: string) { | ||||
| 		sqlLogger.info(message); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export function initDb(justBorrow = false, sync = false, log = false) { | ||||
| 	const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV); | ||||
| 
 | ||||
| 	try { | ||||
| 		const conn = getConnection(); | ||||
| 		return Promise.resolve(conn); | ||||
| 	} catch (e) {} | ||||
| 
 | ||||
| 	return createConnection({ | ||||
| 		type: 'postgres', | ||||
| 		host: config.db.host, | ||||
| 		port: config.db.port, | ||||
| 		username: config.db.user, | ||||
| 		password: config.db.pass, | ||||
| 		database: config.db.db, | ||||
| 		synchronize: process.env.NODE_ENV === 'test' || sync, | ||||
| 		dropSchema: process.env.NODE_ENV === 'test' && !justBorrow, | ||||
| 		logging: enableLogging, | ||||
| 		logger: enableLogging ? new MyCustomLogger() : null, | ||||
| 		entities: [ | ||||
| 			Meta, | ||||
| 			Instance, | ||||
| 			App, | ||||
| 			AuthSession, | ||||
| 			AccessToken, | ||||
| 			User, | ||||
| 			UserKeypair, | ||||
| 			UserPublickey, | ||||
| 			UserList, | ||||
| 			UserListJoining, | ||||
| 			UserNotePining, | ||||
| 			UserServiceLinking, | ||||
| 			Following, | ||||
| 			FollowRequest, | ||||
| 			Muting, | ||||
| 			Blocking, | ||||
| 			Note, | ||||
| 			NoteFavorite, | ||||
| 			NoteReaction, | ||||
| 			NoteWatching, | ||||
| 			NoteUnread, | ||||
| 			Log, | ||||
| 			DriveFile, | ||||
| 			DriveFolder, | ||||
| 			Poll, | ||||
| 			PollVote, | ||||
| 			Notification, | ||||
| 			Emoji, | ||||
| 			Hashtag, | ||||
| 			SwSubscription, | ||||
| 			AbuseUserReport, | ||||
| 			RegistrationTicket, | ||||
| 			MessagingMessage, | ||||
| 			Signin, | ||||
| 			ReversiGame, | ||||
| 			ReversiMatching, | ||||
| 			...charts as any | ||||
| 		] | ||||
| 	}); | ||||
| } | ||||
|  | @ -42,9 +42,9 @@ Misskeyのリバーシ機能に対応したBotの開発方法をここに記し | |||
| ``` | ||||
| pos = x + (y * mapWidth) | ||||
| ``` | ||||
| `mapWidth`は、ゲーム情報の`settings.map`から、次のようにして計算できます: | ||||
| `mapWidth`は、ゲーム情報の`map`から、次のようにして計算できます: | ||||
| ``` | ||||
| mapWidth = settings.map[0].length | ||||
| mapWidth = map[0].length | ||||
| ``` | ||||
| 
 | ||||
| ### Pos から X,Y座標 に変換する | ||||
|  | @ -54,7 +54,7 @@ y = Math.floor(pos / mapWidth) | |||
| ``` | ||||
| 
 | ||||
| ## マップ情報 | ||||
| マップ情報は、ゲーム情報の`settings.map`に入っています。 | ||||
| マップ情報は、ゲーム情報の`map`に入っています。 | ||||
| 文字列の配列になっており、ひとつひとつの文字がマス情報を表しています。 | ||||
| それをもとにマップのデザインを知る事が出来ます: | ||||
| * `(スペース)` ... マス無し | ||||
|  |  | |||
|  | @ -339,7 +339,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま | |||
| #### `note` | ||||
| ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです。 | ||||
| 
 | ||||
| ## `hybridTimeline` | ||||
| ## `socialTimeline` | ||||
| ソーシャルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。 | ||||
| 
 | ||||
| ### 流れてくるイベント一覧 | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue