Compare commits
	
		
			26 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e236c05d79 | ||
| 
						 | 
					454c1e3faf | ||
| 
						 | 
					43daf814df | ||
| 
						 | 
					c40b630530 | ||
| 
						 | 
					7fc0698ecf | ||
| 
						 | 
					4f3c8b940e | ||
| 
						 | 
					1855ab60f1 | ||
| 
						 | 
					af4f1a7bd6 | ||
| 
						 | 
					8646a9c49c | ||
| 
						 | 
					8d7c033cf5 | ||
| 
						 | 
					b8900e32de | ||
| 
						 | 
					d48c25d2c9 | ||
| 
						 | 
					a87c5899c5 | ||
| 
						 | 
					147ad69864 | ||
| 
						 | 
					c146006476 | ||
| 
						 | 
					a0f10d7ca1 | ||
| 
						 | 
					299b91edc4 | ||
| 
						 | 
					95c89ca6db | ||
| 
						 | 
					7fe0d71e7f | ||
| 
						 | 
					fbbb506e86 | ||
| 
						 | 
					ec80b06a45 | ||
| 
						 | 
					41e1619f1f | ||
| 
						 | 
					ba6a9c6a93 | ||
| 
						 | 
					18571c52fb | ||
| 
						 | 
					5d5dfeaa83 | ||
| 
						 | 
					3669d8c0f3 | 
@@ -1,6 +1,3 @@
 | 
			
		||||
name: example-instance-name # Name of your instance
 | 
			
		||||
description: example-description # Description of your instance
 | 
			
		||||
 | 
			
		||||
maintainer:
 | 
			
		||||
  name: example-maitainer-name # Your name
 | 
			
		||||
  url: http://example.com/ # Your contact (http or mailto)
 | 
			
		||||
@@ -25,7 +22,7 @@ url: https://example.tld/
 | 
			
		||||
#   +------+      |+-------------+      +----------------+|
 | 
			
		||||
#                 +---------------------------------------+
 | 
			
		||||
#
 | 
			
		||||
#   You need to setup reverse proxy. (eg. Nginx)
 | 
			
		||||
#   You need to setup reverse proxy. (eg. nginx)
 | 
			
		||||
#   You do not define 'https' section.
 | 
			
		||||
 | 
			
		||||
# Option 2: Standalone
 | 
			
		||||
@@ -148,6 +145,12 @@ drive:
 | 
			
		||||
#  consumer_key: example-twitter-consumer-key
 | 
			
		||||
#  consumer_secret: example-twitter-consumer-secret-key
 | 
			
		||||
 | 
			
		||||
# GitHub integration
 | 
			
		||||
# You need to set the oauth callback url as : https://<your-misskey-instance>/api/gh/cb
 | 
			
		||||
#github:
 | 
			
		||||
#  client_id: example-github-client-id
 | 
			
		||||
#  client_secret: example-github-client-secret
 | 
			
		||||
 | 
			
		||||
# Ghost
 | 
			
		||||
# Ghost account is an account used for the purpose of delegating
 | 
			
		||||
# followers when putting users in the list.
 | 
			
		||||
 
 | 
			
		||||
@@ -417,6 +417,7 @@ common/views/components/signin.vue:
 | 
			
		||||
  signin: "サインイン"
 | 
			
		||||
  or: "または"
 | 
			
		||||
  signin-with-twitter: "Twitterでログイン"
 | 
			
		||||
  signin-with-github: "GitHubでログイン"
 | 
			
		||||
  login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
 | 
			
		||||
 | 
			
		||||
common/views/components/signup.vue:
 | 
			
		||||
@@ -460,6 +461,14 @@ common/views/components/twitter-setting.vue:
 | 
			
		||||
  connect: "Twitterと接続する"
 | 
			
		||||
  disconnect: "切断する"
 | 
			
		||||
 | 
			
		||||
common/views/components/github-setting.vue:
 | 
			
		||||
  description: "お使いのGitHubアカウントをお使いのMisskeyアカウントに接続しておくと、プロフィールでGitHubアカウント情報が表示されるようになったり、GitHubを用いた便利なサインインを利用できるようになります。"
 | 
			
		||||
  connected-to: "次のGitHubアカウントに接続されています"
 | 
			
		||||
  detail: "詳細..."
 | 
			
		||||
  reconnect: "再接続する"
 | 
			
		||||
  connect: "GitHubと接続する"
 | 
			
		||||
  disconnect: "切断する"
 | 
			
		||||
 | 
			
		||||
common/views/components/uploader.vue:
 | 
			
		||||
  waiting: "待機中"
 | 
			
		||||
 | 
			
		||||
@@ -599,32 +608,6 @@ desktop/views/components/calendar.vue:
 | 
			
		||||
  next: "次の月"
 | 
			
		||||
  go: "クリックして時間遡行"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/charts.vue:
 | 
			
		||||
  title: "チャート"
 | 
			
		||||
  per-day: "1日ごと"
 | 
			
		||||
  per-hour: "1時間ごと"
 | 
			
		||||
  federation: "フェデレーション"
 | 
			
		||||
  notes: "投稿"
 | 
			
		||||
  users: "ユーザー"
 | 
			
		||||
  drive: "ドライブ"
 | 
			
		||||
  network: "ネットワーク"
 | 
			
		||||
  charts:
 | 
			
		||||
    federation-instances: "インスタンスの増減"
 | 
			
		||||
    federation-instances-total: "インスタンスの積算"
 | 
			
		||||
    notes: "投稿の増減 (統合)"
 | 
			
		||||
    local-notes: "投稿の増減 (ローカル)"
 | 
			
		||||
    remote-notes: "投稿の増減 (リモート)"
 | 
			
		||||
    notes-total: "投稿の積算"
 | 
			
		||||
    users: "ユーザーの増減"
 | 
			
		||||
    users-total: "ユーザーの積算"
 | 
			
		||||
    drive: "ドライブ使用量の増減"
 | 
			
		||||
    drive-total: "ドライブ使用量の積算"
 | 
			
		||||
    drive-files: "ドライブのファイル数の増減"
 | 
			
		||||
    drive-files-total: "ドライブのファイル数の積算"
 | 
			
		||||
    network-requests: "リクエスト"
 | 
			
		||||
    network-time: "応答時間"
 | 
			
		||||
    network-usage: "通信量"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/choose-file-from-drive-window.vue:
 | 
			
		||||
  choose-file: "ファイル選択中"
 | 
			
		||||
  upload: "PCからドライブにファイルをアップロード"
 | 
			
		||||
@@ -1088,10 +1071,16 @@ admin/views/dashboard.vue:
 | 
			
		||||
  instances: "インスタンス"
 | 
			
		||||
  this-instance: "このインスタンス"
 | 
			
		||||
  federated: "連合"
 | 
			
		||||
 | 
			
		||||
admin/views/instance.vue:
 | 
			
		||||
  instance: "インスタンス"
 | 
			
		||||
  instance-name: "インスタンス名"
 | 
			
		||||
  instance-description: "インスタンスの紹介"
 | 
			
		||||
  banner-url: "バナー画像URL"
 | 
			
		||||
  disableRegistration: "ユーザー登録の受付を停止する"
 | 
			
		||||
  disableLocalTimeline: "ローカルタイムラインを無効にする"
 | 
			
		||||
  invite: "招待"
 | 
			
		||||
  banner-url: "Banner URL"
 | 
			
		||||
  disableRegistration: "Disable new user registration"
 | 
			
		||||
  disableLocalTimeline: "Disable the local timeline"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
 | 
			
		||||
admin/views/charts.vue:
 | 
			
		||||
  title: "チャート"
 | 
			
		||||
@@ -1142,6 +1131,7 @@ admin/views/emoji.vue:
 | 
			
		||||
    aliases-desc: "スペースで区切って複数設定できます。"
 | 
			
		||||
    url: "絵文字画像URL"
 | 
			
		||||
    add: "追加"
 | 
			
		||||
    info: "50KB以下のPNG画像をおすすめします。"
 | 
			
		||||
  emojis:
 | 
			
		||||
    title: "絵文字一覧"
 | 
			
		||||
    update: "更新"
 | 
			
		||||
@@ -1173,12 +1163,6 @@ desktop/views/pages/deck/deck.user-column.vue:
 | 
			
		||||
  pinned-notes: "ピン留めされた投稿"
 | 
			
		||||
  push-to-a-list: "リストに追加"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/stats/stats.vue:
 | 
			
		||||
  all-users: "全てのユーザー"
 | 
			
		||||
  original-users: "このインスタンスのユーザー"
 | 
			
		||||
  all-notes: "全ての投稿"
 | 
			
		||||
  original-notes: "このインスタンスの投稿"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -1560,6 +1544,10 @@ mobile/views/pages/settings.vue:
 | 
			
		||||
  twitter-connect: "Twitterアカウントに接続する"
 | 
			
		||||
  twitter-reconnect: "再接続する"
 | 
			
		||||
  twitter-disconnect: "切断する"
 | 
			
		||||
  github: "GitHub連携"
 | 
			
		||||
  github-connect: "GitHubアカウントに接続する"
 | 
			
		||||
  github-reconnect: "再接続する"
 | 
			
		||||
  github-disconnect: "切断する"
 | 
			
		||||
  update: "Misskey Update"
 | 
			
		||||
  version: "バージョン:"
 | 
			
		||||
  latest-version: "最新のバージョン:"
 | 
			
		||||
 
 | 
			
		||||
@@ -186,7 +186,7 @@ common:
 | 
			
		||||
    stack-left: "左に重ねんで!"
 | 
			
		||||
    pop-right: "右に出すで!"
 | 
			
		||||
  dev: "アプリの作成あかんかったわ。もっぺんやってみて。"
 | 
			
		||||
  ai-chan-kawaii: "藍ちゃかわいい"
 | 
			
		||||
  ai-chan-kawaii: "藍ちゃめっさべっぴんさんや"
 | 
			
		||||
auth/views/form.vue:
 | 
			
		||||
  share-access: "<i>{{ app.name }}</i>があんさんのアカウントにアクセスすんのを<b>許可</b>してもええか?"
 | 
			
		||||
  permission-ask: "このアプリは次の権限を要求してんで:"
 | 
			
		||||
@@ -744,7 +744,7 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  apps: "アプリ"
 | 
			
		||||
  mute-and-block: "ミュート/ブロック"
 | 
			
		||||
  blocking: "ブロック"
 | 
			
		||||
  security: "守護神セキュリティ"
 | 
			
		||||
  security: "セキュリティ"
 | 
			
		||||
  signin: "こんな感じでサインインしたらしいで"
 | 
			
		||||
  password: "パスワード"
 | 
			
		||||
  2fa: "二段階認証"
 | 
			
		||||
@@ -873,15 +873,15 @@ common/views/components/mute-and-block.vue:
 | 
			
		||||
  mute-and-block: "ミュートとブロック"
 | 
			
		||||
  mute: "ミュート"
 | 
			
		||||
  block: "ブロック"
 | 
			
		||||
  no-muted-users: "ミュートしているユーザーはいません"
 | 
			
		||||
  no-blocked-users: "ブロックしているユーザーはいません"
 | 
			
		||||
  no-muted-users: "ミュートしとるユーザーはおらんで"
 | 
			
		||||
  no-blocked-users: "ブロックしとるユーザーはおらんで"
 | 
			
		||||
common/views/components/password-settings.vue:
 | 
			
		||||
  reset: "パスワードを変更する"
 | 
			
		||||
  enter-current-password: "現在のパスワードを入力してください"
 | 
			
		||||
  enter-new-password: "新しいパスワードを入力してください"
 | 
			
		||||
  enter-new-password-again: "もう一度新しいパスワードを入力してください"
 | 
			
		||||
  not-match: "新しいパスワードが一致しません"
 | 
			
		||||
  changed: "パスワードを変更しました"
 | 
			
		||||
  reset: "パスワード変える"
 | 
			
		||||
  enter-current-password: "今のパスワードを入れてや"
 | 
			
		||||
  enter-new-password: "こんどのパスワード入れてや"
 | 
			
		||||
  enter-new-password-again: "もっぺん入れてや"
 | 
			
		||||
  not-match: "パスワードがおうとらん"
 | 
			
		||||
  changed: "パスワード変えたわ"
 | 
			
		||||
desktop/views/components/sub-note-content.vue:
 | 
			
		||||
  private: "この投稿は見せられへんわ"
 | 
			
		||||
  deleted: "この投稿なんか無くなってもうたわ"
 | 
			
		||||
@@ -953,7 +953,7 @@ admin/views/index.vue:
 | 
			
		||||
  emoji: "カスタム絵文字"
 | 
			
		||||
  users: "ユーザー"
 | 
			
		||||
  update: "更新"
 | 
			
		||||
  announcements: "お知らせ"
 | 
			
		||||
  announcements: "知っといてや"
 | 
			
		||||
  hashtags: "ハッシュタグ"
 | 
			
		||||
  back-to-misskey: "Misskeyに戻る"
 | 
			
		||||
admin/views/dashboard.vue:
 | 
			
		||||
@@ -962,9 +962,9 @@ admin/views/dashboard.vue:
 | 
			
		||||
  notes: "投稿"
 | 
			
		||||
  drive: "ドライブ"
 | 
			
		||||
  instances: "インスタンス"
 | 
			
		||||
  this-instance: "このインスタンス"
 | 
			
		||||
  this-instance: "ワイのインスタンス"
 | 
			
		||||
  federated: "連合"
 | 
			
		||||
  invite: "招待"
 | 
			
		||||
  invite: "来てや"
 | 
			
		||||
  banner-url: "Banner URL"
 | 
			
		||||
  disableRegistration: "Disable new user registration"
 | 
			
		||||
  disableLocalTimeline: "Disable the local timeline"
 | 
			
		||||
@@ -980,7 +980,7 @@ admin/views/charts.vue:
 | 
			
		||||
  charts:
 | 
			
		||||
    federation-instances: "インスタンスの増減"
 | 
			
		||||
    federation-instances-total: "インスタンスの積算"
 | 
			
		||||
    notes: "投稿の増減 (統合)"
 | 
			
		||||
    notes: "投稿の増減(統合)"
 | 
			
		||||
    local-notes: "投稿の増減 (ローカル)"
 | 
			
		||||
    remote-notes: "投稿の増減 (リモート)"
 | 
			
		||||
    notes-total: "投稿の積算"
 | 
			
		||||
@@ -1387,7 +1387,7 @@ mobile/views/pages/user.vue:
 | 
			
		||||
  mute: "ミュート"
 | 
			
		||||
  unmute: "ミュート解除"
 | 
			
		||||
  block: "ブロック"
 | 
			
		||||
  unblock: "ブロック解除"
 | 
			
		||||
  unblock: "ブロックやめたる"
 | 
			
		||||
mobile/views/pages/user/home.vue:
 | 
			
		||||
  recent-notes: "最近儲かりまっか?"
 | 
			
		||||
  images: "画像"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "10.38.3",
 | 
			
		||||
	"clientVersion": "1.0.11490",
 | 
			
		||||
	"version": "10.38.6",
 | 
			
		||||
	"clientVersion": "1.0.11516",
 | 
			
		||||
	"codename": "nighthike",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"private": true,
 | 
			
		||||
@@ -62,6 +62,7 @@
 | 
			
		||||
		"@types/mongodb": "3.1.12",
 | 
			
		||||
		"@types/ms": "0.7.30",
 | 
			
		||||
		"@types/node": "10.12.2",
 | 
			
		||||
		"@types/oauth": "0.9.1",
 | 
			
		||||
		"@types/portscanner": "2.1.0",
 | 
			
		||||
		"@types/pug": "2.0.4",
 | 
			
		||||
		"@types/qrcode": "1.3.0",
 | 
			
		||||
@@ -95,7 +96,6 @@
 | 
			
		||||
		"chai": "4.2.0",
 | 
			
		||||
		"chai-http": "4.2.0",
 | 
			
		||||
		"chalk": "2.4.1",
 | 
			
		||||
		"chart.js": "2.7.3",
 | 
			
		||||
		"commander": "2.19.0",
 | 
			
		||||
		"crc-32": "1.2.0",
 | 
			
		||||
		"css-loader": "1.0.1",
 | 
			
		||||
@@ -211,7 +211,6 @@
 | 
			
		||||
		"uuid": "3.3.2",
 | 
			
		||||
		"v-animate-css": "0.0.2",
 | 
			
		||||
		"vue": "2.5.17",
 | 
			
		||||
		"vue-chartjs": "3.4.0",
 | 
			
		||||
		"vue-color": "2.7.0",
 | 
			
		||||
		"vue-content-loading": "1.5.3",
 | 
			
		||||
		"vue-cropperjs": "2.2.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -54,9 +54,9 @@ export default Vue.extend({
 | 
			
		||||
			(this as any).api('admin/update-meta', {
 | 
			
		||||
				broadcasts: this.announcements
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -63,9 +63,9 @@ export default Vue.extend({
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.hyhctythnmwihguaaapnbrbszsjqxpio
 | 
			
		||||
	display block
 | 
			
		||||
	padding 16px
 | 
			
		||||
	padding 12px 16px 16px 16px
 | 
			
		||||
	height 250px
 | 
			
		||||
	overflow auto
 | 
			
		||||
	overflow hidden
 | 
			
		||||
	box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
 | 
			
		||||
	background var(--adminDashboardCardBg)
 | 
			
		||||
	border-radius 8px
 | 
			
		||||
@@ -77,10 +77,10 @@ export default Vue.extend({
 | 
			
		||||
		border-spacing 0
 | 
			
		||||
		border-collapse collapse
 | 
			
		||||
		color var(--adminDashboardCardFg)
 | 
			
		||||
		font-size 15px
 | 
			
		||||
		font-size 14px
 | 
			
		||||
 | 
			
		||||
		thead
 | 
			
		||||
			border-bottom solid 2px var(--adminDashboardCardDivider)
 | 
			
		||||
			border-bottom solid 1px var(--adminDashboardCardDivider)
 | 
			
		||||
 | 
			
		||||
			tr
 | 
			
		||||
				th
 | 
			
		||||
 
 | 
			
		||||
@@ -136,13 +136,16 @@ export default Vue.extend({
 | 
			
		||||
		border-bottom solid 1px var(--adminDashboardHeaderBorder)
 | 
			
		||||
		color var(--adminDashboardHeaderFg)
 | 
			
		||||
		font-size 14px
 | 
			
		||||
		white-space nowrap
 | 
			
		||||
 | 
			
		||||
		@media (max-width 1000px)
 | 
			
		||||
			display none
 | 
			
		||||
 | 
			
		||||
		> p
 | 
			
		||||
			display inline
 | 
			
		||||
			display block
 | 
			
		||||
			margin 0 32px 0 0
 | 
			
		||||
			overflow hidden
 | 
			
		||||
			text-overflow ellipsis
 | 
			
		||||
 | 
			
		||||
			> b
 | 
			
		||||
				&:after
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
			<ui-input v-model="url">
 | 
			
		||||
				<span>%i18n:@add-emoji.url%</span>
 | 
			
		||||
			</ui-input>
 | 
			
		||||
			<ui-info>%i18n:@add-emoji.info%</ui-info>
 | 
			
		||||
			<ui-button @click="add">%i18n:@add-emoji.add%</ui-button>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
@@ -27,11 +28,9 @@
 | 
			
		||||
			<ui-horizon-group inputs>
 | 
			
		||||
				<ui-input v-model="emoji.name">
 | 
			
		||||
					<span>%i18n:@add-emoji.name%</span>
 | 
			
		||||
					<span slot="text">%i18n:@add-emoji.name-desc%</span>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-input v-model="emoji.aliases">
 | 
			
		||||
					<span>%i18n:@add-emoji.aliases%</span>
 | 
			
		||||
					<span slot="text">%i18n:@add-emoji.aliases-desc%</span>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
			<ui-input v-model="emoji.url">
 | 
			
		||||
@@ -70,10 +69,10 @@ export default Vue.extend({
 | 
			
		||||
				url: this.url,
 | 
			
		||||
				aliases: this.aliases.split(' ')
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Added` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Added` });
 | 
			
		||||
				this.fetchEmojis();
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
@@ -91,9 +90,9 @@ export default Vue.extend({
 | 
			
		||||
				url: emoji.url,
 | 
			
		||||
				aliases: emoji.aliases.split(' ')
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Updated` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Updated` });
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
@@ -101,10 +100,10 @@ export default Vue.extend({
 | 
			
		||||
			(this as any).api('admin/emoji/remove', {
 | 
			
		||||
				id: emoji.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Removed` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Removed` });
 | 
			
		||||
				this.fetchEmojis();
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -29,9 +29,9 @@ export default Vue.extend({
 | 
			
		||||
			(this as any).api('admin/update-meta', {
 | 
			
		||||
				hidedTags: this.hidedTags.split('\n')
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="axbwjelsbymowqjyywpirzhdlszoncqs">
 | 
			
		||||
	<ui-card>
 | 
			
		||||
		<div slot="title">%i18n:@banner-url%</div>
 | 
			
		||||
		<div slot="title">%fa:cog% %i18n:@instance%</div>
 | 
			
		||||
		<section class="fit-top">
 | 
			
		||||
			<ui-input v-model="bannerUrl"/>
 | 
			
		||||
			<ui-input v-model="name">%i18n:@instance-name%</ui-input>
 | 
			
		||||
			<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea>
 | 
			
		||||
			<ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input>
 | 
			
		||||
			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
@@ -35,26 +37,40 @@ export default Vue.extend({
 | 
			
		||||
			disableRegistration: false,
 | 
			
		||||
			disableLocalTimeline: false,
 | 
			
		||||
			bannerUrl: null,
 | 
			
		||||
			name: null,
 | 
			
		||||
			description: null,
 | 
			
		||||
			inviteCode: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		(this as any).os.getMeta().then(meta => {
 | 
			
		||||
			this.bannerUrl = meta.bannerUrl;
 | 
			
		||||
			this.name = meta.name;
 | 
			
		||||
			this.description = meta.description;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		invite() {
 | 
			
		||||
			(this as any).api('admin/invite').then(x => {
 | 
			
		||||
				this.inviteCode = x.code;
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		updateMeta() {
 | 
			
		||||
			(this as any).api('admin/update-meta', {
 | 
			
		||||
				disableRegistration: this.disableRegistration,
 | 
			
		||||
				disableLocalTimeline: this.disableLocalTimeline,
 | 
			
		||||
				bannerUrl: this.bannerUrl
 | 
			
		||||
				bannerUrl: this.bannerUrl,
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				description: this.description
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -67,11 +67,11 @@ export default Vue.extend({
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await (this as any).os.api('users/show', parseAcct(this.verifyUsername));
 | 
			
		||||
				await (this as any).os.api('admin/verify-user', { userId: user.id });
 | 
			
		||||
				(this as any).os.apis.dialog({ text: '%i18n:@verified%' });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: '%i18n:@verified%' });
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			await process().catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.verifying = false;
 | 
			
		||||
@@ -83,11 +83,11 @@ export default Vue.extend({
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await (this as any).os.api('users/show', parseAcct(this.unverifyUsername));
 | 
			
		||||
				await (this as any).os.api('admin/unverify-user', { userId: user.id });
 | 
			
		||||
				(this as any).os.apis.dialog({ text: '%i18n:@unverified%' });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: '%i18n:@unverified%' });
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			await process().catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.unverifying = false;
 | 
			
		||||
@@ -99,11 +99,11 @@ export default Vue.extend({
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await (this as any).os.api('users/show', parseAcct(this.suspendUsername));
 | 
			
		||||
				await (this as any).os.api('admin/suspend-user', { userId: user.id });
 | 
			
		||||
				(this as any).os.apis.dialog({ text: '%i18n:@suspended%' });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: '%i18n:@suspended%' });
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			await process().catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.suspending = false;
 | 
			
		||||
@@ -115,11 +115,11 @@ export default Vue.extend({
 | 
			
		||||
			const process = async () => {
 | 
			
		||||
				const user = await (this as any).os.api('users/show', parseAcct(this.unsuspendUsername));
 | 
			
		||||
				await (this as any).os.api('admin/unsuspend-user', { userId: user.id });
 | 
			
		||||
				(this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: '%i18n:@unsuspended%' });
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			await process().catch(e => {
 | 
			
		||||
				(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed: ${e}` });
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.unsuspending = false;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										63
									
								
								src/client/app/common/views/components/github-setting.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/client/app/common/views/components/github-setting.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-github-setting">
 | 
			
		||||
	<p>%i18n:@description%<a :href="`${docsUrl}/link-to-github`" target="_blank">%i18n:@detail%</a></p>
 | 
			
		||||
	<p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">%i18n:@connected-to%: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 | 
			
		||||
	<p>
 | 
			
		||||
		<a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
 | 
			
		||||
		<span v-if="$store.state.i.github"> or </span>
 | 
			
		||||
		<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">%i18n:@disconnect%</a>
 | 
			
		||||
	</p>
 | 
			
		||||
	<p class="id" v-if="$store.state.i.github">GitHub ID: {{ $store.state.i.github.id }}</p>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { apiUrl, docsUrl } from '../../../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			form: null,
 | 
			
		||||
			apiUrl,
 | 
			
		||||
			docsUrl
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$watch('$store.state.i', () => {
 | 
			
		||||
			if (this.$store.state.i.github && this.form)
 | 
			
		||||
				this.form.close();
 | 
			
		||||
		}, {
 | 
			
		||||
			deep: true
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		connect() {
 | 
			
		||||
			this.form = window.open(apiUrl + '/connect/github',
 | 
			
		||||
				'github_connect_window',
 | 
			
		||||
				'height=570, width=520');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disconnect() {
 | 
			
		||||
			window.open(apiUrl + '/disconnect/github',
 | 
			
		||||
				'github_disconnect_window',
 | 
			
		||||
				'height=570, width=520');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.mk-github-setting
 | 
			
		||||
	.account
 | 
			
		||||
		border solid 1px #e1e8ed
 | 
			
		||||
		border-radius 4px
 | 
			
		||||
		padding 16px
 | 
			
		||||
 | 
			
		||||
		a
 | 
			
		||||
			font-weight bold
 | 
			
		||||
			color inherit
 | 
			
		||||
 | 
			
		||||
	.id
 | 
			
		||||
		color #8899a6
 | 
			
		||||
</style>
 | 
			
		||||
@@ -37,6 +37,7 @@ import messaging from './messaging.vue';
 | 
			
		||||
import messagingRoom from './messaging-room.vue';
 | 
			
		||||
import urlPreview from './url-preview.vue';
 | 
			
		||||
import twitterSetting from './twitter-setting.vue';
 | 
			
		||||
import githubSetting from './github-setting.vue';
 | 
			
		||||
import fileTypeIcon from './file-type-icon.vue';
 | 
			
		||||
import Reversi from './games/reversi/reversi.vue';
 | 
			
		||||
import welcomeTimeline from './welcome-timeline.vue';
 | 
			
		||||
@@ -90,6 +91,7 @@ Vue.component('mk-messaging', messaging);
 | 
			
		||||
Vue.component('mk-messaging-room', messagingRoom);
 | 
			
		||||
Vue.component('mk-url-preview', urlPreview);
 | 
			
		||||
Vue.component('mk-twitter-setting', twitterSetting);
 | 
			
		||||
Vue.component('mk-github-setting', githubSetting);
 | 
			
		||||
Vue.component('mk-file-type-icon', fileTypeIcon);
 | 
			
		||||
Vue.component('mk-reversi', Reversi);
 | 
			
		||||
Vue.component('mk-welcome-timeline', welcomeTimeline);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
<span class="mk-nav">
 | 
			
		||||
	<a :href="aboutUrl">%i18n:@about%</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a href="/stats">%i18n:@stats%</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a :href="repositoryUrl">%i18n:@repository%</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
	<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
 | 
			
		||||
	<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
 | 
			
		||||
	<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
 | 
			
		||||
	<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/github`">%i18n:@signin-with-github%</a></p>
 | 
			
		||||
</form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,6 @@ import updateBanner from './api/update-banner';
 | 
			
		||||
import MkIndex from './views/pages/index.vue';
 | 
			
		||||
import MkHome from './views/pages/home.vue';
 | 
			
		||||
import MkDeck from './views/pages/deck/deck.vue';
 | 
			
		||||
import MkStats from './views/pages/stats/stats.vue';
 | 
			
		||||
import MkUser from './views/pages/user/user.vue';
 | 
			
		||||
import MkFavorites from './views/pages/favorites.vue';
 | 
			
		||||
import MkSelectDrive from './views/pages/selectdrive.vue';
 | 
			
		||||
@@ -56,7 +55,6 @@ init(async (launch) => {
 | 
			
		||||
			{ path: '/', name: 'index', component: MkIndex },
 | 
			
		||||
			{ path: '/home', name: 'home', component: MkHome },
 | 
			
		||||
			{ path: '/deck', name: 'deck', component: MkDeck },
 | 
			
		||||
			{ path: '/stats', name: 'stats', component: MkStats },
 | 
			
		||||
			{ path: '/i/customize-home', component: MkHomeCustomize },
 | 
			
		||||
			{ path: '/i/favorites', component: MkFavorites },
 | 
			
		||||
			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
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: 'index',
 | 
			
		||||
					position: 'nearest'
 | 
			
		||||
				}
 | 
			
		||||
			}, this.opts || {}));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
@@ -1,723 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="gkgckalzgidaygcxnugepioremxvxvpt">
 | 
			
		||||
	<header>
 | 
			
		||||
		<b>%i18n:@title%:</b>
 | 
			
		||||
		<select v-model="chartType">
 | 
			
		||||
			<optgroup label="%i18n:@federation%">
 | 
			
		||||
				<option value="federation-instances">%i18n:@charts.federation-instances%</option>
 | 
			
		||||
				<option value="federation-instances-total">%i18n:@charts.federation-instances-total%</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
			<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>
 | 
			
		||||
			<optgroup label="%i18n:@network%">
 | 
			
		||||
				<option value="network-requests">%i18n:@charts.network-requests%</option>
 | 
			
		||||
				<option value="network-time">%i18n:@charts.network-time%</option>
 | 
			
		||||
				<option value="network-usage">%i18n:@charts.network-usage%</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)',
 | 
			
		||||
 | 
			
		||||
	incoming: 'rgb(52, 178, 118)',
 | 
			
		||||
	outgoing: 'rgb(255, 97, 74)',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const rgba = (color: string): string => {
 | 
			
		||||
	return color.replace('rgb', 'rgba').replace(')', ', 0.1)');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const limit = 35;
 | 
			
		||||
 | 
			
		||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
			
		||||
const negate = arr => arr.map(x => -x);
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XChart
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			now: null,
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartType: 'notes',
 | 
			
		||||
			span: 'hour'
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		data(): any {
 | 
			
		||||
			if (this.chart == null) return null;
 | 
			
		||||
			switch (this.chartType) {
 | 
			
		||||
				case 'federation-instances': return this.federationInstancesChart(false);
 | 
			
		||||
				case 'federation-instances-total': return this.federationInstancesChart(true);
 | 
			
		||||
				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();
 | 
			
		||||
				case 'network-requests': return this.networkRequestsChart();
 | 
			
		||||
				case 'network-time': return this.networkTimeChart();
 | 
			
		||||
				case 'network-usage': return this.networkUsageChart();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		stats(): any[] {
 | 
			
		||||
			const stats =
 | 
			
		||||
				this.span == 'day' ? this.chart.perDay :
 | 
			
		||||
				this.span == 'hour' ? this.chart.perHour :
 | 
			
		||||
				null;
 | 
			
		||||
 | 
			
		||||
			return stats;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	async created() {
 | 
			
		||||
		this.now = new Date();
 | 
			
		||||
 | 
			
		||||
		const [perHour, perDay] = await Promise.all([Promise.all([
 | 
			
		||||
			(this as any).api('charts/federation', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/users', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/notes', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/drive', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/network', { limit: limit, span: 'hour' })
 | 
			
		||||
		]), Promise.all([
 | 
			
		||||
			(this as any).api('charts/federation', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/users', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/notes', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/drive', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/network', { limit: limit, span: 'day' })
 | 
			
		||||
		])]);
 | 
			
		||||
 | 
			
		||||
		const chart = {
 | 
			
		||||
			perHour: {
 | 
			
		||||
				federation: perHour[0],
 | 
			
		||||
				users: perHour[1],
 | 
			
		||||
				notes: perHour[2],
 | 
			
		||||
				drive: perHour[3],
 | 
			
		||||
				network: perHour[4]
 | 
			
		||||
			},
 | 
			
		||||
			perDay: {
 | 
			
		||||
				federation: perDay[0],
 | 
			
		||||
				users: perDay[1],
 | 
			
		||||
				notes: perDay[2],
 | 
			
		||||
				drive: perDay[3],
 | 
			
		||||
				network: perDay[4]
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		this.chart = chart;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		getDate(i: number) {
 | 
			
		||||
			const y = this.now.getFullYear();
 | 
			
		||||
			const m = this.now.getMonth();
 | 
			
		||||
			const d = this.now.getDate();
 | 
			
		||||
			const h = this.now.getHours();
 | 
			
		||||
 | 
			
		||||
			return (
 | 
			
		||||
				this.span == 'day' ? new Date(y, m, d - i) :
 | 
			
		||||
				this.span == 'hour' ? new Date(y, m, d, h - i) :
 | 
			
		||||
				null
 | 
			
		||||
			);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		format(arr) {
 | 
			
		||||
			return arr.map((v, i) => ({ t: this.getDate(i).getTime(), y: v }));
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		federationInstancesChart(total: boolean): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Instances',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.federation.instance.total
 | 
			
		||||
						: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)))
 | 
			
		||||
				}]
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		notesChart(type: string): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'All',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
 | 
			
		||||
						: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Renotes',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: 'rgba(161, 222, 65, 0.1)',
 | 
			
		||||
					borderColor: '#a1de41',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
 | 
			
		||||
						: this.stats.notes[type].diffs.renote
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Replies',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: 'rgba(247, 121, 108, 0.1)',
 | 
			
		||||
					borderColor: '#f7796c',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
 | 
			
		||||
						: this.stats.notes[type].diffs.reply
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Normal',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: 'rgba(65, 221, 222, 0.1)',
 | 
			
		||||
					borderColor: '#41ddde',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
 | 
			
		||||
						: this.stats.notes[type].diffs.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 {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.notes.local.total)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.notes.remote.total)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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 {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? sum(this.stats.users.local.total, this.stats.users.remote.total)
 | 
			
		||||
						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.users.local.total
 | 
			
		||||
						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.users.remote.total
 | 
			
		||||
						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
 | 
			
		||||
					)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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 {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'All',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.incSize, negate(this.stats.drive.local.decSize), this.stats.drive.remote.incSize, negate(this.stats.drive.remote.decSize)))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.incSize)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localMinus),
 | 
			
		||||
					borderColor: colors.localMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.local.decSize))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remotePlus),
 | 
			
		||||
					borderColor: colors.remotePlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.incSize)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remoteMinus),
 | 
			
		||||
					borderColor: colors.remoteMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.remote.decSize))
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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 {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.totalSize)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.totalSize)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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 {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'All',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.incCount, negate(this.stats.drive.local.decCount), this.stats.drive.remote.incCount, negate(this.stats.drive.remote.decCount)))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.incCount)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localMinus),
 | 
			
		||||
					borderColor: colors.localMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.local.decCount))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remotePlus),
 | 
			
		||||
					borderColor: colors.remotePlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.incCount)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remoteMinus),
 | 
			
		||||
					borderColor: colors.remoteMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.remote.decCount))
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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 {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.totalCount)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.totalCount)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		networkRequestsChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Incoming',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.network.incomingRequests)
 | 
			
		||||
				}]
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		networkTimeChart(): any {
 | 
			
		||||
			const data = [];
 | 
			
		||||
 | 
			
		||||
			for (let i = 0; i < limit; i++) {
 | 
			
		||||
				data.push(this.stats.network.incomingRequests[i] != 0 ? (this.stats.network.totalTime[i] / this.stats.network.incomingRequests[i]) : 0);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Avg time (ms)',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(data)
 | 
			
		||||
				}]
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		networkUsageChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Incoming',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.incoming),
 | 
			
		||||
					borderColor: colors.incoming,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.network.incomingBytes)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Outgoing',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.outgoing),
 | 
			
		||||
					borderColor: colors.outgoing,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.network.outgoingBytes)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				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)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.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 var(--primary)
 | 
			
		||||
					cursor pointer
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
		> *
 | 
			
		||||
			display block
 | 
			
		||||
			height 350px
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -23,6 +23,13 @@
 | 
			
		||||
					<mk-twitter-setting/>
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
 | 
			
		||||
			<ui-card>
 | 
			
		||||
				<div slot="title">%fa:B github% %i18n:@github%</div>
 | 
			
		||||
				<section>
 | 
			
		||||
					<mk-github-setting/>
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<ui-card class="theme" v-show="page == 'theme'">
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
					<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
 | 
			
		||||
				</li>
 | 
			
		||||
				<li v-if="$store.state.i.isAdmin">
 | 
			
		||||
					<router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link>
 | 
			
		||||
					<a href="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</a>
 | 
			
		||||
				</li>
 | 
			
		||||
			</ul>
 | 
			
		||||
			<ul>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +0,0 @@
 | 
			
		||||
<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">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.tcrwdhwpuxrwmcttxjcsehgpagpstqey
 | 
			
		||||
	width 100%
 | 
			
		||||
	padding 16px
 | 
			
		||||
 | 
			
		||||
	> .stats
 | 
			
		||||
		display flex
 | 
			
		||||
		justify-content center
 | 
			
		||||
		margin 0 auto 16px auto
 | 
			
		||||
		padding 32px
 | 
			
		||||
		background #fff
 | 
			
		||||
		box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
			flex 1
 | 
			
		||||
			text-align center
 | 
			
		||||
 | 
			
		||||
			> *:first-child
 | 
			
		||||
				display block
 | 
			
		||||
				color var(--primary)
 | 
			
		||||
 | 
			
		||||
			> *:last-child
 | 
			
		||||
				font-size 70%
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
		max-width 950px
 | 
			
		||||
		margin 0 auto
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										26
									
								
								src/client/app/desktop/views/pages/user/user.github.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/client/app/desktop/views/pages/user/user.github.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="aqooishiizumijmihokohinatamihoaz">
 | 
			
		||||
	<span>%fa:B github%<a :href="`https://github.com/${user.github.login}`" target="_blank">@{{ user.github.login }}</a></span>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: ['user']
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.aqooishiizumijmihokohinatamihoaz
 | 
			
		||||
	padding 32px
 | 
			
		||||
	background #171515
 | 
			
		||||
	border-radius 6px
 | 
			
		||||
	color #fff
 | 
			
		||||
 | 
			
		||||
	a
 | 
			
		||||
		margin-left 8px
 | 
			
		||||
		color #fff
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
<mk-ui>
 | 
			
		||||
	<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
 | 
			
		||||
		<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
 | 
			
		||||
		<div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
 | 
			
		||||
		<div class="is-remote" v-if="user.host">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
 | 
			
		||||
		<main>
 | 
			
		||||
			<div class="main">
 | 
			
		||||
				<x-header :user="user"/>
 | 
			
		||||
@@ -12,14 +12,15 @@
 | 
			
		||||
			<div class="side">
 | 
			
		||||
				<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
 | 
			
		||||
				<x-profile :user="user"/>
 | 
			
		||||
				<x-twitter :user="user" v-if="user.host === null && user.twitter"/>
 | 
			
		||||
				<x-twitter :user="user" v-if="!user.host && user.twitter"/>
 | 
			
		||||
				<x-github :user="user" v-if="!user.host && user.github"/>
 | 
			
		||||
				<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
 | 
			
		||||
				<mk-activity :user="user"/>
 | 
			
		||||
				<x-photos :user="user"/>
 | 
			
		||||
				<x-friends :user="user"/>
 | 
			
		||||
				<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
 | 
			
		||||
				<div class="nav"><mk-nav/></div>
 | 
			
		||||
				<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 | 
			
		||||
				<p v-if="!user.host">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</main>
 | 
			
		||||
	</div>
 | 
			
		||||
@@ -37,6 +38,7 @@ import XPhotos from './user.photos.vue';
 | 
			
		||||
import XFollowersYouKnow from './user.followers-you-know.vue';
 | 
			
		||||
import XFriends from './user.friends.vue';
 | 
			
		||||
import XTwitter from './user.twitter.vue';
 | 
			
		||||
import XGithub from './user.github.vue'; // ?MEM: Don't fix the intentional typo. (XGitHub -> `<x-git-hub>`)
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -46,7 +48,8 @@ export default Vue.extend({
 | 
			
		||||
		XPhotos,
 | 
			
		||||
		XFollowersYouKnow,
 | 
			
		||||
		XFriends,
 | 
			
		||||
		XTwitter
 | 
			
		||||
		XTwitter,
 | 
			
		||||
		XGithub // ?MEM: Don't fix the intentional typo. (see L41)
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@
 | 
			
		||||
				<ul>
 | 
			
		||||
					<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 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 v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><a href="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</a></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>
 | 
			
		||||
			</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -125,6 +125,19 @@
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
 | 
			
		||||
			<ui-card>
 | 
			
		||||
				<div slot="title">%fa:B github% %i18n:@github%</div>
 | 
			
		||||
 | 
			
		||||
				<section>
 | 
			
		||||
					<p class="account" v-if="$store.state.i.github"><a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 | 
			
		||||
					<p>
 | 
			
		||||
						<a :href="`${apiUrl}/connect/github`" target="_blank">{{ $store.state.i.github ? '%i18n:@github-reconnect%' : '%i18n:@github-connect%' }}</a>
 | 
			
		||||
						<span v-if="$store.state.i.github"> or </span>
 | 
			
		||||
						<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github">%i18n:@github-disconnect%</a>
 | 
			
		||||
					</p>
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
 | 
			
		||||
			<mk-api-settings />
 | 
			
		||||
 | 
			
		||||
			<ui-card>
 | 
			
		||||
 
 | 
			
		||||
@@ -51,8 +51,6 @@ export default function load() {
 | 
			
		||||
 | 
			
		||||
	if (config.maxNoteTextLength == null) config.maxNoteTextLength = 1000;
 | 
			
		||||
 | 
			
		||||
	if (config.name == null) config.name = 'Misskey';
 | 
			
		||||
 | 
			
		||||
	return Object.assign(config, mixin);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ export type Source = {
 | 
			
		||||
		repository_url?: string;
 | 
			
		||||
		feedback_url?: string;
 | 
			
		||||
	};
 | 
			
		||||
	name?: string;
 | 
			
		||||
	description?: string;
 | 
			
		||||
	languages?: string[];
 | 
			
		||||
	welcome_bg_url?: string;
 | 
			
		||||
	url: string;
 | 
			
		||||
@@ -74,6 +72,10 @@ export type Source = {
 | 
			
		||||
		consumer_key: string;
 | 
			
		||||
		consumer_secret: string;
 | 
			
		||||
	};
 | 
			
		||||
	github?: {
 | 
			
		||||
		client_id: string;
 | 
			
		||||
		client_secret: string;
 | 
			
		||||
	};
 | 
			
		||||
	github_bot?: {
 | 
			
		||||
		hook_secret: string;
 | 
			
		||||
		username: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ export type TextElementEmoji = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function(text: string) {
 | 
			
		||||
	const match = text.match(/^:([a-zA-Z0-9+-_]+?):/);
 | 
			
		||||
	const match = text.match(/^:([a-zA-Z0-9+_-]+):/);
 | 
			
		||||
	if (!match) return null;
 | 
			
		||||
	const emoji = match[0];
 | 
			
		||||
	return {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,37 @@
 | 
			
		||||
import db from '../db/mongodb';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
 | 
			
		||||
const Meta = db.get<IMeta>('meta');
 | 
			
		||||
export default Meta;
 | 
			
		||||
 | 
			
		||||
// 後方互換性のため。
 | 
			
		||||
// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
 | 
			
		||||
if ((config as any).name) {
 | 
			
		||||
	Meta.findOne({}).then(m => {
 | 
			
		||||
		if (m != null && m.name == null) {
 | 
			
		||||
			Meta.update({}, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					name: (config as any).name
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
if ((config as any).description) {
 | 
			
		||||
	Meta.findOne({}).then(m => {
 | 
			
		||||
		if (m != null && m.description == null) {
 | 
			
		||||
			Meta.update({}, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					description: (config as any).description
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IMeta = {
 | 
			
		||||
	name?: string;
 | 
			
		||||
	description?: string;
 | 
			
		||||
	broadcasts?: any[];
 | 
			
		||||
	stats?: {
 | 
			
		||||
		notesCount: number;
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,11 @@ export interface ILocalUser extends IUserBase {
 | 
			
		||||
		userId: string;
 | 
			
		||||
		screenName: string;
 | 
			
		||||
	};
 | 
			
		||||
	github: {
 | 
			
		||||
		accessToken: string;
 | 
			
		||||
		id: string;
 | 
			
		||||
		login: string;
 | 
			
		||||
	};
 | 
			
		||||
	line: {
 | 
			
		||||
		userId: string;
 | 
			
		||||
	};
 | 
			
		||||
@@ -280,6 +285,9 @@ export const pack = (
 | 
			
		||||
			delete _user.twitter.accessToken;
 | 
			
		||||
			delete _user.twitter.accessTokenSecret;
 | 
			
		||||
		}
 | 
			
		||||
		if (_user.github) {
 | 
			
		||||
			delete _user.github.accessToken;
 | 
			
		||||
		}
 | 
			
		||||
		delete _user.line;
 | 
			
		||||
 | 
			
		||||
		// Visible via only the official client
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,8 @@ export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, Error] {
 | 
			
		||||
	if (defs.params == null) return [params, null];
 | 
			
		||||
 | 
			
		||||
	const x: any = {};
 | 
			
		||||
	let err: Error = null;
 | 
			
		||||
	Object.entries(defs.params).some(([k, def]) => {
 | 
			
		||||
@@ -38,7 +40,7 @@ function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, E
 | 
			
		||||
			(err as any).param = k;
 | 
			
		||||
			return true;
 | 
			
		||||
		} else {
 | 
			
		||||
			if (v === undefined && def.default) {
 | 
			
		||||
			if (v === undefined && def.hasOwnProperty('default')) {
 | 
			
		||||
				x[k] = def.default;
 | 
			
		||||
			} else {
 | 
			
		||||
				x[k] = v;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ export const meta = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default define(meta, (ps) => new Promise(async (res, rej) => {
 | 
			
		||||
	await Emoji.insert({
 | 
			
		||||
	const emoji = await Emoji.insert({
 | 
			
		||||
		updatedAt: new Date(),
 | 
			
		||||
		name: ps.name,
 | 
			
		||||
		host: null,
 | 
			
		||||
@@ -35,5 +35,7 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
 | 
			
		||||
		url: ps.url
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	res();
 | 
			
		||||
	res({
 | 
			
		||||
		id: emoji._id
 | 
			
		||||
	});
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,20 @@ export const meta = {
 | 
			
		||||
				'ja-JP': 'インスタンスのバナー画像URL'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		name: {
 | 
			
		||||
			validator: $.str.optional.nullable,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': 'インスタンス名'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		description: {
 | 
			
		||||
			validator: $.str.optional.nullable,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': 'インスタンスの紹介文'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -71,6 +85,14 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
 | 
			
		||||
		set.bannerUrl = ps.bannerUrl;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ps.name !== undefined) {
 | 
			
		||||
		set.name = ps.name;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ps.description !== undefined) {
 | 
			
		||||
		set.description = ps.description;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await Meta.update({}, {
 | 
			
		||||
		$set: set
 | 
			
		||||
	}, { upsert: true });
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ export const meta = {
 | 
			
		||||
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
 | 
			
		||||
	const folders = await DriveFolder
 | 
			
		||||
		.find({
 | 
			
		||||
			name: name,
 | 
			
		||||
			name: ps.name,
 | 
			
		||||
			userId: user._id,
 | 
			
		||||
			parentId: ps.parentId
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		||||
		version: pkg.version,
 | 
			
		||||
		clientVersion: client.version,
 | 
			
		||||
 | 
			
		||||
		name: config.name || 'Misskey',
 | 
			
		||||
		description: config.description,
 | 
			
		||||
		name: met.name || 'Misskey',
 | 
			
		||||
		description: met.description,
 | 
			
		||||
 | 
			
		||||
		secure: config.https != null,
 | 
			
		||||
		machine: os.hostname(),
 | 
			
		||||
@@ -73,6 +73,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		||||
			recaptcha: config.recaptcha ? true : false,
 | 
			
		||||
			objectStorage: config.drive && config.drive.storage === 'minio',
 | 
			
		||||
			twitter: config.twitter ? true : false,
 | 
			
		||||
			github: config.github ? true : false,
 | 
			
		||||
			serviceWorker: config.sw ? true : false,
 | 
			
		||||
			userRecommendation: config.user_recommendation ? config.user_recommendation : {}
 | 
			
		||||
		} : undefined
 | 
			
		||||
 
 | 
			
		||||
@@ -37,8 +37,8 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
 | 
			
		||||
 | 
			
		||||
	ctx.body = {
 | 
			
		||||
		uri: config.hostname,
 | 
			
		||||
		title: config.name || 'Misskey',
 | 
			
		||||
		description: config.description || '',
 | 
			
		||||
		title: meta.name || 'Misskey',
 | 
			
		||||
		description: meta.description || '',
 | 
			
		||||
		email: config.maintainer.email || config.maintainer.url.startsWith('mailto:') ? config.maintainer.url.slice(7) : '',
 | 
			
		||||
		version: `0.0.0:compatible:misskey:${pkg.version}`, // TODO: How to tell about that this is an api for compatibility?
 | 
			
		||||
		thumbnail: meta.bannerUrl,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,16 @@
 | 
			
		||||
import * as EventEmitter from 'events';
 | 
			
		||||
import * as Koa from 'koa';
 | 
			
		||||
import * as Router from 'koa-router';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
const crypto = require('crypto');
 | 
			
		||||
 | 
			
		||||
import User, { IUser } from '../../../models/user';
 | 
			
		||||
import { OAuth2 } from 'oauth';
 | 
			
		||||
import User, { IUser, pack, ILocalUser } from '../../../models/user';
 | 
			
		||||
import createNote from '../../../services/note/create';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
import { publishMainStream } from '../../../stream';
 | 
			
		||||
import redis from '../../../db/redis';
 | 
			
		||||
import uuid = require('uuid');
 | 
			
		||||
import signin from '../common/signin';
 | 
			
		||||
const crypto = require('crypto');
 | 
			
		||||
 | 
			
		||||
const handler = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
@@ -28,10 +33,264 @@ const post = async (text: string, home = true) => {
 | 
			
		||||
	createNote(bot, { text, visibility: home ? 'home' : 'public' });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getUserToken(ctx: Koa.Context) {
 | 
			
		||||
	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function compareOrigin(ctx: Koa.Context) {
 | 
			
		||||
	function normalizeUrl(url: string) {
 | 
			
		||||
		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const referer = ctx.headers['referer'];
 | 
			
		||||
 | 
			
		||||
	return (normalizeUrl(referer) == normalizeUrl(config.url));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Init router
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
if (config.github_bot != null) {
 | 
			
		||||
router.get('/disconnect/github', async ctx => {
 | 
			
		||||
	if (!compareOrigin(ctx)) {
 | 
			
		||||
		ctx.throw(400, 'invalid origin');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const userToken = getUserToken(ctx);
 | 
			
		||||
	if (!userToken) {
 | 
			
		||||
		ctx.throw(400, 'signin required');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const user = await User.findOneAndUpdate({
 | 
			
		||||
		host: null,
 | 
			
		||||
		'token': userToken
 | 
			
		||||
	}, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			'github': null
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	ctx.body = `GitHubの連携を解除しました :v:`;
 | 
			
		||||
 | 
			
		||||
	// Publish i updated event
 | 
			
		||||
	publishMainStream(user._id, 'meUpdated', await pack(user, user, {
 | 
			
		||||
		detail: true,
 | 
			
		||||
		includeSecrets: true
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (!config.github || !redis) {
 | 
			
		||||
	router.get('/connect/github', ctx => {
 | 
			
		||||
		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	router.get('/signin/github', ctx => {
 | 
			
		||||
		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	const oauth2 = new OAuth2(
 | 
			
		||||
		config.github.client_id,
 | 
			
		||||
		config.github.client_secret,
 | 
			
		||||
		'https://github.com/',
 | 
			
		||||
		'login/oauth/authorize',
 | 
			
		||||
		'login/oauth/access_token');
 | 
			
		||||
 | 
			
		||||
	router.get('/connect/github', async ctx => {
 | 
			
		||||
		if (!compareOrigin(ctx)) {
 | 
			
		||||
			ctx.throw(400, 'invalid origin');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const userToken = getUserToken(ctx);
 | 
			
		||||
		if (!userToken) {
 | 
			
		||||
			ctx.throw(400, 'signin required');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const params = {
 | 
			
		||||
			redirect_uri: `${config.url}:8089/api/gh/cb`,
 | 
			
		||||
			scope: ['read:user'],
 | 
			
		||||
			state: uuid()
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		redis.set(userToken, JSON.stringify(params));
 | 
			
		||||
		ctx.redirect(oauth2.getAuthorizeUrl(params));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	router.get('/signin/github', async ctx => {
 | 
			
		||||
		const sessid = uuid();
 | 
			
		||||
 | 
			
		||||
		const params = {
 | 
			
		||||
			redirect_uri: `${config.url}:8089/api/gh/cb`,
 | 
			
		||||
			scope: ['read:user'],
 | 
			
		||||
			state: uuid()
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const expires = 1000 * 60 * 60; // 1h
 | 
			
		||||
		ctx.cookies.set('signin_with_github_session_id', sessid, {
 | 
			
		||||
			path: '/',
 | 
			
		||||
			domain: config.host,
 | 
			
		||||
			secure: config.url.startsWith('https'),
 | 
			
		||||
			httpOnly: true,
 | 
			
		||||
			expires: new Date(Date.now() + expires),
 | 
			
		||||
			maxAge: expires
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		redis.set(sessid, JSON.stringify(params));
 | 
			
		||||
		ctx.redirect(oauth2.getAuthorizeUrl(params));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	router.get('/gh/cb', async ctx => {
 | 
			
		||||
		const userToken = getUserToken(ctx);
 | 
			
		||||
 | 
			
		||||
		if (!userToken) {
 | 
			
		||||
			const sessid = ctx.cookies.get('signin_with_github_session_id');
 | 
			
		||||
 | 
			
		||||
			if (!sessid) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const code = ctx.query.code;
 | 
			
		||||
 | 
			
		||||
			if (!code) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
			
		||||
				redis.get(sessid, async (_, state) => {
 | 
			
		||||
					res(JSON.parse(state));
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (ctx.query.state !== state) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { accessToken } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				oauth2.getOAuthAccessToken(
 | 
			
		||||
					code,
 | 
			
		||||
					{ redirect_uri },
 | 
			
		||||
					(err, accessToken, refresh, result) => {
 | 
			
		||||
						if (err)
 | 
			
		||||
							rej(err);
 | 
			
		||||
						else if (result.error)
 | 
			
		||||
							rej(result.error);
 | 
			
		||||
						else
 | 
			
		||||
							res({ accessToken });
 | 
			
		||||
					}));
 | 
			
		||||
 | 
			
		||||
			const { login, id } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				request({
 | 
			
		||||
					url: 'https://api.github.com/user',
 | 
			
		||||
					headers: {
 | 
			
		||||
						'Accept': 'application/vnd.github.v3+json',
 | 
			
		||||
						'Authorization': `bearer ${accessToken}`,
 | 
			
		||||
						'User-Agent': config.user_agent
 | 
			
		||||
					}
 | 
			
		||||
				}, (err, response, body) => {
 | 
			
		||||
					if (err)
 | 
			
		||||
						rej(err);
 | 
			
		||||
					else
 | 
			
		||||
						res(JSON.parse(body));
 | 
			
		||||
				}));
 | 
			
		||||
 | 
			
		||||
			if (!login || !id) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const user = await User.findOne({
 | 
			
		||||
				host: null,
 | 
			
		||||
				'github.id': id
 | 
			
		||||
			}) as ILocalUser;
 | 
			
		||||
 | 
			
		||||
			if (!user) {
 | 
			
		||||
				ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			signin(ctx, user, true);
 | 
			
		||||
		} else {
 | 
			
		||||
			const code = ctx.query.code;
 | 
			
		||||
 | 
			
		||||
			if (!code) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
			
		||||
				redis.get(userToken, async (_, state) => {
 | 
			
		||||
					res(JSON.parse(state));
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (ctx.query.state !== state) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { accessToken } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				oauth2.getOAuthAccessToken(
 | 
			
		||||
					code,
 | 
			
		||||
					{ redirect_uri },
 | 
			
		||||
					(err, accessToken, refresh, result) => {
 | 
			
		||||
						if (err)
 | 
			
		||||
							rej(err);
 | 
			
		||||
						else if (result.error)
 | 
			
		||||
							rej(result.error);
 | 
			
		||||
						else
 | 
			
		||||
							res({ accessToken });
 | 
			
		||||
					}));
 | 
			
		||||
 | 
			
		||||
			const { login, id } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				request({
 | 
			
		||||
					url: 'https://api.github.com/user',
 | 
			
		||||
					headers: {
 | 
			
		||||
						'Accept': 'application/vnd.github.v3+json',
 | 
			
		||||
						'Authorization': `bearer ${accessToken}`,
 | 
			
		||||
						'User-Agent': config.user_agent
 | 
			
		||||
					}
 | 
			
		||||
				}, (err, response, body) => {
 | 
			
		||||
					if (err)
 | 
			
		||||
						rej(err);
 | 
			
		||||
					else
 | 
			
		||||
						res(JSON.parse(body));
 | 
			
		||||
				}));
 | 
			
		||||
 | 
			
		||||
			if (!login || !id) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const user = await User.findOneAndUpdate({
 | 
			
		||||
				host: null,
 | 
			
		||||
				token: userToken
 | 
			
		||||
			}, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					github: {
 | 
			
		||||
						accessToken,
 | 
			
		||||
						id,
 | 
			
		||||
						login
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
 | 
			
		||||
 | 
			
		||||
			// Publish i updated event
 | 
			
		||||
			publishMainStream(user._id, 'meUpdated', await pack(user, user, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
				includeSecrets: true
 | 
			
		||||
			}));
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (config.github_bot) {
 | 
			
		||||
	const secret = config.github_bot.hook_secret;
 | 
			
		||||
 | 
			
		||||
	router.post('/hooks/github', ctx => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user