Merge branch 'develop' into l10n_develop
This commit is contained in:
		| @@ -138,3 +138,6 @@ drive: | |||||||
|  |  | ||||||
| # Clustering | # Clustering | ||||||
| # clusterLimit: 1 | # clusterLimit: 1 | ||||||
|  |  | ||||||
|  | # Summaly proxy | ||||||
|  | # summalyProxy: "http://example.com" | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -5,6 +5,16 @@ ChangeLog | |||||||
|  |  | ||||||
| This document describes breaking changes only. | This document describes breaking changes only. | ||||||
|  |  | ||||||
|  | 8.0.0 | ||||||
|  | ----- | ||||||
|  |  | ||||||
|  | ### Migration | ||||||
|  |  | ||||||
|  | 起動する前に、`node cli/migration/8.0.0`してください。 | ||||||
|  |  | ||||||
|  | Please run `node cli/migration/8.0.0` before launch. | ||||||
|  |  | ||||||
|  |  | ||||||
| 7.0.0 | 7.0.0 | ||||||
| ----- | ----- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,40 +0,0 @@ | |||||||
| const { default: User, deleteUser } = require('../built/models/user'); |  | ||||||
| const { default: zip } = require('@prezzemolo/zip') |  | ||||||
|  |  | ||||||
| const migrate = async (user) => { |  | ||||||
| 	try { |  | ||||||
| 		await deleteUser(user._id); |  | ||||||
| 		return true; |  | ||||||
| 	} catch (e) { |  | ||||||
| 		return false; |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function main() { |  | ||||||
| 	const count = await User.count({ |  | ||||||
| 		uri: /#/ |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	const dop = 1 |  | ||||||
| 	const idop = ((count - (count % dop)) / dop) + 1 |  | ||||||
|  |  | ||||||
| 	return zip( |  | ||||||
| 		1, |  | ||||||
| 		async (time) => { |  | ||||||
| 			console.log(`${time} / ${idop}`) |  | ||||||
| 			const doc = await User.find({ |  | ||||||
| 				uri: /#/ |  | ||||||
| 			}, { |  | ||||||
| 				limit: dop, skip: time * dop |  | ||||||
| 			}) |  | ||||||
| 			return Promise.all(doc.map(migrate)) |  | ||||||
| 		}, |  | ||||||
| 		idop |  | ||||||
| 	).then(a => { |  | ||||||
| 		const rv = [] |  | ||||||
| 		a.forEach(e => rv.push(...e)) |  | ||||||
| 		return rv |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| main().then(console.dir).catch(console.error) |  | ||||||
							
								
								
									
										144
									
								
								cli/migration/8.0.0.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								cli/migration/8.0.0.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | |||||||
|  | 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(); | ||||||
							
								
								
									
										11
									
								
								gulpfile.ts
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								gulpfile.ts
									
									
									
									
									
								
							| @@ -59,7 +59,16 @@ gulp.task('build:copy:views', () => | |||||||
| 	gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views')) | 	gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views')) | ||||||
| ); | ); | ||||||
|  |  | ||||||
| gulp.task('build:copy', ['build:copy:views'], () => | // 互換性のため | ||||||
|  | gulp.task('build:copy:lang', () => | ||||||
|  | 	gulp.src(['./built/client/assets/*.*-*.js']) | ||||||
|  | 		.pipe(rename(path => { | ||||||
|  | 			path.basename = path.basename.replace(/\-(.*)$/, ''); | ||||||
|  | 		})) | ||||||
|  | 		.pipe(gulp.dest('./built/client/assets/')) | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () => | ||||||
| 	gulp.src([ | 	gulp.src([ | ||||||
| 		'./build/Release/crypto_key.node', | 		'./build/Release/crypto_key.node', | ||||||
| 		'./src/const.json', | 		'./src/const.json', | ||||||
|   | |||||||
							
								
								
									
										1219
									
								
								locales/ca.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/ca.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/de.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/de.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/en.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/en.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/es.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/es.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/fr.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/fr.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -11,13 +11,13 @@ const loadLang = lang => yaml.safeLoad( | |||||||
| const native = loadLang('ja-JP'); | const native = loadLang('ja-JP'); | ||||||
|  |  | ||||||
| const langs = { | const langs = { | ||||||
| 	'de': loadLang('de'), | 	'de-DE': loadLang('de-DE'), | ||||||
| 	'en': loadLang('en'), | 	'en-US': loadLang('en-US'), | ||||||
| 	'fr': loadLang('fr'), | 	'fr-FR': loadLang('fr-FR'), | ||||||
| 	'ja': native, | 	'ja-JP': native, | ||||||
| 	'ja-KS': loadLang('ja-KS'), | 	'ja-KS': loadLang('ja-KS'), | ||||||
| 	'pl': loadLang('pl'), | 	'pl-PL': loadLang('pl-PL'), | ||||||
| 	'es': loadLang('es') | 	'es-ES': loadLang('es-ES') | ||||||
| }; | }; | ||||||
|  |  | ||||||
| Object.values(langs).forEach(locale => { | Object.values(langs).forEach(locale => { | ||||||
|   | |||||||
							
								
								
									
										1219
									
								
								locales/it.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/it.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -456,6 +456,7 @@ desktop: | |||||||
|   uploading-avatar: "新しいアバターをアップロードしています" |   uploading-avatar: "新しいアバターをアップロードしています" | ||||||
|   avatar-updated: "アバターを更新しました" |   avatar-updated: "アバターを更新しました" | ||||||
|   choose-avatar: "アバターにする画像を選択" |   choose-avatar: "アバターにする画像を選択" | ||||||
|  |   invalid-filetype: "この形式のファイルはサポートされていません" | ||||||
|  |  | ||||||
| desktop/views/components/activity.chart.vue: | desktop/views/components/activity.chart.vue: | ||||||
|   total: "Black ... Total" |   total: "Black ... Total" | ||||||
| @@ -473,6 +474,25 @@ desktop/views/components/calendar.vue: | |||||||
|   next: "次の月" |   next: "次の月" | ||||||
|   go: "クリックして時間遡行" |   go: "クリックして時間遡行" | ||||||
|  |  | ||||||
|  | desktop/views/components/charts.vue: | ||||||
|  |   title: "チャート" | ||||||
|  |   per-day: "1日ごと" | ||||||
|  |   per-hour: "1時間ごと" | ||||||
|  |   notes: "投稿" | ||||||
|  |   users: "ユーザー" | ||||||
|  |   drive: "ドライブ" | ||||||
|  |   charts: | ||||||
|  |     notes: "投稿の増減 (統合)" | ||||||
|  |     local-notes: "投稿の増減 (ローカル)" | ||||||
|  |     remote-notes: "投稿の増減 (リモート)" | ||||||
|  |     notes-total: "投稿の累計" | ||||||
|  |     users: "ユーザーの増減" | ||||||
|  |     users-total: "ユーザーの累計" | ||||||
|  |     drive: "ドライブ使用量の増減" | ||||||
|  |     drive-total: "ドライブ使用量の累計" | ||||||
|  |     drive-files: "ドライブのファイル数の増減" | ||||||
|  |     drive-files-total: "ドライブのファイル数の累計" | ||||||
|  |  | ||||||
| desktop/views/components/choose-file-from-drive-window.vue: | desktop/views/components/choose-file-from-drive-window.vue: | ||||||
|   choose-file: "ファイル選択中" |   choose-file: "ファイル選択中" | ||||||
|   upload: "PCからドライブにファイルをアップロード" |   upload: "PCからドライブにファイルをアップロード" | ||||||
| @@ -713,6 +733,7 @@ desktop/views/components/settings.vue: | |||||||
|   gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" |   gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用" | ||||||
|   post-form-on-timeline: "タイムライン上部に投稿フォームを表示する" |   post-form-on-timeline: "タイムライン上部に投稿フォームを表示する" | ||||||
|   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" |   suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する" | ||||||
|  |   show-clock-on-header: "右上に時計を表示する" | ||||||
|   show-reply-target: "リプライ先を表示する" |   show-reply-target: "リプライ先を表示する" | ||||||
|   show-my-renotes: "自分の行ったRenoteをタイムラインに表示する" |   show-my-renotes: "自分の行ったRenoteをタイムラインに表示する" | ||||||
|   show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する" |   show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する" | ||||||
| @@ -857,6 +878,7 @@ desktop/views/components/ui.header.account.vue: | |||||||
|   lists: "リスト" |   lists: "リスト" | ||||||
|   follow-requests: "フォロー申請" |   follow-requests: "フォロー申請" | ||||||
|   customize: "ホームのカスタマイズ" |   customize: "ホームのカスタマイズ" | ||||||
|  |   admin: "管理" | ||||||
|   settings: "設定" |   settings: "設定" | ||||||
|   signout: "サインアウト" |   signout: "サインアウト" | ||||||
|   dark: "闇に飲まれる" |   dark: "闇に飲まれる" | ||||||
| @@ -914,8 +936,8 @@ desktop/views/pages/admin/admin.dashboard.vue: | |||||||
|   dashboard: "ダッシュボード" |   dashboard: "ダッシュボード" | ||||||
|   all-users: "全てのユーザー" |   all-users: "全てのユーザー" | ||||||
|   original-users: "このインスタンスのユーザー" |   original-users: "このインスタンスのユーザー" | ||||||
|   all-notes: "全てのノート" |   all-notes: "全ての投稿" | ||||||
|   original-notes: "このインスタンスのノート" |   original-notes: "このインスタンスの投稿" | ||||||
|   invite: "招待" |   invite: "招待" | ||||||
|  |  | ||||||
| desktop/views/pages/admin/admin.suspend-user.vue: | desktop/views/pages/admin/admin.suspend-user.vue: | ||||||
| @@ -938,21 +960,6 @@ desktop/views/pages/admin/admin.unverify-user.vue: | |||||||
|   unverify: "公式アカウントを解除する" |   unverify: "公式アカウントを解除する" | ||||||
|   unverified: "公式アカウントを解除しました" |   unverified: "公式アカウントを解除しました" | ||||||
|  |  | ||||||
| desktop/views/pages/admin/admin.notes-chart.vue: |  | ||||||
|   title: "投稿" |  | ||||||
|   local: "ローカル" |  | ||||||
|   remote: "リモート" |  | ||||||
|  |  | ||||||
| desktop/views/pages/admin/admin.users-chart.vue: |  | ||||||
|   title: "ユーザー" |  | ||||||
|   local: "ローカル" |  | ||||||
|   remote: "リモート" |  | ||||||
|  |  | ||||||
| desktop/views/pages/admin/admin.drive-chart.vue: |  | ||||||
|   title: "ドライブ" |  | ||||||
|   local: "ローカル" |  | ||||||
|   remote: "リモート" |  | ||||||
|  |  | ||||||
| desktop/views/pages/deck/deck.tl-column.vue: | desktop/views/pages/deck/deck.tl-column.vue: | ||||||
|   is-media-only: "メディア投稿のみ" |   is-media-only: "メディア投稿のみ" | ||||||
|   is-media-view: "メディアビュー" |   is-media-view: "メディアビュー" | ||||||
| @@ -963,6 +970,12 @@ desktop/views/pages/deck/deck.note.vue: | |||||||
|   private: "この投稿は非公開です" |   private: "この投稿は非公開です" | ||||||
|   deleted: "この投稿は削除されました" |   deleted: "この投稿は削除されました" | ||||||
|  |  | ||||||
|  | desktop/views/pages/stats/stats.vue: | ||||||
|  |   all-users: "全てのユーザー" | ||||||
|  |   original-users: "このインスタンスのユーザー" | ||||||
|  |   all-notes: "全ての投稿" | ||||||
|  |   original-notes: "このインスタンスの投稿" | ||||||
|  |  | ||||||
| desktop/views/pages/welcome.vue: | desktop/views/pages/welcome.vue: | ||||||
|   about: "詳しく..." |   about: "詳しく..." | ||||||
|   gotit: "わかった" |   gotit: "わかった" | ||||||
| @@ -1214,6 +1227,7 @@ mobile/views/components/ui.nav.vue: | |||||||
|   game: "ゲーム" |   game: "ゲーム" | ||||||
|   darkmode: "ダークモード" |   darkmode: "ダークモード" | ||||||
|   settings: "設定" |   settings: "設定" | ||||||
|  |   admin: "管理" | ||||||
|   about: "Misskeyについて" |   about: "Misskeyについて" | ||||||
|  |  | ||||||
| mobile/views/components/user-timeline.vue: | mobile/views/components/user-timeline.vue: | ||||||
| @@ -1355,6 +1369,8 @@ mobile/views/pages/settings.vue: | |||||||
|   update-available-desc: "ページを再度読み込みすると更新が適用されます。" |   update-available-desc: "ページを再度読み込みすると更新が適用されます。" | ||||||
|   settings: "設定" |   settings: "設定" | ||||||
|   signout: "サインアウト" |   signout: "サインアウト" | ||||||
|  |   sound: "サウンド" | ||||||
|  |   enableSounds: "サウンドを有効にする" | ||||||
|  |  | ||||||
| mobile/views/pages/user.vue: | mobile/views/pages/user.vue: | ||||||
|   follows-you: "フォローされています" |   follows-you: "フォローされています" | ||||||
|   | |||||||
							
								
								
									
										1219
									
								
								locales/ko.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/ko.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/pl.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/pl.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/pt.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/pt.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/ru.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/ru.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/zh.yml
									
									
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/zh.yml
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <i@syuilo.com>", | 	"author": "syuilo <i@syuilo.com>", | ||||||
| 	"version": "7.3.0", | 	"version": "8.15.0", | ||||||
| 	"clientVersion": "1.0.8741", | 	"clientVersion": "1.0.9031", | ||||||
| 	"codename": "nighthike", | 	"codename": "nighthike", | ||||||
| 	"main": "./built/index.js", | 	"main": "./built/index.js", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| @@ -32,7 +32,7 @@ | |||||||
| 		"@types/debug": "0.0.30", | 		"@types/debug": "0.0.30", | ||||||
| 		"@types/deep-equal": "1.0.1", | 		"@types/deep-equal": "1.0.1", | ||||||
| 		"@types/double-ended-queue": "2.1.0", | 		"@types/double-ended-queue": "2.1.0", | ||||||
| 		"@types/elasticsearch": "5.0.25", | 		"@types/elasticsearch": "5.0.26", | ||||||
| 		"@types/file-type": "5.2.1", | 		"@types/file-type": "5.2.1", | ||||||
| 		"@types/gulp": "3.8.36", | 		"@types/gulp": "3.8.36", | ||||||
| 		"@types/gulp-htmlmin": "1.3.32", | 		"@types/gulp-htmlmin": "1.3.32", | ||||||
| @@ -60,7 +60,7 @@ | |||||||
| 		"@types/mocha": "5.2.3", | 		"@types/mocha": "5.2.3", | ||||||
| 		"@types/mongodb": "3.1.4", | 		"@types/mongodb": "3.1.4", | ||||||
| 		"@types/ms": "0.7.30", | 		"@types/ms": "0.7.30", | ||||||
| 		"@types/node": "10.7.1", | 		"@types/node": "10.9.3", | ||||||
| 		"@types/portscanner": "2.1.0", | 		"@types/portscanner": "2.1.0", | ||||||
| 		"@types/pug": "2.0.4", | 		"@types/pug": "2.0.4", | ||||||
| 		"@types/qrcode": "1.2.0", | 		"@types/qrcode": "1.2.0", | ||||||
| @@ -70,14 +70,14 @@ | |||||||
| 		"@types/request-promise-native": "1.0.15", | 		"@types/request-promise-native": "1.0.15", | ||||||
| 		"@types/rimraf": "2.0.2", | 		"@types/rimraf": "2.0.2", | ||||||
| 		"@types/seedrandom": "2.4.27", | 		"@types/seedrandom": "2.4.27", | ||||||
| 		"@types/sharp": "0.17.9", | 		"@types/sharp": "0.17.10", | ||||||
| 		"@types/showdown": "1.7.5", | 		"@types/showdown": "1.7.5", | ||||||
| 		"@types/single-line-log": "1.1.0", | 		"@types/single-line-log": "1.1.0", | ||||||
| 		"@types/speakeasy": "2.0.2", | 		"@types/speakeasy": "2.0.2", | ||||||
| 		"@types/systeminformation": "3.23.0", | 		"@types/systeminformation": "3.23.0", | ||||||
| 		"@types/tmp": "0.0.33", | 		"@types/tmp": "0.0.33", | ||||||
| 		"@types/uuid": "3.4.3", | 		"@types/uuid": "3.4.3", | ||||||
| 		"@types/webpack": "4.4.10", | 		"@types/webpack": "4.4.11", | ||||||
| 		"@types/webpack-stream": "3.2.10", | 		"@types/webpack-stream": "3.2.10", | ||||||
| 		"@types/websocket": "0.0.39", | 		"@types/websocket": "0.0.39", | ||||||
| 		"@types/ws": "6.0.0", | 		"@types/ws": "6.0.0", | ||||||
| @@ -89,6 +89,7 @@ | |||||||
| 		"bootstrap-vue": "2.0.0-rc.11", | 		"bootstrap-vue": "2.0.0-rc.11", | ||||||
| 		"cafy": "11.3.0", | 		"cafy": "11.3.0", | ||||||
| 		"chalk": "2.4.1", | 		"chalk": "2.4.1", | ||||||
|  | 		"chart.js": "2.7.2", | ||||||
| 		"commander": "2.17.1", | 		"commander": "2.17.1", | ||||||
| 		"crc-32": "1.2.0", | 		"crc-32": "1.2.0", | ||||||
| 		"css-loader": "1.0.0", | 		"css-loader": "1.0.0", | ||||||
| @@ -149,6 +150,7 @@ | |||||||
| 		"loader-utils": "1.1.0", | 		"loader-utils": "1.1.0", | ||||||
| 		"lodash.assign": "4.2.0", | 		"lodash.assign": "4.2.0", | ||||||
| 		"mecab-async": "0.1.2", | 		"mecab-async": "0.1.2", | ||||||
|  | 		"merge-options": "1.0.1", | ||||||
| 		"minio": "7.0.0", | 		"minio": "7.0.0", | ||||||
| 		"mkdirp": "0.5.1", | 		"mkdirp": "0.5.1", | ||||||
| 		"mocha": "5.2.0", | 		"mocha": "5.2.0", | ||||||
| @@ -156,7 +158,7 @@ | |||||||
| 		"mongodb": "3.1.1", | 		"mongodb": "3.1.1", | ||||||
| 		"monk": "6.0.6", | 		"monk": "6.0.6", | ||||||
| 		"ms": "2.1.1", | 		"ms": "2.1.1", | ||||||
| 		"nan": "2.10.0", | 		"nan": "2.11.0", | ||||||
| 		"nested-property": "0.0.7", | 		"nested-property": "0.0.7", | ||||||
| 		"node-sass": "4.9.3", | 		"node-sass": "4.9.3", | ||||||
| 		"node-sass-json-importer": "3.3.1", | 		"node-sass-json-importer": "3.3.1", | ||||||
| @@ -188,11 +190,11 @@ | |||||||
| 		"single-line-log": "1.1.2", | 		"single-line-log": "1.1.2", | ||||||
| 		"speakeasy": "2.0.0", | 		"speakeasy": "2.0.0", | ||||||
| 		"stringz": "1.0.0", | 		"stringz": "1.0.0", | ||||||
| 		"style-loader": "0.22.1", | 		"style-loader": "0.23.0", | ||||||
| 		"stylus": "0.54.5", | 		"stylus": "0.54.5", | ||||||
| 		"stylus-loader": "3.0.2", | 		"stylus-loader": "3.0.2", | ||||||
| 		"summaly": "2.1.4", | 		"summaly": "2.1.4", | ||||||
| 		"systeminformation": "3.42.9", | 		"systeminformation": "3.44.2", | ||||||
| 		"syuilo-password-strength": "0.0.1", | 		"syuilo-password-strength": "0.0.1", | ||||||
| 		"textarea-caret": "3.1.0", | 		"textarea-caret": "3.1.0", | ||||||
| 		"tmp": "0.0.33", | 		"tmp": "0.0.33", | ||||||
| @@ -206,10 +208,11 @@ | |||||||
| 		"uuid": "3.3.2", | 		"uuid": "3.3.2", | ||||||
| 		"v-animate-css": "0.0.2", | 		"v-animate-css": "0.0.2", | ||||||
| 		"vue": "2.5.17", | 		"vue": "2.5.17", | ||||||
|  | 		"vue-chartjs": "3.4.0", | ||||||
| 		"vue-cropperjs": "2.2.1", | 		"vue-cropperjs": "2.2.1", | ||||||
| 		"vue-js-modal": "1.3.17", | 		"vue-js-modal": "1.3.23", | ||||||
| 		"vue-json-tree-view": "2.1.4", | 		"vue-json-tree-view": "2.1.4", | ||||||
| 		"vue-loader": "15.4.0", | 		"vue-loader": "15.4.1", | ||||||
| 		"vue-router": "3.0.1", | 		"vue-router": "3.0.1", | ||||||
| 		"vue-style-loader": "4.1.2", | 		"vue-style-loader": "4.1.2", | ||||||
| 		"vue-template-compiler": "2.5.17", | 		"vue-template-compiler": "2.5.17", | ||||||
| @@ -218,7 +221,7 @@ | |||||||
| 		"vuex-persistedstate": "2.5.4", | 		"vuex-persistedstate": "2.5.4", | ||||||
| 		"web-push": "3.3.2", | 		"web-push": "3.3.2", | ||||||
| 		"webfinger.js": "2.6.6", | 		"webfinger.js": "2.6.6", | ||||||
| 		"webpack": "4.17.0", | 		"webpack": "4.17.1", | ||||||
| 		"webpack-cli": "3.1.0", | 		"webpack-cli": "3.1.0", | ||||||
| 		"websocket": "1.0.26", | 		"websocket": "1.0.26", | ||||||
| 		"ws": "6.0.0", | 		"ws": "6.0.0", | ||||||
|   | |||||||
| @@ -38,15 +38,22 @@ | |||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	//#region Detect the user language | 	//#region Detect the user language | ||||||
| 	let lang = navigator.language; | 	let lang = null; | ||||||
|  |  | ||||||
| 	if (!LANGS.includes(lang)) lang = lang.split('-')[0]; | 	if (LANGS.includes(navigator.language)) { | ||||||
|  | 		lang = navigator.language; | ||||||
|  | 	} else { | ||||||
|  | 		lang = LANGS.find(x => x.split('-')[0] == navigator.language); | ||||||
|  |  | ||||||
| 	// The default language is English | 		if (lang == null) { | ||||||
| 	if (!LANGS.includes(lang)) lang = 'en'; | 			// Fallback | ||||||
|  | 			lang = 'en-US'; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if (settings) { | 	if (settings && settings.device.lang && | ||||||
| 		if (settings.device.lang) lang = settings.device.lang; | 		LANGS.includes(settings.device.lang)) { | ||||||
|  | 		lang = settings.device.lang; | ||||||
| 	} | 	} | ||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,8 +26,8 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	created() { | 	created() { | ||||||
| 		(this as any).os.getMeta().then(meta => { | 		(this as any).os.getMeta().then(meta => { | ||||||
| 			if (meta.repositoryUrl) this.repositoryUrl = meta.repositoryUrl; | 			if (meta.maintainer.repository_url) this.repositoryUrl = meta.maintainer.repository_url; | ||||||
| 			if (meta.feedbackUrl) this.feedbackUrl = meta.feedbackUrl; | 			if (meta.maintainer.feedback_url) this.feedbackUrl = meta.maintainer.feedback_url; | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -28,69 +28,8 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { url as misskeyUrl } from '../../../config'; | import { url as misskeyUrl } from '../../../config'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | // THIS IS THE WHITELIST FOR THE EMBED PLAYER | ||||||
| 	props: { | const whiteList = [ | ||||||
| 		url: { |  | ||||||
| 			type: String, |  | ||||||
| 			require: true |  | ||||||
| 		}, |  | ||||||
| 		detail: { |  | ||||||
| 			type: Boolean, |  | ||||||
| 			required: false, |  | ||||||
| 			default: false |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			fetching: true, |  | ||||||
| 			title: null, |  | ||||||
| 			description: null, |  | ||||||
| 			thumbnail: null, |  | ||||||
| 			icon: null, |  | ||||||
| 			sitename: null, |  | ||||||
| 			player: { |  | ||||||
| 				url: null, |  | ||||||
| 				width: null, |  | ||||||
| 				height: null |  | ||||||
| 			}, |  | ||||||
| 			tweetUrl: null, |  | ||||||
| 			misskeyUrl |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	created() { |  | ||||||
| 		const url = new URL(this.url); |  | ||||||
|  |  | ||||||
| 		if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) { |  | ||||||
| 			this.tweetUrl = url; |  | ||||||
| 			const twttr = (window as any).twttr || {}; |  | ||||||
| 			const loadTweet = () => twttr.widgets.load(this.$refs.tweet); |  | ||||||
|  |  | ||||||
| 			if (twttr.widgets) { |  | ||||||
| 				Vue.nextTick(loadTweet); |  | ||||||
| 			} else { |  | ||||||
| 				const wjsId = 'twitter-wjs'; |  | ||||||
| 				if (!document.getElementById(wjsId)) { |  | ||||||
| 					const head = document.getElementsByTagName('head')[0]; |  | ||||||
| 					const script = document.createElement('script'); |  | ||||||
| 					script.setAttribute('id', wjsId); |  | ||||||
| 					script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); |  | ||||||
| 					head.appendChild(script); |  | ||||||
| 				} |  | ||||||
| 				twttr.ready = loadTweet; |  | ||||||
| 				(window as any).twttr = twttr; |  | ||||||
| 			} |  | ||||||
| 			return; |  | ||||||
| 		} |  | ||||||
| 		fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { |  | ||||||
| 			res.json().then(info => { |  | ||||||
| 				if (info.url != null) { |  | ||||||
| 					this.title = info.title; |  | ||||||
| 					this.description = info.description; |  | ||||||
| 					this.thumbnail = info.thumbnail; |  | ||||||
| 					this.icon = info.icon; |  | ||||||
| 					this.sitename = info.sitename; |  | ||||||
| 					this.fetching = false; |  | ||||||
| 					if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER |  | ||||||
| 	'afreecatv.com', | 	'afreecatv.com', | ||||||
| 	'aparat.com', | 	'aparat.com', | ||||||
| 	'applemusic.com', | 	'applemusic.com', | ||||||
| @@ -166,17 +105,85 @@ export default Vue.extend({ | |||||||
| 	'web.tv', | 	'web.tv', | ||||||
| 	'youtube.com', | 	'youtube.com', | ||||||
| 	'youtu.be' | 	'youtu.be' | ||||||
| 					].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) | ]; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	props: { | ||||||
|  | 		url: { | ||||||
|  | 			type: String, | ||||||
|  | 			require: true | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		detail: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			fetching: true, | ||||||
|  | 			title: null, | ||||||
|  | 			description: null, | ||||||
|  | 			thumbnail: null, | ||||||
|  | 			icon: null, | ||||||
|  | 			sitename: null, | ||||||
|  | 			player: { | ||||||
|  | 				url: null, | ||||||
|  | 				width: null, | ||||||
|  | 				height: null | ||||||
|  | 			}, | ||||||
|  | 			tweetUrl: null, | ||||||
|  | 			misskeyUrl | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	created() { | ||||||
|  | 		const url = new URL(this.url); | ||||||
|  |  | ||||||
|  | 		if (this.detail && url.hostname == 'twitter.com' && /^\/.+\/status(es)?\/\d+/.test(url.pathname)) { | ||||||
|  | 			this.tweetUrl = url; | ||||||
|  | 			const twttr = (window as any).twttr || {}; | ||||||
|  | 			const loadTweet = () => twttr.widgets.load(this.$refs.tweet); | ||||||
|  |  | ||||||
|  | 			if (twttr.widgets) { | ||||||
|  | 				Vue.nextTick(loadTweet); | ||||||
|  | 			} else { | ||||||
|  | 				const wjsId = 'twitter-wjs'; | ||||||
|  | 				if (!document.getElementById(wjsId)) { | ||||||
|  | 					const head = document.getElementsByTagName('head')[0]; | ||||||
|  | 					const script = document.createElement('script'); | ||||||
|  | 					script.setAttribute('id', wjsId); | ||||||
|  | 					script.setAttribute('src', 'https://platform.twitter.com/widgets.js'); | ||||||
|  | 					head.appendChild(script); | ||||||
|  | 				} | ||||||
|  | 				twttr.ready = loadTweet; | ||||||
|  | 				(window as any).twttr = twttr; | ||||||
|  | 			} | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { | ||||||
|  | 			res.json().then(info => { | ||||||
|  | 				if (info.url == null) return; | ||||||
|  | 				this.title = info.title; | ||||||
|  | 				this.description = info.description; | ||||||
|  | 				this.thumbnail = info.thumbnail; | ||||||
|  | 				this.icon = info.icon; | ||||||
|  | 				this.sitename = info.sitename; | ||||||
|  | 				this.fetching = false; | ||||||
|  | 				if (whiteList.some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) { | ||||||
| 					this.player = info.player; | 					this.player = info.player; | ||||||
| 				}	// info.url | 				} | ||||||
| 			})	// json | 			}) | ||||||
| 		});	// fetch | 		}); | ||||||
| 	}	// created | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> | <style lang="stylus" scoped> | ||||||
| .twitter | .player | ||||||
| 	position relative | 	position relative | ||||||
| 	width 100% | 	width 100% | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  |  | ||||||
| Vue.filter('bytes', (v, digits = 0) => { | Vue.filter('bytes', (v, digits = 0) => { | ||||||
| 	const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; | 	const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | ||||||
| 	if (v == 0) return '0Byte'; | 	if (v == 0) return '0'; | ||||||
|  | 	const isMinus = v < 0; | ||||||
|  | 	if (isMinus) v = -v; | ||||||
| 	const i = Math.floor(Math.log(v) / Math.log(1024)); | 	const i = Math.floor(Math.log(v) / Math.log(1024)); | ||||||
| 	return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; | 	return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -2,9 +2,9 @@ | |||||||
| <div class="mkw-donation" :data-mobile="platform == 'mobile'"> | <div class="mkw-donation" :data-mobile="platform == 'mobile'"> | ||||||
| 	<article> | 	<article> | ||||||
| 		<h1>%fa:heart%%i18n:@title%</h1> | 		<h1>%fa:heart%%i18n:@title%</h1> | ||||||
| 		<p> | 		<p v-if="meta"> | ||||||
| 			{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} | 			{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} | ||||||
| 			<a href="https://syuilo.com">@syuilo</a> | 			<a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a> | ||||||
| 			{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} | 			{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} | ||||||
| 		</p> | 		</p> | ||||||
| 	</article> | 	</article> | ||||||
| @@ -15,6 +15,17 @@ | |||||||
| import define from '../../../common/define-widget'; | import define from '../../../common/define-widget'; | ||||||
| export default define({ | export default define({ | ||||||
| 	name: 'donation' | 	name: 'donation' | ||||||
|  | }).extend({ | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			meta: null | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	created() { | ||||||
|  | 		(this as any).os.getMeta().then(meta => { | ||||||
|  | 			this.meta = meta; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,8 +3,21 @@ import { apiUrl } from '../../config'; | |||||||
| import CropWindow from '../views/components/crop-window.vue'; | import CropWindow from '../views/components/crop-window.vue'; | ||||||
| import ProgressDialog from '../views/components/progress-dialog.vue'; | import ProgressDialog from '../views/components/progress-dialog.vue'; | ||||||
|  |  | ||||||
| export default (os: OS) => (cb, file = null) => { | export default (os: OS) => { | ||||||
| 	const fileSelected = file => { |  | ||||||
|  | 	const cropImage = file => new Promise((resolve, reject) => { | ||||||
|  |  | ||||||
|  | 		const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$'); | ||||||
|  | 		if (!regex.test(file.name) ) { | ||||||
|  | 			os.apis.dialog({ | ||||||
|  | 				title: '%fa:info-circle% %i18n:desktop.invalid-filetype%', | ||||||
|  | 				text: null, | ||||||
|  | 				actions: [{ | ||||||
|  | 					text: '%i18n:common.got-it%' | ||||||
|  | 				}] | ||||||
|  | 			}); | ||||||
|  | 			reject(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		const w = os.new(CropWindow, { | 		const w = os.new(CropWindow, { | ||||||
| 			image: file, | 			image: file, | ||||||
| @@ -19,27 +32,29 @@ export default (os: OS) => (cb, file = null) => { | |||||||
|  |  | ||||||
| 			os.api('drive/folders/find', { | 			os.api('drive/folders/find', { | ||||||
| 				name: '%i18n:desktop.avatar%' | 				name: '%i18n:desktop.avatar%' | ||||||
| 			}).then(iconFolder => { | 			}).then(avatarFolder => { | ||||||
| 				if (iconFolder.length === 0) { | 				if (avatarFolder.length === 0) { | ||||||
| 					os.api('drive/folders/create', { | 					os.api('drive/folders/create', { | ||||||
| 						name: '%i18n:desktop.avatar%' | 						name: '%i18n:desktop.avatar%' | ||||||
| 					}).then(iconFolder => { | 					}).then(iconFolder => { | ||||||
| 						upload(data, iconFolder); | 						resolve(upload(data, iconFolder)); | ||||||
| 					}); | 					}); | ||||||
| 				} else { | 				} else { | ||||||
| 					upload(data, iconFolder[0]); | 					resolve(upload(data, avatarFolder[0])); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		w.$once('skipped', () => { | 		w.$once('skipped', () => { | ||||||
| 			set(file); | 			resolve(file); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		document.body.appendChild(w.$el); | 		w.$once('cancelled', reject); | ||||||
| 	}; |  | ||||||
|  |  | ||||||
| 	const upload = (data, folder) => { | 		document.body.appendChild(w.$el); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	const upload = (data, folder) => new Promise((resolve, reject) => { | ||||||
| 		const dialog = os.new(ProgressDialog, { | 		const dialog = os.new(ProgressDialog, { | ||||||
| 			title: '%i18n:desktop.uploading-avatar%' | 			title: '%i18n:desktop.uploading-avatar%' | ||||||
| 		}); | 		}); | ||||||
| @@ -52,18 +67,19 @@ export default (os: OS) => (cb, file = null) => { | |||||||
| 		xhr.onload = e => { | 		xhr.onload = e => { | ||||||
| 			const file = JSON.parse((e.target as any).response); | 			const file = JSON.parse((e.target as any).response); | ||||||
| 			(dialog as any).close(); | 			(dialog as any).close(); | ||||||
| 			set(file); | 			resolve(file); | ||||||
| 		}; | 		}; | ||||||
|  | 		xhr.onerror = reject; | ||||||
|  |  | ||||||
| 		xhr.upload.onprogress = e => { | 		xhr.upload.onprogress = e => { | ||||||
| 			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); | 			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		xhr.send(data); | 		xhr.send(data); | ||||||
| 	}; | 	}); | ||||||
|  |  | ||||||
| 	const set = file => { | 	const setAvatar = file => { | ||||||
| 		os.api('i/update', { | 		return os.api('i/update', { | ||||||
| 			avatarId: file.id | 			avatarId: file.id | ||||||
| 		}).then(i => { | 		}).then(i => { | ||||||
| 			os.store.commit('updateIKeyValue', { | 			os.store.commit('updateIKeyValue', { | ||||||
| @@ -83,18 +99,21 @@ export default (os: OS) => (cb, file = null) => { | |||||||
| 				}] | 				}] | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
| 			if (cb) cb(i); | 			return i; | ||||||
| 		}); | 		}); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	if (file) { | 	return (file = null) => { | ||||||
| 		fileSelected(file); | 		const selectedFile = file | ||||||
| 	} else { | 			? Promise.resolve(file) | ||||||
| 		os.apis.chooseDriveFile({ | 			: os.apis.chooseDriveFile({ | ||||||
| 				multiple: false, | 				multiple: false, | ||||||
| 				title: '%fa:image% %i18n:desktop.choose-avatar%' | 				title: '%fa:image% %i18n:desktop.choose-avatar%' | ||||||
| 		}).then(file => { |  | ||||||
| 			fileSelected(file); |  | ||||||
| 			}); | 			}); | ||||||
| 	} |  | ||||||
|  | 		return selectedFile | ||||||
|  | 			.then(cropImage) | ||||||
|  | 			.then(setAvatar) | ||||||
|  | 			.catch(err => err && console.warn(err)); | ||||||
|  | 	}; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -6,6 +6,19 @@ import ProgressDialog from '../views/components/progress-dialog.vue'; | |||||||
| export default (os: OS) => { | export default (os: OS) => { | ||||||
|  |  | ||||||
| 	const cropImage = file => new Promise((resolve, reject) => { | 	const cropImage = file => new Promise((resolve, reject) => { | ||||||
|  |  | ||||||
|  | 		const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$'); | ||||||
|  | 		if (!regex.test(file.name) ) { | ||||||
|  | 			os.apis.dialog({ | ||||||
|  | 				title: '%fa:info-circle% %i18n:desktop.invalid-filetype%', | ||||||
|  | 				text: null, | ||||||
|  | 				actions: [{ | ||||||
|  | 					text: '%i18n:common.got-it%' | ||||||
|  | 				}] | ||||||
|  | 			}); | ||||||
|  | 			reject(); | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		const w = os.new(CropWindow, { | 		const w = os.new(CropWindow, { | ||||||
| 			image: file, | 			image: file, | ||||||
| 			title: '%i18n:desktop.banner-crop-title%', | 			title: '%i18n:desktop.banner-crop-title%', | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import updateBanner from './api/update-banner'; | |||||||
| import MkIndex from './views/pages/index.vue'; | import MkIndex from './views/pages/index.vue'; | ||||||
| import MkDeck from './views/pages/deck/deck.vue'; | import MkDeck from './views/pages/deck/deck.vue'; | ||||||
| import MkAdmin from './views/pages/admin/admin.vue'; | import MkAdmin from './views/pages/admin/admin.vue'; | ||||||
|  | import MkStats from './views/pages/stats/stats.vue'; | ||||||
| import MkUser from './views/pages/user/user.vue'; | import MkUser from './views/pages/user/user.vue'; | ||||||
| import MkFavorites from './views/pages/favorites.vue'; | import MkFavorites from './views/pages/favorites.vue'; | ||||||
| import MkSelectDrive from './views/pages/selectdrive.vue'; | import MkSelectDrive from './views/pages/selectdrive.vue'; | ||||||
| @@ -57,6 +58,7 @@ init(async (launch) => { | |||||||
| 			{ path: '/', name: 'index', component: MkIndex }, | 			{ path: '/', name: 'index', component: MkIndex }, | ||||||
| 			{ path: '/deck', name: 'deck', component: MkDeck }, | 			{ path: '/deck', name: 'deck', component: MkDeck }, | ||||||
| 			{ path: '/admin', name: 'admin', component: MkAdmin }, | 			{ path: '/admin', name: 'admin', component: MkAdmin }, | ||||||
|  | 			{ path: '/stats', name: 'stats', component: MkStats }, | ||||||
| 			{ path: '/i/customize-home', component: MkHomeCustomize }, | 			{ path: '/i/customize-home', component: MkHomeCustomize }, | ||||||
| 			{ path: '/i/favorites', component: MkFavorites }, | 			{ path: '/i/favorites', component: MkFavorites }, | ||||||
| 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | 			{ path: '/i/messaging/:user', component: MkMessagingRoom }, | ||||||
| @@ -94,7 +96,7 @@ init(async (launch) => { | |||||||
| 	/** | 	/** | ||||||
| 	 * Init Notification | 	 * Init Notification | ||||||
| 	 */ | 	 */ | ||||||
| 	if ('Notification' in window) { | 	if ('Notification' in window && os.store.getters.isSignedIn) { | ||||||
| 		// 許可を得ていなかったらリクエスト | 		// 許可を得ていなかったらリクエスト | ||||||
| 		if ((Notification as any).permission == 'default') { | 		if ((Notification as any).permission == 'default') { | ||||||
| 			await Notification.requestPermission(); | 			await Notification.requestPermission(); | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								src/client/app/desktop/views/components/charts.chart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/client/app/desktop/views/components/charts.chart.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | import Vue from 'vue'; | ||||||
|  | import { Line } from 'vue-chartjs'; | ||||||
|  | import * as mergeOptions from 'merge-options'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	extends: Line, | ||||||
|  | 	props: { | ||||||
|  | 		data: { | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		opts: { | ||||||
|  | 			required: false | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	watch: { | ||||||
|  | 		data() { | ||||||
|  | 			this.render(); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		this.render(); | ||||||
|  | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		render() { | ||||||
|  | 			this.renderChart(this.data, mergeOptions({ | ||||||
|  | 				responsive: true, | ||||||
|  | 				maintainAspectRatio: false, | ||||||
|  | 				scales: { | ||||||
|  | 					xAxes: [{ | ||||||
|  | 						type: 'time', | ||||||
|  | 						distribution: 'series' | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					intersect: false, | ||||||
|  | 					mode: 'x', | ||||||
|  | 					position: 'nearest' | ||||||
|  | 				} | ||||||
|  | 			}, this.opts || {})); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
							
								
								
									
										587
									
								
								src/client/app/desktop/views/components/charts.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										587
									
								
								src/client/app/desktop/views/components/charts.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,587 @@ | |||||||
|  | <template> | ||||||
|  | <div class="gkgckalzgidaygcxnugepioremxvxvpt"> | ||||||
|  | 	<header> | ||||||
|  | 		<b>%i18n:@title%:</b> | ||||||
|  | 		<select v-model="chartType"> | ||||||
|  | 			<optgroup label="%i18n:@users%"> | ||||||
|  | 				<option value="users">%i18n:@charts.users%</option> | ||||||
|  | 				<option value="users-total">%i18n:@charts.users-total%</option> | ||||||
|  | 			</optgroup> | ||||||
|  | 			<optgroup label="%i18n:@notes%"> | ||||||
|  | 				<option value="notes">%i18n:@charts.notes%</option> | ||||||
|  | 				<option value="local-notes">%i18n:@charts.local-notes%</option> | ||||||
|  | 				<option value="remote-notes">%i18n:@charts.remote-notes%</option> | ||||||
|  | 				<option value="notes-total">%i18n:@charts.notes-total%</option> | ||||||
|  | 			</optgroup> | ||||||
|  | 			<optgroup label="%i18n:@drive%"> | ||||||
|  | 				<option value="drive-files">%i18n:@charts.drive-files%</option> | ||||||
|  | 				<option value="drive-files-total">%i18n:@charts.drive-files-total%</option> | ||||||
|  | 				<option value="drive">%i18n:@charts.drive%</option> | ||||||
|  | 				<option value="drive-total">%i18n:@charts.drive-total%</option> | ||||||
|  | 			</optgroup> | ||||||
|  | 		</select> | ||||||
|  | 		<div> | ||||||
|  | 			<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span> | ||||||
|  | 		</div> | ||||||
|  | 	</header> | ||||||
|  | 	<div> | ||||||
|  | 		<x-chart v-if="chart" :data="data[0]" :opts="data[1]"/> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  | import XChart from './charts.chart.ts'; | ||||||
|  |  | ||||||
|  | const colors = { | ||||||
|  | 	local: 'rgb(246, 88, 79)', | ||||||
|  | 	remote: 'rgb(65, 221, 222)', | ||||||
|  |  | ||||||
|  | 	localPlus: 'rgb(52, 178, 118)', | ||||||
|  | 	remotePlus: 'rgb(158, 255, 209)', | ||||||
|  | 	localMinus: 'rgb(255, 97, 74)', | ||||||
|  | 	remoteMinus: 'rgb(255, 149, 134)' | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const rgba = (color: string): string => { | ||||||
|  | 	return color.replace('rgb', 'rgba').replace(')', ', 0.1)'); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XChart | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			chart: null, | ||||||
|  | 			chartType: 'notes', | ||||||
|  | 			span: 'hour' | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	computed: { | ||||||
|  | 		data(): any { | ||||||
|  | 			if (this.chart == null) return null; | ||||||
|  | 			switch (this.chartType) { | ||||||
|  | 				case 'users': return this.usersChart(false); | ||||||
|  | 				case 'users-total': return this.usersChart(true); | ||||||
|  | 				case 'notes': return this.notesChart('combined'); | ||||||
|  | 				case 'local-notes': return this.notesChart('local'); | ||||||
|  | 				case 'remote-notes': return this.notesChart('remote'); | ||||||
|  | 				case 'notes-total': return this.notesTotalChart(); | ||||||
|  | 				case 'drive': return this.driveChart(); | ||||||
|  | 				case 'drive-total': return this.driveTotalChart(); | ||||||
|  | 				case 'drive-files': return this.driveFilesChart(); | ||||||
|  | 				case 'drive-files-total': return this.driveFilesTotalChart(); | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		stats(): any[] { | ||||||
|  | 			return ( | ||||||
|  | 				this.span == 'day' ? this.chart.perDay : | ||||||
|  | 				this.span == 'hour' ? this.chart.perHour : | ||||||
|  | 				null | ||||||
|  | 			); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	created() { | ||||||
|  | 		(this as any).api('chart', { | ||||||
|  | 			limit: 32 | ||||||
|  | 		}).then(chart => { | ||||||
|  | 			this.chart = chart; | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	methods: { | ||||||
|  | 		notesChart(type: string): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				normal: type == 'local' ? x.notes.local.diffs.normal : type == 'remote' ? x.notes.remote.diffs.normal : x.notes.local.diffs.normal + x.notes.remote.diffs.normal, | ||||||
|  | 				reply: type == 'local' ? x.notes.local.diffs.reply : type == 'remote' ? x.notes.remote.diffs.reply : x.notes.local.diffs.reply + x.notes.remote.diffs.reply, | ||||||
|  | 				renote: type == 'local' ? x.notes.local.diffs.renote : type == 'remote' ? x.notes.remote.diffs.renote : x.notes.local.diffs.renote + x.notes.remote.diffs.renote, | ||||||
|  | 				all: type == 'local' ? (x.notes.local.inc + -x.notes.local.dec) : type == 'remote' ? (x.notes.remote.inc + -x.notes.remote.dec) : (x.notes.local.inc + -x.notes.local.dec) + (x.notes.remote.inc + -x.notes.remote.dec) | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'All', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.all })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Renotes', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: 'rgba(161, 222, 65, 0.1)', | ||||||
|  | 					borderColor: '#a1de41', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.renote })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Replies', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: 'rgba(247, 121, 108, 0.1)', | ||||||
|  | 					borderColor: '#f7796c', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.reply })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Normal', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: 'rgba(65, 221, 222, 0.1)', | ||||||
|  | 					borderColor: '#41ddde', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.normal })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('number')(value); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		notesTotalChart(): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				localCount: x.notes.local.total, | ||||||
|  | 				remoteCount: x.notes.remote.total | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'Combined', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.local), | ||||||
|  | 					borderColor: colors.local, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localCount })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remote), | ||||||
|  | 					borderColor: colors.remote, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteCount })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('number')(value); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		usersChart(total: boolean): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				localCount: total ? x.users.local.total : (x.users.local.inc + -x.users.local.dec), | ||||||
|  | 				remoteCount: total ? x.users.remote.total : (x.users.remote.inc + -x.users.remote.dec) | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'Combined', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.local), | ||||||
|  | 					borderColor: colors.local, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localCount })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remote), | ||||||
|  | 					borderColor: colors.remote, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteCount })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('number')(value); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		driveChart(): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				localInc: x.drive.local.incSize, | ||||||
|  | 				localDec: -x.drive.local.decSize, | ||||||
|  | 				remoteInc: x.drive.remote.incSize, | ||||||
|  | 				remoteDec: -x.drive.remote.decSize, | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'All', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local +', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.localPlus), | ||||||
|  | 					borderColor: colors.localPlus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localInc })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local -', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.localMinus), | ||||||
|  | 					borderColor: colors.localMinus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localDec })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote +', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remotePlus), | ||||||
|  | 					borderColor: colors.remotePlus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteInc })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote -', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remoteMinus), | ||||||
|  | 					borderColor: colors.remoteMinus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteDec })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('bytes')(value, 1); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		driveTotalChart(): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				localSize: x.drive.local.totalSize, | ||||||
|  | 				remoteSize: x.drive.remote.totalSize | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'Combined', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteSize + x.localSize })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.local), | ||||||
|  | 					borderColor: colors.local, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localSize })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remote), | ||||||
|  | 					borderColor: colors.remote, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteSize })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('bytes')(value, 1); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		driveFilesChart(): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				localInc: x.drive.local.incCount, | ||||||
|  | 				localDec: -x.drive.local.decCount, | ||||||
|  | 				remoteInc: x.drive.remote.incCount, | ||||||
|  | 				remoteDec: -x.drive.remote.decCount | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'All', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local +', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.localPlus), | ||||||
|  | 					borderColor: colors.localPlus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localInc })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local -', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.localMinus), | ||||||
|  | 					borderColor: colors.localMinus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localDec })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote +', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remotePlus), | ||||||
|  | 					borderColor: colors.remotePlus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteInc })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote -', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remoteMinus), | ||||||
|  | 					borderColor: colors.remoteMinus, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteDec })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('number')(value); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		driveFilesTotalChart(): any { | ||||||
|  | 			const data = this.stats.slice().reverse().map(x => ({ | ||||||
|  | 				date: new Date(x.date), | ||||||
|  | 				localCount: x.drive.local.totalCount, | ||||||
|  | 				remoteCount: x.drive.remote.totalCount, | ||||||
|  | 			})); | ||||||
|  |  | ||||||
|  | 			return [{ | ||||||
|  | 				datasets: [{ | ||||||
|  | 					label: 'Combined', | ||||||
|  | 					fill: false, | ||||||
|  | 					borderColor: '#555', | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					borderDash: [4, 4], | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localCount + x.remoteCount })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Local', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.local), | ||||||
|  | 					borderColor: colors.local, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.localCount })) | ||||||
|  | 				}, { | ||||||
|  | 					label: 'Remote', | ||||||
|  | 					fill: true, | ||||||
|  | 					backgroundColor: rgba(colors.remote), | ||||||
|  | 					borderColor: colors.remote, | ||||||
|  | 					borderWidth: 2, | ||||||
|  | 					pointBackgroundColor: '#fff', | ||||||
|  | 					lineTension: 0, | ||||||
|  | 					data: data.map(x => ({ t: x.date, y: x.remoteCount })) | ||||||
|  | 				}] | ||||||
|  | 			}, { | ||||||
|  | 				scales: { | ||||||
|  | 					yAxes: [{ | ||||||
|  | 						ticks: { | ||||||
|  | 							callback: value => { | ||||||
|  | 								return Vue.filter('number')(value); | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}] | ||||||
|  | 				}, | ||||||
|  | 				tooltips: { | ||||||
|  | 					callbacks: { | ||||||
|  | 						label: (tooltipItem, data) => { | ||||||
|  | 							const label = data.datasets[tooltipItem.datasetIndex].label || ''; | ||||||
|  | 							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			}]; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | @import '~const.styl' | ||||||
|  |  | ||||||
|  | .gkgckalzgidaygcxnugepioremxvxvpt | ||||||
|  | 	padding 32px | ||||||
|  | 	background #fff | ||||||
|  | 	box-shadow 0 2px 8px rgba(#000, 0.1) | ||||||
|  |  | ||||||
|  | 	* | ||||||
|  | 		user-select none | ||||||
|  |  | ||||||
|  | 	> header | ||||||
|  | 		display flex | ||||||
|  | 		margin 0 0 1em 0 | ||||||
|  | 		padding 0 0 8px 0 | ||||||
|  | 		font-size 1em | ||||||
|  | 		color #555 | ||||||
|  | 		border-bottom solid 1px #eee | ||||||
|  |  | ||||||
|  | 		> b | ||||||
|  | 			margin-right 8px | ||||||
|  |  | ||||||
|  | 		> *:last-child | ||||||
|  | 			margin-left auto | ||||||
|  |  | ||||||
|  | 			* | ||||||
|  | 				&:not(.active) | ||||||
|  | 					color $theme-color | ||||||
|  | 					cursor pointer | ||||||
|  |  | ||||||
|  | 	> div | ||||||
|  | 		> * | ||||||
|  | 			display block | ||||||
|  | 			height 320px | ||||||
|  |  | ||||||
|  | </style> | ||||||
| @@ -47,7 +47,7 @@ | |||||||
| 			</div> | 			</div> | ||||||
| 			<mk-poll v-if="p.poll" :note="p"/> | 			<mk-poll v-if="p.poll" :note="p"/> | ||||||
| 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||||
| 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | 			<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||||
| 			<div class="map" v-if="p.geo" ref="map"></div> | 			<div class="map" v-if="p.geo" ref="map"></div> | ||||||
| 			<div class="renote" v-if="p.renote"> | 			<div class="renote" v-if="p.renote"> | ||||||
| 				<mk-note-preview :note="p.renote"/> | 				<mk-note-preview :note="p.renote"/> | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ | |||||||
| 						<mk-media-list :media-list="p.media"/> | 						<mk-media-list :media-list="p.media"/> | ||||||
| 					</div> | 					</div> | ||||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||||
| 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> | 					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> | ||||||
| 					<div class="map" v-if="p.geo" ref="map"></div> | 					<div class="map" v-if="p.geo" ref="map"></div> | ||||||
| 					<div class="renote" v-if="p.renote"> | 					<div class="renote" v-if="p.renote"> | ||||||
| 						<mk-note-preview :note="p.renote"/> | 						<mk-note-preview :note="p.renote"/> | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ | |||||||
| 			</div> | 			</div> | ||||||
| 			<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> | 			<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> | ||||||
| 			<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> | 			<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> | ||||||
|  | 			<mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/> | ||||||
| 			<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> | 			<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> | ||||||
| 			<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> | 			<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> | ||||||
| 			<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> | 			<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> | ||||||
| @@ -333,6 +334,12 @@ export default Vue.extend({ | |||||||
| 				value: v | 				value: v | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  | 		onChangeShowClockOnHeader(v) { | ||||||
|  | 			this.$store.dispatch('settings/set', { | ||||||
|  | 				key: 'showClockOnHeader', | ||||||
|  | 				value: v | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
| 		onChangeShowReplyTarget(v) { | 		onChangeShowReplyTarget(v) { | ||||||
| 			this.$store.dispatch('settings/set', { | 			this.$store.dispatch('settings/set', { | ||||||
| 				key: 'showReplyTarget', | 				key: 'showReplyTarget', | ||||||
|   | |||||||
| @@ -30,10 +30,8 @@ | |||||||
| 				<li @click="settings"> | 				<li @click="settings"> | ||||||
| 					<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p> | 					<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p> | ||||||
| 				</li> | 				</li> | ||||||
| 			</ul> | 				<li v-if="$store.state.i.isAdmin"> | ||||||
| 			<ul> | 					<router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link> | ||||||
| 				<li @click="signout"> |  | ||||||
| 					<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> |  | ||||||
| 				</li> | 				</li> | ||||||
| 			</ul> | 			</ul> | ||||||
| 			<ul> | 			<ul> | ||||||
| @@ -41,6 +39,11 @@ | |||||||
| 					<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p> | 					<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p> | ||||||
| 				</li> | 				</li> | ||||||
| 			</ul> | 			</ul> | ||||||
|  | 			<ul> | ||||||
|  | 				<li @click="signout"> | ||||||
|  | 					<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> | ||||||
|  | 				</li> | ||||||
|  | 			</ul> | ||||||
| 		</div> | 		</div> | ||||||
| 	</transition> | 	</transition> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
| 			<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> | 			<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> | ||||||
| 				<router-link to="/deck"> | 				<router-link to="/deck"> | ||||||
| 					%fa:columns% | 					%fa:columns% | ||||||
| 					<p>%i18n:@deck% <small>(beta)</small></p> | 					<p>%i18n:@deck%</p> | ||||||
| 				</router-link> | 				</router-link> | ||||||
| 			</li> | 			</li> | ||||||
| 			<li class="messaging"> | 			<li class="messaging"> | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
| 					<x-account v-if="$store.getters.isSignedIn"/> | 					<x-account v-if="$store.getters.isSignedIn"/> | ||||||
| 					<x-notifications v-if="$store.getters.isSignedIn"/> | 					<x-notifications v-if="$store.getters.isSignedIn"/> | ||||||
| 					<x-post v-if="$store.getters.isSignedIn"/> | 					<x-post v-if="$store.getters.isSignedIn"/> | ||||||
| 					<x-clock/> | 					<x-clock v-if="$store.state.settings.showClockOnHeader"/> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ export default Vue.extend({ | |||||||
| 				this.open(); | 				this.open(); | ||||||
| 			}); | 			}); | ||||||
| 		} else { | 		} else { | ||||||
| 			const query = this.user[0] == '@' ? | 			const query = this.user.startsWith('@') ? | ||||||
| 				parseAcct(this.user.substr(1)) : | 				parseAcct(this.user.substr(1)) : | ||||||
| 				{ userId: this.user }; | 				{ userId: this.user }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,16 +1,20 @@ | |||||||
| <template> | <template> | ||||||
| <div class="obdskegsannmntldydackcpzezagxqfy card"> | <div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card"> | ||||||
| 	<header>%i18n:@dashboard%</header> | 	<header>%i18n:@dashboard%</header> | ||||||
| 	<div v-if="stats" class="stats"> | 	<div v-if="stats" class="stats"> | ||||||
| 		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> | 		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> | ||||||
| 		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> | 		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> | ||||||
| 		<div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> | 		<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> | ||||||
| 		<div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> | 		<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="cpu-memory"> | 	<div class="cpu-memory"> | ||||||
| 		<x-cpu-memory :connection="connection"/> | 		<x-cpu-memory :connection="connection"/> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div> | 	<div> | ||||||
|  | 		<label> | ||||||
|  | 			<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> | ||||||
|  | 			<span>disableRegistration</span> | ||||||
|  | 		</label> | ||||||
| 		<button class="ui" @click="invite">%i18n:@invite%</button> | 		<button class="ui" @click="invite">%i18n:@invite%</button> | ||||||
| 		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> | 		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> | ||||||
| 	</div> | 	</div> | ||||||
| @@ -28,6 +32,7 @@ export default Vue.extend({ | |||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			stats: null, | 			stats: null, | ||||||
|  | 			disableRegistration: false, | ||||||
| 			inviteCode: null, | 			inviteCode: null, | ||||||
| 			connection: null, | 			connection: null, | ||||||
| 			connectionId: null | 			connectionId: null | ||||||
| @@ -37,6 +42,10 @@ export default Vue.extend({ | |||||||
| 		this.connection = (this as any).os.streams.serverStatsStream.getConnection(); | 		this.connection = (this as any).os.streams.serverStatsStream.getConnection(); | ||||||
| 		this.connectionId = (this as any).os.streams.serverStatsStream.use(); | 		this.connectionId = (this as any).os.streams.serverStatsStream.use(); | ||||||
|  |  | ||||||
|  | 		(this as any).os.getMeta().then(meta => { | ||||||
|  | 			this.disableRegistration = meta.disableRegistration; | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		(this as any).api('stats').then(stats => { | 		(this as any).api('stats').then(stats => { | ||||||
| 			this.stats = stats; | 			this.stats = stats; | ||||||
| 		}); | 		}); | ||||||
| @@ -49,6 +58,11 @@ export default Vue.extend({ | |||||||
| 			(this as any).api('admin/invite').then(x => { | 			(this as any).api('admin/invite').then(x => { | ||||||
| 				this.inviteCode = x.code; | 				this.inviteCode = x.code; | ||||||
| 			}); | 			}); | ||||||
|  | 		}, | ||||||
|  | 		updateMeta() { | ||||||
|  | 			(this as any).api('admin/update-meta', { | ||||||
|  | 				disableRegistration: this.disableRegistration | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,51 +0,0 @@ | |||||||
| <template> |  | ||||||
| <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> |  | ||||||
| 	<polyline |  | ||||||
| 		:points="points" |  | ||||||
| 		fill="none" |  | ||||||
| 		stroke-width="1" |  | ||||||
| 		stroke="#555"/> |  | ||||||
| </svg> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	props: { |  | ||||||
| 		chart: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		type: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			viewBoxX: 365, |  | ||||||
| 			viewBoxY: 70, |  | ||||||
| 			points: null |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	created() { |  | ||||||
| 		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize)); |  | ||||||
|  |  | ||||||
| 		if (peak != 0) { |  | ||||||
| 			const data = this.chart.slice().reverse().map(x => ({ |  | ||||||
| 				size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize |  | ||||||
| 			})); |  | ||||||
|  |  | ||||||
| 			this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' '); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| svg |  | ||||||
| 	display block |  | ||||||
| 	padding 10px |  | ||||||
| 	width 100% |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| <template> |  | ||||||
| <div class="card"> |  | ||||||
| 	<header>%i18n:@title%</header> |  | ||||||
| 	<div class="card"> |  | ||||||
| 		<header>%i18n:@local%</header> |  | ||||||
| 		<x-chart v-if="chart" :chart="chart" type="local"/> |  | ||||||
| 	</div> |  | ||||||
| 	<div class="card"> |  | ||||||
| 		<header>%i18n:@remote%</header> |  | ||||||
| 		<x-chart v-if="chart" :chart="chart" type="remote"/> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from "vue"; |  | ||||||
| import XChart from "./admin.drive-chart.chart.vue"; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XChart |  | ||||||
| 	}, |  | ||||||
| 	props: { |  | ||||||
| 		chart: { |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| @import '~const.styl' |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,76 +0,0 @@ | |||||||
| <template> |  | ||||||
| <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> |  | ||||||
| 	<polyline |  | ||||||
| 		:points="pointsNote" |  | ||||||
| 		fill="none" |  | ||||||
| 		stroke-width="1" |  | ||||||
| 		stroke="#41ddde"/> |  | ||||||
| 	<polyline |  | ||||||
| 		:points="pointsReply" |  | ||||||
| 		fill="none" |  | ||||||
| 		stroke-width="1" |  | ||||||
| 		stroke="#f7796c"/> |  | ||||||
| 	<polyline |  | ||||||
| 		:points="pointsRenote" |  | ||||||
| 		fill="none" |  | ||||||
| 		stroke-width="1" |  | ||||||
| 		stroke="#a1de41"/> |  | ||||||
| 	<polyline |  | ||||||
| 		:points="pointsTotal" |  | ||||||
| 		fill="none" |  | ||||||
| 		stroke-width="1" |  | ||||||
| 		stroke="#555" |  | ||||||
| 		stroke-dasharray="2 2"/> |  | ||||||
| </svg> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	props: { |  | ||||||
| 		chart: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		type: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			viewBoxX: 365, |  | ||||||
| 			viewBoxY: 70, |  | ||||||
| 			pointsNote: null, |  | ||||||
| 			pointsReply: null, |  | ||||||
| 			pointsRenote: null, |  | ||||||
| 			pointsTotal: null |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	created() { |  | ||||||
| 		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff)); |  | ||||||
|  |  | ||||||
| 		if (peak != 0) { |  | ||||||
| 			const data = this.chart.slice().reverse().map(x => ({ |  | ||||||
| 				normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal, |  | ||||||
| 				reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply, |  | ||||||
| 				renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote, |  | ||||||
| 				total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff |  | ||||||
| 			})); |  | ||||||
|  |  | ||||||
| 			this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' '); |  | ||||||
| 			this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' '); |  | ||||||
| 			this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' '); |  | ||||||
| 			this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| svg |  | ||||||
| 	display block |  | ||||||
| 	padding 10px |  | ||||||
| 	width 100% |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| <template> |  | ||||||
| <div class="card"> |  | ||||||
| 	<header>%i18n:@title%</header> |  | ||||||
| 	<div class="card"> |  | ||||||
| 		<header>%i18n:@local%</header> |  | ||||||
| 		<x-chart v-if="chart" :chart="chart" type="local"/> |  | ||||||
| 	</div> |  | ||||||
| 	<div class="card"> |  | ||||||
| 		<header>%i18n:@remote%</header> |  | ||||||
| 		<x-chart v-if="chart" :chart="chart" type="remote"/> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from "vue"; |  | ||||||
| import XChart from "./admin.notes-chart.chart.vue"; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XChart |  | ||||||
| 	}, |  | ||||||
| 	props: { |  | ||||||
| 		chart: { |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| @import '~const.styl' |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
| <div class="card"> | <div class="mk-admin-card"> | ||||||
| 	<header>%i18n:@suspend-user%</header> | 	<header>%i18n:@suspend-user%</header> | ||||||
| 	<input v-model="username" type="text" class="ui"/> | 	<input v-model="username" type="text" class="ui"/> | ||||||
| 	<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> | 	<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
| <div class="card"> | <div class="mk-admin-card"> | ||||||
| 	<header>%i18n:@unsuspend-user%</header> | 	<header>%i18n:@unsuspend-user%</header> | ||||||
| 	<input v-model="username" type="text" class="ui"/> | 	<input v-model="username" type="text" class="ui"/> | ||||||
| 	<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> | 	<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
| <div class="card"> | <div class="mk-admin-card"> | ||||||
| 	<header>%i18n:@unverify-user%</header> | 	<header>%i18n:@unverify-user%</header> | ||||||
| 	<input v-model="username" type="text" class="ui"/> | 	<input v-model="username" type="text" class="ui"/> | ||||||
| 	<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> | 	<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> | ||||||
|   | |||||||
| @@ -1,51 +0,0 @@ | |||||||
| <template> |  | ||||||
| <svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> |  | ||||||
| 	<polyline |  | ||||||
| 		:points="points" |  | ||||||
| 		fill="none" |  | ||||||
| 		stroke-width="1" |  | ||||||
| 		stroke="#555"/> |  | ||||||
| </svg> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from 'vue'; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	props: { |  | ||||||
| 		chart: { |  | ||||||
| 			required: true |  | ||||||
| 		}, |  | ||||||
| 		type: { |  | ||||||
| 			type: String, |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	}, |  | ||||||
| 	data() { |  | ||||||
| 		return { |  | ||||||
| 			viewBoxX: 365, |  | ||||||
| 			viewBoxY: 70, |  | ||||||
| 			points: null |  | ||||||
| 		}; |  | ||||||
| 	}, |  | ||||||
| 	created() { |  | ||||||
| 		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff)); |  | ||||||
|  |  | ||||||
| 		if (peak != 0) { |  | ||||||
| 			const data = this.chart.slice().reverse().map(x => ({ |  | ||||||
| 				count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff |  | ||||||
| 			})); |  | ||||||
|  |  | ||||||
| 			this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' '); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| svg |  | ||||||
| 	display block |  | ||||||
| 	padding 10px |  | ||||||
| 	width 100% |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,34 +0,0 @@ | |||||||
| <template> |  | ||||||
| <div class="card"> |  | ||||||
| 	<header>%i18n:@title%</header> |  | ||||||
| 	<div class="card"> |  | ||||||
| 		<header>%i18n:@local%</header> |  | ||||||
| 		<x-chart v-if="chart" :chart="chart" type="local"/> |  | ||||||
| 	</div> |  | ||||||
| 	<div class="card"> |  | ||||||
| 		<header>%i18n:@remote%</header> |  | ||||||
| 		<x-chart v-if="chart" :chart="chart" type="remote"/> |  | ||||||
| 	</div> |  | ||||||
| </div> |  | ||||||
| </template> |  | ||||||
|  |  | ||||||
| <script lang="ts"> |  | ||||||
| import Vue from "vue"; |  | ||||||
| import XChart from "./admin.users-chart.chart.vue"; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ |  | ||||||
| 	components: { |  | ||||||
| 		XChart |  | ||||||
| 	}, |  | ||||||
| 	props: { |  | ||||||
| 		chart: { |  | ||||||
| 			required: true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
| </script> |  | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> |  | ||||||
| @import '~const.styl' |  | ||||||
|  |  | ||||||
| </style> |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
| <div class="card"> | <div class="mk-admin-card"> | ||||||
| 	<header>%i18n:@verify-user%</header> | 	<header>%i18n:@verify-user%</header> | ||||||
| 	<input v-model="username" type="text" class="ui"/> | 	<input v-model="username" type="text" class="ui"/> | ||||||
| 	<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> | 	<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> | ||||||
|   | |||||||
| @@ -11,9 +11,7 @@ | |||||||
| 	<main> | 	<main> | ||||||
| 		<div v-show="page == 'dashboard'"> | 		<div v-show="page == 'dashboard'"> | ||||||
| 			<x-dashboard/> | 			<x-dashboard/> | ||||||
| 			<x-users-chart :chart="chart"/> | 			<x-charts/> | ||||||
| 			<x-notes-chart :chart="chart"/> |  | ||||||
| 			<x-drive-chart :chart="chart"/> |  | ||||||
| 		</div> | 		</div> | ||||||
| 		<div v-if="page == 'users'"> | 		<div v-if="page == 'users'"> | ||||||
| 			<x-suspend-user/> | 			<x-suspend-user/> | ||||||
| @@ -34,9 +32,7 @@ import XSuspendUser from "./admin.suspend-user.vue"; | |||||||
| import XUnsuspendUser from "./admin.unsuspend-user.vue"; | import XUnsuspendUser from "./admin.unsuspend-user.vue"; | ||||||
| import XVerifyUser from "./admin.verify-user.vue"; | import XVerifyUser from "./admin.verify-user.vue"; | ||||||
| import XUnverifyUser from "./admin.unverify-user.vue"; | import XUnverifyUser from "./admin.unverify-user.vue"; | ||||||
| import XUsersChart from "./admin.users-chart.vue"; | import XCharts from "../../components/charts.vue"; | ||||||
| import XNotesChart from "./admin.notes-chart.vue"; |  | ||||||
| import XDriveChart from "./admin.drive-chart.vue"; |  | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| @@ -45,21 +41,13 @@ export default Vue.extend({ | |||||||
| 		XUnsuspendUser, | 		XUnsuspendUser, | ||||||
| 		XVerifyUser, | 		XVerifyUser, | ||||||
| 		XUnverifyUser, | 		XUnverifyUser, | ||||||
| 		XUsersChart, | 		XCharts | ||||||
| 		XNotesChart, |  | ||||||
| 		XDriveChart |  | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			page: 'dashboard', | 			page: 'dashboard' | ||||||
| 			chart: null |  | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 	created() { |  | ||||||
| 		(this as any).api('admin/chart').then(chart => { |  | ||||||
| 			this.chart = chart; |  | ||||||
| 		}); |  | ||||||
| 	}, |  | ||||||
| 	methods: { | 	methods: { | ||||||
| 		nav(page: string) { | 		nav(page: string) { | ||||||
| 			this.page = page; | 			this.page = page; | ||||||
| @@ -115,7 +103,7 @@ export default Vue.extend({ | |||||||
| 			> div | 			> div | ||||||
| 				max-width 800px | 				max-width 800px | ||||||
|  |  | ||||||
| .card | .mk-admin-card | ||||||
| 	padding 32px | 	padding 32px | ||||||
| 	background #fff | 	background #fff | ||||||
| 	box-shadow 0 2px 8px rgba(#000, 0.1) | 	box-shadow 0 2px 8px rgba(#000, 0.1) | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ | |||||||
| 						<mk-media-list :media-list="p.media"/> | 						<mk-media-list :media-list="p.media"/> | ||||||
| 					</div> | 					</div> | ||||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||||
| 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | 					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||||
| 					<div class="renote" v-if="p.renote"> | 					<div class="renote" v-if="p.renote"> | ||||||
| 						<mk-note-preview :note="p.renote" :mini="true"/> | 						<mk-note-preview :note="p.renote" :mini="true"/> | ||||||
| 					</div> | 					</div> | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import Vue from 'vue'; | |||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			name: (this as any).os.instanceName, | 			name: null, | ||||||
| 			posted: false, | 			posted: false, | ||||||
| 			text: new URLSearchParams(location.search).get('text') | 			text: new URLSearchParams(location.search).get('text') | ||||||
| 		}; | 		}; | ||||||
| @@ -25,6 +25,11 @@ export default Vue.extend({ | |||||||
| 		close() { | 		close() { | ||||||
| 			window.close(); | 			window.close(); | ||||||
| 		} | 		} | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		(this as any).os.getMeta().then(meta => { | ||||||
|  | 			this.name = meta.name; | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								src/client/app/desktop/views/pages/stats/stats.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/client/app/desktop/views/pages/stats/stats.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | <template> | ||||||
|  | <div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey"> | ||||||
|  | 	<div v-if="stats" class="stats"> | ||||||
|  | 		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> | ||||||
|  | 		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> | ||||||
|  | 		<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> | ||||||
|  | 		<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> | ||||||
|  | 	</div> | ||||||
|  | 	<div> | ||||||
|  | 		<x-charts/> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from "vue"; | ||||||
|  | import XCharts from "../../components/charts.vue"; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XCharts | ||||||
|  | 	}, | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			stats: null | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	created() { | ||||||
|  | 		(this as any).api('stats').then(stats => { | ||||||
|  | 			this.stats = stats; | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="stylus"> | ||||||
|  | @import '~const.styl' | ||||||
|  |  | ||||||
|  | .tcrwdhwpuxrwmcttxjcsehgpagpstqey | ||||||
|  | 	width 100% | ||||||
|  | 	padding 16px | ||||||
|  |  | ||||||
|  | 	> .stats | ||||||
|  | 		display flex | ||||||
|  | 		justify-content center | ||||||
|  | 		margin-bottom 16px | ||||||
|  | 		padding 32px | ||||||
|  | 		background #fff | ||||||
|  | 		box-shadow 0 2px 8px rgba(#000, 0.1) | ||||||
|  |  | ||||||
|  | 		> div | ||||||
|  | 			flex 1 | ||||||
|  | 			text-align center | ||||||
|  |  | ||||||
|  | 			> *:first-child | ||||||
|  | 				display block | ||||||
|  | 				color $theme-color | ||||||
|  |  | ||||||
|  | 			> *:last-child | ||||||
|  | 				font-size 70% | ||||||
|  |  | ||||||
|  | 	> div | ||||||
|  | 		max-width 850px | ||||||
|  | </style> | ||||||
| @@ -40,10 +40,12 @@ export default Vue.extend({ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> | <style lang="stylus" scoped> | ||||||
|  | root(isDark) | ||||||
| .friends | .friends | ||||||
| 	background #fff | 	background isDark ? #282C37 : #fff | ||||||
| 	border solid 1px rgba(#000, 0.075) | 	border solid 1px rgba(#000, 0.075) | ||||||
| 	border-radius 6px | 	border-radius 6px | ||||||
|  | 	overflow hidden | ||||||
|  |  | ||||||
| 	> .title | 	> .title | ||||||
| 		z-index 1 | 		z-index 1 | ||||||
| @@ -52,7 +54,8 @@ export default Vue.extend({ | |||||||
| 		line-height 42px | 		line-height 42px | ||||||
| 		font-size 0.9em | 		font-size 0.9em | ||||||
| 		font-weight bold | 		font-weight bold | ||||||
| 		color #888 | 		background isDark ? #313543 : inherit | ||||||
|  | 		color isDark ? #e3e5e8 : #888 | ||||||
| 		box-shadow 0 1px rgba(#000, 0.07) | 		box-shadow 0 1px rgba(#000, 0.07) | ||||||
|  |  | ||||||
| 		> i | 		> i | ||||||
| @@ -70,7 +73,7 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 	> .user | 	> .user | ||||||
| 		padding 16px | 		padding 16px | ||||||
| 		border-bottom solid 1px #eee | 		border-bottom solid 1px isDark ? #21242f : #eee | ||||||
|  |  | ||||||
| 		&:last-child | 		&:last-child | ||||||
| 			border-bottom none | 			border-bottom none | ||||||
| @@ -96,18 +99,24 @@ export default Vue.extend({ | |||||||
| 				margin 0 | 				margin 0 | ||||||
| 				font-size 16px | 				font-size 16px | ||||||
| 				line-height 24px | 				line-height 24px | ||||||
| 				color #555 | 				color isDark ? #ccc : #555 | ||||||
|  |  | ||||||
| 			> .username | 			> .username | ||||||
| 				display block | 				display block | ||||||
| 				margin 0 | 				margin 0 | ||||||
| 				font-size 15px | 				font-size 15px | ||||||
| 				line-height 16px | 				line-height 16px | ||||||
| 				color #ccc | 				color isDark ? #555 : #ccc | ||||||
|  |  | ||||||
| 		> .mk-follow-button | 		> .mk-follow-button | ||||||
| 			position absolute | 			position absolute | ||||||
| 			top 16px | 			top 16px | ||||||
| 			right 16px | 			right 16px | ||||||
|  |  | ||||||
|  | .friends[data-darkmode] | ||||||
|  | 	root(true) | ||||||
|  |  | ||||||
|  | .friends:not([data-darkmode]) | ||||||
|  | 	root(false) | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -39,10 +39,12 @@ export default Vue.extend({ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <style lang="stylus" scoped> | <style lang="stylus" scoped> | ||||||
|  | root(isDark) | ||||||
| .photos | .photos | ||||||
| 	background #fff | 	background isDark ? #282C37 : #fff | ||||||
| 	border solid 1px rgba(#000, 0.075) | 	border solid 1px rgba(#000, 0.075) | ||||||
| 	border-radius 6px | 	border-radius 6px | ||||||
|  | 	overflow hidden | ||||||
|  |  | ||||||
| 	> .title | 	> .title | ||||||
| 		z-index 1 | 		z-index 1 | ||||||
| @@ -51,7 +53,8 @@ export default Vue.extend({ | |||||||
| 		line-height 42px | 		line-height 42px | ||||||
| 		font-size 0.9em | 		font-size 0.9em | ||||||
| 		font-weight bold | 		font-weight bold | ||||||
| 		color #888 | 		background: isDark ? #313543 : inherit | ||||||
|  | 		color isDark ? #e3e5e8 : #888 | ||||||
| 		box-shadow 0 1px rgba(#000, 0.07) | 		box-shadow 0 1px rgba(#000, 0.07) | ||||||
|  |  | ||||||
| 		> i | 		> i | ||||||
| @@ -85,4 +88,10 @@ export default Vue.extend({ | |||||||
| 		> i | 		> i | ||||||
| 			margin-right 4px | 			margin-right 4px | ||||||
|  |  | ||||||
|  | .photos[data-darkmode] | ||||||
|  | 	root(true) | ||||||
|  |  | ||||||
|  | .photos:not([data-darkmode]) | ||||||
|  | 	root(false) | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ root(isDark) | |||||||
| 				padding 16px | 				padding 16px | ||||||
| 				font-size 12px | 				font-size 12px | ||||||
| 				color #aaa | 				color #aaa | ||||||
| 				background #fff | 				background isDark ? #21242f : #fff | ||||||
| 				border solid 1px rgba(#000, 0.075) | 				border solid 1px rgba(#000, 0.075) | ||||||
| 				border-radius 6px | 				border-radius 6px | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,8 +19,8 @@ import { version, codename, lang } from './config'; | |||||||
|  |  | ||||||
| let elementLocale; | let elementLocale; | ||||||
| switch (lang) { | switch (lang) { | ||||||
| 	case 'ja': elementLocale = ElementLocaleJa; break; | 	case 'ja-JP': elementLocale = ElementLocaleJa; break; | ||||||
| 	case 'en': elementLocale = ElementLocaleEn; break; | 	case 'en-US': elementLocale = ElementLocaleEn; break; | ||||||
| 	default: elementLocale = ElementLocaleEn; break; | 	default: elementLocale = ElementLocaleEn; break; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ | |||||||
| 			</div> | 			</div> | ||||||
| 			<mk-poll v-if="p.poll" :note="p"/> | 			<mk-poll v-if="p.poll" :note="p"/> | ||||||
| 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | 			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> | ||||||
| 			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | 			<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||||
| 			<div class="map" v-if="p.geo" ref="map"></div> | 			<div class="map" v-if="p.geo" ref="map"></div> | ||||||
| 			<div class="renote" v-if="p.renote"> | 			<div class="renote" v-if="p.renote"> | ||||||
| 				<mk-note-preview :note="p.renote"/> | 				<mk-note-preview :note="p.renote"/> | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
| 					</div> | 					</div> | ||||||
| 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | 					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> | ||||||
| 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | 					<mk-url-preview v-for="url in urls" :url="url" :key="url"/> | ||||||
| 					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | 					<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> | ||||||
| 					<div class="map" v-if="p.geo" ref="map"></div> | 					<div class="map" v-if="p.geo" ref="map"></div> | ||||||
| 					<div class="renote" v-if="p.renote"> | 					<div class="renote" v-if="p.renote"> | ||||||
| 						<mk-note-preview :note="p.renote"/> | 						<mk-note-preview :note="p.renote"/> | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ | |||||||
| 				<ul> | 				<ul> | ||||||
| 					<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> | 					<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> | ||||||
| 					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> | 					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> | ||||||
|  | 					<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li> | ||||||
| 					<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li> | 					<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li> | ||||||
| 				</ul> | 				</ul> | ||||||
| 			</div> | 			</div> | ||||||
|   | |||||||
| @@ -41,6 +41,12 @@ | |||||||
| 				<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch> | 				<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch> | ||||||
| 			</ui-card> | 			</ui-card> | ||||||
|  |  | ||||||
|  | 			<ui-card> | ||||||
|  | 				<div slot="title">%fa:volume-up% %i18n:@sound%</div> | ||||||
|  |  | ||||||
|  | 				<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch> | ||||||
|  | 			</ui-card> | ||||||
|  |  | ||||||
| 			<ui-card> | 			<ui-card> | ||||||
| 				<div slot="title">%fa:language% %i18n:@lang%</div> | 				<div slot="title">%fa:language% %i18n:@lang%</div> | ||||||
|  |  | ||||||
| @@ -142,6 +148,11 @@ export default Vue.extend({ | |||||||
| 			get() { return this.$store.state.device.lang; }, | 			get() { return this.$store.state.device.lang; }, | ||||||
| 			set(value) { this.$store.commit('device/set', { key: 'lang', value }); } | 			set(value) { this.$store.commit('device/set', { key: 'lang', value }); } | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		enableSounds: { | ||||||
|  | 			get() { return this.$store.state.device.enableSounds; }, | ||||||
|  | 			set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } | ||||||
|  | 		}, | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ import Vue from 'vue'; | |||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			name: (this as any).os.instanceName, | 			name: null, | ||||||
| 			posted: false, | 			posted: false, | ||||||
| 			text: new URLSearchParams(location.search).get('text') | 			text: new URLSearchParams(location.search).get('text') | ||||||
| 		}; | 		}; | ||||||
| @@ -25,6 +25,11 @@ export default Vue.extend({ | |||||||
| 		close() { | 		close() { | ||||||
| 			window.close(); | 			window.close(); | ||||||
| 		} | 		} | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		(this as any).os.getMeta().then(meta => { | ||||||
|  | 			this.name = meta.name; | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
| 					<a class="avatar"> | 					<a class="avatar"> | ||||||
| 						<img :src="user.avatarUrl" alt="avatar"/> | 						<img :src="user.avatarUrl" alt="avatar"/> | ||||||
| 					</a> | 					</a> | ||||||
| 					<mk-mute-button v-if="$store.state.i.id != user.id" :user="user"/> | 					<mk-mute-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> | ||||||
| 					<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> | 					<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> | ||||||
| 				</div> | 				</div> | ||||||
| 				<div class="title"> | 				<div class="title"> | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ const defaultSettings = { | |||||||
| 	showMaps: true, | 	showMaps: true, | ||||||
| 	showPostFormOnTopOfTl: false, | 	showPostFormOnTopOfTl: false, | ||||||
| 	suggestRecentHashtags: true, | 	suggestRecentHashtags: true, | ||||||
|  | 	showClockOnHeader: true, | ||||||
| 	circleIcons: true, | 	circleIcons: true, | ||||||
| 	gradientWindowHeader: false, | 	gradientWindowHeader: false, | ||||||
| 	showReplyTarget: true, | 	showReplyTarget: true, | ||||||
|   | |||||||
| @@ -53,5 +53,5 @@ export default function load() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function normalizeUrl(url: string) { | function normalizeUrl(url: string) { | ||||||
| 	return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; | 	return url.endsWith('/') ? url.substr(0, url.length - 1) : url; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -62,6 +62,8 @@ export type Source = { | |||||||
| 	 */ | 	 */ | ||||||
| 	ghost?: string; | 	ghost?: string; | ||||||
|  |  | ||||||
|  | 	summalyProxy?: string; | ||||||
|  |  | ||||||
| 	accesslog?: string; | 	accesslog?: string; | ||||||
| 	twitter?: { | 	twitter?: { | ||||||
| 		consumer_key: string; | 		consumer_key: string; | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ block main | |||||||
| 		span.path= endpointUrl.path | 		span.path= endpointUrl.path | ||||||
|  |  | ||||||
| 	if endpoint.desc | 	if endpoint.desc | ||||||
| 		p#desc= endpoint.desc[lang] || endpoint.desc['ja'] | 		p#desc= endpoint.desc[lang] || endpoint.desc['ja-JP'] | ||||||
|  |  | ||||||
| 	if endpoint.requireCredential | 	if endpoint.requireCredential | ||||||
| 		div.ui.info: p | 		div.ui.info: p | ||||||
|   | |||||||
| @@ -1,90 +1,90 @@ | |||||||
| name: "DriveFile" | name: "DriveFile" | ||||||
|  |  | ||||||
| desc: | desc: | ||||||
|   ja: "ドライブのファイル。" |   ja-JP: "ドライブのファイル。" | ||||||
|   en: "A file of Drive." |   en-US: "A file of Drive." | ||||||
|  |  | ||||||
| props: | props: | ||||||
|   id: |   id: | ||||||
|     type: "id" |     type: "id" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ファイルID" |       ja-JP: "ファイルID" | ||||||
|       en: "The ID of this file" |       en-US: "The ID of this file" | ||||||
|  |  | ||||||
|   createdAt: |   createdAt: | ||||||
|     type: "date" |     type: "date" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "アップロード日時" |       ja-JP: "アップロード日時" | ||||||
|       en: "The upload date of this file" |       en-US: "The upload date of this file" | ||||||
|  |  | ||||||
|   userId: |   userId: | ||||||
|     type: "id(User)" |     type: "id(User)" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "所有者ID" |       ja-JP: "所有者ID" | ||||||
|       en: "The ID of the owner of this file" |       en-US: "The ID of the owner of this file" | ||||||
|  |  | ||||||
|   user: |   user: | ||||||
|     type: "entity(User)" |     type: "entity(User)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "所有者" |       ja-JP: "所有者" | ||||||
|       en: "The owner of this file" |       en-US: "The owner of this file" | ||||||
|  |  | ||||||
|   name: |   name: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ファイル名" |       ja-JP: "ファイル名" | ||||||
|       en: "The name of this file" |       en-US: "The name of this file" | ||||||
|  |  | ||||||
|   md5: |   md5: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ファイルのMD5ハッシュ値" |       ja-JP: "ファイルのMD5ハッシュ値" | ||||||
|       en: "The md5 hash value of this file" |       en-US: "The md5 hash value of this file" | ||||||
|  |  | ||||||
|   type: |   type: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ファイルの種類" |       ja-JP: "ファイルの種類" | ||||||
|       en: "The type of this file" |       en-US: "The type of this file" | ||||||
|  |  | ||||||
|   datasize: |   datasize: | ||||||
|     type: "number" |     type: "number" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ファイルサイズ(bytes)" |       ja-JP: "ファイルサイズ(bytes)" | ||||||
|       en: "The size of this file (bytes)" |       en-US: "The size of this file (bytes)" | ||||||
|  |  | ||||||
|   url: |   url: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ファイルのURL" |       ja-JP: "ファイルのURL" | ||||||
|       en: "The URL of this file" |       en-US: "The URL of this file" | ||||||
|  |  | ||||||
|   folderId: |   folderId: | ||||||
|     type: "id(DriveFolder)" |     type: "id(DriveFolder)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "フォルダID" |       ja-JP: "フォルダID" | ||||||
|       en: "The ID of the folder of this file" |       en-US: "The ID of the folder of this file" | ||||||
|  |  | ||||||
|   folder: |   folder: | ||||||
|     type: "entity(DriveFolder)" |     type: "entity(DriveFolder)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "フォルダ" |       ja-JP: "フォルダ" | ||||||
|       en: "The folder of this file" |       en-US: "The folder of this file" | ||||||
|  |  | ||||||
|   isSensitive: |   isSensitive: | ||||||
|     type: "boolean" |     type: "boolean" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "このメディアが「閲覧注意」(NSFW)かどうか" |       ja-JP: "このメディアが「閲覧注意」(NSFW)かどうか" | ||||||
|       en: "Whether this media is NSFW" |       en-US: "Whether this media is NSFW" | ||||||
|   | |||||||
| @@ -1,41 +1,41 @@ | |||||||
| name: "DriveFolder" | name: "DriveFolder" | ||||||
|  |  | ||||||
| desc: | desc: | ||||||
|   ja: "ドライブのフォルダを表します。" |   ja-JP: "ドライブのフォルダを表します。" | ||||||
|   en: "A folder of Drive." |   en-US: "A folder of Drive." | ||||||
|  |  | ||||||
| props: | props: | ||||||
|   id: |   id: | ||||||
|     type: "id" |     type: "id" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "フォルダID" |       ja-JP: "フォルダID" | ||||||
|       en: "The ID of this folder" |       en-US: "The ID of this folder" | ||||||
|  |  | ||||||
|   createdAt: |   createdAt: | ||||||
|     type: "date" |     type: "date" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "作成日時" |       ja-JP: "作成日時" | ||||||
|       en: "The created date of this folder" |       en-US: "The created date of this folder" | ||||||
|  |  | ||||||
|   userId: |   userId: | ||||||
|     type: "id(User)" |     type: "id(User)" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "所有者ID" |       ja-JP: "所有者ID" | ||||||
|       en: "The ID of the owner of this folder" |       en-US: "The ID of the owner of this folder" | ||||||
|  |  | ||||||
|   parentId: |   parentId: | ||||||
|     type: "entity(DriveFolder)" |     type: "entity(DriveFolder)" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "親フォルダのID (ルートなら null)" |       ja-JP: "親フォルダのID (ルートなら null)" | ||||||
|       en: "The ID of parent folder" |       en-US: "The ID of parent folder" | ||||||
|  |  | ||||||
|   name: |   name: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "フォルダ名" |       ja-JP: "フォルダ名" | ||||||
|       en: "The name of this folder" |       en-US: "The name of this folder" | ||||||
|   | |||||||
| @@ -1,190 +1,190 @@ | |||||||
| name: "Note" | name: "Note" | ||||||
|  |  | ||||||
| desc: | desc: | ||||||
|   ja: "投稿。" |   ja-JP: "投稿。" | ||||||
|   en: "A note." |   en-US: "A note." | ||||||
|  |  | ||||||
| props: | props: | ||||||
|   id: |   id: | ||||||
|     type: "id" |     type: "id" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "投稿ID" |       ja-JP: "投稿ID" | ||||||
|       en: "The ID of this note" |       en-US: "The ID of this note" | ||||||
|  |  | ||||||
|   createdAt: |   createdAt: | ||||||
|     type: "date" |     type: "date" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "投稿日時" |       ja-JP: "投稿日時" | ||||||
|       en: "The posted date of this note" |       en-US: "The posted date of this note" | ||||||
|  |  | ||||||
|   viaMobile: |   viaMobile: | ||||||
|     type: "boolean" |     type: "boolean" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" |       ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)" | ||||||
|       en: "Whether this note sent via a mobile device" |       en-US: "Whether this note sent via a mobile device" | ||||||
|  |  | ||||||
|   text: |   text: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "投稿の本文" |       ja-JP: "投稿の本文" | ||||||
|       en: "The text of this note" |       en-US: "The text of this note" | ||||||
|  |  | ||||||
|   mediaIds: |   mediaIds: | ||||||
|     type: "id(DriveFile)[]" |     type: "id(DriveFile)[]" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "添付されているメディアのID (なければレスポンスでは空配列)" |       ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)" | ||||||
|       en: "The IDs of the attached media (empty array for response if no media is attached)" |       en-US: "The IDs of the attached media (empty array for response if no media is attached)" | ||||||
|  |  | ||||||
|   media: |   media: | ||||||
|     type: "entity(DriveFile)[]" |     type: "entity(DriveFile)[]" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "添付されているメディア" |       ja-JP: "添付されているメディア" | ||||||
|       en: "The attached media" |       en-US: "The attached media" | ||||||
|  |  | ||||||
|   userId: |   userId: | ||||||
|     type: "id(User)" |     type: "id(User)" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "投稿者ID" |       ja-JP: "投稿者ID" | ||||||
|       en: "The ID of author of this note" |       en-US: "The ID of author of this note" | ||||||
|  |  | ||||||
|   user: |   user: | ||||||
|     type: "entity(User)" |     type: "entity(User)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "投稿者" |       ja-JP: "投稿者" | ||||||
|       en: "The author of this note" |       en-US: "The author of this note" | ||||||
|  |  | ||||||
|   myReaction: |   myReaction: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" |       ja-JP: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" | ||||||
|       en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" |       en-US: "The your <a href='/docs/api/reactions'>reaction</a> of this note" | ||||||
|  |  | ||||||
|   reactionCounts: |   reactionCounts: | ||||||
|     type: "object" |     type: "object" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" |       ja-JP: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" | ||||||
|  |  | ||||||
|   replyId: |   replyId: | ||||||
|     type: "id(Note)" |     type: "id(Note)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "返信した投稿のID" |       ja-JP: "返信した投稿のID" | ||||||
|       en: "The ID of the replyed note" |       en-US: "The ID of the replyed note" | ||||||
|  |  | ||||||
|   reply: |   reply: | ||||||
|     type: "entity(Note)" |     type: "entity(Note)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "返信した投稿" |       ja-JP: "返信した投稿" | ||||||
|       en: "The replyed note" |       en-US: "The replyed note" | ||||||
|  |  | ||||||
|   renoteId: |   renoteId: | ||||||
|     type: "id(Note)" |     type: "id(Note)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "引用した投稿のID" |       ja-JP: "引用した投稿のID" | ||||||
|       en: "The ID of the quoted note" |       en-US: "The ID of the quoted note" | ||||||
|  |  | ||||||
|   renote: |   renote: | ||||||
|     type: "entity(Note)" |     type: "entity(Note)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "引用した投稿" |       ja-JP: "引用した投稿" | ||||||
|       en: "The quoted note" |       en-US: "The quoted note" | ||||||
|  |  | ||||||
|   poll: |   poll: | ||||||
|     type: "object" |     type: "object" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "投票" |       ja-JP: "投票" | ||||||
|       en: "The poll" |       en-US: "The poll" | ||||||
|  |  | ||||||
|     props: |     props: | ||||||
|       choices: |       choices: | ||||||
|         type: "object[]" |         type: "object[]" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "投票の選択肢" |           ja-JP: "投票の選択肢" | ||||||
|           en: "The choices of this poll" |           en-US: "The choices of this poll" | ||||||
|  |  | ||||||
|         props: |         props: | ||||||
|           id: |           id: | ||||||
|             type: "number" |             type: "number" | ||||||
|             optional: false |             optional: false | ||||||
|             desc: |             desc: | ||||||
|               ja: "選択肢ID" |               ja-JP: "選択肢ID" | ||||||
|               en: "The ID of this choice" |               en-US: "The ID of this choice" | ||||||
|  |  | ||||||
|           isVoted: |           isVoted: | ||||||
|             type: "boolean" |             type: "boolean" | ||||||
|             optional: true |             optional: true | ||||||
|             desc: |             desc: | ||||||
|               ja: "自分がこの選択肢に投票したかどうか" |               ja-JP: "自分がこの選択肢に投票したかどうか" | ||||||
|               en: "Whether you voted to this choice" |               en-US: "Whether you voted to this choice" | ||||||
|  |  | ||||||
|           text: |           text: | ||||||
|             type: "string" |             type: "string" | ||||||
|             optional: false |             optional: false | ||||||
|             desc: |             desc: | ||||||
|               ja: "選択肢本文" |               ja-JP: "選択肢本文" | ||||||
|               en: "The text of this choice" |               en-US: "The text of this choice" | ||||||
|  |  | ||||||
|           votes: |           votes: | ||||||
|             type: "number" |             type: "number" | ||||||
|             optional: false |             optional: false | ||||||
|             desc: |             desc: | ||||||
|               ja: "この選択肢に投票された数" |               ja-JP: "この選択肢に投票された数" | ||||||
|               en: "The number voted for this choice" |               en-US: "The number voted for this choice" | ||||||
|   geo: |   geo: | ||||||
|     type: "object" |     type: "object" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "位置情報" |       ja-JP: "位置情報" | ||||||
|       en: "Geo location" |       en-US: "Geo location" | ||||||
|  |  | ||||||
|     props: |     props: | ||||||
|       coordinates: |       coordinates: | ||||||
|         type: "number[]" |         type: "number[]" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" |           ja-JP: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" | ||||||
|  |  | ||||||
|       altitude: |       altitude: | ||||||
|         type: "number" |         type: "number" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "高度。メートル単位で表す。" |           ja-JP: "高度。メートル単位で表す。" | ||||||
|  |  | ||||||
|       accuracy: |       accuracy: | ||||||
|         type: "number" |         type: "number" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "緯度、経度の精度。メートル単位で表す。" |           ja-JP: "緯度、経度の精度。メートル単位で表す。" | ||||||
|  |  | ||||||
|       altitudeAccuracy: |       altitudeAccuracy: | ||||||
|         type: "number" |         type: "number" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "高度の精度。メートル単位で表す。" |           ja-JP: "高度の精度。メートル単位で表す。" | ||||||
|  |  | ||||||
|       heading: |       heading: | ||||||
|         type: "number" |         type: "number" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" |           ja-JP: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" | ||||||
|  |  | ||||||
|       speed: |       speed: | ||||||
|         type: "number" |         type: "number" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "速度。メートル / 秒数で表す。" |           ja-JP: "速度。メートル / 秒数で表す。" | ||||||
|   | |||||||
| @@ -1,174 +1,174 @@ | |||||||
| name: "User" | name: "User" | ||||||
|  |  | ||||||
| desc: | desc: | ||||||
|   ja: "ユーザー。" |   ja-JP: "ユーザー。" | ||||||
|   en: "A user." |   en-US: "A user." | ||||||
|  |  | ||||||
| props: | props: | ||||||
|   id: |   id: | ||||||
|     type: "id" |     type: "id" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ユーザーID" |       ja-JP: "ユーザーID" | ||||||
|       en: "The ID of this user" |       en-US: "The ID of this user" | ||||||
|  |  | ||||||
|   createdAt: |   createdAt: | ||||||
|     type: "date" |     type: "date" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "アカウント作成日時" |       ja-JP: "アカウント作成日時" | ||||||
|       en: "The registered date of this user" |       en-US: "The registered date of this user" | ||||||
|  |  | ||||||
|   username: |   username: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ユーザー名" |       ja-JP: "ユーザー名" | ||||||
|       en: "The username of this user" |       en-US: "The username of this user" | ||||||
|  |  | ||||||
|   description: |   description: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "アカウントの説明(自己紹介)" |       ja-JP: "アカウントの説明(自己紹介)" | ||||||
|       en: "The description of this user" |       en-US: "The description of this user" | ||||||
|  |  | ||||||
|   avatarId: |   avatarId: | ||||||
|     type: "id(DriveFile)" |     type: "id(DriveFile)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "アバターのID" |       ja-JP: "アバターのID" | ||||||
|       en: "The ID of the avatar of this user" |       en-US: "The ID of the avatar of this user" | ||||||
|  |  | ||||||
|   avatarUrl: |   avatarUrl: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "アバターのURL" |       ja-JP: "アバターのURL" | ||||||
|       en: "The URL of the avatar of this user" |       en-US: "The URL of the avatar of this user" | ||||||
|  |  | ||||||
|   bannerId: |   bannerId: | ||||||
|     type: "id(DriveFile)" |     type: "id(DriveFile)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "バナーのID" |       ja-JP: "バナーのID" | ||||||
|       en: "The ID of the banner of this user" |       en-US: "The ID of the banner of this user" | ||||||
|  |  | ||||||
|   bannerUrl: |   bannerUrl: | ||||||
|     type: "string" |     type: "string" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "バナーのURL" |       ja-JP: "バナーのURL" | ||||||
|       en: "The URL of the banner of this user" |       en-US: "The URL of the banner of this user" | ||||||
|  |  | ||||||
|   followersCount: |   followersCount: | ||||||
|     type: "number" |     type: "number" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "フォロワーの数" |       ja-JP: "フォロワーの数" | ||||||
|       en: "The number of the followers for this user" |       en-US: "The number of the followers for this user" | ||||||
|  |  | ||||||
|   followingCount: |   followingCount: | ||||||
|     type: "number" |     type: "number" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "フォローしているユーザーの数" |       ja-JP: "フォローしているユーザーの数" | ||||||
|       en: "The number of the following users for this user" |       en-US: "The number of the following users for this user" | ||||||
|  |  | ||||||
|   isFollowing: |   isFollowing: | ||||||
|     type: "boolean" |     type: "boolean" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "自分がこのユーザーをフォローしているか" |       ja-JP: "自分がこのユーザーをフォローしているか" | ||||||
|  |  | ||||||
|   isFollowed: |   isFollowed: | ||||||
|     type: "boolean" |     type: "boolean" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "自分がこのユーザーにフォローされているか" |       ja-JP: "自分がこのユーザーにフォローされているか" | ||||||
|  |  | ||||||
|   isMuted: |   isMuted: | ||||||
|     type: "boolean" |     type: "boolean" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "自分がこのユーザーをミュートしているか" |       ja-JP: "自分がこのユーザーをミュートしているか" | ||||||
|       en: "Whether you muted this user" |       en-US: "Whether you muted this user" | ||||||
|  |  | ||||||
|   notesCount: |   notesCount: | ||||||
|     type: "number" |     type: "number" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "投稿の数" |       ja-JP: "投稿の数" | ||||||
|       en: "The number of the notes of this user" |       en-US: "The number of the notes of this user" | ||||||
|  |  | ||||||
|   pinnedNote: |   pinnedNote: | ||||||
|     type: "entity(Note)" |     type: "entity(Note)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "ピン留めされた投稿" |       ja-JP: "ピン留めされた投稿" | ||||||
|       en: "The pinned note of this user" |       en-US: "The pinned note of this user" | ||||||
|  |  | ||||||
|   pinnedNoteId: |   pinnedNoteId: | ||||||
|     type: "id(Note)" |     type: "id(Note)" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "ピン留めされた投稿のID" |       ja-JP: "ピン留めされた投稿のID" | ||||||
|       en: "The ID of the pinned note of this user" |       en-US: "The ID of the pinned note of this user" | ||||||
|  |  | ||||||
|   host: |   host: | ||||||
|     type: "string | null" |     type: "string | null" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "ホスト (例: example.com:3000)" |       ja-JP: "ホスト (例: example.com:3000)" | ||||||
|       en: "Host (e.g. example.com:3000)" |       en-US: "Host (e.g. example.com:3000)" | ||||||
|  |  | ||||||
|   twitter: |   twitter: | ||||||
|     type: "object" |     type: "object" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "連携されているTwitterアカウント情報" |       ja-JP: "連携されているTwitterアカウント情報" | ||||||
|       en: "The info of the connected twitter account of this user" |       en-US: "The info of the connected twitter account of this user" | ||||||
|  |  | ||||||
|     props: |     props: | ||||||
|       userId: |       userId: | ||||||
|         type: "string" |         type: "string" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "ユーザーID" |           ja-JP: "ユーザーID" | ||||||
|           en: "The user ID" |           en-US: "The user ID" | ||||||
|  |  | ||||||
|       screenName: |       screenName: | ||||||
|         type: "string" |         type: "string" | ||||||
|         optional: false |         optional: false | ||||||
|         desc: |         desc: | ||||||
|           ja: "ユーザー名" |           ja-JP: "ユーザー名" | ||||||
|           en: "The screen name of this user" |           en-US: "The screen name of this user" | ||||||
|  |  | ||||||
|   isBot: |   isBot: | ||||||
|     type: "boolean" |     type: "boolean" | ||||||
|     optional: true |     optional: true | ||||||
|     desc: |     desc: | ||||||
|       ja: "botか否か(自己申告であることに留意)" |       ja-JP: "botか否か(自己申告であることに留意)" | ||||||
|       en: "Whether is bot or not" |       en-US: "Whether is bot or not" | ||||||
|  |  | ||||||
|   profile: |   profile: | ||||||
|     type: "object" |     type: "object" | ||||||
|     optional: false |     optional: false | ||||||
|     desc: |     desc: | ||||||
|       ja: "プロフィール" |       ja-JP: "プロフィール" | ||||||
|       en: "The profile of this user" |       en-US: "The profile of this user" | ||||||
|  |  | ||||||
|     props: |     props: | ||||||
|       location: |       location: | ||||||
|         type: "string" |         type: "string" | ||||||
|         optional: true |         optional: true | ||||||
|         desc: |         desc: | ||||||
|           ja: "場所" |           ja-JP: "場所" | ||||||
|           en: "The location of this user" |           en-US: "The location of this user" | ||||||
|  |  | ||||||
|       birthday: |       birthday: | ||||||
|         type: "string" |         type: "string" | ||||||
|         optional: true |         optional: true | ||||||
|         desc: |         desc: | ||||||
|           ja: "誕生日 (YYYY-MM-DD)" |           ja-JP: "誕生日 (YYYY-MM-DD)" | ||||||
|           en: "The birthday of this user (YYYY-MM-DD)" |           en-US: "The birthday of this user (YYYY-MM-DD)" | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ block meta | |||||||
| block main | block main | ||||||
| 	h1= name | 	h1= name | ||||||
|  |  | ||||||
| 	p#desc= desc[lang] || desc['ja'] | 	p#desc= desc[lang] || desc['ja-JP'] | ||||||
|  |  | ||||||
| 	section | 	section | ||||||
| 		h2= i18n('docs.api.entities.properties') | 		h2= i18n('docs.api.entities.properties') | ||||||
|   | |||||||
| @@ -31,4 +31,4 @@ mixin propTable(props) | |||||||
| 					td.name= prop.name | 					td.name= prop.name | ||||||
| 					td.type | 					td.type | ||||||
| 						+type(prop) | 						+type(prop) | ||||||
| 					td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja'] : null | 					td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja-JP'] : null | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ html(lang= lang) | |||||||
| 		nav | 		nav | ||||||
| 			ul | 			ul | ||||||
| 				each doc in docs | 				each doc in docs | ||||||
| 					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] | 					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja-JP'] | ||||||
| 			section | 			section | ||||||
| 				h2 API | 				h2 API | ||||||
| 				ul | 				ul | ||||||
|   | |||||||
| @@ -197,7 +197,7 @@ const elements: Element[] = [ | |||||||
|  |  | ||||||
| 		if (thisIsNotARegexp) return null; | 		if (thisIsNotARegexp) return null; | ||||||
| 		if (regexp == '') return null; | 		if (regexp == '') return null; | ||||||
| 		if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null; | 		if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null; | ||||||
|  |  | ||||||
| 		return { | 		return { | ||||||
| 			html: `<span class="regexp">/${escape(regexp)}/</span>`, | 			html: `<span class="regexp">/${escape(regexp)}/</span>`, | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ export type TextElementHashtag = { | |||||||
|  |  | ||||||
| export default function(text: string, i: number) { | export default function(text: string, i: number) { | ||||||
| 	if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; | 	if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; | ||||||
| 	const isHead = text[0] == '#'; | 	const isHead = text.startsWith('#'); | ||||||
| 	const hashtag = text.match(/^\s?#[^\s]+/)[0]; | 	const hashtag = text.match(/^\s?#[^\s]+/)[0]; | ||||||
| 	const res: any[] = !isHead ? [{ | 	const res: any[] = !isHead ? [{ | ||||||
| 		type: 'text', | 		type: 'text', | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ export type TextElementLink = { | |||||||
| export default function(text: string) { | export default function(text: string) { | ||||||
| 	const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); | 	const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); | ||||||
| 	if (!match) return null; | 	if (!match) return null; | ||||||
| 	const silent = text[0] == '?'; | 	const silent = text.startsWith('?'); | ||||||
| 	const link = match[0]; | 	const link = match[0]; | ||||||
| 	const title = match[1]; | 	const title = match[1]; | ||||||
| 	const url = match[2]; | 	const url = match[2]; | ||||||
|   | |||||||
| @@ -25,9 +25,9 @@ export const replacement = (match: string, key: string) => { | |||||||
| 				arg == 'S' ? 'fas' : | 				arg == 'S' ? 'fas' : | ||||||
| 				arg == 'B' ? 'fab' : | 				arg == 'B' ? 'fab' : | ||||||
| 				''; | 				''; | ||||||
| 		} else if (arg[0] == '.') { | 		} else if (arg.startsWith('.')) { | ||||||
| 			classes.push('fa-' + arg.substr(1)); | 			classes.push('fa-' + arg.substr(1)); | ||||||
| 		} else if (arg[0] == '-') { | 		} else if (arg.startsWith('-')) { | ||||||
| 			transform = arg.substr(1).split('|').join(' '); | 			transform = arg.substr(1).split('|').join(' '); | ||||||
| 		} else { | 		} else { | ||||||
| 			name = arg; | 			name = arg; | ||||||
|   | |||||||
| @@ -27,10 +27,12 @@ export default class Replacer { | |||||||
| 		let text = texts; | 		let text = texts; | ||||||
|  |  | ||||||
| 		if (path) { | 		if (path) { | ||||||
|  | 			path = path.replace('.ts', ''); | ||||||
|  |  | ||||||
| 			if (text.hasOwnProperty(path)) { | 			if (text.hasOwnProperty(path)) { | ||||||
| 				text = text[path]; | 				text = text[path]; | ||||||
| 			} else { | 			} else { | ||||||
| 				if (this.lang === 'ja') console.warn(`path '${path}' not found`); | 				if (this.lang === 'ja-JP') console.warn(`path '${path}' not found`); | ||||||
| 				return key; // Fallback | 				return key; // Fallback | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @@ -46,10 +48,10 @@ export default class Replacer { | |||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		if (error) { | 		if (error) { | ||||||
| 			if (this.lang === 'ja') console.warn(`key '${key}' not found in '${path}'`); | 			if (this.lang === 'ja-JP') console.warn(`key '${key}' not found in '${path}'`); | ||||||
| 			return key; // Fallback | 			return key; // Fallback | ||||||
| 		} else if (typeof text !== 'string') { | 		} else if (typeof text !== 'string') { | ||||||
| 			if (this.lang === 'ja') console.warn(`key '${key}' is not string in '${path}'`); | 			if (this.lang === 'ja-JP') console.warn(`key '${key}' is not string in '${path}'`); | ||||||
| 			return key; // Fallback | 			return key; // Fallback | ||||||
| 		} else { | 		} else { | ||||||
| 			return text; | 			return text; | ||||||
|   | |||||||
| @@ -2,40 +2,59 @@ import * as mongo from 'mongodb'; | |||||||
| import db from '../db/mongodb'; | import db from '../db/mongodb'; | ||||||
|  |  | ||||||
| const Stats = db.get<IStats>('stats'); | const Stats = db.get<IStats>('stats'); | ||||||
| Stats.createIndex({ date: -1 }, { unique: true }); | Stats.dropIndex({ date: -1 }); // 後方互換性のため | ||||||
|  | Stats.createIndex({ span: -1, date: -1 }, { unique: true }); | ||||||
| export default Stats; | export default Stats; | ||||||
|  |  | ||||||
| export interface IStats { | export interface IStats { | ||||||
| 	_id: mongo.ObjectID; | 	_id: mongo.ObjectID; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 集計日時 | ||||||
|  | 	 */ | ||||||
| 	date: Date; | 	date: Date; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * 集計期間 | ||||||
|  | 	 */ | ||||||
|  | 	span: 'day' | 'hour'; | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * ユーザーに関する統計 | 	 * ユーザーに関する統計 | ||||||
| 	 */ | 	 */ | ||||||
| 	users: { | 	users: { | ||||||
| 		local: { | 		local: { | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、ローカルのユーザーの総計 | 			 * 集計期間時点での、全ユーザー数 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			total: number; | 			total: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * ローカルのユーザー数の前日比 | 			 * 増加したユーザー数 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			diff: number; | 			inc: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少したユーザー数 (ローカル) | ||||||
|  | 			 */ | ||||||
|  | 			dec: number; | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		remote: { | 		remote: { | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、リモートのユーザーの総計 | 			 * 集計期間時点での、全ユーザー数 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			total: number; | 			total: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * リモートのユーザー数の前日比 | 			 * 増加したユーザー数 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			diff: number; | 			inc: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少したユーザー数 (リモート) | ||||||
|  | 			 */ | ||||||
|  | 			dec: number; | ||||||
| 		}; | 		}; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| @@ -45,28 +64,33 @@ export interface IStats { | |||||||
| 	notes: { | 	notes: { | ||||||
| 		local: { | 		local: { | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、ローカルの投稿の総計 | 			 * 集計期間時点での、全投稿数 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			total: number; | 			total: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * ローカルの投稿数の前日比 | 			 * 増加した投稿数 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			diff: number; | 			inc: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少した投稿数 (ローカル) | ||||||
|  | 			 */ | ||||||
|  | 			dec: number; | ||||||
|  |  | ||||||
| 			diffs: { | 			diffs: { | ||||||
| 				/** | 				/** | ||||||
| 				 * ローカルの通常の投稿数の前日比 | 				 * 通常の投稿数の差分 (ローカル) | ||||||
| 				 */ | 				 */ | ||||||
| 				normal: number; | 				normal: number; | ||||||
|  |  | ||||||
| 				/** | 				/** | ||||||
| 				 * ローカルのリプライの投稿数の前日比 | 				 * リプライの投稿数の差分 (ローカル) | ||||||
| 				 */ | 				 */ | ||||||
| 				reply: number; | 				reply: number; | ||||||
|  |  | ||||||
| 				/** | 				/** | ||||||
| 				 * ローカルのRenoteの投稿数の前日比 | 				 * Renoteの投稿数の差分 (ローカル) | ||||||
| 				 */ | 				 */ | ||||||
| 				renote: number; | 				renote: number; | ||||||
| 			}; | 			}; | ||||||
| @@ -74,28 +98,33 @@ export interface IStats { | |||||||
|  |  | ||||||
| 		remote: { | 		remote: { | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、リモートの投稿の総計 | 			 * 集計期間時点での、全投稿数 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			total: number; | 			total: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * リモートの投稿数の前日比 | 			 * 増加した投稿数 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			diff: number; | 			inc: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少した投稿数 (リモート) | ||||||
|  | 			 */ | ||||||
|  | 			dec: number; | ||||||
|  |  | ||||||
| 			diffs: { | 			diffs: { | ||||||
| 				/** | 				/** | ||||||
| 				 * リモートの通常の投稿数の前日比 | 				 * 通常の投稿数の差分 (リモート) | ||||||
| 				 */ | 				 */ | ||||||
| 				normal: number; | 				normal: number; | ||||||
|  |  | ||||||
| 				/** | 				/** | ||||||
| 				 * リモートのリプライの投稿数の前日比 | 				 * リプライの投稿数の差分 (リモート) | ||||||
| 				 */ | 				 */ | ||||||
| 				reply: number; | 				reply: number; | ||||||
|  |  | ||||||
| 				/** | 				/** | ||||||
| 				 * リモートのRenoteの投稿数の前日比 | 				 * Renoteの投稿数の差分 (リモート) | ||||||
| 				 */ | 				 */ | ||||||
| 				renote: number; | 				renote: number; | ||||||
| 			}; | 			}; | ||||||
| @@ -108,46 +137,66 @@ export interface IStats { | |||||||
| 	drive: { | 	drive: { | ||||||
| 		local: { | 		local: { | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、ローカルのドライブファイル数の総計 | 			 * 集計期間時点での、全ドライブファイル数 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			totalCount: number; | 			totalCount: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、ローカルのドライブファイルサイズの総計 | 			 * 集計期間時点での、全ドライブファイルの合計サイズ (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			totalSize: number; | 			totalSize: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * ローカルのドライブファイル数の前日比 | 			 * 増加したドライブファイル数 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			diffCount: number; | 			incCount: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * ローカルのドライブファイルサイズの前日比 | 			 * 増加したドライブ使用量 (ローカル) | ||||||
| 			 */ | 			 */ | ||||||
| 			diffSize: number; | 			incSize: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少したドライブファイル数 (ローカル) | ||||||
|  | 			 */ | ||||||
|  | 			decCount: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少したドライブ使用量 (ローカル) | ||||||
|  | 			 */ | ||||||
|  | 			decSize: number; | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		remote: { | 		remote: { | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、リモートのドライブファイル数の総計 | 			 * 集計期間時点での、全ドライブファイル数 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			totalCount: number; | 			totalCount: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * この日時点での、リモートのドライブファイルサイズの総計 | 			 * 集計期間時点での、全ドライブファイルの合計サイズ (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			totalSize: number; | 			totalSize: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * リモートのドライブファイル数の前日比 | 			 * 増加したドライブファイル数 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			diffCount: number; | 			incCount: number; | ||||||
|  |  | ||||||
| 			/** | 			/** | ||||||
| 			 * リモートのドライブファイルサイズの前日比 | 			 * 増加したドライブ使用量 (リモート) | ||||||
| 			 */ | 			 */ | ||||||
| 			diffSize: number; | 			incSize: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少したドライブファイル数 (リモート) | ||||||
|  | 			 */ | ||||||
|  | 			decCount: number; | ||||||
|  |  | ||||||
|  | 			/** | ||||||
|  | 			 * 減少したドライブ使用量 (リモート) | ||||||
|  | 			 */ | ||||||
|  | 			decSize: number; | ||||||
| 		}; | 		}; | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ export default async (job: bq.Job, done: any): Promise<void> => { | |||||||
|  |  | ||||||
| 		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する | 		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する | ||||||
| 		if (user === null) { | 		if (user === null) { | ||||||
| 			user = await resolvePerson(signature.keyId) as IRemoteUser; | 			user = await resolvePerson(activity.actor) as IRemoteUser; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -131,5 +131,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): | |||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	// リモートサーバーからフェッチしてきて登録 | 	// リモートサーバーからフェッチしてきて登録 | ||||||
| 	return await createNote(value, resolver); | 	// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが | ||||||
|  | 	// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 | ||||||
|  | 	return await createNote(uri, resolver); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,18 +4,25 @@ import * as debug from 'debug'; | |||||||
|  |  | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; | import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; | ||||||
| import webFinger from '../../webfinger'; |  | ||||||
| import Resolver from '../resolver'; | import Resolver from '../resolver'; | ||||||
| import { resolveImage } from './image'; | import { resolveImage } from './image'; | ||||||
| import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; | import { isCollectionOrOrderedCollection, IPerson } from '../type'; | ||||||
| import { IDriveFile } from '../../../models/drive-file'; | import { IDriveFile } from '../../../models/drive-file'; | ||||||
| import Meta from '../../../models/meta'; | import Meta from '../../../models/meta'; | ||||||
| import htmlToMFM from '../../../mfm/html-to-mfm'; | import htmlToMFM from '../../../mfm/html-to-mfm'; | ||||||
| import { updateUserStats } from '../../../services/update-chart'; | import { updateUserStats } from '../../../services/update-chart'; | ||||||
|  | import { URL } from 'url'; | ||||||
|  |  | ||||||
| const log = debug('misskey:activitypub'); | const log = debug('misskey:activitypub'); | ||||||
|  |  | ||||||
| function validatePerson(x: any) { | /** | ||||||
|  |  * Validate Person object | ||||||
|  |  * @param x Fetched person object | ||||||
|  |  * @param uri Fetch target URI | ||||||
|  |  */ | ||||||
|  | function validatePerson(x: any, uri: string) { | ||||||
|  | 	const expectHost = toUnicode(new URL(uri).hostname.toLowerCase()); | ||||||
|  |  | ||||||
| 	if (x == null) { | 	if (x == null) { | ||||||
| 		return new Error('invalid person: object is null'); | 		return new Error('invalid person: object is null'); | ||||||
| 	} | 	} | ||||||
| @@ -40,6 +47,24 @@ function validatePerson(x: any) { | |||||||
| 		return new Error('invalid person: invalid name'); | 		return new Error('invalid person: invalid name'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if (typeof x.id !== 'string') { | ||||||
|  | 		return new Error('invalid person: id is not a string'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const idHost = toUnicode(new URL(x.id).hostname.toLowerCase()); | ||||||
|  | 	if (idHost !== expectHost) { | ||||||
|  | 		return new Error('invalid person: id has different host'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (typeof x.publicKey.id !== 'string') { | ||||||
|  | 		return new Error('invalid person: publicKey.id is not a string'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase()); | ||||||
|  | 	if (publicKeyIdHost !== expectHost) { | ||||||
|  | 		return new Error('invalid person: publicKey.id has different host'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return null; | 	return null; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -48,8 +73,8 @@ function validatePerson(x: any) { | |||||||
|  * |  * | ||||||
|  * Misskeyに対象のPersonが登録されていればそれを返します。 |  * Misskeyに対象のPersonが登録されていればそれを返します。 | ||||||
|  */ |  */ | ||||||
| export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> { | export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> { | ||||||
| 	const uri = typeof value == 'string' ? value : value.id; | 	if (typeof uri !== 'string') throw 'uri is not string'; | ||||||
|  |  | ||||||
| 	// URIがこのサーバーを指しているならデータベースからフェッチ | 	// URIがこのサーバーを指しているならデータベースからフェッチ | ||||||
| 	if (uri.startsWith(config.url + '/')) { | 	if (uri.startsWith(config.url + '/')) { | ||||||
| @@ -71,12 +96,14 @@ export async function fetchPerson(value: string | IObject, resolver?: Resolver): | |||||||
| /** | /** | ||||||
|  * Personを作成します。 |  * Personを作成します。 | ||||||
|  */ |  */ | ||||||
| export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> { | export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> { | ||||||
|  | 	if (typeof uri !== 'string') throw 'uri is not string'; | ||||||
|  |  | ||||||
| 	if (resolver == null) resolver = new Resolver(); | 	if (resolver == null) resolver = new Resolver(); | ||||||
|  |  | ||||||
| 	const object = await resolver.resolve(value) as any; | 	const object = await resolver.resolve(uri) as any; | ||||||
|  |  | ||||||
| 	const err = validatePerson(object); | 	const err = validatePerson(object, uri); | ||||||
|  |  | ||||||
| 	if (err) { | 	if (err) { | ||||||
| 		throw err; | 		throw err; | ||||||
| @@ -86,7 +113,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs | |||||||
|  |  | ||||||
| 	log(`Creating the Person: ${person.id}`); | 	log(`Creating the Person: ${person.id}`); | ||||||
|  |  | ||||||
| 	const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ | 	const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ | ||||||
| 		resolver.resolve(person.followers).then( | 		resolver.resolve(person.followers).then( | ||||||
| 			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, | 			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, | ||||||
| 			() => undefined | 			() => undefined | ||||||
| @@ -98,11 +125,10 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs | |||||||
| 		resolver.resolve(person.outbox).then( | 		resolver.resolve(person.outbox).then( | ||||||
| 			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, | 			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, | ||||||
| 			() => undefined | 			() => undefined | ||||||
| 		), | 		) | ||||||
| 		webFinger(person.id) |  | ||||||
| 	]); | 	]); | ||||||
|  |  | ||||||
| 	const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase(); | 	const host = toUnicode(new URL(object.id).hostname.toLowerCase()); | ||||||
|  |  | ||||||
| 	const isBot = object.type == 'Service'; | 	const isBot = object.type == 'Service'; | ||||||
|  |  | ||||||
| @@ -166,8 +192,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs | |||||||
|  |  | ||||||
| 	const avatarId = avatar ? avatar._id : null; | 	const avatarId = avatar ? avatar._id : null; | ||||||
| 	const bannerId = banner ? banner._id : null; | 	const bannerId = banner ? banner._id : null; | ||||||
| 	const avatarUrl = avatar && avatar.metadata.url ? avatar.metadata.url : null; | 	const avatarUrl = (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null; | ||||||
| 	const bannerUrl = banner && banner.metadata.url ? banner.metadata.url : null; | 	const bannerUrl = (banner && banner.metadata.url) ? banner.metadata.url : null; | ||||||
|  |  | ||||||
| 	await User.update({ _id: user._id }, { | 	await User.update({ _id: user._id }, { | ||||||
| 		$set: { | 		$set: { | ||||||
| @@ -192,8 +218,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs | |||||||
|  * |  * | ||||||
|  * Misskeyに対象のPersonが登録されていなければ無視します。 |  * Misskeyに対象のPersonが登録されていなければ無視します。 | ||||||
|  */ |  */ | ||||||
| export async function updatePerson(value: string | IObject, resolver?: Resolver): Promise<void> { | export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> { | ||||||
| 	const uri = typeof value == 'string' ? value : value.id; | 	if (typeof uri !== 'string') throw 'uri is not string'; | ||||||
|  |  | ||||||
| 	// URIがこのサーバーを指しているならスキップ | 	// URIがこのサーバーを指しているならスキップ | ||||||
| 	if (uri.startsWith(config.url + '/')) { | 	if (uri.startsWith(config.url + '/')) { | ||||||
| @@ -210,9 +236,9 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) | |||||||
|  |  | ||||||
| 	if (resolver == null) resolver = new Resolver(); | 	if (resolver == null) resolver = new Resolver(); | ||||||
|  |  | ||||||
| 	const object = await resolver.resolve(value) as any; | 	const object = await resolver.resolve(uri) as any; | ||||||
|  |  | ||||||
| 	const err = validatePerson(object); | 	const err = validatePerson(object, uri); | ||||||
|  |  | ||||||
| 	if (err) { | 	if (err) { | ||||||
| 		throw err; | 		throw err; | ||||||
| @@ -255,7 +281,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) | |||||||
| 			sharedInbox: person.sharedInbox, | 			sharedInbox: person.sharedInbox, | ||||||
| 			avatarId: avatar ? avatar._id : null, | 			avatarId: avatar ? avatar._id : null, | ||||||
| 			bannerId: banner ? banner._id : null, | 			bannerId: banner ? banner._id : null, | ||||||
| 			avatarUrl: avatar && avatar.metadata.url ? avatar.metadata.url : null, | 			avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null, | ||||||
| 			bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null, | 			bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null, | ||||||
| 			description: htmlToMFM(person.summary), | 			description: htmlToMFM(person.summary), | ||||||
| 			followersCount, | 			followersCount, | ||||||
| @@ -275,8 +301,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) | |||||||
|  * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ |  * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ | ||||||
|  * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 |  * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 | ||||||
|  */ |  */ | ||||||
| export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> { | export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> { | ||||||
| 	const uri = typeof value == 'string' ? value : value.id; | 	if (typeof uri !== 'string') throw 'uri is not string'; | ||||||
|  |  | ||||||
| 	//#region このサーバーに既に登録されていたらそれを返す | 	//#region このサーバーに既に登録されていたらそれを返す | ||||||
| 	const exist = await fetchPerson(uri); | 	const exist = await fetchPerson(uri); | ||||||
| @@ -287,5 +313,5 @@ export async function resolvePerson(value: string | IObject, verifier?: string): | |||||||
| 	//#endregion | 	//#endregion | ||||||
|  |  | ||||||
| 	// リモートサーバーからフェッチしてきて登録 | 	// リモートサーバーからフェッチしてきて登録 | ||||||
| 	return await createPerson(value); | 	return await createPerson(uri); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ export default (object: any, note: INote) => { | |||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		id: `${config.url}/notes/${note._id}`, | 		id: `${config.url}/notes/${note._id}`, | ||||||
|  | 		actor: `${config.url}/users/${note.userId}`, | ||||||
| 		type: 'Announce', | 		type: 'Announce', | ||||||
| 		published: note.createdAt.toISOString(), | 		published: note.createdAt.toISOString(), | ||||||
| 		to: ['https://www.w3.org/ns/activitystreams#Public'], | 		to: ['https://www.w3.org/ns/activitystreams#Public'], | ||||||
|   | |||||||
| @@ -1,4 +1,17 @@ | |||||||
| export default (object: any) => ({ | import config from '../../../config'; | ||||||
|  | import { INote } from '../../../models/note'; | ||||||
|  |  | ||||||
|  | export default (object: any, note: INote) => { | ||||||
|  | 	const activity = { | ||||||
|  | 		id: `${config.url}/notes/${note._id}/activity`, | ||||||
|  | 		actor: `${config.url}/users/${note.userId}`, | ||||||
| 		type: 'Create', | 		type: 'Create', | ||||||
|  | 		published: note.createdAt.toISOString(), | ||||||
| 		object | 		object | ||||||
| }); | 	} as any; | ||||||
|  |  | ||||||
|  | 	if (object.to) activity.to = object.to; | ||||||
|  | 	if (object.cc) activity.cc = object.cc; | ||||||
|  |  | ||||||
|  | 	return activity; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
| export default (object: any) => ({ | import config from '../../../config'; | ||||||
|  | import { ILocalUser } from "../../../models/user"; | ||||||
|  |  | ||||||
|  | export default (object: any, user: ILocalUser) => ({ | ||||||
| 	type: 'Delete', | 	type: 'Delete', | ||||||
|  | 	actor: `${config.url}/users/${user._id}`, | ||||||
| 	object | 	object | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,7 +1,16 @@ | |||||||
| export default (x: any) => Object.assign({ | import config from '../../../config'; | ||||||
|  | import * as uuid from 'uuid'; | ||||||
|  |  | ||||||
|  | export default (x: any) => { | ||||||
|  | 	if (x !== null && typeof x === 'object' && x.id == null) { | ||||||
|  | 		x.id = `${config.url}/${uuid.v4()}`; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return Object.assign({ | ||||||
| 		'@context': [ | 		'@context': [ | ||||||
| 			'https://www.w3.org/ns/activitystreams', | 			'https://www.w3.org/ns/activitystreams', | ||||||
| 			'https://w3id.org/security/v1', | 			'https://w3id.org/security/v1', | ||||||
| 			{ Hashtag: 'as:Hashtag' } | 			{ Hashtag: 'as:Hashtag' } | ||||||
| 		] | 		] | ||||||
| }, x); | 	}, x); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
| export default (object: any) => ({ | import config from '../../../config'; | ||||||
|  | import { ILocalUser, IUser } from "../../../models/user"; | ||||||
|  |  | ||||||
|  | export default (object: any, user: ILocalUser | IUser) => ({ | ||||||
| 	type: 'Undo', | 	type: 'Undo', | ||||||
|  | 	actor: `${config.url}/users/${user._id}`, | ||||||
| 	object | 	object | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -19,6 +19,9 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso | |||||||
| 		port, | 		port, | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		path: pathname + search, | 		path: pathname + search, | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/activity+json' | ||||||
|  | 		} | ||||||
| 	}, res => { | 	}, res => { | ||||||
| 		log(`${url} --> ${res.statusCode}`); | 		log(`${url} --> ${res.statusCode}`); | ||||||
|  |  | ||||||
| @@ -32,7 +35,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso | |||||||
| 	sign(req, { | 	sign(req, { | ||||||
| 		authorizationHeaderName: 'Signature', | 		authorizationHeaderName: 'Signature', | ||||||
| 		key: user.keypair, | 		key: user.keypair, | ||||||
| 		keyId: `acct:${user.username}@${config.host}` | 		keyId: `${config.url}/users/${user._id}/publickey` | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	// Signature: Signature ... => Signature: ... | 	// Signature: Signature ... => Signature: ... | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ export default async (username: string, _host: string, option?: any): Promise<IU | |||||||
| 	const host = toUnicode(hostAscii); | 	const host = toUnicode(hostAscii); | ||||||
|  |  | ||||||
| 	if (config.host == host) { | 	if (config.host == host) { | ||||||
| 		return await User.findOne({ usernameLower }); | 		return await User.findOne({ usernameLower, host: null }); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	let user = await User.findOne({ usernameLower, host }, option); | 	let user = await User.findOne({ usernameLower, host }, option); | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ function inbox(ctx: Router.IRouterContext) { | |||||||
| 	ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; | 	ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; | ||||||
|  |  | ||||||
| 	try { | 	try { | ||||||
| 		signature = httpSignature.parseRequest(ctx.req); | 		signature = httpSignature.parseRequest(ctx.req, { 'headers': [] }); | ||||||
| 	} catch (e) { | 	} catch (e) { | ||||||
| 		ctx.status = 401; | 		ctx.status = 401; | ||||||
| 		return; | 		return; | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| export default (token: string) => token[0] == '!'; | export default (token: string) => token.startsWith('!'); | ||||||
|   | |||||||
| @@ -1,101 +0,0 @@ | |||||||
| import Stats, { IStats } from '../../../../models/stats'; |  | ||||||
|  |  | ||||||
| type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; |  | ||||||
|  |  | ||||||
| export const meta = { |  | ||||||
| 	requireCredential: true, |  | ||||||
| 	requireAdmin: true |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export default (params: any) => new Promise(async (res, rej) => { |  | ||||||
| 	const now = new Date(); |  | ||||||
| 	const y = now.getFullYear(); |  | ||||||
| 	const m = now.getMonth(); |  | ||||||
| 	const d = now.getDate(); |  | ||||||
|  |  | ||||||
| 	const stats = await Stats.find({ |  | ||||||
| 		date: { |  | ||||||
| 			$gt: new Date(y - 1, m, d) |  | ||||||
| 		} |  | ||||||
| 	}, { |  | ||||||
| 		sort: { |  | ||||||
| 			date: -1 |  | ||||||
| 		}, |  | ||||||
| 		fields: { |  | ||||||
| 			_id: 0 |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	const chart: Array<Omit<IStats, '_id'>> = []; |  | ||||||
|  |  | ||||||
| 	for (let i = 364; i >= 0; i--) { |  | ||||||
| 		const day = new Date(y, m, d - i); |  | ||||||
|  |  | ||||||
| 		const stat = stats.find(s => s.date.getTime() == day.getTime()); |  | ||||||
|  |  | ||||||
| 		if (stat) { |  | ||||||
| 			chart.unshift(stat); |  | ||||||
| 		} else { // 隙間埋め |  | ||||||
| 			const mostRecent = stats.find(s => s.date.getTime() < day.getTime()); |  | ||||||
| 			if (mostRecent) { |  | ||||||
| 				chart.unshift(Object.assign({}, mostRecent, { |  | ||||||
| 					date: day |  | ||||||
| 				})); |  | ||||||
| 			} else { |  | ||||||
| 				chart.unshift({ |  | ||||||
| 					date: day, |  | ||||||
| 					users: { |  | ||||||
| 						local: { |  | ||||||
| 							total: 0, |  | ||||||
| 							diff: 0 |  | ||||||
| 						}, |  | ||||||
| 						remote: { |  | ||||||
| 							total: 0, |  | ||||||
| 							diff: 0 |  | ||||||
| 						} |  | ||||||
| 					}, |  | ||||||
| 					notes: { |  | ||||||
| 						local: { |  | ||||||
| 							total: 0, |  | ||||||
| 							diff: 0, |  | ||||||
| 							diffs: { |  | ||||||
| 								normal: 0, |  | ||||||
| 								reply: 0, |  | ||||||
| 								renote: 0 |  | ||||||
| 							} |  | ||||||
| 						}, |  | ||||||
| 						remote: { |  | ||||||
| 							total: 0, |  | ||||||
| 							diff: 0, |  | ||||||
| 							diffs: { |  | ||||||
| 								normal: 0, |  | ||||||
| 								reply: 0, |  | ||||||
| 								renote: 0 |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					}, |  | ||||||
| 					drive: { |  | ||||||
| 						local: { |  | ||||||
| 							totalCount: 0, |  | ||||||
| 							totalSize: 0, |  | ||||||
| 							diffCount: 0, |  | ||||||
| 							diffSize: 0 |  | ||||||
| 						}, |  | ||||||
| 						remote: { |  | ||||||
| 							totalCount: 0, |  | ||||||
| 							totalSize: 0, |  | ||||||
| 							diffCount: 0, |  | ||||||
| 							diffSize: 0 |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	chart.forEach(x => { |  | ||||||
| 		delete x.date; |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	res(chart); |  | ||||||
| }); |  | ||||||
| @@ -3,7 +3,7 @@ import RegistrationTicket from '../../../../models/registration-tickets'; | |||||||
|  |  | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| 		ja: '招待コードを発行します。' | 		'ja-JP': '招待コードを発行します。' | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	requireCredential: true, | 	requireCredential: true, | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo