Compare commits
	
		
			56 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a29d7a0475 | ||
|   | d5408c429b | ||
|   | 501b07c383 | ||
|   | 9dd21a19ff | ||
|   | a8d05cba5a | ||
|   | f5ddfb29f2 | ||
|   | ba228a6b10 | ||
|   | cb6f390fb6 | ||
|   | 5675ecead9 | ||
|   | 001bb7bbcd | ||
|   | 1585bb12cf | ||
|   | 26b47c18fd | ||
|   | 665fa7f2aa | ||
|   | 0068dc30d3 | ||
|   | 8f39655fef | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b1a4fc03bc | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 05d20f1044 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 66a90b3fb1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 826d9d9fdf | ||
|   | 4a9a61f108 | ||
|   | b72d15b56c | ||
|   | 8c68992594 | ||
|   | c052028fc3 | ||
|   | c46fbcf345 | ||
|   | 06b66f0209 | ||
|   | 2de48110bb | ||
|   | 87d4452d19 | ||
|   | 328fc64ca9 | ||
|   | a6f8327aa2 | ||
|   | d5ab6b41c9 | ||
|   | ffdd0b7de7 | ||
|   | 1808eb6eee | ||
|   | 438563b505 | ||
|   | 92dfcdad57 | ||
|   | c178cfabfa | ||
|   | 260e4c955d | ||
|   | 0c46f5ce70 | ||
|   | 6d67cd07a0 | ||
|   | fb8af53751 | ||
|   | 37999f4af7 | ||
|   | 3b6ab327c1 | ||
|   | d3ff3a7d54 | ||
|   | cf36106520 | ||
|   | 1642fbec31 | ||
|   | b195fd8145 | ||
|   | 5f59b980a7 | ||
|   | 2a5c19cd01 | ||
|   | 42e007ddb7 | ||
|   | 756dc397d9 | ||
|   | 8f714b5b12 | ||
|   | 06bb2a1c7c | ||
|   | ac50bb9225 | ||
|   | 8fd95de25b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0e14b2eba4 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 08413a7550 | ||
|   | 5e0f2a5b06 | 
| @@ -43,8 +43,8 @@ jobs: | |||||||
|       - run: |       - run: | ||||||
|           name: Configure |           name: Configure | ||||||
|           command: | |           command: | | ||||||
|             cp .ci/default.yml .config |             cp .circleci/misskey/default.yml .config | ||||||
|             cp .ci/test.yml .config |             cp .circleci/misskey/test.yml .config | ||||||
|       - run: |       - run: | ||||||
|           name: Build |           name: Build | ||||||
|           command: | |           command: | | ||||||
| @@ -102,7 +102,7 @@ jobs: | |||||||
|       - run: |       - run: | ||||||
|           name: Build |           name: Build | ||||||
|           command: | |           command: | | ||||||
|             docker build . | tee docker.log |             docker build -t misskey/misskey . | ||||||
|       - when: |       - when: | ||||||
|           condition: <<parameters.with_deploy>> |           condition: <<parameters.with_deploy>> | ||||||
|           steps: |           steps: | ||||||
| @@ -111,8 +111,6 @@ jobs: | |||||||
|                 command: | |                 command: | | ||||||
|                   if [ "$DOCKERHUB_USERNAME$DOCKERHUB_PASSWORD" ] |                   if [ "$DOCKERHUB_USERNAME$DOCKERHUB_PASSWORD" ] | ||||||
|                    then |                    then | ||||||
|                     tail -n 1 docker.log | read __Successfully __built tag |  | ||||||
|                     docker tag $tag misskey/misskey |  | ||||||
|                     docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD |                     docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD | ||||||
|                     docker push misskey/misskey |                     docker push misskey/misskey | ||||||
|                    else |                    else | ||||||
|   | |||||||
| @@ -1,6 +1,3 @@ | |||||||
| maintainer: |  | ||||||
|   name: syuilo |  | ||||||
|   url: 'https://syuilo.com' |  | ||||||
| url: 'http://misskey.local' | url: 'http://misskey.local' | ||||||
| port: 80 | port: 80 | ||||||
| mongodb: | mongodb: | ||||||
| @@ -1,6 +1,3 @@ | |||||||
| maintainer: |  | ||||||
|   name: syuilo |  | ||||||
|   url: 'https://syuilo.com' |  | ||||||
| url: 'http://misskey.local' | url: 'http://misskey.local' | ||||||
| port: 80 | port: 80 | ||||||
| mongodb: | mongodb: | ||||||
| @@ -1,10 +1,3 @@ | |||||||
| maintainer: |  | ||||||
|   name: example-maitainer-name # Your name |  | ||||||
|   url: http://example.com/ # Your contact (http or mailto) |  | ||||||
|   repository_url: https://github.com/syuilo/misskey # Repository URL |  | ||||||
|   feedback_url: https://github.com/syuilo/misskey/issues # Feedback URL (e.g. github issue) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Final accessible URL seen by a user. | # Final accessible URL seen by a user. | ||||||
| url: https://example.tld/ | url: https://example.tld/ | ||||||
|  |  | ||||||
| @@ -57,21 +50,6 @@ mongodb: | |||||||
|   user: example-misskey-user |   user: example-misskey-user | ||||||
|   pass: example-misskey-pass |   pass: example-misskey-pass | ||||||
|  |  | ||||||
| # Drive capacity of a local user (MB) |  | ||||||
| localDriveCapacityMb: 256 |  | ||||||
|  |  | ||||||
| # Drive capacity of a remote user (MB) |  | ||||||
| remoteDriveCapacityMb: 8 |  | ||||||
|  |  | ||||||
| # If enabled: |  | ||||||
| #  Server will not cache remote files (Using direct link instead). |  | ||||||
| #  You can save your storage. |  | ||||||
| # |  | ||||||
| #  NOTE: |  | ||||||
| #  * Users cannot see remote images when they turn off "Show media from a remote server" setting. |  | ||||||
| #  * Since thumbnails are not provided, traffic increases. |  | ||||||
| preventCacheRemoteFiles: false |  | ||||||
|  |  | ||||||
| drive: | drive: | ||||||
|   storage: 'db' |   storage: 'db' | ||||||
|  |  | ||||||
| @@ -110,6 +88,10 @@ drive: | |||||||
|   #   accessKey: XXX |   #   accessKey: XXX | ||||||
|   #   secretKey: YYY |   #   secretKey: YYY | ||||||
|  |  | ||||||
|  | # If enabled: | ||||||
|  | #  The first account created is automatically marked as Admin. | ||||||
|  | autoAdmin: true | ||||||
|  |  | ||||||
| # | # | ||||||
| # Below settings are optional | # Below settings are optional | ||||||
| # | # | ||||||
| @@ -126,11 +108,6 @@ drive: | |||||||
| #  port: 9200 | #  port: 9200 | ||||||
| #  pass: null | #  pass: null | ||||||
|  |  | ||||||
| # reCAPTCHA |  | ||||||
| #recaptcha: |  | ||||||
| #  site_key: example-site-key |  | ||||||
| #  secret_key: example-secret-key |  | ||||||
|  |  | ||||||
| # ServiceWorker | # ServiceWorker | ||||||
| #sw: | #sw: | ||||||
| #  # Public key of VAPID | #  # Public key of VAPID | ||||||
| @@ -139,23 +116,6 @@ drive: | |||||||
| #  # Private key of VAPID | #  # Private key of VAPID | ||||||
| #  private_key: example-sw-private-key | #  private_key: example-sw-private-key | ||||||
|  |  | ||||||
| # Twitter integration |  | ||||||
| # You need to set the oauth callback url as : https://<your-misskey-instance>/api/tw/cb |  | ||||||
| #twitter: |  | ||||||
| #  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. |  | ||||||
| #ghost: user-id-of-your-ghost-account |  | ||||||
|  |  | ||||||
| # Clustering | # Clustering | ||||||
| #clusterLimit: 1 | #clusterLimit: 1 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -47,11 +47,6 @@ In root : | |||||||
| 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) | 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) | ||||||
| 5. `npm install` Install misskey dependencies. | 5. `npm install` Install misskey dependencies. | ||||||
|  |  | ||||||
| *(optional)* reCAPTCHA tokens |  | ||||||
| ---------------------------------------------------------------- |  | ||||||
| If you want to enable reCAPTCHA, you need to generate reCAPTCHA tokens: |  | ||||||
| Please visit https://www.google.com/recaptcha/intro/ and generate keys. |  | ||||||
|  |  | ||||||
| *(optional)* Generating VAPID keys | *(optional)* Generating VAPID keys | ||||||
| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ||||||
| If you want to enable ServiceWorker, you need to generate VAPID keys: | If you want to enable ServiceWorker, you need to generate VAPID keys: | ||||||
| @@ -62,13 +57,6 @@ npm install web-push -g | |||||||
| web-push generate-vapid-keys | web-push generate-vapid-keys | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| *(optional)* Create a twitter application |  | ||||||
| ---------------------------------------------------------------- |  | ||||||
| If you want to enable the twitter integration, you need to create a twitter app at [https://developer.twitter.com/en/apply/user](https://developer.twitter.com/en/apply/user). |  | ||||||
|  |  | ||||||
| In the app you need to set the oauth callback url as : https://misskey-instance/api/tw/cb |  | ||||||
|  |  | ||||||
|  |  | ||||||
| *5.* Make configuration file | *5.* Make configuration file | ||||||
| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ||||||
| 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. | 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. | ||||||
|   | |||||||
| @@ -53,11 +53,6 @@ adduser --disabled-password --disabled-login misskey | |||||||
| 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 | 4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 | ||||||
| 5. `npm install` Misskeyの依存パッケージをインストール | 5. `npm install` Misskeyの依存パッケージをインストール | ||||||
|  |  | ||||||
| *(オプション)* reCAPTCHAトークン |  | ||||||
| ---------------------------------------------------------------- |  | ||||||
| reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。 |  | ||||||
| https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。 |  | ||||||
|  |  | ||||||
| *(オプション)* VAPIDキーペアの生成 | *(オプション)* VAPIDキーペアの生成 | ||||||
| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ||||||
| ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります: | ServiceWorkerを有効にする場合、VAPIDキーペアを生成する必要があります: | ||||||
|   | |||||||
| @@ -947,6 +947,7 @@ common/views/components/api-settings.vue: | |||||||
|     title: 'APIコンソール' |     title: 'APIコンソール' | ||||||
|     endpoint: 'エンドポイント' |     endpoint: 'エンドポイント' | ||||||
|     parameter: 'パラメータ' |     parameter: 'パラメータ' | ||||||
|  |     credential-info: "「i」パラメータは自動で付与されます。" | ||||||
|     send: '送信' |     send: '送信' | ||||||
|     sending: '応答待ち' |     sending: '応答待ち' | ||||||
|     response: '結果' |     response: '結果' | ||||||
| @@ -1078,6 +1079,37 @@ admin/views/instance.vue: | |||||||
|   instance-name: "インスタンス名" |   instance-name: "インスタンス名" | ||||||
|   instance-description: "インスタンスの紹介" |   instance-description: "インスタンスの紹介" | ||||||
|   banner-url: "バナー画像URL" |   banner-url: "バナー画像URL" | ||||||
|  |   languages: "インスタンスの対象言語" | ||||||
|  |   languages-desc: "スペースで区切って複数設定できます。" | ||||||
|  |   maintainer-config: "管理者情報" | ||||||
|  |   maintainer-name: "管理者名" | ||||||
|  |   maintainer-email: "管理者の連絡先" | ||||||
|  |   drive-config: "ドライブの設定" | ||||||
|  |   cache-remote-files: "リモートのファイルをキャッシュする" | ||||||
|  |   cache-remote-files-desc: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。そのためサーバーのストレージを節約できますが、プライバシー設定で直リンクを無効にしているユーザーにはファイルが見えなくなったり、サムネイルが生成されないので通信量が増加します。通常はこの設定をオンにしておくことをおすすめします。" | ||||||
|  |   local-drive-capacity-mb: "ローカルユーザーひとりあたりのドライブ容量" | ||||||
|  |   remote-drive-capacity-mb: "リモートユーザーひとりあたりのドライブ容量" | ||||||
|  |   mb: "メガバイト単位" | ||||||
|  |   recaptcha-config: "reCAPTCHAの設定" | ||||||
|  |   recaptcha-info: "reCAPTCHAを有効にする場合、reCAPTCHAトークンを取得する必要があります。https://www.google.com/recaptcha/intro/ にアクセスしてトークンを取得してください。" | ||||||
|  |   enable-recaptcha: "reCAPTCHAを有効にする" | ||||||
|  |   recaptcha-site-key: "reCAPTCHA site key" | ||||||
|  |   recaptcha-secret-key: "reCAPTCHA secret key" | ||||||
|  |   twitter-integration-config: "Twitter連携の設定" | ||||||
|  |   twitter-integration-info: "コールバックURLは /api/tw/cb に設定します。" | ||||||
|  |   enable-twitter-integration: "Twitter連携を有効にする" | ||||||
|  |   twitter-integration-consumer-key: "Consumer key" | ||||||
|  |   twitter-integration-consumer-secret: "Consumer secret" | ||||||
|  |   github-integration-config: "GitHub連携の設定" | ||||||
|  |   github-integration-info: "コールバックURLは /api/gh/cb に設定します。" | ||||||
|  |   enable-github-integration: "GitHub連携を有効にする" | ||||||
|  |   github-integration-client-id: "Client ID" | ||||||
|  |   github-integration-client-secret: "Client secret" | ||||||
|  |   proxy-account-config: "プロキシアカウントの設定" | ||||||
|  |   proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" | ||||||
|  |   proxy-account-username: "プロキシアカウントのユーザー名" | ||||||
|  |   proxy-account-username-desc: "プロキシとして使用するアカウントのユーザー名を指定してください。" | ||||||
|  |   proxy-account-warn: "アカウントは自動で作られないため、そのユーザー名のアカウントを予め作成しておく必要があります。" | ||||||
|   max-note-text-length: "投稿の最大文字数" |   max-note-text-length: "投稿の最大文字数" | ||||||
|   disable-registration: "ユーザー登録の受付を停止する" |   disable-registration: "ユーザー登録の受付を停止する" | ||||||
|   disable-local-timeline: "ローカルタイムラインを無効にする" |   disable-local-timeline: "ローカルタイムラインを無効にする" | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <i@syuilo.com>", | 	"author": "syuilo <i@syuilo.com>", | ||||||
| 	"version": "10.40.1", | 	"version": "10.44.2", | ||||||
| 	"clientVersion": "1.0.11579", | 	"clientVersion": "1.0.11630", | ||||||
| 	"codename": "nighthike", | 	"codename": "nighthike", | ||||||
| 	"main": "./built/index.js", | 	"main": "./built/index.js", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| @@ -53,7 +53,7 @@ | |||||||
| 		"@types/koa-logger": "3.1.1", | 		"@types/koa-logger": "3.1.1", | ||||||
| 		"@types/koa-mount": "3.0.1", | 		"@types/koa-mount": "3.0.1", | ||||||
| 		"@types/koa-multer": "1.0.0", | 		"@types/koa-multer": "1.0.0", | ||||||
| 		"@types/koa-router": "7.0.32", | 		"@types/koa-router": "7.0.33", | ||||||
| 		"@types/koa-send": "4.1.1", | 		"@types/koa-send": "4.1.1", | ||||||
| 		"@types/koa-views": "2.0.3", | 		"@types/koa-views": "2.0.3", | ||||||
| 		"@types/koa__cors": "2.2.3", | 		"@types/koa__cors": "2.2.3", | ||||||
| @@ -69,7 +69,7 @@ | |||||||
| 		"@types/qrcode": "1.3.0", | 		"@types/qrcode": "1.3.0", | ||||||
| 		"@types/ratelimiter": "2.1.28", | 		"@types/ratelimiter": "2.1.28", | ||||||
| 		"@types/redis": "2.8.7", | 		"@types/redis": "2.8.7", | ||||||
| 		"@types/request": "2.48.0", | 		"@types/request": "2.48.1", | ||||||
| 		"@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", | ||||||
| @@ -113,7 +113,7 @@ | |||||||
| 		"eslint-plugin-vue": "4.7.1", | 		"eslint-plugin-vue": "4.7.1", | ||||||
| 		"eventemitter3": "3.1.0", | 		"eventemitter3": "3.1.0", | ||||||
| 		"file-loader": "2.0.0", | 		"file-loader": "2.0.0", | ||||||
| 		"file-type": "10.3.0", | 		"file-type": "10.4.0", | ||||||
| 		"fuckadblock": "3.2.1", | 		"fuckadblock": "3.2.1", | ||||||
| 		"gulp": "3.9.1", | 		"gulp": "3.9.1", | ||||||
| 		"gulp-cssnano": "2.1.3", | 		"gulp-cssnano": "2.1.3", | ||||||
| @@ -174,7 +174,7 @@ | |||||||
| 		"promise-sequential": "1.1.1", | 		"promise-sequential": "1.1.1", | ||||||
| 		"pug": "2.0.3", | 		"pug": "2.0.3", | ||||||
| 		"punycode": "2.1.1", | 		"punycode": "2.1.1", | ||||||
| 		"qrcode": "1.3.0", | 		"qrcode": "1.3.2", | ||||||
| 		"ratelimiter": "3.2.0", | 		"ratelimiter": "3.2.0", | ||||||
| 		"recaptcha-promise": "0.1.3", | 		"recaptcha-promise": "0.1.3", | ||||||
| 		"reconnecting-websocket": "4.1.10", | 		"reconnecting-websocket": "4.1.10", | ||||||
| @@ -205,7 +205,7 @@ | |||||||
| 		"ts-loader": "5.3.0", | 		"ts-loader": "5.3.0", | ||||||
| 		"ts-node": "7.0.1", | 		"ts-node": "7.0.1", | ||||||
| 		"tslint": "5.10.0", | 		"tslint": "5.10.0", | ||||||
| 		"typescript": "3.1.5", | 		"typescript": "3.1.6", | ||||||
| 		"typescript-eslint-parser": "20.1.1", | 		"typescript-eslint-parser": "20.1.1", | ||||||
| 		"uglify-es": "3.3.9", | 		"uglify-es": "3.3.9", | ||||||
| 		"url-loader": "1.1.2", | 		"url-loader": "1.1.2", | ||||||
| @@ -228,7 +228,7 @@ | |||||||
| 		"vuex-persistedstate": "2.5.4", | 		"vuex-persistedstate": "2.5.4", | ||||||
| 		"web-push": "3.3.3", | 		"web-push": "3.3.3", | ||||||
| 		"webfinger.js": "2.6.6", | 		"webfinger.js": "2.6.6", | ||||||
| 		"webpack": "4.23.1", | 		"webpack": "4.25.1", | ||||||
| 		"webpack-cli": "3.1.2", | 		"webpack-cli": "3.1.2", | ||||||
| 		"websocket": "1.0.28", | 		"websocket": "1.0.28", | ||||||
| 		"ws": "6.1.0", | 		"ws": "6.1.0", | ||||||
|   | |||||||
| @@ -274,12 +274,15 @@ export default Vue.extend({ | |||||||
| 			return { | 			return { | ||||||
| 				series: [{ | 				series: [{ | ||||||
| 					name: 'Combined', | 					name: 'Combined', | ||||||
|  | 					type: 'line', | ||||||
| 					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total)) | 					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total)) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local', | 					name: 'Local', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.notes.local.total) | 					data: this.format(this.stats.notes.local.total) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote', | 					name: 'Remote', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.notes.remote.total) | 					data: this.format(this.stats.notes.remote.total) | ||||||
| 				}] | 				}] | ||||||
| 			}; | 			}; | ||||||
| @@ -289,18 +292,21 @@ export default Vue.extend({ | |||||||
| 			return { | 			return { | ||||||
| 				series: [{ | 				series: [{ | ||||||
| 					name: 'Combined', | 					name: 'Combined', | ||||||
|  | 					type: 'line', | ||||||
| 					data: this.format(total | 					data: this.format(total | ||||||
| 						? sum(this.stats.users.local.total, this.stats.users.remote.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)) | 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) | ||||||
| 					) | 					) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local', | 					name: 'Local', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(total | 					data: this.format(total | ||||||
| 						? this.stats.users.local.total | 						? this.stats.users.local.total | ||||||
| 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec)) | 						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec)) | ||||||
| 					) | 					) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote', | 					name: 'Remote', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(total | 					data: this.format(total | ||||||
| 						? this.stats.users.remote.total | 						? this.stats.users.remote.total | ||||||
| 						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) | 						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec)) | ||||||
| @@ -314,6 +320,7 @@ export default Vue.extend({ | |||||||
| 				bytes: true, | 				bytes: true, | ||||||
| 				series: [{ | 				series: [{ | ||||||
| 					name: 'All', | 					name: 'All', | ||||||
|  | 					type: 'line', | ||||||
| 					data: this.format( | 					data: this.format( | ||||||
| 						sum( | 						sum( | ||||||
| 							this.stats.drive.local.incSize, | 							this.stats.drive.local.incSize, | ||||||
| @@ -324,15 +331,19 @@ export default Vue.extend({ | |||||||
| 					) | 					) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local +', | 					name: 'Local +', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.local.incSize) | 					data: this.format(this.stats.drive.local.incSize) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local -', | 					name: 'Local -', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(negate(this.stats.drive.local.decSize)) | 					data: this.format(negate(this.stats.drive.local.decSize)) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote +', | 					name: 'Remote +', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.remote.incSize) | 					data: this.format(this.stats.drive.remote.incSize) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote -', | 					name: 'Remote -', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(negate(this.stats.drive.remote.decSize)) | 					data: this.format(negate(this.stats.drive.remote.decSize)) | ||||||
| 				}] | 				}] | ||||||
| 			}; | 			}; | ||||||
| @@ -343,12 +354,15 @@ export default Vue.extend({ | |||||||
| 				bytes: true, | 				bytes: true, | ||||||
| 				series: [{ | 				series: [{ | ||||||
| 					name: 'Combined', | 					name: 'Combined', | ||||||
|  | 					type: 'line', | ||||||
| 					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize)) | 					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize)) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local', | 					name: 'Local', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.local.totalSize) | 					data: this.format(this.stats.drive.local.totalSize) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote', | 					name: 'Remote', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.remote.totalSize) | 					data: this.format(this.stats.drive.remote.totalSize) | ||||||
| 				}] | 				}] | ||||||
| 			}; | 			}; | ||||||
| @@ -358,6 +372,7 @@ export default Vue.extend({ | |||||||
| 			return { | 			return { | ||||||
| 				series: [{ | 				series: [{ | ||||||
| 					name: 'All', | 					name: 'All', | ||||||
|  | 					type: 'line', | ||||||
| 					data: this.format( | 					data: this.format( | ||||||
| 						sum( | 						sum( | ||||||
| 							this.stats.drive.local.incCount, | 							this.stats.drive.local.incCount, | ||||||
| @@ -368,15 +383,19 @@ export default Vue.extend({ | |||||||
| 					) | 					) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local +', | 					name: 'Local +', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.local.incCount) | 					data: this.format(this.stats.drive.local.incCount) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local -', | 					name: 'Local -', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(negate(this.stats.drive.local.decCount)) | 					data: this.format(negate(this.stats.drive.local.decCount)) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote +', | 					name: 'Remote +', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.remote.incCount) | 					data: this.format(this.stats.drive.remote.incCount) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote -', | 					name: 'Remote -', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(negate(this.stats.drive.remote.decCount)) | 					data: this.format(negate(this.stats.drive.remote.decCount)) | ||||||
| 				}] | 				}] | ||||||
| 			}; | 			}; | ||||||
| @@ -386,12 +405,15 @@ export default Vue.extend({ | |||||||
| 			return { | 			return { | ||||||
| 				series: [{ | 				series: [{ | ||||||
| 					name: 'Combined', | 					name: 'Combined', | ||||||
|  | 					type: 'line', | ||||||
| 					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount)) | 					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount)) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Local', | 					name: 'Local', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.local.totalCount) | 					data: this.format(this.stats.drive.local.totalCount) | ||||||
| 				}, { | 				}, { | ||||||
| 					name: 'Remote', | 					name: 'Remote', | ||||||
|  | 					type: 'area', | ||||||
| 					data: this.format(this.stats.drive.remote.totalCount) | 					data: this.format(this.stats.drive.remote.totalCount) | ||||||
| 				}] | 				}] | ||||||
| 			}; | 			}; | ||||||
|   | |||||||
| @@ -6,14 +6,15 @@ | |||||||
| 			<ui-horizon-group inputs> | 			<ui-horizon-group inputs> | ||||||
| 				<ui-input v-model="name"> | 				<ui-input v-model="name"> | ||||||
| 					<span>%i18n:@add-emoji.name%</span> | 					<span>%i18n:@add-emoji.name%</span> | ||||||
| 					<span slot="text">%i18n:@add-emoji.name-desc%</span> | 					<span slot="desc">%i18n:@add-emoji.name-desc%</span> | ||||||
| 				</ui-input> | 				</ui-input> | ||||||
| 				<ui-input v-model="aliases"> | 				<ui-input v-model="aliases"> | ||||||
| 					<span>%i18n:@add-emoji.aliases%</span> | 					<span>%i18n:@add-emoji.aliases%</span> | ||||||
| 					<span slot="text">%i18n:@add-emoji.aliases-desc%</span> | 					<span slot="desc">%i18n:@add-emoji.aliases-desc%</span> | ||||||
| 				</ui-input> | 				</ui-input> | ||||||
| 			</ui-horizon-group> | 			</ui-horizon-group> | ||||||
| 			<ui-input v-model="url"> | 			<ui-input v-model="url"> | ||||||
|  | 				<i slot="icon"><fa icon="link"/></i> | ||||||
| 				<span>%i18n:@add-emoji.url%</span> | 				<span>%i18n:@add-emoji.url%</span> | ||||||
| 			</ui-input> | 			</ui-input> | ||||||
| 			<ui-info>%i18n:@add-emoji.info%</ui-info> | 			<ui-info>%i18n:@add-emoji.info%</ui-info> | ||||||
| @@ -34,6 +35,7 @@ | |||||||
| 				</ui-input> | 				</ui-input> | ||||||
| 			</ui-horizon-group> | 			</ui-horizon-group> | ||||||
| 			<ui-input v-model="emoji.url"> | 			<ui-input v-model="emoji.url"> | ||||||
|  | 				<i slot="icon"><fa icon="link"/></i> | ||||||
| 				<span>%i18n:@add-emoji.url%</span> | 				<span>%i18n:@add-emoji.url%</span> | ||||||
| 			</ui-input> | 			</ui-input> | ||||||
| 			<ui-horizon-group> | 			<ui-horizon-group> | ||||||
|   | |||||||
| @@ -2,28 +2,77 @@ | |||||||
| <div class="axbwjelsbymowqjyywpirzhdlszoncqs"> | <div class="axbwjelsbymowqjyywpirzhdlszoncqs"> | ||||||
| 	<ui-card> | 	<ui-card> | ||||||
| 		<div slot="title"><fa icon="cog"/> %i18n:@instance%</div> | 		<div slot="title"><fa icon="cog"/> %i18n:@instance%</div> | ||||||
| 		<section class="fit-top"> | 		<section class="fit-top fit-bottom"> | ||||||
| 			<ui-input v-model="name">%i18n:@instance-name%</ui-input> | 			<ui-input v-model="name">%i18n:@instance-name%</ui-input> | ||||||
| 			<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea> | 			<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea> | ||||||
| 			<ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input> | 			<ui-input v-model="bannerUrl"><i slot="icon"><fa icon="link"/></i>%i18n:@banner-url%</ui-input> | ||||||
|  | 			<ui-input v-model="languages"><i slot="icon"><fa icon="language"/></i>%i18n:@languages%<span slot="desc">%i18n:@languages-desc%</span></ui-input> | ||||||
|  | 		</section> | ||||||
|  | 		<section class="fit-bottom"> | ||||||
|  | 			<header><fa icon="headset"/> %i18n:@maintainer-config%</header> | ||||||
|  | 			<ui-input v-model="maintainerName">%i18n:@maintainer-name%</ui-input> | ||||||
|  | 			<ui-input v-model="maintainerEmail" type="email"><i slot="icon"><fa :icon="['far', 'envelope']"/></i>%i18n:@maintainer-email%</ui-input> | ||||||
|  | 		</section> | ||||||
|  | 		<section class="fit-top fit-bottom"> | ||||||
| 			<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input> | 			<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input> | ||||||
|  | 		</section> | ||||||
|  | 		<section class="fit-bottom"> | ||||||
|  | 			<header><fa icon="cloud"/> %i18n:@drive-config%</header> | ||||||
|  | 			<ui-switch v-model="cacheRemoteFiles">%i18n:@cache-remote-files%<span slot="desc">%i18n:@cache-remote-files-desc%</span></ui-switch> | ||||||
|  | 			<ui-input v-model="localDriveCapacityMb">%i18n:@local-drive-capacity-mb%<span slot="suffix">MB</span><span slot="desc">%i18n:@mb%</span></ui-input> | ||||||
|  | 			<ui-input v-model="remoteDriveCapacityMb" :disabled="!cacheRemoteFiles">%i18n:@remote-drive-capacity-mb%<span slot="suffix">MB</span><span slot="desc">%i18n:@mb%</span></ui-input> | ||||||
|  | 		</section> | ||||||
|  | 		<section class="fit-bottom"> | ||||||
|  | 			<header><fa icon="shield-alt"/> %i18n:@recaptcha-config%</header> | ||||||
|  | 			<ui-switch v-model="enableRecaptcha">%i18n:@enable-recaptcha%</ui-switch> | ||||||
|  | 			<ui-info>%i18n:@recaptcha-info%</ui-info> | ||||||
|  | 			<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>%i18n:@recaptcha-site-key%</ui-input> | ||||||
|  | 			<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><i slot="icon"><fa icon="key"/></i>%i18n:@recaptcha-secret-key%</ui-input> | ||||||
|  | 		</section> | ||||||
|  | 		<section> | ||||||
|  | 			<header><fa icon="ghost"/> %i18n:@proxy-account-config%</header> | ||||||
|  | 			<ui-info>%i18n:@proxy-account-info%</ui-info> | ||||||
|  | 			<ui-input v-model="proxyAccount"><span slot="prefix">@</span>%i18n:@proxy-account-username%<span slot="desc">%i18n:@proxy-account-username-desc%</span></ui-input> | ||||||
|  | 			<ui-info warn>%i18n:@proxy-account-warn%</ui-info> | ||||||
|  | 		</section> | ||||||
|  | 		<section> | ||||||
|  | 			<ui-switch v-model="disableRegistration">%i18n:@disable-registration%</ui-switch> | ||||||
|  | 		</section> | ||||||
|  | 		<section> | ||||||
|  | 			<ui-switch v-model="disableLocalTimeline">%i18n:@disable-local-timeline%</ui-switch> | ||||||
|  | 		</section> | ||||||
|  | 		<section> | ||||||
| 			<ui-button @click="updateMeta">%i18n:@save%</ui-button> | 			<ui-button @click="updateMeta">%i18n:@save%</ui-button> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</ui-card> | ||||||
|  |  | ||||||
| 	<ui-card> | 	<ui-card> | ||||||
| 		<div slot="title">%i18n:@disable-registration%</div> | 		<div slot="title">%i18n:@invite%</div> | ||||||
| 		<section> | 		<section> | ||||||
| 			<input type="checkbox" v-model="disableRegistration" @change="updateMeta"> | 			<ui-button @click="invite">%i18n:@invite%</ui-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> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</ui-card> | ||||||
|  |  | ||||||
| 	<ui-card> | 	<ui-card> | ||||||
| 		<div slot="title">%i18n:@disable-local-timeline%</div> | 		<div slot="title"><fa :icon="['fab', 'twitter']"/> %i18n:@twitter-integration-config%</div> | ||||||
| 		<section> | 		<section> | ||||||
| 			<input type="checkbox" v-model="disableLocalTimeline" @change="updateMeta"> | 			<ui-switch v-model="enableTwitterIntegration">%i18n:@enable-twitter-integration%</ui-switch> | ||||||
|  | 			<ui-info>%i18n:@twitter-integration-info%</ui-info> | ||||||
|  | 			<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@twitter-integration-consumer-key%</ui-input> | ||||||
|  | 			<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@twitter-integration-consumer-secret%</ui-input> | ||||||
|  | 			<ui-button @click="updateMeta">%i18n:@save%</ui-button> | ||||||
|  | 		</section> | ||||||
|  | 	</ui-card> | ||||||
|  |  | ||||||
|  | 	<ui-card> | ||||||
|  | 		<div slot="title"><fa :icon="['fab', 'github']"/> %i18n:@github-integration-config%</div> | ||||||
|  | 		<section> | ||||||
|  | 			<ui-switch v-model="enableGithubIntegration">%i18n:@enable-github-integration%</ui-switch> | ||||||
|  | 			<ui-info>%i18n:@github-integration-info%</ui-info> | ||||||
|  | 			<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@github-integration-client-id%</ui-input> | ||||||
|  | 			<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@github-integration-client-secret%</ui-input> | ||||||
|  | 			<ui-button @click="updateMeta">%i18n:@save%</ui-button> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</ui-card> | ||||||
| </div> | </div> | ||||||
| @@ -35,22 +84,54 @@ import Vue from "vue"; | |||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  | 			maintainerName: null, | ||||||
|  | 			maintainerEmail: null, | ||||||
| 			disableRegistration: false, | 			disableRegistration: false, | ||||||
| 			disableLocalTimeline: false, | 			disableLocalTimeline: false, | ||||||
| 			bannerUrl: null, | 			bannerUrl: null, | ||||||
| 			name: null, | 			name: null, | ||||||
| 			description: null, | 			description: null, | ||||||
|  | 			languages: null, | ||||||
|  | 			cacheRemoteFiles: false, | ||||||
|  | 			localDriveCapacityMb: null, | ||||||
|  | 			remoteDriveCapacityMb: null, | ||||||
| 			maxNoteTextLength: null, | 			maxNoteTextLength: null, | ||||||
|  | 			enableRecaptcha: false, | ||||||
|  | 			recaptchaSiteKey: null, | ||||||
|  | 			recaptchaSecretKey: null, | ||||||
|  | 			enableTwitterIntegration: false, | ||||||
|  | 			twitterConsumerKey: null, | ||||||
|  | 			twitterConsumerSecret: null, | ||||||
|  | 			enableGithubIntegration: false, | ||||||
|  | 			githubClientId: null, | ||||||
|  | 			githubClientSecret: null, | ||||||
|  | 			proxyAccount: null, | ||||||
| 			inviteCode: null, | 			inviteCode: null, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	created() { | 	created() { | ||||||
| 		(this as any).os.getMeta().then(meta => { | 		(this as any).os.getMeta().then(meta => { | ||||||
|  | 			this.maintainerName = meta.maintainer.name; | ||||||
|  | 			this.maintainerEmail = meta.maintainer.email; | ||||||
| 			this.bannerUrl = meta.bannerUrl; | 			this.bannerUrl = meta.bannerUrl; | ||||||
| 			this.name = meta.name; | 			this.name = meta.name; | ||||||
| 			this.description = meta.description; | 			this.description = meta.description; | ||||||
|  | 			this.languages = meta.langs.join(' '); | ||||||
|  | 			this.cacheRemoteFiles = meta.cacheRemoteFiles; | ||||||
|  | 			this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb; | ||||||
|  | 			this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb; | ||||||
| 			this.maxNoteTextLength = meta.maxNoteTextLength; | 			this.maxNoteTextLength = meta.maxNoteTextLength; | ||||||
|  | 			this.enableRecaptcha = meta.enableRecaptcha; | ||||||
|  | 			this.recaptchaSiteKey = meta.recaptchaSiteKey; | ||||||
|  | 			this.recaptchaSecretKey = meta.recaptchaSecretKey; | ||||||
|  | 			this.proxyAccount = meta.proxyAccount; | ||||||
|  | 			this.enableTwitterIntegration = meta.enableTwitterIntegration; | ||||||
|  | 			this.twitterConsumerKey = meta.twitterConsumerKey; | ||||||
|  | 			this.twitterConsumerSecret = meta.twitterConsumerSecret; | ||||||
|  | 			this.enableGithubIntegration = meta.enableGithubIntegration; | ||||||
|  | 			this.githubClientId = meta.githubClientId; | ||||||
|  | 			this.githubClientSecret = meta.githubClientSecret; | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| @@ -68,12 +149,28 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 		updateMeta() { | 		updateMeta() { | ||||||
| 			(this as any).api('admin/update-meta', { | 			(this as any).api('admin/update-meta', { | ||||||
|  | 				maintainerName: this.maintainerName, | ||||||
|  | 				maintainerEmail: this.maintainerEmail, | ||||||
| 				disableRegistration: this.disableRegistration, | 				disableRegistration: this.disableRegistration, | ||||||
| 				disableLocalTimeline: this.disableLocalTimeline, | 				disableLocalTimeline: this.disableLocalTimeline, | ||||||
| 				bannerUrl: this.bannerUrl, | 				bannerUrl: this.bannerUrl, | ||||||
| 				name: this.name, | 				name: this.name, | ||||||
| 				description: this.description, | 				description: this.description, | ||||||
| 				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10) | 				langs: this.languages.split(' '), | ||||||
|  | 				cacheRemoteFiles: this.cacheRemoteFiles, | ||||||
|  | 				localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10), | ||||||
|  | 				remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10), | ||||||
|  | 				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10), | ||||||
|  | 				enableRecaptcha: this.enableRecaptcha, | ||||||
|  | 				recaptchaSiteKey: this.recaptchaSiteKey, | ||||||
|  | 				recaptchaSecretKey: this.recaptchaSecretKey, | ||||||
|  | 				proxyAccount: this.proxyAccount, | ||||||
|  | 				enableTwitterIntegration: this.enableTwitterIntegration, | ||||||
|  | 				twitterConsumerKey: this.twitterConsumerKey, | ||||||
|  | 				twitterConsumerSecret: this.twitterConsumerSecret, | ||||||
|  | 				enableGithubIntegration: this.enableGithubIntegration, | ||||||
|  | 				githubClientId: this.githubClientId, | ||||||
|  | 				githubClientSecret: this.githubClientSecret, | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				this.$swal({ | 				this.$swal({ | ||||||
| 					type: 'success', | 					type: 'success', | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ | |||||||
| 		</ui-input> | 		</ui-input> | ||||||
| 		<ui-textarea v-model="body"> | 		<ui-textarea v-model="body"> | ||||||
| 			<span>%i18n:@console.parameter% (JSON or JSON5)</span> | 			<span>%i18n:@console.parameter% (JSON or JSON5)</span> | ||||||
|  | 			<span slot="desc">%i18n:@console.credential-info%</span> | ||||||
| 		</ui-textarea> | 		</ui-textarea> | ||||||
| 		<ui-button @click="send" :disabled="sending"> | 		<ui-button @click="send" :disabled="sending"> | ||||||
| 			<template v-if="sending">%i18n:@console.sending%</template> | 			<template v-if="sending">%i18n:@console.sending%</template> | ||||||
|   | |||||||
| @@ -41,11 +41,17 @@ const lib = Object.entries(emojilib.lib).filter((x: any) => { | |||||||
| 	return x[1].category != 'flags'; | 	return x[1].category != 'flags'; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | const char2file = (char: string) => { | ||||||
|  | 	let codes = [...char].map(x => x.codePointAt(0).toString(16)); | ||||||
|  | 	if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); | ||||||
|  | 	return codes.join('-'); | ||||||
|  | }; | ||||||
|  |  | ||||||
| const emjdb: EmojiDef[] = lib.map((x: any) => ({ | const emjdb: EmojiDef[] = lib.map((x: any) => ({ | ||||||
| 	emoji: x[1].char, | 	emoji: x[1].char, | ||||||
| 	name: x[0], | 	name: x[0], | ||||||
| 	aliasOf: null, | 	aliasOf: null, | ||||||
| 	url: `https://twemoji.maxcdn.com/2/svg/${x[1].char.codePointAt(0).toString(16)}.svg` | 	url: `https://twemoji.maxcdn.com/2/svg/${char2file(x[1].char)}.svg` | ||||||
| })); | })); | ||||||
|  |  | ||||||
| lib.forEach((x: any) => { | lib.forEach((x: any) => { | ||||||
| @@ -55,7 +61,7 @@ lib.forEach((x: any) => { | |||||||
| 				emoji: x[1].char, | 				emoji: x[1].char, | ||||||
| 				name: k, | 				name: k, | ||||||
| 				aliasOf: x[0], | 				aliasOf: x[0], | ||||||
| 				url: `https://twemoji.maxcdn.com/2/svg/${x[1].char.codePointAt(0).toString(16)}.svg` | 				url: `https://twemoji.maxcdn.com/2/svg/${char2file(x[1].char)}.svg` | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| @@ -216,7 +222,11 @@ export default Vue.extend({ | |||||||
| 				} | 				} | ||||||
| 			} else if (this.type == 'emoji') { | 			} else if (this.type == 'emoji') { | ||||||
| 				if (this.q == null || this.q == '') { | 				if (this.q == null || this.q == '') { | ||||||
| 					this.emojis = this.emojiDb.filter(x => x.isCustomEmoji && !x.aliasOf); | 					this.emojis = this.emojiDb.filter(x => x.isCustomEmoji && !x.aliasOf).sort((a, b) => { | ||||||
|  | 						var textA = a.name.toUpperCase(); | ||||||
|  | 						var textB = b.name.toUpperCase(); | ||||||
|  | 						return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; | ||||||
|  | 					}); | ||||||
| 					return; | 					return; | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -60,7 +60,10 @@ export default Vue.extend({ | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if (this.char) { | 		if (this.char) { | ||||||
| 			this.url = `https://twemoji.maxcdn.com/2/svg/${this.char.codePointAt(0).toString(16)}.svg`; | 			let codes = [...this.char].map(x => x.codePointAt(0).toString(16)); | ||||||
|  | 			if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f'); | ||||||
|  |  | ||||||
|  | 			this.url = `https://twemoji.maxcdn.com/2/svg/${codes.join('-')}.svg`; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -40,7 +40,6 @@ import twitterSetting from './twitter-setting.vue'; | |||||||
| import githubSetting from './github-setting.vue'; | import githubSetting from './github-setting.vue'; | ||||||
| import fileTypeIcon from './file-type-icon.vue'; | import fileTypeIcon from './file-type-icon.vue'; | ||||||
| import emoji from './emoji.vue'; | import emoji from './emoji.vue'; | ||||||
| import Reversi from './games/reversi/reversi.vue'; |  | ||||||
| import welcomeTimeline from './welcome-timeline.vue'; | import welcomeTimeline from './welcome-timeline.vue'; | ||||||
| import uiInput from './ui/input.vue'; | import uiInput from './ui/input.vue'; | ||||||
| import uiButton from './ui/button.vue'; | import uiButton from './ui/button.vue'; | ||||||
| @@ -95,7 +94,6 @@ Vue.component('mk-twitter-setting', twitterSetting); | |||||||
| Vue.component('mk-github-setting', githubSetting); | Vue.component('mk-github-setting', githubSetting); | ||||||
| Vue.component('mk-file-type-icon', fileTypeIcon); | Vue.component('mk-file-type-icon', fileTypeIcon); | ||||||
| Vue.component('mk-emoji', emoji); | Vue.component('mk-emoji', emoji); | ||||||
| Vue.component('mk-reversi', Reversi); |  | ||||||
| Vue.component('mk-welcome-timeline', welcomeTimeline); | Vue.component('mk-welcome-timeline', welcomeTimeline); | ||||||
| Vue.component('ui-input', uiInput); | Vue.component('ui-input', uiInput); | ||||||
| Vue.component('ui-button', uiButton); | Vue.component('ui-button', uiButton); | ||||||
|   | |||||||
| @@ -179,6 +179,9 @@ export default Vue.extend({ | |||||||
| 			font-size 10px | 			font-size 10px | ||||||
| 			color var(--messagingRoomMessageInfo) | 			color var(--messagingRoomMessageInfo) | ||||||
|  |  | ||||||
|  | 			> .read | ||||||
|  | 				margin 0 8px | ||||||
|  |  | ||||||
| 			> [data-icon] | 			> [data-icon] | ||||||
| 				margin-left 4px | 				margin-left 4px | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,13 +31,13 @@ | |||||||
| 			<ui-input type="file" @change="onAvatarChange"> | 			<ui-input type="file" @change="onAvatarChange"> | ||||||
| 				<span>%i18n:@avatar%</span> | 				<span>%i18n:@avatar%</span> | ||||||
| 				<span slot="icon"><fa icon="image"/></span> | 				<span slot="icon"><fa icon="image"/></span> | ||||||
| 				<span slot="text" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span> | 				<span slot="desc" v-if="avatarUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||||
| 			</ui-input> | 			</ui-input> | ||||||
|  |  | ||||||
| 			<ui-input type="file" @change="onBannerChange"> | 			<ui-input type="file" @change="onBannerChange"> | ||||||
| 				<span>%i18n:@banner%</span> | 				<span>%i18n:@banner%</span> | ||||||
| 				<span slot="icon"><fa icon="image"/></span> | 				<span slot="icon"><fa icon="image"/></span> | ||||||
| 				<span slot="text" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span> | 				<span slot="desc" v-if="bannerUploading">%i18n:@uploading%<mk-ellipsis/></span> | ||||||
| 			</ui-input> | 			</ui-input> | ||||||
|  |  | ||||||
| 			<ui-button @click="save(true)">%i18n:@save%</ui-button> | 			<ui-button @click="save(true)">%i18n:@save%</ui-button> | ||||||
|   | |||||||
| @@ -4,38 +4,38 @@ | |||||||
| 		<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> | 		<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> | ||||||
| 			<span>%i18n:@invitation-code%</span> | 			<span>%i18n:@invitation-code%</span> | ||||||
| 			<span slot="prefix"><fa icon="id-card-alt"/></span> | 			<span slot="prefix"><fa icon="id-card-alt"/></span> | ||||||
| 			<p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p> | 			<p slot="desc" v-html="'%i18n:@invitation-info%'.replace('{}', 'mailto:' + meta.maintainer.email)"></p> | ||||||
| 		</ui-input> | 		</ui-input> | ||||||
| 		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> | 		<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> | ||||||
| 			<span>%i18n:@username%</span> | 			<span>%i18n:@username%</span> | ||||||
| 			<span slot="prefix">@</span> | 			<span slot="prefix">@</span> | ||||||
| 			<span slot="suffix">@{{ host }}</span> | 			<span slot="suffix">@{{ host }}</span> | ||||||
| 			<p slot="text" v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner .pulse" fixed-width/> %i18n:@checking%</p> | 			<p slot="desc" v-if="usernameState == 'wait'" style="color:#999"><fa icon="spinner .pulse" fixed-width/> %i18n:@checking%</p> | ||||||
| 			<p slot="text" v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@available%</p> | 			<p slot="desc" v-if="usernameState == 'ok'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@available%</p> | ||||||
| 			<p slot="text" v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@unavailable%</p> | 			<p slot="desc" v-if="usernameState == 'unavailable'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@unavailable%</p> | ||||||
| 			<p slot="text" v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@error%</p> | 			<p slot="desc" v-if="usernameState == 'error'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@error%</p> | ||||||
| 			<p slot="text" v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@invalid-format%</p> | 			<p slot="desc" v-if="usernameState == 'invalid-format'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@invalid-format%</p> | ||||||
| 			<p slot="text" v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@too-short%</p> | 			<p slot="desc" v-if="usernameState == 'min-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@too-short%</p> | ||||||
| 			<p slot="text" v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@too-long%</p> | 			<p slot="desc" v-if="usernameState == 'max-range'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@too-long%</p> | ||||||
| 		</ui-input> | 		</ui-input> | ||||||
| 		<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill"> | 		<ui-input v-model="password" type="password" :autocomplete="Math.random()" required @input="onChangePassword" :with-password-meter="true" styl="fill"> | ||||||
| 			<span>%i18n:@password%</span> | 			<span>%i18n:@password%</span> | ||||||
| 			<span slot="prefix"><fa icon="lock"/></span> | 			<span slot="prefix"><fa icon="lock"/></span> | ||||||
| 			<div slot="text"> | 			<div slot="desc"> | ||||||
| 				<p slot="text" v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@weak-password%</p> | 				<p v-if="passwordStrength == 'low'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@weak-password%</p> | ||||||
| 				<p slot="text" v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@normal-password%</p> | 				<p v-if="passwordStrength == 'medium'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@normal-password%</p> | ||||||
| 				<p slot="text" v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@strong-password%</p> | 				<p v-if="passwordStrength == 'high'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@strong-password%</p> | ||||||
| 			</div> | 			</div> | ||||||
| 		</ui-input> | 		</ui-input> | ||||||
| 		<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill"> | 		<ui-input v-model="retypedPassword" type="password" :autocomplete="Math.random()" required @input="onChangePasswordRetype" styl="fill"> | ||||||
| 			<span>%i18n:@password% (%i18n:@retype%)</span> | 			<span>%i18n:@password% (%i18n:@retype%)</span> | ||||||
| 			<span slot="prefix"><fa icon="lock"/></span> | 			<span slot="prefix"><fa icon="lock"/></span> | ||||||
| 			<div slot="text"> | 			<div slot="desc"> | ||||||
| 				<p slot="text" v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@password-matched%</p> | 				<p v-if="passwordRetypeState == 'match'" style="color:#3CB7B5"><fa icon="check" fixed-width/> %i18n:@password-matched%</p> | ||||||
| 				<p slot="text" v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@password-not-matched%</p> | 				<p v-if="passwordRetypeState == 'not-match'" style="color:#FF1161"><fa icon="exclamation-triangle" fixed-width/> %i18n:@password-not-matched%</p> | ||||||
| 			</div> | 			</div> | ||||||
| 		</ui-input> | 		</ui-input> | ||||||
| 		<div v-if="meta.recaptchaSitekey != null" class="g-recaptcha" :data-sitekey="meta.recaptchaSitekey" style="margin: 16px 0;"></div> | 		<div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div> | ||||||
| 		<ui-button type="submit">%i18n:@create%</ui-button> | 		<ui-button type="submit">%i18n:@create%</ui-button> | ||||||
| 	</template> | 	</template> | ||||||
| </form> | </form> | ||||||
| @@ -130,7 +130,7 @@ export default Vue.extend({ | |||||||
| 				username: this.username, | 				username: this.username, | ||||||
| 				password: this.password, | 				password: this.password, | ||||||
| 				invitationCode: this.invitationCode, | 				invitationCode: this.invitationCode, | ||||||
| 				'g-recaptcha-response': this.meta.recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null | 				'g-recaptcha-response': this.meta.enableRecaptcha ? (window as any).grecaptcha.getResponse() : null | ||||||
| 			}, true).then(() => { | 			}, true).then(() => { | ||||||
| 				(this as any).api('signin', { | 				(this as any).api('signin', { | ||||||
| 					username: this.username, | 					username: this.username, | ||||||
| @@ -141,7 +141,7 @@ export default Vue.extend({ | |||||||
| 			}).catch(() => { | 			}).catch(() => { | ||||||
| 				alert('%i18n:@some-error%'); | 				alert('%i18n:@some-error%'); | ||||||
|  |  | ||||||
| 				if (this.meta.recaptchaSitekey != null) { | 				if (this.meta.enableRecaptcha) { | ||||||
| 					(window as any).grecaptcha.reset(); | 					(window as any).grecaptcha.reset(); | ||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
|   | |||||||
| @@ -48,6 +48,9 @@ export default Vue.extend({ | |||||||
| 		&.fit-top | 		&.fit-top | ||||||
| 			padding-top 0 | 			padding-top 0 | ||||||
|  |  | ||||||
|  | 		&.fit-bottom | ||||||
|  | 			padding-bottom 0 | ||||||
|  |  | ||||||
| 		> header | 		> header | ||||||
| 			margin-bottom 16px | 			margin-bottom 16px | ||||||
| 			font-weight bold | 			font-weight bold | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <template> | <template> | ||||||
| <div class="ui-input" :class="[{ focused, filled, inline }, styl]"> | <div class="ui-input" :class="[{ focused, filled, inline, disabled }, styl]"> | ||||||
| 	<div class="icon" ref="icon"><slot name="icon"></slot></div> | 	<div class="icon" ref="icon"><slot name="icon"></slot></div> | ||||||
| 	<div class="input"> | 	<div class="input"> | ||||||
| 		<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> | 		<div class="password-meter" v-if="withPasswordMeter" v-show="passwordStrength != ''" :data-strength="passwordStrength"> | ||||||
| @@ -11,6 +11,7 @@ | |||||||
| 			<input ref="input" | 			<input ref="input" | ||||||
| 					:type="type" | 					:type="type" | ||||||
| 					v-model="v" | 					v-model="v" | ||||||
|  | 					:disabled="disabled" | ||||||
| 					:required="required" | 					:required="required" | ||||||
| 					:readonly="readonly" | 					:readonly="readonly" | ||||||
| 					:pattern="pattern" | 					:pattern="pattern" | ||||||
| @@ -32,7 +33,7 @@ | |||||||
| 		</template> | 		</template> | ||||||
| 		<div class="suffix" ref="suffix"><slot name="suffix"></slot></div> | 		<div class="suffix" ref="suffix"><slot name="suffix"></slot></div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="text"><slot name="text"></slot></div> | 	<div class="desc"><slot name="desc"></slot></div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -62,6 +63,10 @@ export default Vue.extend({ | |||||||
| 			type: Boolean, | 			type: Boolean, | ||||||
| 			required: false | 			required: false | ||||||
| 		}, | 		}, | ||||||
|  | 		disabled: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false | ||||||
|  | 		}, | ||||||
| 		pattern: { | 		pattern: { | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: false | 			required: false | ||||||
| @@ -316,7 +321,7 @@ root(fill) | |||||||
| 			if fill | 			if fill | ||||||
| 				padding-right 12px | 				padding-right 12px | ||||||
|  |  | ||||||
| 	> .text | 	> .desc | ||||||
| 		margin 6px 0 | 		margin 6px 0 | ||||||
| 		font-size 13px | 		font-size 13px | ||||||
|  |  | ||||||
| @@ -353,4 +358,10 @@ root(fill) | |||||||
| 		display inline-block | 		display inline-block | ||||||
| 		margin 0 | 		margin 0 | ||||||
|  |  | ||||||
|  | 	&.disabled | ||||||
|  | 		opacity 0.7 | ||||||
|  |  | ||||||
|  | 		&, * | ||||||
|  | 			cursor not-allowed !important | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -129,5 +129,6 @@ export default Vue.extend({ | |||||||
| 		> p | 		> p | ||||||
| 			margin 0 | 			margin 0 | ||||||
| 			opacity 0.7 | 			opacity 0.7 | ||||||
|  | 			font-size 90% | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ | |||||||
| 			@blur="focused = false" | 			@blur="focused = false" | ||||||
| 		></textarea> | 		></textarea> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="text"><slot name="text"></slot></div> | 	<div class="desc"><slot name="desc"></slot></div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -139,7 +139,7 @@ root(fill) | |||||||
| 			outline none | 			outline none | ||||||
| 			box-shadow none | 			box-shadow none | ||||||
|  |  | ||||||
| 	> .text | 	> .desc | ||||||
| 		margin 6px 0 | 		margin 6px 0 | ||||||
| 		font-size 13px | 		font-size 13px | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ | |||||||
| 			<h1><fa icon="heart"/>%i18n:@title%</h1> | 			<h1><fa icon="heart"/>%i18n:@title%</h1> | ||||||
| 			<p v-if="meta"> | 			<p v-if="meta"> | ||||||
| 				{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} | 				{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} | ||||||
| 				<a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a> | 				<a :href="'mailto:' + meta.maintainer.email">{{ meta.maintainer.name }}</a> | ||||||
| 				{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} | 				{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} | ||||||
| 			</p> | 			</p> | ||||||
| 		</article> | 		</article> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <div class="info"> | <div class="info"> | ||||||
| 	<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> | 	<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||||
| 	<p>Machine: {{ meta.machine }}</p> | 	<p>Machine: {{ meta.machine }}</p> | ||||||
| 	<p>Node: {{ meta.node }}</p> | 	<p>Node: {{ meta.node }}</p> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -359,7 +359,7 @@ export default Vue.extend({ | |||||||
| 			}).then(name => { | 			}).then(name => { | ||||||
| 				(this as any).api('drive/folders/create', { | 				(this as any).api('drive/folders/create', { | ||||||
| 					name: name, | 					name: name, | ||||||
| 					folderId: this.folder ? this.folder.id : undefined | 					parentId: this.folder ? this.folder.id : undefined | ||||||
| 				}).then(folder => { | 				}).then(folder => { | ||||||
| 					this.addFolder(folder, true); | 					this.addFolder(folder, true); | ||||||
| 				}); | 				}); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> | <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="destroyDom"> | ||||||
| 	<span slot="header" :class="$style.header"><fa icon="gamepad"/>%i18n:@game%</span> | 	<span slot="header" :class="$style.header"><fa icon="gamepad"/>%i18n:@game%</span> | ||||||
| 	<mk-reversi :class="$style.content" @gamed="g => game = g"/> | 	<x-reversi :class="$style.content" @gamed="g => game = g"/> | ||||||
| </mk-window> | </mk-window> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -10,6 +10,9 @@ import Vue from 'vue'; | |||||||
| import { url } from '../../../config'; | import { url } from '../../../config'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XReversi: () => import('../../../common/views/components/games/reversi/reversi.vue') | ||||||
|  | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			game: null | 			game: null | ||||||
|   | |||||||
| @@ -255,7 +255,7 @@ export default Vue.extend({ | |||||||
| 					p | 					p | ||||||
| 						margin 0 | 						margin 0 | ||||||
|  |  | ||||||
| 						i, .mk-reaction-icon | 						[data-icon], .mk-reaction-icon | ||||||
| 							margin-right 4px | 							margin-right 4px | ||||||
|  |  | ||||||
| 				.note-preview | 				.note-preview | ||||||
| @@ -272,19 +272,19 @@ export default Vue.extend({ | |||||||
| 						margin-right 3px | 						margin-right 3px | ||||||
|  |  | ||||||
| 				&.renote, &.quote | 				&.renote, &.quote | ||||||
| 					.text p i | 					.text p [data-icon] | ||||||
| 						color #77B255 | 						color #77B255 | ||||||
|  |  | ||||||
| 				&.follow | 				&.follow | ||||||
| 					.text p i | 					.text p [data-icon] | ||||||
| 						color #53c7ce | 						color #53c7ce | ||||||
|  |  | ||||||
| 				&.receiveFollowRequest | 				&.receiveFollowRequest | ||||||
| 					.text p i | 					.text p [data-icon] | ||||||
| 						color #888 | 						color #888 | ||||||
|  |  | ||||||
| 				&.reply, &.mention | 				&.reply, &.mention | ||||||
| 					.text p i | 					.text p [data-icon] | ||||||
| 						color #555 | 						color #555 | ||||||
|  |  | ||||||
| 			> .date | 			> .date | ||||||
|   | |||||||
| @@ -247,7 +247,7 @@ | |||||||
| 		<ui-card class="other" v-show="page == 'other'"> | 		<ui-card class="other" v-show="page == 'other'"> | ||||||
| 			<div slot="title"><fa icon="info-circle"/> %i18n:@about%</div> | 			<div slot="title"><fa icon="info-circle"/> %i18n:@about%</div> | ||||||
| 			<section> | 			<section> | ||||||
| 				<p v-if="meta">%i18n:@operator%: <i><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></i></p> | 				<p v-if="meta">%i18n:@operator%: <i><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></i></p> | ||||||
| 			</section> | 			</section> | ||||||
| 		</ui-card> | 		</ui-card> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <component :is="ui ? 'mk-ui' : 'div'"> | <component :is="ui ? 'mk-ui' : 'div'"> | ||||||
| 	<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> | 	<x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> | ||||||
| </component> | </component> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -8,6 +8,9 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue') | ||||||
|  | 	}, | ||||||
| 	props: { | 	props: { | ||||||
| 		ui: { | 		ui: { | ||||||
| 			default: false | 			default: false | ||||||
|   | |||||||
| @@ -87,7 +87,7 @@ | |||||||
| 					<div> | 					<div> | ||||||
| 						<div v-if="meta" class="body"> | 						<div v-if="meta" class="body"> | ||||||
| 							<p>Version: <b>{{ meta.version }}</b></p> | 							<p>Version: <b>{{ meta.version }}</b></p> | ||||||
| 							<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> | 							<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
|   | |||||||
| @@ -105,7 +105,7 @@ export default Vue.extend({ | |||||||
| 		p | 		p | ||||||
| 			margin 0 | 			margin 0 | ||||||
|  |  | ||||||
| 			i, mk-reaction-icon | 			[data-icon], mk-reaction-icon | ||||||
| 				margin-right 4px | 				margin-right 4px | ||||||
|  |  | ||||||
| 	.note-ref | 	.note-ref | ||||||
| @@ -118,19 +118,19 @@ export default Vue.extend({ | |||||||
| 			margin-right 3px | 			margin-right 3px | ||||||
|  |  | ||||||
| 	&.renote, &.quote | 	&.renote, &.quote | ||||||
| 		.text p i | 		.text p [data-icon] | ||||||
| 			color #77B255 | 			color #77B255 | ||||||
|  |  | ||||||
| 	&.follow | 	&.follow | ||||||
| 		.text p i | 		.text p [data-icon] | ||||||
| 			color #53c7ce | 			color #53c7ce | ||||||
|  |  | ||||||
| 	&.receiveFollowRequest | 	&.receiveFollowRequest | ||||||
| 		.text p i | 		.text p [data-icon] | ||||||
| 			color #888 | 			color #888 | ||||||
|  |  | ||||||
| 	&.reply, &.mention | 	&.reply, &.mention | ||||||
| 		.text p i | 		.text p [data-icon] | ||||||
| 			color #fff | 			color #fff | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -149,7 +149,7 @@ export default Vue.extend({ | |||||||
| 				align-items baseline | 				align-items baseline | ||||||
| 				white-space nowrap | 				white-space nowrap | ||||||
|  |  | ||||||
| 				i, .mk-reaction-icon | 				[data-icon], .mk-reaction-icon | ||||||
| 					margin-right 4px | 					margin-right 4px | ||||||
|  |  | ||||||
| 				> .mk-time | 				> .mk-time | ||||||
| @@ -171,15 +171,15 @@ export default Vue.extend({ | |||||||
| 					margin-right 3px | 					margin-right 3px | ||||||
|  |  | ||||||
| 		&.renote | 		&.renote | ||||||
| 			> div > header i | 			> div > header [data-icon] | ||||||
| 				color #77B255 | 				color #77B255 | ||||||
|  |  | ||||||
| 		&.follow | 		&.follow | ||||||
| 			> div > header i | 			> div > header [data-icon] | ||||||
| 				color #53c7ce | 				color #53c7ce | ||||||
|  |  | ||||||
| 		&.receiveFollowRequest | 		&.receiveFollowRequest | ||||||
| 			> div > header i | 			> div > header [data-icon] | ||||||
| 				color #888 | 				color #888 | ||||||
|  |  | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
| <mk-ui> | <mk-ui> | ||||||
| 	<span slot="header"><span style="margin-right:4px;"><fa icon="gamepad"/></span>%i18n:@reversi%</span> | 	<span slot="header"><span style="margin-right:4px;"><fa icon="gamepad"/></span>%i18n:@reversi%</span> | ||||||
| 	<mk-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> | 	<x-reversi :game-id="$route.params.game" @nav="nav" :self-nav="false"/> | ||||||
| </mk-ui> | </mk-ui> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -9,6 +9,9 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
|  | 	components: { | ||||||
|  | 		XReversi: () => import('../../../../common/views/components/games/reversi/reversi.vue') | ||||||
|  | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		document.title = `${(this as any).os.instanceName} %i18n:@reversi%`; | 		document.title = `${(this as any).os.instanceName} %i18n:@reversi%`; | ||||||
| 	}, | 	}, | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ | |||||||
| 		</article> | 		</article> | ||||||
| 		<div class="info" v-if="meta"> | 		<div class="info" v-if="meta"> | ||||||
| 			<p>Version: <b>{{ meta.version }}</b></p> | 			<p>Version: <b>{{ meta.version }}</b></p> | ||||||
| 			<p>Maintainer: <b><a :href="meta.maintainer.url" target="_blank">{{ meta.maintainer.name }}</a></b></p> | 			<p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> | ||||||
| 		</div> | 		</div> | ||||||
| 		<footer> | 		<footer> | ||||||
| 			<small>{{ copyright }}</small> | 			<small>{{ copyright }}</small> | ||||||
|   | |||||||
| @@ -46,8 +46,7 @@ export default function load() { | |||||||
| 	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`; | 	mixin.drive_url = `${mixin.scheme}://${mixin.host}/files`; | ||||||
| 	mixin.user_agent = `Misskey/${pkg.version} (${config.url})`; | 	mixin.user_agent = `Misskey/${pkg.version} (${config.url})`; | ||||||
|  |  | ||||||
| 	if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256; | 	if (config.autoAdmin == null) config.autoAdmin = false; | ||||||
| 	if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8; |  | ||||||
|  |  | ||||||
| 	return Object.assign(config, mixin); | 	return Object.assign(config, mixin); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,24 +2,8 @@ | |||||||
|  * ユーザーが設定する必要のある情報 |  * ユーザーが設定する必要のある情報 | ||||||
|  */ |  */ | ||||||
| export type Source = { | export type Source = { | ||||||
| 	/** |  | ||||||
| 	 * メンテナ情報 |  | ||||||
| 	 */ |  | ||||||
| 	maintainer: { |  | ||||||
| 		/** |  | ||||||
| 		 * メンテナの名前 |  | ||||||
| 		 */ |  | ||||||
| 		name: string; |  | ||||||
| 		/** |  | ||||||
| 		 * メンテナの連絡先(URLかmailto形式のURL) |  | ||||||
| 		 */ |  | ||||||
| 		url: string; |  | ||||||
| 		email?: string; |  | ||||||
| 	repository_url?: string; | 	repository_url?: string; | ||||||
| 	feedback_url?: string; | 	feedback_url?: string; | ||||||
| 	}; |  | ||||||
| 	languages?: string[]; |  | ||||||
| 	welcome_bg_url?: string; |  | ||||||
| 	url: string; | 	url: string; | ||||||
| 	port: number; | 	port: number; | ||||||
| 	https?: { [x: string]: string }; | 	https?: { [x: string]: string }; | ||||||
| @@ -41,15 +25,6 @@ export type Source = { | |||||||
| 		port: number; | 		port: number; | ||||||
| 		pass: string; | 		pass: string; | ||||||
| 	}; | 	}; | ||||||
| 	recaptcha?: { |  | ||||||
| 		site_key: string; |  | ||||||
| 		secret_key: string; |  | ||||||
| 	}; |  | ||||||
|  |  | ||||||
| 	localDriveCapacityMb: number; |  | ||||||
| 	remoteDriveCapacityMb: number; |  | ||||||
| 	preventCacheRemoteFiles: boolean; |  | ||||||
|  |  | ||||||
| 	drive?: { | 	drive?: { | ||||||
| 		storage: string; | 		storage: string; | ||||||
| 		bucket?: string; | 		bucket?: string; | ||||||
| @@ -58,39 +33,18 @@ export type Source = { | |||||||
| 		config?: any; | 		config?: any; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	/** | 	autoAdmin?: boolean; | ||||||
| 	 * ゴーストアカウントのID |  | ||||||
| 	 */ |  | ||||||
| 	ghost?: string; |  | ||||||
|  |  | ||||||
| 	proxy?: string; | 	proxy?: string; | ||||||
|  |  | ||||||
| 	summalyProxy?: string; | 	summalyProxy?: string; | ||||||
|  |  | ||||||
| 	accesslog?: string; | 	accesslog?: string; | ||||||
| 	twitter?: { |  | ||||||
| 		consumer_key: string; |  | ||||||
| 		consumer_secret: string; |  | ||||||
| 	}; |  | ||||||
| 	github?: { |  | ||||||
| 		client_id: string; |  | ||||||
| 		client_secret: string; |  | ||||||
| 	}; |  | ||||||
| 	github_bot?: { | 	github_bot?: { | ||||||
| 		hook_secret: string; | 		hook_secret: string; | ||||||
| 		username: string; | 		username: string; | ||||||
| 	}; | 	}; | ||||||
| 	reversi_ai?: { |  | ||||||
| 		id: string; |  | ||||||
| 		i: string; |  | ||||||
| 	}; |  | ||||||
| 	line_bot?: { |  | ||||||
| 		channel_secret: string; |  | ||||||
| 		channel_access_token: string; |  | ||||||
| 	}; |  | ||||||
| 	analysis?: { |  | ||||||
| 		mecab_command?: string; |  | ||||||
| 	}; |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Service Worker | 	 * Service Worker | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/mfm/parse/elements/emoji.regex.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/mfm/parse/elements/emoji.regex.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -2,6 +2,8 @@ | |||||||
|  * Emoji |  * Emoji | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | import { emojiRegex } from "./emoji.regex"; | ||||||
|  |  | ||||||
| export type TextElementEmoji = { | export type TextElementEmoji = { | ||||||
| 	type: 'emoji'; | 	type: 'emoji'; | ||||||
| 	content: string; | 	content: string; | ||||||
| @@ -9,8 +11,6 @@ export type TextElementEmoji = { | |||||||
| 	name?: string; | 	name?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const emojiRegex = /^[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug; |  | ||||||
|  |  | ||||||
| export default function(text: string) { | export default function(text: string) { | ||||||
| 	const name = text.match(/^:([a-zA-Z0-9+_-]+):/); | 	const name = text.match(/^:([a-zA-Z0-9+_-]+):/); | ||||||
| 	if (name) { | 	if (name) { | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								src/misc/fetch-meta.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/misc/fetch-meta.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | import Meta, { IMeta } from '../models/meta'; | ||||||
|  |  | ||||||
|  | const defaultMeta: any = { | ||||||
|  | 	name: 'Misskey', | ||||||
|  | 	maintainer: {}, | ||||||
|  | 	langs: [], | ||||||
|  | 	cacheRemoteFiles: true, | ||||||
|  | 	localDriveCapacityMb: 256, | ||||||
|  | 	remoteDriveCapacityMb: 8, | ||||||
|  | 	hidedTags: [], | ||||||
|  | 	stats: { | ||||||
|  | 		originalNotesCount: 0, | ||||||
|  | 		originalUsersCount: 0 | ||||||
|  | 	}, | ||||||
|  | 	maxNoteTextLength: 1000, | ||||||
|  | 	enableTwitterIntegration: false, | ||||||
|  | 	enableGithubIntegration: false, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default async function(): Promise<IMeta> { | ||||||
|  | 	const meta = await Meta.findOne({}); | ||||||
|  |  | ||||||
|  | 	return Object.assign({}, defaultMeta, meta); | ||||||
|  | } | ||||||
| @@ -54,7 +54,7 @@ export default class Replacer { | |||||||
| 			if (this.lang === 'ja-JP') 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.replace(/\n/g, ' '); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| import db from '../db/mongodb'; | import db from '../db/mongodb'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
|  | import User from './user'; | ||||||
|  | import { transform } from '../misc/cafy-id'; | ||||||
|  |  | ||||||
| const Meta = db.get<IMeta>('meta'); | const Meta = db.get<IMeta>('meta'); | ||||||
| export default Meta; | export default Meta; | ||||||
| @@ -28,24 +30,165 @@ if ((config as any).description) { | |||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  | if ((config as any).localDriveCapacityMb) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.localDriveCapacityMb == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					localDriveCapacityMb: (config as any).localDriveCapacityMb | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).remoteDriveCapacityMb) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.remoteDriveCapacityMb == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					remoteDriveCapacityMb: (config as any).remoteDriveCapacityMb | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).preventCacheRemoteFiles) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.cacheRemoteFiles == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					cacheRemoteFiles: !(config as any).preventCacheRemoteFiles | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).recaptcha) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.enableRecaptcha == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					enableRecaptcha: (config as any).recaptcha != null, | ||||||
|  | 					recaptchaSiteKey: (config as any).recaptcha.site_key, | ||||||
|  | 					recaptchaSecretKey: (config as any).recaptcha.secret_key, | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).ghost) { | ||||||
|  | 	Meta.findOne({}).then(async m => { | ||||||
|  | 		if (m != null && m.proxyAccount == null) { | ||||||
|  | 			const account = await User.findOne({ _id: transform((config as any).ghost) }); | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					proxyAccount: account.username | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).maintainer) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.maintainer == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					maintainer: (config as any).maintainer | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).twitter) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.enableTwitterIntegration == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					enableTwitterIntegration: true, | ||||||
|  | 					twitterConsumerKey: (config as any).twitter.consumer_key, | ||||||
|  | 					twitterConsumerSecret: (config as any).twitter.consumer_secret | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | if ((config as any).github) { | ||||||
|  | 	Meta.findOne({}).then(m => { | ||||||
|  | 		if (m != null && m.enableGithubIntegration == null) { | ||||||
|  | 			Meta.update({}, { | ||||||
|  | 				$set: { | ||||||
|  | 					enableGithubIntegration: true, | ||||||
|  | 					githubClientId: (config as any).github.client_id, | ||||||
|  | 					githubClientSecret: (config as any).github.client_secret | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
| export type IMeta = { | export type IMeta = { | ||||||
| 	name?: string; | 	name?: string; | ||||||
| 	description?: string; | 	description?: string; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * メンテナ情報 | ||||||
|  | 	 */ | ||||||
|  | 	maintainer: { | ||||||
|  | 		/** | ||||||
|  | 		 * メンテナの名前 | ||||||
|  | 		 */ | ||||||
|  | 		name: string; | ||||||
|  |  | ||||||
|  | 		/** | ||||||
|  | 		 * メンテナの連絡先 | ||||||
|  | 		 */ | ||||||
|  | 		email?: string; | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	langs?: string[]; | ||||||
|  |  | ||||||
| 	broadcasts?: any[]; | 	broadcasts?: any[]; | ||||||
|  |  | ||||||
| 	stats?: { | 	stats?: { | ||||||
| 		notesCount: number; | 		notesCount: number; | ||||||
| 		originalNotesCount: number; | 		originalNotesCount: number; | ||||||
| 		usersCount: number; | 		usersCount: number; | ||||||
| 		originalUsersCount: number; | 		originalUsersCount: number; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	disableRegistration?: boolean; | 	disableRegistration?: boolean; | ||||||
| 	disableLocalTimeline?: boolean; | 	disableLocalTimeline?: boolean; | ||||||
| 	hidedTags?: string[]; | 	hidedTags?: string[]; | ||||||
| 	bannerUrl?: string; | 	bannerUrl?: string; | ||||||
|  |  | ||||||
|  | 	cacheRemoteFiles?: boolean; | ||||||
|  |  | ||||||
|  | 	proxyAccount?: string; | ||||||
|  |  | ||||||
|  | 	enableRecaptcha?: boolean; | ||||||
|  | 	recaptchaSiteKey?: string; | ||||||
|  | 	recaptchaSecretKey?: string; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Drive capacity of a local user (MB) | ||||||
|  | 	 */ | ||||||
|  | 	localDriveCapacityMb?: number; | ||||||
|  |  | ||||||
|  | 	/** | ||||||
|  | 	 * Drive capacity of a remote user (MB) | ||||||
|  | 	 */ | ||||||
|  | 	remoteDriveCapacityMb?: number; | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Max allowed note text length in charactors | 	 * Max allowed note text length in charactors | ||||||
| 	 */ | 	 */ | ||||||
| 	maxNoteTextLength?: number; | 	maxNoteTextLength?: number; | ||||||
|  |  | ||||||
|  | 	enableTwitterIntegration?: boolean; | ||||||
|  | 	twitterConsumerKey?: string; | ||||||
|  | 	twitterConsumerSecret?: string; | ||||||
|  |  | ||||||
|  | 	enableGithubIntegration?: boolean; | ||||||
|  | 	githubClientId?: string; | ||||||
|  | 	githubClientSecret?: string; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import Mute from './mute'; | |||||||
| import { getFriendIds } from '../server/api/common/get-friends'; | import { getFriendIds } from '../server/api/common/get-friends'; | ||||||
| import config from '../config'; | import config from '../config'; | ||||||
| import FollowRequest from './follow-request'; | import FollowRequest from './follow-request'; | ||||||
|  | import fetchMeta from '../misc/fetch-meta'; | ||||||
|  |  | ||||||
| const User = db.get<IUser>('users'); | const User = db.get<IUser>('users'); | ||||||
|  |  | ||||||
| @@ -376,6 +377,7 @@ function img(url) { | |||||||
| } | } | ||||||
| */ | */ | ||||||
|  |  | ||||||
| export function getGhost(): Promise<ILocalUser> { | export async function fetchProxyAccount(): Promise<ILocalUser> { | ||||||
| 	return User.findOne({ _id: new mongo.ObjectId(config.ghost) }); | 	const meta = await fetchMeta(); | ||||||
|  | 	return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import config from './config'; | |||||||
| if (config.sw) { | if (config.sw) { | ||||||
| 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 | 	// アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 | ||||||
| 	push.setVapidDetails( | 	push.setVapidDetails( | ||||||
| 		config.maintainer.url, | 		config.url, | ||||||
| 		config.sw.public_key, | 		config.sw.public_key, | ||||||
| 		config.sw.private_key); | 		config.sw.private_key); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import uploadFromUrl from '../../../services/drive/upload-from-url'; | |||||||
| import { IRemoteUser } from '../../../models/user'; | import { IRemoteUser } from '../../../models/user'; | ||||||
| import DriveFile, { IDriveFile } from '../../../models/drive-file'; | import DriveFile, { IDriveFile } from '../../../models/drive-file'; | ||||||
| import Resolver from '../resolver'; | import Resolver from '../resolver'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| const log = debug('misskey:activitypub'); | const log = debug('misskey:activitypub'); | ||||||
|  |  | ||||||
| @@ -24,7 +25,10 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv | |||||||
|  |  | ||||||
| 	log(`Creating the Image: ${image.url}`); | 	log(`Creating the Image: ${image.url}`); | ||||||
|  |  | ||||||
| 	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive); | 	const instance = await fetchMeta(); | ||||||
|  | 	const cache = instance.cacheRemoteFiles; | ||||||
|  |  | ||||||
|  | 	let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache); | ||||||
|  |  | ||||||
| 	if (file.metadata.isRemote) { | 	if (file.metadata.isRemote) { | ||||||
| 		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 | 		// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 | ||||||
|   | |||||||
| @@ -65,7 +65,121 @@ export const meta = { | |||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': '投稿の最大文字数' | 				'ja-JP': '投稿の最大文字数' | ||||||
| 			} | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		localDriveCapacityMb: { | ||||||
|  | 			validator: $.num.optional.min(0), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'ローカルユーザーひとりあたりのドライブ容量 (メガバイト単位)', | ||||||
|  | 				'en-US': 'Drive capacity of a local user (MB)' | ||||||
| 			} | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		remoteDriveCapacityMb: { | ||||||
|  | 			validator: $.num.optional.min(0), | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'リモートユーザーひとりあたりのドライブ容量 (メガバイト単位)', | ||||||
|  | 				'en-US': 'Drive capacity of a remote user (MB)' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		cacheRemoteFiles: { | ||||||
|  | 			validator: $.bool.optional, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'リモートのファイルをキャッシュするか否か' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		enableRecaptcha: { | ||||||
|  | 			validator: $.bool.optional, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'reCAPTCHAを使用するか否か' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		recaptchaSiteKey: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'reCAPTCHA site key' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		recaptchaSecretKey: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'reCAPTCHA secret key' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		proxyAccount: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'プロキシアカウントのユーザー名' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		maintainerName: { | ||||||
|  | 			validator: $.str.optional, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'インスタンスの管理者名' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		maintainerEmail: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'インスタンス管理者の連絡先メールアドレス' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		langs: { | ||||||
|  | 			validator: $.arr($.str).optional, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'インスタンスの対象言語' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		enableTwitterIntegration: { | ||||||
|  | 			validator: $.bool.optional, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'Twitter連携機能を有効にするか否か' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		twitterConsumerKey: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'TwitterアプリのConsumer key' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		twitterConsumerSecret: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'TwitterアプリのConsumer secret' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		enableGithubIntegration: { | ||||||
|  | 			validator: $.bool.optional, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'GitHub連携機能を有効にするか否か' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		githubClientId: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'GitHubアプリのClient ID' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		githubClientSecret: { | ||||||
|  | 			validator: $.str.optional.nullable, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'GitHubアプリのClient secret' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -104,6 +218,70 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { | |||||||
| 		set.maxNoteTextLength = ps.maxNoteTextLength; | 		set.maxNoteTextLength = ps.maxNoteTextLength; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.localDriveCapacityMb !== undefined) { | ||||||
|  | 		set.localDriveCapacityMb = ps.localDriveCapacityMb; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.remoteDriveCapacityMb !== undefined) { | ||||||
|  | 		set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.cacheRemoteFiles !== undefined) { | ||||||
|  | 		set.cacheRemoteFiles = ps.cacheRemoteFiles; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.enableRecaptcha !== undefined) { | ||||||
|  | 		set.enableRecaptcha = ps.enableRecaptcha; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.recaptchaSiteKey !== undefined) { | ||||||
|  | 		set.recaptchaSiteKey = ps.recaptchaSiteKey; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.recaptchaSecretKey !== undefined) { | ||||||
|  | 		set.recaptchaSecretKey = ps.recaptchaSecretKey; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.proxyAccount !== undefined) { | ||||||
|  | 		set.proxyAccount = ps.proxyAccount; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.maintainerName !== undefined) { | ||||||
|  | 		set['maintainer.name'] = ps.maintainerName; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.maintainerEmail !== undefined) { | ||||||
|  | 		set['maintainer.email'] = ps.maintainerEmail; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.langs !== undefined) { | ||||||
|  | 		set.langs = ps.langs; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.enableTwitterIntegration !== undefined) { | ||||||
|  | 		set.enableTwitterIntegration = ps.enableTwitterIntegration; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.twitterConsumerKey !== undefined) { | ||||||
|  | 		set.twitterConsumerKey = ps.twitterConsumerKey; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.twitterConsumerSecret !== undefined) { | ||||||
|  | 		set.twitterConsumerSecret = ps.twitterConsumerSecret; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.enableGithubIntegration !== undefined) { | ||||||
|  | 		set.enableGithubIntegration = ps.enableGithubIntegration; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.githubClientId !== undefined) { | ||||||
|  | 		set.githubClientId = ps.githubClientId; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (ps.githubClientSecret !== undefined) { | ||||||
|  | 		set.githubClientSecret = ps.githubClientSecret; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	await Meta.update({}, { | 	await Meta.update({}, { | ||||||
| 		$set: set | 		$set: set | ||||||
| 	}, { upsert: true }); | 	}, { upsert: true }); | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| import Note from '../../../../models/note'; | import Note from '../../../../models/note'; | ||||||
| import Meta from '../../../../models/meta'; |  | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
|  | import fetchMeta from '../../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| export const meta = { | export const meta = { | ||||||
| 	requireCredential: false, | 	requireCredential: false, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default define(meta, (ps) => new Promise(async (res, rej) => { | export default define(meta, (ps) => new Promise(async (res, rej) => { | ||||||
| 	const meta = await Meta.findOne({}); | 	const instance = await fetchMeta(); | ||||||
| 	const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : []; | 	const hidedTags = instance.hidedTags.map(t => t.toLowerCase()); | ||||||
|  |  | ||||||
| 	const span = 1000 * 60 * 60 * 24 * 7; // 1週間 | 	const span = 1000 * 60 * 60 * 24 * 7; // 1週間 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import DriveFile from '../../../models/drive-file'; | import DriveFile from '../../../models/drive-file'; | ||||||
| import config from '../../../config'; |  | ||||||
| import define from '../define'; | import define from '../define'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| export const meta = { | export const meta = { | ||||||
| 	desc: { | 	desc: { | ||||||
| @@ -14,6 +14,8 @@ export const meta = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export default define(meta, (ps, user) => new Promise(async (res, rej) => { | export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||||
|  | 	const instance = await fetchMeta(); | ||||||
|  |  | ||||||
| 	// Calculate drive usage | 	// Calculate drive usage | ||||||
| 	const usage = await DriveFile | 	const usage = await DriveFile | ||||||
| 		.aggregate([{ | 		.aggregate([{ | ||||||
| @@ -39,7 +41,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { | |||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 	res({ | 	res({ | ||||||
| 		capacity: 1024 * 1024 * config.localDriveCapacityMb, | 		capacity: 1024 * 1024 * instance.localDriveCapacityMb, | ||||||
| 		usage: usage | 		usage: usage | ||||||
| 	}); | 	}); | ||||||
| })); | })); | ||||||
|   | |||||||
| @@ -32,8 +32,9 @@ export const meta = { | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		isSensitive: { | 		isSensitive: { | ||||||
| 			validator: $.bool.optional, | 			validator: $.or($.bool, $.str).optional, | ||||||
| 			default: false, | 			default: false, | ||||||
|  | 			transform: (v: any): boolean => v === true || v === 'true', | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', | 				'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', | ||||||
| 				'en-US': 'Whether this media is NSFW' | 				'en-US': 'Whether this media is NSFW' | ||||||
| @@ -41,8 +42,9 @@ export const meta = { | |||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		force: { | 		force: { | ||||||
| 			validator: $.bool.optional, | 			validator: $.or($.bool, $.str).optional, | ||||||
| 			default: false, | 			default: false, | ||||||
|  | 			transform: (v: any): boolean => v === true || v === 'true', | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': 'true にすると、同じハッシュを持つファイルが既にアップロードされていても強制的にファイルを作成します。', | 				'ja-JP': 'true にすると、同じハッシュを持つファイルが既にアップロードされていても強制的にファイルを作成します。', | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ export const meta = { | |||||||
| export default define(meta, (ps, user) => new Promise(async (res, rej) => { | export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||||
| 	const files = await DriveFile | 	const files = await DriveFile | ||||||
| 		.find({ | 		.find({ | ||||||
| 			filename: name, | 			filename: ps.name, | ||||||
| 			'metadata.userId': user._id, | 			'metadata.userId': user._id, | ||||||
| 			'metadata.folderId': ps.folderId | 			'metadata.folderId': ps.folderId | ||||||
| 		}); | 		}); | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ export const meta = { | |||||||
|  |  | ||||||
| 	limit: { | 	limit: { | ||||||
| 		duration: ms('1hour'), | 		duration: ms('1hour'), | ||||||
| 		max: 10 | 		max: 60 | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	requireCredential: true, | 	requireCredential: true, | ||||||
| @@ -29,9 +29,26 @@ export const meta = { | |||||||
| 			default: null as any as any, | 			default: null as any as any, | ||||||
| 			transform: transform | 			transform: transform | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
|  | 		isSensitive: { | ||||||
|  | 			validator: $.bool.optional, | ||||||
|  | 			default: false, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', | ||||||
|  | 				'en-US': 'Whether this media is NSFW' | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		force: { | ||||||
|  | 			validator: $.bool.optional, | ||||||
|  | 			default: false, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': 'true にすると、同じハッシュを持つファイルが既にアップロードされていても強制的にファイルを作成します。', | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default define(meta, (ps, user) => new Promise(async (res, rej) => { | export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||||
| 	res(pack(await uploadFromUrl(ps.url, user, ps.folderId))); | 	res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force))); | ||||||
| })); | })); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import Note from '../../../../models/note'; | import Note from '../../../../models/note'; | ||||||
| import { erase } from '../../../../prelude/array'; | import { erase } from '../../../../prelude/array'; | ||||||
| import Meta from '../../../../models/meta'; |  | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
|  | import fetchMeta from '../../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| /* | /* | ||||||
| トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 | トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 | ||||||
| @@ -20,8 +20,8 @@ export const meta = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export default define(meta, () => new Promise(async (res, rej) => { | export default define(meta, () => new Promise(async (res, rej) => { | ||||||
| 	const meta = await Meta.findOne({}); | 	const instance = await fetchMeta(); | ||||||
| 	const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : []; | 	const hidedTags = instance.hidedTags.map(t => t.toLowerCase()); | ||||||
|  |  | ||||||
| 	//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計 | 	//#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計 | ||||||
| 	const data = await Note.aggregate([{ | 	const data = await Note.aggregate([{ | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import $ from 'cafy'; | import $ from 'cafy'; | ||||||
| import * as os from 'os'; | import * as os from 'os'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import Meta from '../../../models/meta'; |  | ||||||
| import Emoji from '../../../models/emoji'; | import Emoji from '../../../models/emoji'; | ||||||
| import define from '../define'; | import define from '../define'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| const pkg = require('../../../../package.json'); | const pkg = require('../../../../package.json'); | ||||||
| const client = require('../../../../built/client/meta.json'); | const client = require('../../../../built/client/meta.json'); | ||||||
| @@ -27,7 +27,7 @@ export const meta = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export default define(meta, (ps, me) => new Promise(async (res, rej) => { | export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||||
| 	const met: any = (await Meta.findOne()) || {}; | 	const instance = await fetchMeta(); | ||||||
|  |  | ||||||
| 	const emojis = await Emoji.find({ host: null }, { | 	const emojis = await Emoji.find({ host: null }, { | ||||||
| 		fields: { | 		fields: { | ||||||
| @@ -35,14 +35,15 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | |||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	res({ | 	const response: any = { | ||||||
| 		maintainer: config.maintainer, | 		maintainer: instance.maintainer, | ||||||
|  |  | ||||||
| 		version: pkg.version, | 		version: pkg.version, | ||||||
| 		clientVersion: client.version, | 		clientVersion: client.version, | ||||||
|  |  | ||||||
| 		name: met.name || 'Misskey', | 		name: instance.name, | ||||||
| 		description: met.description, | 		description: instance.description, | ||||||
|  | 		langs: instance.langs, | ||||||
|  |  | ||||||
| 		secure: config.https != null, | 		secure: config.https != null, | ||||||
| 		machine: os.hostname(), | 		machine: os.hostname(), | ||||||
| @@ -54,28 +55,40 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | |||||||
| 			cores: os.cpus().length | 			cores: os.cpus().length | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		broadcasts: met.broadcasts || [], | 		broadcasts: instance.broadcasts || [], | ||||||
| 		disableRegistration: met.disableRegistration, | 		disableRegistration: instance.disableRegistration, | ||||||
| 		disableLocalTimeline: met.disableLocalTimeline, | 		disableLocalTimeline: instance.disableLocalTimeline, | ||||||
| 		driveCapacityPerLocalUserMb: config.localDriveCapacityMb, | 		driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, | ||||||
| 		recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, | 		driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, | ||||||
|  | 		cacheRemoteFiles: instance.cacheRemoteFiles, | ||||||
|  | 		enableRecaptcha: instance.enableRecaptcha, | ||||||
|  | 		recaptchaSiteKey: instance.recaptchaSiteKey, | ||||||
| 		swPublickey: config.sw ? config.sw.public_key : null, | 		swPublickey: config.sw ? config.sw.public_key : null, | ||||||
| 		hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined, | 		bannerUrl: instance.bannerUrl, | ||||||
| 		bannerUrl: met.bannerUrl, | 		maxNoteTextLength: instance.maxNoteTextLength, | ||||||
| 		maxNoteTextLength: met.maxNoteTextLength || 1000, |  | ||||||
|  |  | ||||||
| 		emojis: emojis, | 		emojis: emojis, | ||||||
|  | 	}; | ||||||
|  |  | ||||||
| 		features: ps.detail ? { | 	if (ps.detail) { | ||||||
| 			registration: !met.disableRegistration, | 		response.features = { | ||||||
| 			localTimeLine: !met.disableLocalTimeline, | 			registration: !instance.disableRegistration, | ||||||
|  | 			localTimeLine: !instance.disableLocalTimeline, | ||||||
| 			elasticsearch: config.elasticsearch ? true : false, | 			elasticsearch: config.elasticsearch ? true : false, | ||||||
| 			recaptcha: config.recaptcha ? true : false, | 			recaptcha: instance.enableRecaptcha, | ||||||
| 			objectStorage: config.drive && config.drive.storage === 'minio', | 			objectStorage: config.drive && config.drive.storage === 'minio', | ||||||
| 			twitter: config.twitter ? true : false, | 			twitter: instance.enableTwitterIntegration, | ||||||
| 			github: config.github ? true : false, | 			github: instance.enableGithubIntegration, | ||||||
| 			serviceWorker: config.sw ? true : false, | 			serviceWorker: config.sw ? true : false, | ||||||
| 			userRecommendation: config.user_recommendation ? config.user_recommendation : {} | 			userRecommendation: config.user_recommendation ? config.user_recommendation : {} | ||||||
| 		} : undefined | 		}; | ||||||
| 	}); | 	} | ||||||
|  |  | ||||||
|  | 	if (me && me.isAdmin) { | ||||||
|  | 		response.hidedTags = instance.hidedTags; | ||||||
|  | 		response.recaptchaSecretKey = instance.recaptchaSecretKey; | ||||||
|  | 		response.proxyAccount = instance.proxyAccount; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	res(response); | ||||||
| })); | })); | ||||||
|   | |||||||
| @@ -6,13 +6,13 @@ import User, { IUser } from '../../../../models/user'; | |||||||
| import DriveFile, { IDriveFile } from '../../../../models/drive-file'; | import DriveFile, { IDriveFile } from '../../../../models/drive-file'; | ||||||
| import create from '../../../../services/note/create'; | import create from '../../../../services/note/create'; | ||||||
| import define from '../../define'; | import define from '../../define'; | ||||||
| import Meta from '../../../../models/meta'; | import fetchMeta from '../../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| let maxNoteTextLength = 1000; | let maxNoteTextLength = 1000; | ||||||
|  |  | ||||||
| setInterval(() => { | setInterval(() => { | ||||||
| 	Meta.findOne({}).then(m => { | 	fetchMeta().then(m => { | ||||||
| 		if (m.maxNoteTextLength) maxNoteTextLength = m.maxNoteTextLength; | 		maxNoteTextLength = m.maxNoteTextLength; | ||||||
| 	}); | 	}); | ||||||
| }, 3000); | }, 3000); | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								src/server/api/endpoints/notes/watching/create.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/server/api/endpoints/notes/watching/create.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; | ||||||
|  | import Note from '../../../../../models/note'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import watch from '../../../../../services/note/watch'; | ||||||
|  |  | ||||||
|  | export const meta = { | ||||||
|  | 	stability: 'stable', | ||||||
|  |  | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定した投稿をウォッチします。', | ||||||
|  | 		'en-US': 'Watch a note.' | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	requireCredential: true, | ||||||
|  |  | ||||||
|  | 	kind: 'account-write', | ||||||
|  |  | ||||||
|  | 	params: { | ||||||
|  | 		noteId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			transform: transform, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象の投稿のID', | ||||||
|  | 				'en-US': 'Target note ID.' | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||||
|  | 	// Get note | ||||||
|  | 	const note = await Note.findOne({ | ||||||
|  | 		_id: ps.noteId | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	if (note === null) { | ||||||
|  | 		return rej('note not found'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	await watch(user._id, note); | ||||||
|  |  | ||||||
|  | 	// Send response | ||||||
|  | 	res(); | ||||||
|  | })); | ||||||
							
								
								
									
										44
									
								
								src/server/api/endpoints/notes/watching/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/server/api/endpoints/notes/watching/delete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; | ||||||
|  | import Note from '../../../../../models/note'; | ||||||
|  | import define from '../../../define'; | ||||||
|  | import unwatch from '../../../../../services/note/unwatch'; | ||||||
|  |  | ||||||
|  | export const meta = { | ||||||
|  | 	stability: 'stable', | ||||||
|  |  | ||||||
|  | 	desc: { | ||||||
|  | 		'ja-JP': '指定した投稿のウォッチを解除します。', | ||||||
|  | 		'en-US': 'Unwatch a note.' | ||||||
|  | 	}, | ||||||
|  |  | ||||||
|  | 	requireCredential: true, | ||||||
|  |  | ||||||
|  | 	kind: 'account-write', | ||||||
|  |  | ||||||
|  | 	params: { | ||||||
|  | 		noteId: { | ||||||
|  | 			validator: $.type(ID), | ||||||
|  | 			transform: transform, | ||||||
|  | 			desc: { | ||||||
|  | 				'ja-JP': '対象の投稿のID', | ||||||
|  | 				'en-US': 'Target note ID.' | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||||
|  | 	// Get note | ||||||
|  | 	const note = await Note.findOne({ | ||||||
|  | 		_id: ps.noteId | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	if (note === null) { | ||||||
|  | 		return rej('note not found'); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	await unwatch(user._id, note); | ||||||
|  |  | ||||||
|  | 	// Send response | ||||||
|  | 	res(); | ||||||
|  | })); | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import Meta from '../../../models/meta'; |  | ||||||
| import define from '../define'; | import define from '../define'; | ||||||
| import driveChart from '../../../chart/drive'; | import driveChart from '../../../chart/drive'; | ||||||
| import federationChart from '../../../chart/federation'; | import federationChart from '../../../chart/federation'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| export const meta = { | export const meta = { | ||||||
| 	requireCredential: false, | 	requireCredential: false, | ||||||
| @@ -15,9 +15,9 @@ export const meta = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export default define(meta, () => new Promise(async (res, rej) => { | export default define(meta, () => new Promise(async (res, rej) => { | ||||||
| 	const meta = await Meta.findOne(); | 	const instance = await fetchMeta(); | ||||||
|  |  | ||||||
| 	const stats: any = meta ? meta.stats : {}; | 	const stats: any = instance.stats; | ||||||
|  |  | ||||||
| 	const driveStats = await driveChart.getChart('hour', 1); | 	const driveStats = await driveChart.getChart('hour', 1); | ||||||
| 	stats.driveUsageLocal = driveStats.local.totalSize[0]; | 	stats.driveUsageLocal = driveStats.local.totalSize[0]; | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; | import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id'; | ||||||
| import UserList from '../../../../../models/user-list'; | import UserList from '../../../../../models/user-list'; | ||||||
| import User, { pack as packUser, isRemoteUser, getGhost } from '../../../../../models/user'; | import User, { pack as packUser, isRemoteUser, fetchProxyAccount } from '../../../../../models/user'; | ||||||
| import { publishUserListStream } from '../../../../../stream'; | import { publishUserListStream } from '../../../../../stream'; | ||||||
| import ap from '../../../../../remote/activitypub/renderer'; | import ap from '../../../../../remote/activitypub/renderer'; | ||||||
| import renderFollow from '../../../../../remote/activitypub/renderer/follow'; | import renderFollow from '../../../../../remote/activitypub/renderer/follow'; | ||||||
| @@ -71,8 +71,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | |||||||
|  |  | ||||||
| 	// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする | 	// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする | ||||||
| 	if (isRemoteUser(user)) { | 	if (isRemoteUser(user)) { | ||||||
| 		const ghost = await getGhost(); | 		const proxy = await fetchProxyAccount(); | ||||||
| 		const content = ap(renderFollow(ghost, user)); | 		const content = ap(renderFollow(proxy, user)); | ||||||
| 		deliver(ghost, content, user.inbox); | 		deliver(proxy, content, user.inbox); | ||||||
| 	} | 	} | ||||||
| })); | })); | ||||||
|   | |||||||
| @@ -44,6 +44,7 @@ router.post('/signup', require('./private/signup').default); | |||||||
| router.post('/signin', require('./private/signin').default); | router.post('/signin', require('./private/signin').default); | ||||||
|  |  | ||||||
| router.use(require('./service/github').routes()); | router.use(require('./service/github').routes()); | ||||||
|  | router.use(require('./service/github-bot').routes()); | ||||||
| router.use(require('./service/twitter').routes()); | router.use(require('./service/twitter').routes()); | ||||||
|  |  | ||||||
| router.use(require('./mastodon').routes()); | router.use(require('./mastodon').routes()); | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ import * as Router from 'koa-router'; | |||||||
| import User from '../../../models/user'; | import User from '../../../models/user'; | ||||||
| import { toASCII } from 'punycode'; | import { toASCII } from 'punycode'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import Meta from '../../../models/meta'; |  | ||||||
| import { ObjectID } from 'bson'; | import { ObjectID } from 'bson'; | ||||||
| import Emoji from '../../../models/emoji'; | import Emoji from '../../../models/emoji'; | ||||||
| import { toMastodonEmojis } from './emoji'; | import { toMastodonEmojis } from './emoji'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
| const pkg = require('../../../../package.json'); | const pkg = require('../../../../package.json'); | ||||||
|  |  | ||||||
| // Init router | // Init router | ||||||
| @@ -19,11 +19,8 @@ router.get('/v1/custom_emojis', async ctx => ctx.body = | |||||||
| 	})).map(x => toMastodonEmojis(x))); | 	})).map(x => toMastodonEmojis(x))); | ||||||
|  |  | ||||||
| router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods! | router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods! | ||||||
| 	const meta = await Meta.findOne() || {}; | 	const meta = await fetchMeta(); | ||||||
| 	const { originalNotesCount, originalUsersCount } = meta.stats || { | 	const { originalNotesCount, originalUsersCount } = meta.stats; | ||||||
| 		originalNotesCount: 0, |  | ||||||
| 		originalUsersCount: 0 |  | ||||||
| 	}; |  | ||||||
| 	const domains = await User.distinct('host', { host: { $ne: null } }) as any as [] || []; | 	const domains = await User.distinct('host', { host: { $ne: null } }) as any as [] || []; | ||||||
| 	const maintainer = await User.findOne({ isAdmin: true }) || { | 	const maintainer = await User.findOne({ isAdmin: true }) || { | ||||||
| 		_id: ObjectID.createFromTime(0), | 		_id: ObjectID.createFromTime(0), | ||||||
| @@ -51,7 +48,7 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement | |||||||
| 		uri: config.hostname, | 		uri: config.hostname, | ||||||
| 		title: meta.name || 'Misskey', | 		title: meta.name || 'Misskey', | ||||||
| 		description: meta.description || '', | 		description: meta.description || '', | ||||||
| 		email: config.maintainer.email || config.maintainer.url.startsWith('mailto:') ? config.maintainer.url.slice(7) : '', | 		email: meta.maintainer.email, | ||||||
| 		version: `0.0.0:compatible:misskey:${pkg.version}`, // TODO: How to tell about that this is an api for compatibility? | 		version: `0.0.0:compatible:misskey:${pkg.version}`, // TODO: How to tell about that this is an api for compatibility? | ||||||
| 		thumbnail: meta.bannerUrl, | 		thumbnail: meta.bannerUrl, | ||||||
| 		/* | 		/* | ||||||
| @@ -63,7 +60,7 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement | |||||||
| 			status_count: originalNotesCount, | 			status_count: originalNotesCount, | ||||||
| 			domain_count: domains.length | 			domain_count: domains.length | ||||||
| 		}, | 		}, | ||||||
| 		languages: config.languages || [ 'ja' ], | 		languages: meta.langs || [ 'ja' ], | ||||||
| 		contact_account: { | 		contact_account: { | ||||||
| 			id: maintainer._id, | 			id: maintainer._id, | ||||||
| 			username: maintainer.username, | 			username: maintainer.username, | ||||||
|   | |||||||
| @@ -1,26 +1,28 @@ | |||||||
| import * as Koa from 'koa'; | import * as Koa from 'koa'; | ||||||
| import * as bcrypt from 'bcryptjs'; | import * as bcrypt from 'bcryptjs'; | ||||||
| import { generate as generateKeypair } from '../../../crypto_key'; | import { generate as generateKeypair } from '../../../crypto_key'; | ||||||
| const recaptcha = require('recaptcha-promise'); |  | ||||||
| import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user'; | import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user'; | ||||||
| import generateUserToken from '../common/generate-native-user-token'; | import generateUserToken from '../common/generate-native-user-token'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import Meta from '../../../models/meta'; | import Meta from '../../../models/meta'; | ||||||
| import RegistrationTicket from '../../../models/registration-tickets'; | import RegistrationTicket from '../../../models/registration-tickets'; | ||||||
| import usersChart from '../../../chart/users'; | import usersChart from '../../../chart/users'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
| if (config.recaptcha) { |  | ||||||
| 	recaptcha.init({ |  | ||||||
| 		secret_key: config.recaptcha.secret_key |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export default async (ctx: Koa.Context) => { | export default async (ctx: Koa.Context) => { | ||||||
| 	const body = ctx.request.body as any; | 	const body = ctx.request.body as any; | ||||||
|  |  | ||||||
|  | 	const instance = await fetchMeta(); | ||||||
|  |  | ||||||
|  | 	const recaptcha = require('recaptcha-promise'); | ||||||
|  |  | ||||||
| 	// Verify recaptcha | 	// Verify recaptcha | ||||||
| 	// ただしテスト時はこの機構は障害となるため無効にする | 	// ただしテスト時はこの機構は障害となるため無効にする | ||||||
| 	if (process.env.NODE_ENV !== 'test' && config.recaptcha != null) { | 	if (process.env.NODE_ENV !== 'test' && instance.enableRecaptcha) { | ||||||
|  | 		recaptcha.init({ | ||||||
|  | 			secret_key: instance.recaptchaSecretKey | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		const success = await recaptcha(body['g-recaptcha-response']); | 		const success = await recaptcha(body['g-recaptcha-response']); | ||||||
|  |  | ||||||
| 		if (!success) { | 		if (!success) { | ||||||
| @@ -33,9 +35,7 @@ export default async (ctx: Koa.Context) => { | |||||||
| 	const password = body['password']; | 	const password = body['password']; | ||||||
| 	const invitationCode = body['invitationCode']; | 	const invitationCode = body['invitationCode']; | ||||||
|  |  | ||||||
| 	const meta = await Meta.findOne({}); | 	if (instance && instance.disableRegistration) { | ||||||
|  |  | ||||||
| 	if (meta && meta.disableRegistration) { |  | ||||||
| 		if (invitationCode == null || typeof invitationCode != 'string') { | 		if (invitationCode == null || typeof invitationCode != 'string') { | ||||||
| 			ctx.status = 400; | 			ctx.status = 400; | ||||||
| 			return; | 			return; | ||||||
| @@ -67,6 +67,8 @@ export default async (ctx: Koa.Context) => { | |||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const usersCount = await User.count({}); | ||||||
|  |  | ||||||
| 	// Fetch exist user that same username | 	// Fetch exist user that same username | ||||||
| 	const usernameExist = await User | 	const usernameExist = await User | ||||||
| 		.count({ | 		.count({ | ||||||
| @@ -104,17 +106,12 @@ export default async (ctx: Koa.Context) => { | |||||||
| 		host: null, | 		host: null, | ||||||
| 		keypair: generateKeypair(), | 		keypair: generateKeypair(), | ||||||
| 		token: secret, | 		token: secret, | ||||||
| 		email: null, |  | ||||||
| 		password: hash, | 		password: hash, | ||||||
|  | 		isAdmin: config.autoAdmin && usersCount === 0, | ||||||
| 		profile: { | 		profile: { | ||||||
| 			bio: null, | 			bio: null, | ||||||
| 			birthday: null, | 			birthday: null, | ||||||
| 			blood: null, | 			location: null | ||||||
| 			gender: null, |  | ||||||
| 			handedness: null, |  | ||||||
| 			height: null, |  | ||||||
| 			location: null, |  | ||||||
| 			weight: null |  | ||||||
| 		}, | 		}, | ||||||
| 		settings: { | 		settings: { | ||||||
| 			autoWatch: false | 			autoWatch: false | ||||||
|   | |||||||
							
								
								
									
										156
									
								
								src/server/api/service/github-bot.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/server/api/service/github-bot.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | |||||||
|  | import * as EventEmitter from 'events'; | ||||||
|  | import * as Router from 'koa-router'; | ||||||
|  | import * as request from 'request'; | ||||||
|  | import User, { IUser } from '../../../models/user'; | ||||||
|  | import createNote from '../../../services/note/create'; | ||||||
|  | import config from '../../../config'; | ||||||
|  | const crypto = require('crypto'); | ||||||
|  |  | ||||||
|  | const handler = new EventEmitter(); | ||||||
|  |  | ||||||
|  | let bot: IUser; | ||||||
|  |  | ||||||
|  | const post = async (text: string, home = true) => { | ||||||
|  | 	if (bot == null) { | ||||||
|  | 		const account = await User.findOne({ | ||||||
|  | 			usernameLower: config.github_bot.username.toLowerCase() | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		if (account == null) { | ||||||
|  | 			console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); | ||||||
|  | 			return; | ||||||
|  | 		} else { | ||||||
|  | 			bot = account; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	createNote(bot, { text, visibility: home ? 'home' : 'public' }); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // Init router | ||||||
|  | const router = new Router(); | ||||||
|  |  | ||||||
|  | if (config.github_bot) { | ||||||
|  | 	const secret = config.github_bot.hook_secret; | ||||||
|  |  | ||||||
|  | 	router.post('/hooks/github', ctx => { | ||||||
|  | 		const body = JSON.stringify(ctx.request.body); | ||||||
|  | 		const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); | ||||||
|  | 		const sig1 = new Buffer(ctx.headers['x-hub-signature']); | ||||||
|  | 		const sig2 = new Buffer(`sha1=${hash}`); | ||||||
|  |  | ||||||
|  | 		// シグネチャ比較 | ||||||
|  | 		if (sig1.equals(sig2)) { | ||||||
|  | 			handler.emit(ctx.headers['x-github-event'], ctx.request.body); | ||||||
|  | 			ctx.status = 204; | ||||||
|  | 		} else { | ||||||
|  | 			ctx.status = 400; | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
|  |  | ||||||
|  | handler.on('status', event => { | ||||||
|  | 	const state = event.state; | ||||||
|  | 	switch (state) { | ||||||
|  | 		case 'error': | ||||||
|  | 		case 'failure': | ||||||
|  | 			const commit = event.commit; | ||||||
|  | 			const parent = commit.parents[0]; | ||||||
|  |  | ||||||
|  | 			// Fetch parent status | ||||||
|  | 			request({ | ||||||
|  | 				url: `${parent.url}/statuses`, | ||||||
|  | 				proxy: config.proxy, | ||||||
|  | 				headers: { | ||||||
|  | 					'User-Agent': 'misskey' | ||||||
|  | 				} | ||||||
|  | 			}, (err, res, body) => { | ||||||
|  | 				if (err) { | ||||||
|  | 					console.error(err); | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 				const parentStatuses = JSON.parse(body); | ||||||
|  | 				const parentState = parentStatuses[0].state; | ||||||
|  | 				const stillFailed = parentState == 'failure' || parentState == 'error'; | ||||||
|  | 				if (stillFailed) { | ||||||
|  | 					post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); | ||||||
|  | 				} else { | ||||||
|  | 					post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 			break; | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | handler.on('push', event => { | ||||||
|  | 	const ref = event.ref; | ||||||
|  | 	switch (ref) { | ||||||
|  | 		case 'refs/heads/master': | ||||||
|  | 			const pusher = event.pusher; | ||||||
|  | 			const compare = event.compare; | ||||||
|  | 			const commits: any[] = event.commits; | ||||||
|  | 			post([ | ||||||
|  | 				`Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, | ||||||
|  | 				commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), | ||||||
|  | 			].join('\n')); | ||||||
|  | 			break; | ||||||
|  | 		case 'refs/heads/release': | ||||||
|  | 			const commit = event.commits[0]; | ||||||
|  | 			post(`RELEASED: ${commit.message}`); | ||||||
|  | 			break; | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | handler.on('issues', event => { | ||||||
|  | 	const issue = event.issue; | ||||||
|  | 	const action = event.action; | ||||||
|  | 	let title: string; | ||||||
|  | 	switch (action) { | ||||||
|  | 		case 'opened': title = 'Issue opened'; break; | ||||||
|  | 		case 'closed': title = 'Issue closed'; break; | ||||||
|  | 		case 'reopened': title = 'Issue reopened'; break; | ||||||
|  | 		default: return; | ||||||
|  | 	} | ||||||
|  | 	post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | handler.on('issue_comment', event => { | ||||||
|  | 	const issue = event.issue; | ||||||
|  | 	const comment = event.comment; | ||||||
|  | 	const action = event.action; | ||||||
|  | 	let text: string; | ||||||
|  | 	switch (action) { | ||||||
|  | 		case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; | ||||||
|  | 		default: return; | ||||||
|  | 	} | ||||||
|  | 	post(text); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | handler.on('watch', event => { | ||||||
|  | 	const sender = event.sender; | ||||||
|  | 	post(`(((⭐️))) Starred by **${sender.login}** (((⭐️)))`, false); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | handler.on('fork', event => { | ||||||
|  | 	const repo = event.forkee; | ||||||
|  | 	post(`🍴 Forked:\n${repo.html_url} 🍴`); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | handler.on('pull_request', event => { | ||||||
|  | 	const pr = event.pull_request; | ||||||
|  | 	const action = event.action; | ||||||
|  | 	let text: string; | ||||||
|  | 	switch (action) { | ||||||
|  | 		case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; | ||||||
|  | 		case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; | ||||||
|  | 		case 'closed': | ||||||
|  | 			text = pr.merged | ||||||
|  | 				? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` | ||||||
|  | 				: `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; | ||||||
|  | 			break; | ||||||
|  | 		default: return; | ||||||
|  | 	} | ||||||
|  | 	post(text); | ||||||
|  | }); | ||||||
| @@ -1,37 +1,14 @@ | |||||||
| import * as EventEmitter from 'events'; |  | ||||||
| import * as Koa from 'koa'; | import * as Koa from 'koa'; | ||||||
| import * as Router from 'koa-router'; | import * as Router from 'koa-router'; | ||||||
| import * as request from 'request'; | import * as request from 'request'; | ||||||
| import { OAuth2 } from 'oauth'; | import { OAuth2 } from 'oauth'; | ||||||
| import User, { IUser, pack, ILocalUser } from '../../../models/user'; | import User, { pack, ILocalUser } from '../../../models/user'; | ||||||
| import createNote from '../../../services/note/create'; |  | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import { publishMainStream } from '../../../stream'; | import { publishMainStream } from '../../../stream'; | ||||||
| import redis from '../../../db/redis'; | import redis from '../../../db/redis'; | ||||||
| import uuid = require('uuid'); | import uuid = require('uuid'); | ||||||
| import signin from '../common/signin'; | import signin from '../common/signin'; | ||||||
| const crypto = require('crypto'); | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| const handler = new EventEmitter(); |  | ||||||
|  |  | ||||||
| let bot: IUser; |  | ||||||
|  |  | ||||||
| const post = async (text: string, home = true) => { |  | ||||||
| 	if (bot == null) { |  | ||||||
| 		const account = await User.findOne({ |  | ||||||
| 			usernameLower: config.github_bot.username.toLowerCase() |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		if (account == null) { |  | ||||||
| 			console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); |  | ||||||
| 			return; |  | ||||||
| 		} else { |  | ||||||
| 			bot = account; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	createNote(bot, { text, visibility: home ? 'home' : 'public' }); |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| function getUserToken(ctx: Koa.Context) { | function getUserToken(ctx: Koa.Context) { | ||||||
| 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | ||||||
| @@ -80,23 +57,22 @@ router.get('/disconnect/github', async ctx => { | |||||||
| 	})); | 	})); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| if (!config.github || !redis) { | async function getOath2() { | ||||||
| 	router.get('/connect/github', ctx => { | 	const meta = await fetchMeta(); | ||||||
| 		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)'; |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	router.get('/signin/github', ctx => { | 	if (meta.enableGithubIntegration) { | ||||||
| 		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)'; | 		return new OAuth2( | ||||||
| 	}); | 			meta.githubClientId, | ||||||
| } else { | 			meta.githubClientSecret, | ||||||
| 	const oauth2 = new OAuth2( |  | ||||||
| 		config.github.client_id, |  | ||||||
| 		config.github.client_secret, |  | ||||||
| 			'https://github.com/', | 			'https://github.com/', | ||||||
| 			'login/oauth/authorize', | 			'login/oauth/authorize', | ||||||
| 			'login/oauth/access_token'); | 			'login/oauth/access_token'); | ||||||
|  | 	} else { | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| 	router.get('/connect/github', async ctx => { | router.get('/connect/github', async ctx => { | ||||||
| 	if (!compareOrigin(ctx)) { | 	if (!compareOrigin(ctx)) { | ||||||
| 		ctx.throw(400, 'invalid origin'); | 		ctx.throw(400, 'invalid origin'); | ||||||
| 		return; | 		return; | ||||||
| @@ -115,10 +91,12 @@ if (!config.github || !redis) { | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	redis.set(userToken, JSON.stringify(params)); | 	redis.set(userToken, JSON.stringify(params)); | ||||||
| 		ctx.redirect(oauth2.getAuthorizeUrl(params)); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	router.get('/signin/github', async ctx => { | 	const oauth2 = await getOath2(); | ||||||
|  | 	ctx.redirect(oauth2.getAuthorizeUrl(params)); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | router.get('/signin/github', async ctx => { | ||||||
| 	const sessid = uuid(); | 	const sessid = uuid(); | ||||||
|  |  | ||||||
| 	const params = { | 	const params = { | ||||||
| @@ -138,12 +116,16 @@ if (!config.github || !redis) { | |||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	redis.set(sessid, JSON.stringify(params)); | 	redis.set(sessid, JSON.stringify(params)); | ||||||
| 		ctx.redirect(oauth2.getAuthorizeUrl(params)); |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	router.get('/gh/cb', async ctx => { | 	const oauth2 = await getOath2(); | ||||||
|  | 	ctx.redirect(oauth2.getAuthorizeUrl(params)); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | router.get('/gh/cb', async ctx => { | ||||||
| 	const userToken = getUserToken(ctx); | 	const userToken = getUserToken(ctx); | ||||||
|  |  | ||||||
|  | 	const oauth2 = await getOath2(); | ||||||
|  |  | ||||||
| 	if (!userToken) { | 	if (!userToken) { | ||||||
| 		const sessid = ctx.cookies.get('signin_with_github_session_id'); | 		const sessid = ctx.cookies.get('signin_with_github_session_id'); | ||||||
|  |  | ||||||
| @@ -287,130 +269,6 @@ if (!config.github || !redis) { | |||||||
| 			includeSecrets: true | 			includeSecrets: true | ||||||
| 		})); | 		})); | ||||||
| 	} | 	} | ||||||
| 	}); | }); | ||||||
| } |  | ||||||
|  |  | ||||||
| if (config.github_bot) { |  | ||||||
| 	const secret = config.github_bot.hook_secret; |  | ||||||
|  |  | ||||||
| 	router.post('/hooks/github', ctx => { |  | ||||||
| 		const body = JSON.stringify(ctx.request.body); |  | ||||||
| 		const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); |  | ||||||
| 		const sig1 = new Buffer(ctx.headers['x-hub-signature']); |  | ||||||
| 		const sig2 = new Buffer(`sha1=${hash}`); |  | ||||||
|  |  | ||||||
| 		// シグネチャ比較 |  | ||||||
| 		if (sig1.equals(sig2)) { |  | ||||||
| 			handler.emit(ctx.headers['x-github-event'], ctx.request.body); |  | ||||||
| 			ctx.status = 204; |  | ||||||
| 		} else { |  | ||||||
| 			ctx.status = 400; |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
|  |  | ||||||
| handler.on('status', event => { |  | ||||||
| 	const state = event.state; |  | ||||||
| 	switch (state) { |  | ||||||
| 		case 'error': |  | ||||||
| 		case 'failure': |  | ||||||
| 			const commit = event.commit; |  | ||||||
| 			const parent = commit.parents[0]; |  | ||||||
|  |  | ||||||
| 			// Fetch parent status |  | ||||||
| 			request({ |  | ||||||
| 				url: `${parent.url}/statuses`, |  | ||||||
| 				proxy: config.proxy, |  | ||||||
| 				headers: { |  | ||||||
| 					'User-Agent': 'misskey' |  | ||||||
| 				} |  | ||||||
| 			}, (err, res, body) => { |  | ||||||
| 				if (err) { |  | ||||||
| 					console.error(err); |  | ||||||
| 					return; |  | ||||||
| 				} |  | ||||||
| 				const parentStatuses = JSON.parse(body); |  | ||||||
| 				const parentState = parentStatuses[0].state; |  | ||||||
| 				const stillFailed = parentState == 'failure' || parentState == 'error'; |  | ||||||
| 				if (stillFailed) { |  | ||||||
| 					post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); |  | ||||||
| 				} else { |  | ||||||
| 					post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 			break; |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| handler.on('push', event => { |  | ||||||
| 	const ref = event.ref; |  | ||||||
| 	switch (ref) { |  | ||||||
| 		case 'refs/heads/master': |  | ||||||
| 			const pusher = event.pusher; |  | ||||||
| 			const compare = event.compare; |  | ||||||
| 			const commits: any[] = event.commits; |  | ||||||
| 			post([ |  | ||||||
| 				`Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, |  | ||||||
| 				commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), |  | ||||||
| 			].join('\n')); |  | ||||||
| 			break; |  | ||||||
| 		case 'refs/heads/release': |  | ||||||
| 			const commit = event.commits[0]; |  | ||||||
| 			post(`RELEASED: ${commit.message}`); |  | ||||||
| 			break; |  | ||||||
| 	} |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| handler.on('issues', event => { |  | ||||||
| 	const issue = event.issue; |  | ||||||
| 	const action = event.action; |  | ||||||
| 	let title: string; |  | ||||||
| 	switch (action) { |  | ||||||
| 		case 'opened': title = 'Issue opened'; break; |  | ||||||
| 		case 'closed': title = 'Issue closed'; break; |  | ||||||
| 		case 'reopened': title = 'Issue reopened'; break; |  | ||||||
| 		default: return; |  | ||||||
| 	} |  | ||||||
| 	post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| handler.on('issue_comment', event => { |  | ||||||
| 	const issue = event.issue; |  | ||||||
| 	const comment = event.comment; |  | ||||||
| 	const action = event.action; |  | ||||||
| 	let text: string; |  | ||||||
| 	switch (action) { |  | ||||||
| 		case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; |  | ||||||
| 		default: return; |  | ||||||
| 	} |  | ||||||
| 	post(text); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| handler.on('watch', event => { |  | ||||||
| 	const sender = event.sender; |  | ||||||
| 	post(`(((⭐️))) Starred by **${sender.login}** (((⭐️)))`, false); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| handler.on('fork', event => { |  | ||||||
| 	const repo = event.forkee; |  | ||||||
| 	post(`🍴 Forked:\n${repo.html_url} 🍴`); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| handler.on('pull_request', event => { |  | ||||||
| 	const pr = event.pull_request; |  | ||||||
| 	const action = event.action; |  | ||||||
| 	let text: string; |  | ||||||
| 	switch (action) { |  | ||||||
| 		case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; |  | ||||||
| 		case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; |  | ||||||
| 		case 'closed': |  | ||||||
| 			text = pr.merged |  | ||||||
| 				? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` |  | ||||||
| 				: `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; |  | ||||||
| 			break; |  | ||||||
| 		default: return; |  | ||||||
| 	} |  | ||||||
| 	post(text); |  | ||||||
| }); |  | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import User, { pack, ILocalUser } from '../../../models/user'; | |||||||
| import { publishMainStream } from '../../../stream'; | import { publishMainStream } from '../../../stream'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import signin from '../common/signin'; | import signin from '../common/signin'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| function getUserToken(ctx: Koa.Context) { | function getUserToken(ctx: Koa.Context) { | ||||||
| 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | ||||||
| @@ -55,22 +56,21 @@ router.get('/disconnect/twitter', async ctx => { | |||||||
| 	})); | 	})); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| if (config.twitter == null || redis == null) { | async function getTwAuth() { | ||||||
| 	router.get('/connect/twitter', ctx => { | 	const meta = await fetchMeta(); | ||||||
| 		ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	router.get('/signin/twitter', ctx => { | 	if (meta.enableTwitterIntegration) { | ||||||
| 		ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; | 		return autwh({ | ||||||
| 	}); | 			consumerKey: meta.twitterConsumerKey, | ||||||
| } else { | 			consumerSecret: meta.twitterConsumerSecret, | ||||||
| 	const twAuth = autwh({ |  | ||||||
| 		consumerKey: config.twitter.consumer_key, |  | ||||||
| 		consumerSecret: config.twitter.consumer_secret, |  | ||||||
| 			callbackUrl: `${config.url}/api/tw/cb` | 			callbackUrl: `${config.url}/api/tw/cb` | ||||||
| 		}); | 		}); | ||||||
|  | 	} else { | ||||||
|  | 		return null; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| 	router.get('/connect/twitter', async ctx => { | router.get('/connect/twitter', async ctx => { | ||||||
| 	if (!compareOrigin(ctx)) { | 	if (!compareOrigin(ctx)) { | ||||||
| 		ctx.throw(400, 'invalid origin'); | 		ctx.throw(400, 'invalid origin'); | ||||||
| 		return; | 		return; | ||||||
| @@ -82,12 +82,14 @@ if (config.twitter == null || redis == null) { | |||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const twAuth = await getTwAuth(); | ||||||
| 	const twCtx = await twAuth.begin(); | 	const twCtx = await twAuth.begin(); | ||||||
| 	redis.set(userToken, JSON.stringify(twCtx)); | 	redis.set(userToken, JSON.stringify(twCtx)); | ||||||
| 	ctx.redirect(twCtx.url); | 	ctx.redirect(twCtx.url); | ||||||
| 	}); | }); | ||||||
|  |  | ||||||
| 	router.get('/signin/twitter', async ctx => { | router.get('/signin/twitter', async ctx => { | ||||||
|  | 	const twAuth = await getTwAuth(); | ||||||
| 	const twCtx = await twAuth.begin(); | 	const twCtx = await twAuth.begin(); | ||||||
|  |  | ||||||
| 	const sessid = uuid(); | 	const sessid = uuid(); | ||||||
| @@ -105,11 +107,13 @@ if (config.twitter == null || redis == null) { | |||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	ctx.redirect(twCtx.url); | 	ctx.redirect(twCtx.url); | ||||||
| 	}); | }); | ||||||
|  |  | ||||||
| 	router.get('/tw/cb', async ctx => { | router.get('/tw/cb', async ctx => { | ||||||
| 	const userToken = getUserToken(ctx); | 	const userToken = getUserToken(ctx); | ||||||
|  |  | ||||||
|  | 	const twAuth = await getTwAuth(); | ||||||
|  |  | ||||||
| 	if (userToken == null) { | 	if (userToken == null) { | ||||||
| 		const sessid = ctx.cookies.get('signin_with_twitter_session_id'); | 		const sessid = ctx.cookies.get('signin_with_twitter_session_id'); | ||||||
|  |  | ||||||
| @@ -179,7 +183,6 @@ if (config.twitter == null || redis == null) { | |||||||
| 			includeSecrets: true | 			includeSecrets: true | ||||||
| 		})); | 		})); | ||||||
| 	} | 	} | ||||||
| 	}); | }); | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ import config from '../../config'; | |||||||
| import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; | import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; | ||||||
| import driveChart from '../../chart/drive'; | import driveChart from '../../chart/drive'; | ||||||
| import perUserDriveChart from '../../chart/per-user-drive'; | import perUserDriveChart from '../../chart/per-user-drive'; | ||||||
|  | import fetchMeta from '../../misc/fetch-meta'; | ||||||
|  |  | ||||||
| const log = debug('misskey:drive:add-file'); | const log = debug('misskey:drive:add-file'); | ||||||
|  |  | ||||||
| @@ -255,7 +256,8 @@ export default async function( | |||||||
|  |  | ||||||
| 		log(`drive usage is ${usage}`); | 		log(`drive usage is ${usage}`); | ||||||
|  |  | ||||||
| 		const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? config.localDriveCapacityMb : config.remoteDriveCapacityMb); | 		const instance = await fetchMeta(); | ||||||
|  | 		const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); | ||||||
|  |  | ||||||
| 		// If usage limit exceeded | 		// If usage limit exceeded | ||||||
| 		if (usage + size > driveCapacity) { | 		if (usage + size > driveCapacity) { | ||||||
|   | |||||||
| @@ -13,7 +13,15 @@ import * as mongodb from 'mongodb'; | |||||||
|  |  | ||||||
| const log = debug('misskey:drive:upload-from-url'); | const log = debug('misskey:drive:upload-from-url'); | ||||||
|  |  | ||||||
| export default async (url: string, user: IUser, folderId: mongodb.ObjectID = null, uri: string = null, sensitive = false): Promise<IDriveFile> => { | export default async ( | ||||||
|  | 	url: string, | ||||||
|  | 	user: IUser, | ||||||
|  | 	folderId: mongodb.ObjectID = null, | ||||||
|  | 	uri: string = null, | ||||||
|  | 	sensitive = false, | ||||||
|  | 	force = false, | ||||||
|  | 	link = false | ||||||
|  | ): Promise<IDriveFile> => { | ||||||
| 	log(`REQUESTED: ${url}`); | 	log(`REQUESTED: ${url}`); | ||||||
|  |  | ||||||
| 	let name = URL.parse(url).pathname.split('/').pop(); | 	let name = URL.parse(url).pathname.split('/').pop(); | ||||||
| @@ -34,28 +42,46 @@ export default async (url: string, user: IUser, folderId: mongodb.ObjectID = nul | |||||||
| 	// write content at URL to temp file | 	// write content at URL to temp file | ||||||
| 	await new Promise((res, rej) => { | 	await new Promise((res, rej) => { | ||||||
| 		const writable = fs.createWriteStream(path); | 		const writable = fs.createWriteStream(path); | ||||||
|  |  | ||||||
|  | 		writable.on('finish', () => { | ||||||
|  | 			res(); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		writable.on('error', error => { | ||||||
|  | 			rej(error); | ||||||
|  | 		}); | ||||||
|  |  | ||||||
| 		const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url; | 		const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url; | ||||||
| 		request({ |  | ||||||
|  | 		const req = request({ | ||||||
| 			url: requestUrl, | 			url: requestUrl, | ||||||
| 			proxy: config.proxy, | 			proxy: config.proxy, | ||||||
|  | 			timeout: 10 * 1000, | ||||||
| 			headers: { | 			headers: { | ||||||
| 				'User-Agent': config.user_agent | 				'User-Agent': config.user_agent | ||||||
| 			} | 			} | ||||||
| 		}) | 		}); | ||||||
| 			.on('error', rej) |  | ||||||
| 			.on('end', () => { | 		req.pipe(writable); | ||||||
|  |  | ||||||
|  | 		req.on('response', response => { | ||||||
|  | 			if (response.statusCode !== 200) { | ||||||
| 				writable.close(); | 				writable.close(); | ||||||
| 				res(); | 				rej(response.statusCode); | ||||||
| 			}) | 			} | ||||||
| 			.pipe(writable) | 		}); | ||||||
| 			.on('error', rej); |  | ||||||
|  | 		req.on('error', error => { | ||||||
|  | 			writable.close(); | ||||||
|  | 			rej(error); | ||||||
|  | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	let driveFile: IDriveFile; | 	let driveFile: IDriveFile; | ||||||
| 	let error; | 	let error; | ||||||
|  |  | ||||||
| 	try { | 	try { | ||||||
| 		driveFile = await create(user, path, name, null, folderId, false, config.preventCacheRemoteFiles, url, uri, sensitive); | 		driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive); | ||||||
| 		log(`got: ${driveFile._id}`); | 		log(`got: ${driveFile._id}`); | ||||||
| 	} catch (e) { | 	} catch (e) { | ||||||
| 		error = e; | 		error = e; | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								src/services/note/unwatch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/services/note/unwatch.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import * as mongodb from 'mongodb'; | ||||||
|  | import Watching from '../../models/note-watching'; | ||||||
|  |  | ||||||
|  | export default async (me: mongodb.ObjectID, note: object) => { | ||||||
|  | 	await Watching.remove({ | ||||||
|  | 		noteId: (note as any)._id, | ||||||
|  | 		userId: me | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
| @@ -1,7 +1,8 @@ | |||||||
| import * as mongo from 'mongodb'; | import * as mongo from 'mongodb'; | ||||||
| import redis from './db/redis'; | import redis from './db/redis'; | ||||||
| import Xev from 'xev'; | import Xev from 'xev'; | ||||||
| import Meta, { IMeta } from './models/meta'; | import { IMeta } from './models/meta'; | ||||||
|  | import fetchMeta from './misc/fetch-meta'; | ||||||
|  |  | ||||||
| type ID = string | mongo.ObjectID; | type ID = string | mongo.ObjectID; | ||||||
|  |  | ||||||
| @@ -16,14 +17,14 @@ class Publisher { | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		setInterval(async () => { | 		setInterval(async () => { | ||||||
| 			this.meta = await Meta.findOne({}); | 			this.meta = await fetchMeta(); | ||||||
| 		}, 5000); | 		}, 5000); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public getMeta = async () => { | 	public fetchMeta = async () => { | ||||||
| 		if (this.meta != null) return this.meta; | 		if (this.meta != null) return this.meta; | ||||||
|  |  | ||||||
| 		this.meta = await Meta.findOne({}); | 		this.meta = await fetchMeta(); | ||||||
| 		return this.meta; | 		return this.meta; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -82,13 +83,13 @@ class Publisher { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public publishLocalTimelineStream = async (note: any): Promise<void> => { | 	public publishLocalTimelineStream = async (note: any): Promise<void> => { | ||||||
| 		const meta = await this.getMeta(); | 		const meta = await this.fetchMeta(); | ||||||
| 		if (meta.disableLocalTimeline) return; | 		if (meta.disableLocalTimeline) return; | ||||||
| 		this.publish('localTimeline', null, note); | 		this.publish('localTimeline', null, note); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => { | 	public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => { | ||||||
| 		const meta = await this.getMeta(); | 		const meta = await this.fetchMeta(); | ||||||
| 		if (meta.disableLocalTimeline) return; | 		if (meta.disableLocalTimeline) return; | ||||||
| 		this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note); | 		this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note); | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								test/mfm.ts
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								test/mfm.ts
									
									
									
									
									
								
							| @@ -213,40 +213,47 @@ describe('Text', () => { | |||||||
| 		it('search', () => { | 		it('search', () => { | ||||||
| 			const tokens1 = analyze('a b c 検索'); | 			const tokens1 = analyze('a b c 検索'); | ||||||
| 			assert.deepEqual([ | 			assert.deepEqual([ | ||||||
| 				{ type: 'search', content: 'a b c 検索', query: 'a b c'} | 				{ type: 'search', content: 'a b c 検索', query: 'a b c' } | ||||||
| 			], tokens1); | 			], tokens1); | ||||||
|  |  | ||||||
| 			const tokens2 = analyze('a b c Search'); | 			const tokens2 = analyze('a b c Search'); | ||||||
| 			assert.deepEqual([ | 			assert.deepEqual([ | ||||||
| 				{ type: 'search', content: 'a b c Search', query: 'a b c'} | 				{ type: 'search', content: 'a b c Search', query: 'a b c' } | ||||||
| 			], tokens2); | 			], tokens2); | ||||||
|  |  | ||||||
| 			const tokens3 = analyze('a b c search'); | 			const tokens3 = analyze('a b c search'); | ||||||
| 			assert.deepEqual([ | 			assert.deepEqual([ | ||||||
| 				{ type: 'search', content: 'a b c search', query: 'a b c'} | 				{ type: 'search', content: 'a b c search', query: 'a b c' } | ||||||
| 			], tokens3); | 			], tokens3); | ||||||
|  |  | ||||||
| 			const tokens4 = analyze('a b c SEARCH'); | 			const tokens4 = analyze('a b c SEARCH'); | ||||||
| 			assert.deepEqual([ | 			assert.deepEqual([ | ||||||
| 				{ type: 'search', content: 'a b c SEARCH', query: 'a b c'} | 				{ type: 'search', content: 'a b c SEARCH', query: 'a b c' } | ||||||
| 			], tokens4); | 			], tokens4); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		it('title', () => { | 		it('title', () => { | ||||||
| 			const tokens1 = analyze('【yee】\nhaw'); | 			const tokens1 = analyze('【yee】\nhaw'); | ||||||
| 			assert.deepEqual( | 			assert.deepEqual( | ||||||
| 				{ type: 'title', content: '【yee】\n', title: 'yee'} | 				{ type: 'title', content: '【yee】\n', title: 'yee' } | ||||||
| 			, tokens1[0]); | 			, tokens1[0]); | ||||||
|  |  | ||||||
| 			const tokens2 = analyze('[yee]\nhaw'); | 			const tokens2 = analyze('[yee]\nhaw'); | ||||||
| 			assert.deepEqual( | 			assert.deepEqual( | ||||||
| 				{ type: 'title', content: '[yee]\n', title: 'yee'} | 				{ type: 'title', content: '[yee]\n', title: 'yee' } | ||||||
| 			, tokens2[0]); | 			, tokens2[0]); | ||||||
|  |  | ||||||
| 			const tokens3 = analyze('a [a]\nb [b]\nc [c]'); | 			const tokens3 = analyze('a [a]\nb [b]\nc [c]'); | ||||||
| 			assert.deepEqual( | 			assert.deepEqual( | ||||||
| 				{ type: 'text', content: 'a [a]\nb [b]\nc [c]' } | 				{ type: 'text', content: 'a [a]\nb [b]\nc [c]' } | ||||||
| 			, tokens3[0]); | 			, tokens3[0]); | ||||||
|  |  | ||||||
|  | 			const tokens4 = analyze('foo\n【bar】\nbuzz'); | ||||||
|  | 			assert.deepEqual([ | ||||||
|  | 				{ type: 'foo', content: 'foo\n' }, | ||||||
|  | 				{ type: 'title', content: '【bar】\n', title: 'bar' }, | ||||||
|  | 				{ type: 'foo', content: 'buzz' }, | ||||||
|  | 			], tokens4); | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user