Compare commits
	
		
			61 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9f2d8e1d51 | ||
| 
						 | 
					0c98a90b75 | ||
| 
						 | 
					0047920c1a | ||
| 
						 | 
					e4bb534f20 | ||
| 
						 | 
					3fc04fcdc5 | ||
| 
						 | 
					e542dcac30 | ||
| 
						 | 
					a0b13505a0 | ||
| 
						 | 
					389f9bfea2 | ||
| 
						 | 
					630a534cee | ||
| 
						 | 
					5744c391e6 | ||
| 
						 | 
					b9b05a7401 | ||
| 
						 | 
					359470a263 | ||
| 
						 | 
					3fe934ee62 | ||
| 
						 | 
					3abe632f06 | ||
| 
						 | 
					65961bc15b | ||
| 
						 | 
					12f932d48a | ||
| 
						 | 
					54e9147782 | ||
| 
						 | 
					31b7626d01 | ||
| 
						 | 
					200ebefe92 | ||
| 
						 | 
					9d29a2e85a | ||
| 
						 | 
					c62a225542 | ||
| 
						 | 
					d5d995a3e6 | ||
| 
						 | 
					b7f10fdc10 | ||
| 
						 | 
					cbba03b376 | ||
| 
						 | 
					f84e9c7dc8 | ||
| 
						 | 
					a22ddb1fb9 | ||
| 
						 | 
					0d23ce3d45 | ||
| 
						 | 
					9719387bee | ||
| 
						 | 
					dca110ebaa | ||
| 
						 | 
					136f23c7ad | ||
| 
						 | 
					0963e6d6e1 | ||
| 
						 | 
					712802e682 | ||
| 
						 | 
					abe99c3c73 | ||
| 
						 | 
					d7a3b71028 | ||
| 
						 | 
					10c434f24a | ||
| 
						 | 
					fe46c53ea6 | ||
| 
						 | 
					cdd123dfd3 | ||
| 
						 | 
					a1a3ee44b5 | ||
| 
						 | 
					4e7fbd8967 | ||
| 
						 | 
					a86c419f95 | ||
| 
						 | 
					e3ec0ad97e | ||
| 
						 | 
					75791981ce | ||
| 
						 | 
					e813fe16b9 | ||
| 
						 | 
					42ac7b954d | ||
| 
						 | 
					c1bbf5dab6 | ||
| 
						 | 
					e16dc2a910 | ||
| 
						 | 
					e236c05d79 | ||
| 
						 | 
					454c1e3faf | ||
| 
						 | 
					43daf814df | ||
| 
						 | 
					c40b630530 | ||
| 
						 | 
					7fc0698ecf | ||
| 
						 | 
					4f3c8b940e | ||
| 
						 | 
					1855ab60f1 | ||
| 
						 | 
					af4f1a7bd6 | ||
| 
						 | 
					8646a9c49c | ||
| 
						 | 
					8d7c033cf5 | ||
| 
						 | 
					b8900e32de | ||
| 
						 | 
					d48c25d2c9 | ||
| 
						 | 
					a87c5899c5 | ||
| 
						 | 
					147ad69864 | ||
| 
						 | 
					c146006476 | 
@@ -23,6 +23,10 @@ jobs:
 | 
			
		||||
    executor: default
 | 
			
		||||
    steps:
 | 
			
		||||
      - checkout
 | 
			
		||||
      - run:
 | 
			
		||||
          name: Ensure package-lock.json
 | 
			
		||||
          command: |
 | 
			
		||||
            [ ! -e package-lock.json ] && echo '{}' > package-lock.json
 | 
			
		||||
      - restore_cache:
 | 
			
		||||
          name: Restore npm package caches
 | 
			
		||||
          keys:
 | 
			
		||||
@@ -35,6 +39,7 @@ jobs:
 | 
			
		||||
          name: Install Dependencies
 | 
			
		||||
          command: |
 | 
			
		||||
            npm install
 | 
			
		||||
            npm prune
 | 
			
		||||
      - run:
 | 
			
		||||
          name: Configure
 | 
			
		||||
          command: |
 | 
			
		||||
@@ -50,8 +55,8 @@ jobs:
 | 
			
		||||
          key: npm-v1-arch-{{ arch }}-env-{{ .Environment.variableName }}-package-{{ checksum "package.json" }}-lock-{{ checksum "package-lock.json" }}-ls-{{ checksum "ls" }}
 | 
			
		||||
          paths:
 | 
			
		||||
            - node_modules
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: built
 | 
			
		||||
#      - store_artifacts:
 | 
			
		||||
#          path: built
 | 
			
		||||
      - persist_to_workspace:
 | 
			
		||||
          root: .
 | 
			
		||||
          paths:
 | 
			
		||||
@@ -98,7 +103,6 @@ jobs:
 | 
			
		||||
          name: Build
 | 
			
		||||
          command: |
 | 
			
		||||
            docker build . | tee docker.log
 | 
			
		||||
            tail -n 1 docker.log | read __Successfully __built tag
 | 
			
		||||
      - when:
 | 
			
		||||
          condition: <<parameters.with_deploy>>
 | 
			
		||||
          steps:
 | 
			
		||||
@@ -107,6 +111,7 @@ jobs:
 | 
			
		||||
                command: |
 | 
			
		||||
                  if [ "$DOCKERHUB_USERNAME$DOCKERHUB_PASSWORD" ]
 | 
			
		||||
                   then
 | 
			
		||||
                    tail -n 1 docker.log | read __Successfully __built tag
 | 
			
		||||
                    docker tag $tag misskey/misskey
 | 
			
		||||
                    docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD
 | 
			
		||||
                    docker push misskey/misskey
 | 
			
		||||
@@ -126,10 +131,13 @@ workflows:
 | 
			
		||||
          without_redis: "true"
 | 
			
		||||
          requires:
 | 
			
		||||
            - build
 | 
			
		||||
      - docker:
 | 
			
		||||
          filters:
 | 
			
		||||
            branches:
 | 
			
		||||
              ignore: master
 | 
			
		||||
              only: master
 | 
			
		||||
#      - docker:
 | 
			
		||||
#          filters:
 | 
			
		||||
#            branches:
 | 
			
		||||
#              ignore: master
 | 
			
		||||
      - docker:
 | 
			
		||||
          with_deploy: "true"
 | 
			
		||||
          filters:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,3 @@
 | 
			
		||||
name: example-instance-name # Name of your instance
 | 
			
		||||
description: example-description # Description of your instance
 | 
			
		||||
 | 
			
		||||
maintainer:
 | 
			
		||||
  name: example-maitainer-name # Your name
 | 
			
		||||
  url: http://example.com/ # Your contact (http or mailto)
 | 
			
		||||
@@ -25,7 +22,7 @@ url: https://example.tld/
 | 
			
		||||
#   +------+      |+-------------+      +----------------+|
 | 
			
		||||
#                 +---------------------------------------+
 | 
			
		||||
#
 | 
			
		||||
#   You need to setup reverse proxy. (eg. Nginx)
 | 
			
		||||
#   You need to setup reverse proxy. (eg. nginx)
 | 
			
		||||
#   You do not define 'https' section.
 | 
			
		||||
 | 
			
		||||
# Option 2: Standalone
 | 
			
		||||
@@ -148,6 +145,12 @@ drive:
 | 
			
		||||
#  consumer_key: example-twitter-consumer-key
 | 
			
		||||
#  consumer_secret: example-twitter-consumer-secret-key
 | 
			
		||||
 | 
			
		||||
# GitHub integration
 | 
			
		||||
# You need to set the oauth callback url as : https://<your-misskey-instance>/api/gh/cb
 | 
			
		||||
#github:
 | 
			
		||||
#  client_id: example-github-client-id
 | 
			
		||||
#  client_secret: example-github-client-secret
 | 
			
		||||
 | 
			
		||||
# Ghost
 | 
			
		||||
# Ghost account is an account used for the purpose of delegating
 | 
			
		||||
# followers when putting users in the list.
 | 
			
		||||
@@ -164,6 +167,3 @@ drive:
 | 
			
		||||
#  external: true
 | 
			
		||||
#  engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
 | 
			
		||||
#  timeout: 300000
 | 
			
		||||
 | 
			
		||||
# Max allowed note text length in charactors
 | 
			
		||||
maxNoteTextLength: 1000
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								.travis.yml
									
									
									
									
									
								
							@@ -1,41 +0,0 @@
 | 
			
		||||
# travis file
 | 
			
		||||
# https://docs.travis-ci.com/user/customizing-the-build
 | 
			
		||||
 | 
			
		||||
notifications:
 | 
			
		||||
  email: false
 | 
			
		||||
 | 
			
		||||
branches:
 | 
			
		||||
  except:
 | 
			
		||||
    - l10n_master
 | 
			
		||||
 | 
			
		||||
language: node_js
 | 
			
		||||
 | 
			
		||||
node_js:
 | 
			
		||||
  - 11.0.0
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  - CXX=g++-4.8 NODE_ENV=production
 | 
			
		||||
 | 
			
		||||
addons:
 | 
			
		||||
  apt:
 | 
			
		||||
    sources:
 | 
			
		||||
      - ubuntu-toolchain-r-test
 | 
			
		||||
    packages:
 | 
			
		||||
      - g++-4.8
 | 
			
		||||
 | 
			
		||||
cache:
 | 
			
		||||
  directories:
 | 
			
		||||
    - node_modules
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  - mongodb
 | 
			
		||||
  - redis-server
 | 
			
		||||
 | 
			
		||||
before_script:
 | 
			
		||||
  - npm install
 | 
			
		||||
 | 
			
		||||
  # 設定ファイルを配置
 | 
			
		||||
  - cp ./.ci/default.yml ./.config
 | 
			
		||||
  - cp ./.ci/test.yml ./.config
 | 
			
		||||
 | 
			
		||||
  - travis_wait npm run build
 | 
			
		||||
@@ -23,5 +23,5 @@ Please use [Crowdin](https://crowdin.com/project/misskey) for localization.
 | 
			
		||||
* Test codes are located in `/test`.
 | 
			
		||||
 | 
			
		||||
## Continuous integration
 | 
			
		||||
Misskey uses Travis for automated test.
 | 
			
		||||
Configuration files are located in `/.travis`.
 | 
			
		||||
Misskey uses CircleCI for automated test.
 | 
			
		||||
Configuration files are located in `/.circleci`.
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
================================================================
 | 
			
		||||
 | 
			
		||||
[](https://circleci.com/gh/syuilo/misskey)
 | 
			
		||||
[![][travis-badge]][travis-link]
 | 
			
		||||
[![][dependencies-badge]][dependencies-link]
 | 
			
		||||
[](http://makeapullrequest.com)
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +43,7 @@ Easiest way to tell your emotions. Misskey allows you to add various type of rea
 | 
			
		||||
 | 
			
		||||
<h3 align="left">Interface</h3>
 | 
			
		||||
<p align="left">
 | 
			
		||||
No UI fits for everyone. Therefore, Misskey has a highly customizable UI for your taste. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
 | 
			
		||||
Highly customizable UI for your taste. We understand no UI fits for everyone. You can edit layouts of your timeline, place selectable widgets you can easily move and create your unique home as this place will be your home.
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
@@ -124,8 +123,6 @@ Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE).
 | 
			
		||||
 | 
			
		||||
[agpl-3.0]:           https://www.gnu.org/licenses/agpl-3.0.en.html
 | 
			
		||||
[agpl-3.0-badge]:     https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
 | 
			
		||||
[travis-link]:        https://travis-ci.org/syuilo/misskey
 | 
			
		||||
[travis-badge]:       http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
 | 
			
		||||
[dependencies-link]:  https://david-dm.org/syuilo/misskey
 | 
			
		||||
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
 | 
			
		||||
Please install and setup these softwares:
 | 
			
		||||
 | 
			
		||||
#### Dependencies :package:
 | 
			
		||||
* **[Node.js](https://nodejs.org/en/)**
 | 
			
		||||
* **[Node.js](https://nodejs.org/en/)** >= 10.0.0
 | 
			
		||||
* **[MongoDB](https://www.mongodb.com/)** >= 3.6
 | 
			
		||||
 | 
			
		||||
##### Optional
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ adduser --disabled-password --disabled-login misskey
 | 
			
		||||
これらのソフトウェアをインストール・設定してください:
 | 
			
		||||
 | 
			
		||||
#### 依存関係 :package:
 | 
			
		||||
* **[Node.js](https://nodejs.org/en/)**
 | 
			
		||||
* **[Node.js](https://nodejs.org/en/)** (10.0.0以上)
 | 
			
		||||
* **[MongoDB](https://www.mongodb.com/)** (3.6以上)
 | 
			
		||||
 | 
			
		||||
##### オプション
 | 
			
		||||
 
 | 
			
		||||
@@ -131,6 +131,7 @@ common:
 | 
			
		||||
  show-full-acct: "ユーザー名のホストを省略しない"
 | 
			
		||||
  reduce-motion: "UIの動きを減らす"
 | 
			
		||||
  this-setting-is-this-device-only: "このデバイスのみ"
 | 
			
		||||
  use-os-default-emojis: "OS標準の絵文字を使用"
 | 
			
		||||
 | 
			
		||||
  do-not-use-in-production: 'これは開発ビルドです。本番環境で使用しないでください。'
 | 
			
		||||
 | 
			
		||||
@@ -417,6 +418,7 @@ common/views/components/signin.vue:
 | 
			
		||||
  signin: "サインイン"
 | 
			
		||||
  or: "または"
 | 
			
		||||
  signin-with-twitter: "Twitterでログイン"
 | 
			
		||||
  signin-with-github: "GitHubでログイン"
 | 
			
		||||
  login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
 | 
			
		||||
 | 
			
		||||
common/views/components/signup.vue:
 | 
			
		||||
@@ -460,6 +462,14 @@ common/views/components/twitter-setting.vue:
 | 
			
		||||
  connect: "Twitterと接続する"
 | 
			
		||||
  disconnect: "切断する"
 | 
			
		||||
 | 
			
		||||
common/views/components/github-setting.vue:
 | 
			
		||||
  description: "お使いのGitHubアカウントをお使いのMisskeyアカウントに接続しておくと、プロフィールでGitHubアカウント情報が表示されるようになったり、GitHubを用いた便利なサインインを利用できるようになります。"
 | 
			
		||||
  connected-to: "次のGitHubアカウントに接続されています"
 | 
			
		||||
  detail: "詳細..."
 | 
			
		||||
  reconnect: "再接続する"
 | 
			
		||||
  connect: "GitHubと接続する"
 | 
			
		||||
  disconnect: "切断する"
 | 
			
		||||
 | 
			
		||||
common/views/components/uploader.vue:
 | 
			
		||||
  waiting: "待機中"
 | 
			
		||||
 | 
			
		||||
@@ -599,32 +609,6 @@ desktop/views/components/calendar.vue:
 | 
			
		||||
  next: "次の月"
 | 
			
		||||
  go: "クリックして時間遡行"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/charts.vue:
 | 
			
		||||
  title: "チャート"
 | 
			
		||||
  per-day: "1日ごと"
 | 
			
		||||
  per-hour: "1時間ごと"
 | 
			
		||||
  federation: "フェデレーション"
 | 
			
		||||
  notes: "投稿"
 | 
			
		||||
  users: "ユーザー"
 | 
			
		||||
  drive: "ドライブ"
 | 
			
		||||
  network: "ネットワーク"
 | 
			
		||||
  charts:
 | 
			
		||||
    federation-instances: "インスタンスの増減"
 | 
			
		||||
    federation-instances-total: "インスタンスの積算"
 | 
			
		||||
    notes: "投稿の増減 (統合)"
 | 
			
		||||
    local-notes: "投稿の増減 (ローカル)"
 | 
			
		||||
    remote-notes: "投稿の増減 (リモート)"
 | 
			
		||||
    notes-total: "投稿の積算"
 | 
			
		||||
    users: "ユーザーの増減"
 | 
			
		||||
    users-total: "ユーザーの積算"
 | 
			
		||||
    drive: "ドライブ使用量の増減"
 | 
			
		||||
    drive-total: "ドライブ使用量の積算"
 | 
			
		||||
    drive-files: "ドライブのファイル数の増減"
 | 
			
		||||
    drive-files-total: "ドライブのファイル数の積算"
 | 
			
		||||
    network-requests: "リクエスト"
 | 
			
		||||
    network-time: "応答時間"
 | 
			
		||||
    network-usage: "通信量"
 | 
			
		||||
 | 
			
		||||
desktop/views/components/choose-file-from-drive-window.vue:
 | 
			
		||||
  choose-file: "ファイル選択中"
 | 
			
		||||
  upload: "PCからドライブにファイルをアップロード"
 | 
			
		||||
@@ -1088,10 +1072,18 @@ admin/views/dashboard.vue:
 | 
			
		||||
  instances: "インスタンス"
 | 
			
		||||
  this-instance: "このインスタンス"
 | 
			
		||||
  federated: "連合"
 | 
			
		||||
 | 
			
		||||
admin/views/instance.vue:
 | 
			
		||||
  instance: "インスタンス"
 | 
			
		||||
  instance-name: "インスタンス名"
 | 
			
		||||
  instance-description: "インスタンスの紹介"
 | 
			
		||||
  banner-url: "バナー画像URL"
 | 
			
		||||
  max-note-text-length: "投稿の最大文字数"
 | 
			
		||||
  disable-registration: "ユーザー登録の受付を停止する"
 | 
			
		||||
  disable-local-timeline: "ローカルタイムラインを無効にする"
 | 
			
		||||
  invite: "招待"
 | 
			
		||||
  banner-url: "Banner URL"
 | 
			
		||||
  disableRegistration: "Disable new user registration"
 | 
			
		||||
  disableLocalTimeline: "Disable the local timeline"
 | 
			
		||||
  save: "保存"
 | 
			
		||||
  saved: "保存しました"
 | 
			
		||||
 | 
			
		||||
admin/views/charts.vue:
 | 
			
		||||
  title: "チャート"
 | 
			
		||||
@@ -1142,10 +1134,16 @@ admin/views/emoji.vue:
 | 
			
		||||
    aliases-desc: "スペースで区切って複数設定できます。"
 | 
			
		||||
    url: "絵文字画像URL"
 | 
			
		||||
    add: "追加"
 | 
			
		||||
    info: "50KB以下のPNG画像をおすすめします。"
 | 
			
		||||
    added: "絵文字を登録しました"
 | 
			
		||||
  emojis:
 | 
			
		||||
    title: "絵文字一覧"
 | 
			
		||||
    update: "更新"
 | 
			
		||||
    remove: "削除"
 | 
			
		||||
  updated: "更新しました"
 | 
			
		||||
  remove-emoji:
 | 
			
		||||
    are-you-sure: "「$1」を削除しますか?"
 | 
			
		||||
    removed: "削除しました"
 | 
			
		||||
 | 
			
		||||
admin/views/announcements.vue:
 | 
			
		||||
  announcements: "お知らせ"
 | 
			
		||||
@@ -1154,6 +1152,10 @@ admin/views/announcements.vue:
 | 
			
		||||
  add: "追加"
 | 
			
		||||
  title: "タイトル"
 | 
			
		||||
  text: "内容"
 | 
			
		||||
  saved: "保存しました"
 | 
			
		||||
  _remove:
 | 
			
		||||
    are-you-sure: "「$1」を削除しますか?"
 | 
			
		||||
    removed: "削除しました"
 | 
			
		||||
 | 
			
		||||
admin/views/hashtags.vue:
 | 
			
		||||
  hided-tags: "Hidden Tags"
 | 
			
		||||
@@ -1173,12 +1175,6 @@ desktop/views/pages/deck/deck.user-column.vue:
 | 
			
		||||
  pinned-notes: "ピン留めされた投稿"
 | 
			
		||||
  push-to-a-list: "リストに追加"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/stats/stats.vue:
 | 
			
		||||
  all-users: "全てのユーザー"
 | 
			
		||||
  original-users: "このインスタンスのユーザー"
 | 
			
		||||
  all-notes: "全ての投稿"
 | 
			
		||||
  original-notes: "このインスタンスの投稿"
 | 
			
		||||
 | 
			
		||||
desktop/views/pages/welcome.vue:
 | 
			
		||||
  about: "詳しく..."
 | 
			
		||||
  gotit: "わかった"
 | 
			
		||||
@@ -1560,6 +1556,10 @@ mobile/views/pages/settings.vue:
 | 
			
		||||
  twitter-connect: "Twitterアカウントに接続する"
 | 
			
		||||
  twitter-reconnect: "再接続する"
 | 
			
		||||
  twitter-disconnect: "切断する"
 | 
			
		||||
  github: "GitHub連携"
 | 
			
		||||
  github-connect: "GitHubアカウントに接続する"
 | 
			
		||||
  github-reconnect: "再接続する"
 | 
			
		||||
  github-disconnect: "切断する"
 | 
			
		||||
  update: "Misskey Update"
 | 
			
		||||
  version: "バージョン:"
 | 
			
		||||
  latest-version: "最新のバージョン:"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "10.38.4",
 | 
			
		||||
	"clientVersion": "1.0.11501",
 | 
			
		||||
	"version": "10.39.0",
 | 
			
		||||
	"clientVersion": "1.0.11562",
 | 
			
		||||
	"codename": "nighthike",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"private": true,
 | 
			
		||||
@@ -62,6 +62,7 @@
 | 
			
		||||
		"@types/mongodb": "3.1.12",
 | 
			
		||||
		"@types/ms": "0.7.30",
 | 
			
		||||
		"@types/node": "10.12.2",
 | 
			
		||||
		"@types/oauth": "0.9.1",
 | 
			
		||||
		"@types/portscanner": "2.1.0",
 | 
			
		||||
		"@types/pug": "2.0.4",
 | 
			
		||||
		"@types/qrcode": "1.3.0",
 | 
			
		||||
@@ -95,7 +96,6 @@
 | 
			
		||||
		"chai": "4.2.0",
 | 
			
		||||
		"chai-http": "4.2.0",
 | 
			
		||||
		"chalk": "2.4.1",
 | 
			
		||||
		"chart.js": "2.7.3",
 | 
			
		||||
		"commander": "2.19.0",
 | 
			
		||||
		"crc-32": "1.2.0",
 | 
			
		||||
		"css-loader": "1.0.1",
 | 
			
		||||
@@ -211,7 +211,6 @@
 | 
			
		||||
		"uuid": "3.3.2",
 | 
			
		||||
		"v-animate-css": "0.0.2",
 | 
			
		||||
		"vue": "2.5.17",
 | 
			
		||||
		"vue-chartjs": "3.4.0",
 | 
			
		||||
		"vue-color": "2.7.0",
 | 
			
		||||
		"vue-content-loading": "1.5.3",
 | 
			
		||||
		"vue-cropperjs": "2.2.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@
 | 
			
		||||
				<span>%i18n:@text%</span>
 | 
			
		||||
			</ui-textarea>
 | 
			
		||||
			<ui-horizon-group>
 | 
			
		||||
				<ui-button @click="save">%fa:save R% %i18n:@save%</ui-button>
 | 
			
		||||
				<ui-button @click="save()">%fa:save R% %i18n:@save%</ui-button>
 | 
			
		||||
				<ui-button @click="remove(i)">%fa:trash-alt R% %i18n:@remove%</ui-button>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
		</section>
 | 
			
		||||
@@ -46,17 +46,36 @@ export default Vue.extend({
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		remove(i) {
 | 
			
		||||
			this.announcements = this.announcements.filter((_, j) => j !== i);
 | 
			
		||||
			this.save();
 | 
			
		||||
			this.$swal({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: '%i18n:@_remove.are-you-sure%'.replace('$1', this.announcements.find((_, j) => j == i).title),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			}).then(res => {
 | 
			
		||||
				if (!res.value) return;
 | 
			
		||||
				this.announcements = this.announcements.filter((_, j) => j !== i);
 | 
			
		||||
				this.save(true);
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: '%i18n:@_remove.removed%'
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		save() {
 | 
			
		||||
		save(silent) {
 | 
			
		||||
			(this as any).api('admin/update-meta', {
 | 
			
		||||
				broadcasts: this.announcements
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
				if (!silent) {
 | 
			
		||||
					this.$swal({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: '%i18n:@saved%'
 | 
			
		||||
					});
 | 
			
		||||
				}
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
			<ui-input v-model="url">
 | 
			
		||||
				<span>%i18n:@add-emoji.url%</span>
 | 
			
		||||
			</ui-input>
 | 
			
		||||
			<ui-info>%i18n:@add-emoji.info%</ui-info>
 | 
			
		||||
			<ui-button @click="add">%i18n:@add-emoji.add%</ui-button>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
@@ -27,11 +28,9 @@
 | 
			
		||||
			<ui-horizon-group inputs>
 | 
			
		||||
				<ui-input v-model="emoji.name">
 | 
			
		||||
					<span>%i18n:@add-emoji.name%</span>
 | 
			
		||||
					<span slot="text">%i18n:@add-emoji.name-desc%</span>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
				<ui-input v-model="emoji.aliases">
 | 
			
		||||
					<span>%i18n:@add-emoji.aliases%</span>
 | 
			
		||||
					<span slot="text">%i18n:@add-emoji.aliases-desc%</span>
 | 
			
		||||
				</ui-input>
 | 
			
		||||
			</ui-horizon-group>
 | 
			
		||||
			<ui-input v-model="emoji.url">
 | 
			
		||||
@@ -68,17 +67,24 @@ export default Vue.extend({
 | 
			
		||||
			(this as any).api('admin/emoji/add', {
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				url: this.url,
 | 
			
		||||
				aliases: this.aliases.split(' ')
 | 
			
		||||
				aliases: this.aliases.split(' ').filter(x => x.length > 0)
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Added` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: '%i18n:@add-emoji.added%'
 | 
			
		||||
				});
 | 
			
		||||
				this.fetchEmojis();
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		fetchEmojis() {
 | 
			
		||||
			(this as any).api('admin/emoji/list').then(emojis => {
 | 
			
		||||
				emojis.reverse();
 | 
			
		||||
				emojis.forEach(e => e.aliases = (e.aliases || []).join(' '));
 | 
			
		||||
				this.emojis = emojis;
 | 
			
		||||
			});
 | 
			
		||||
@@ -89,22 +95,42 @@ export default Vue.extend({
 | 
			
		||||
				id: emoji.id,
 | 
			
		||||
				name: emoji.name,
 | 
			
		||||
				url: emoji.url,
 | 
			
		||||
				aliases: emoji.aliases.split(' ')
 | 
			
		||||
				aliases: emoji.aliases.split(' ').filter(x => x.length > 0)
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Updated` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: '%i18n:@updated%'
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		removeEmoji(emoji) {
 | 
			
		||||
			(this as any).api('admin/emoji/remove', {
 | 
			
		||||
				id: emoji.id
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Removed` });
 | 
			
		||||
				this.fetchEmojis();
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
			this.$swal({
 | 
			
		||||
				type: 'warning',
 | 
			
		||||
				text: '%i18n:@remove-emoji.are-you-sure%'.replace('$1', emoji.name),
 | 
			
		||||
				showCancelButton: true
 | 
			
		||||
			}).then(res => {
 | 
			
		||||
				if (!res.value) return;
 | 
			
		||||
 | 
			
		||||
				(this as any).api('admin/emoji/remove', {
 | 
			
		||||
					id: emoji.id
 | 
			
		||||
				}).then(() => {
 | 
			
		||||
					this.$swal({
 | 
			
		||||
						type: 'success',
 | 
			
		||||
						text: '%i18n:@remove-emoji.removed%'
 | 
			
		||||
					});
 | 
			
		||||
					this.fetchEmojis();
 | 
			
		||||
				}).catch(e => {
 | 
			
		||||
					this.$swal({
 | 
			
		||||
						type: 'error',
 | 
			
		||||
						text: e
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="axbwjelsbymowqjyywpirzhdlszoncqs">
 | 
			
		||||
	<ui-card>
 | 
			
		||||
		<div slot="title">%i18n:@banner-url%</div>
 | 
			
		||||
		<div slot="title">%fa:cog% %i18n:@instance%</div>
 | 
			
		||||
		<section class="fit-top">
 | 
			
		||||
			<ui-input v-model="bannerUrl"/>
 | 
			
		||||
			<ui-input v-model="name">%i18n:@instance-name%</ui-input>
 | 
			
		||||
			<ui-textarea v-model="description">%i18n:@instance-description%</ui-textarea>
 | 
			
		||||
			<ui-input v-model="bannerUrl">%i18n:@banner-url%</ui-input>
 | 
			
		||||
			<ui-input v-model="maxNoteTextLength">%i18n:@max-note-text-length%</ui-input>
 | 
			
		||||
			<ui-button @click="updateMeta">%i18n:@save%</ui-button>
 | 
			
		||||
		</section>
 | 
			
		||||
	</ui-card>
 | 
			
		||||
@@ -35,26 +38,52 @@ export default Vue.extend({
 | 
			
		||||
			disableRegistration: false,
 | 
			
		||||
			disableLocalTimeline: false,
 | 
			
		||||
			bannerUrl: null,
 | 
			
		||||
			name: null,
 | 
			
		||||
			description: null,
 | 
			
		||||
			maxNoteTextLength: null,
 | 
			
		||||
			inviteCode: null,
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		(this as any).os.getMeta().then(meta => {
 | 
			
		||||
			this.bannerUrl = meta.bannerUrl;
 | 
			
		||||
			this.name = meta.name;
 | 
			
		||||
			this.description = meta.description;
 | 
			
		||||
			this.maxNoteTextLength = meta.maxNoteTextLength;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		invite() {
 | 
			
		||||
			(this as any).api('admin/invite').then(x => {
 | 
			
		||||
				this.inviteCode = x.code;
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		updateMeta() {
 | 
			
		||||
			(this as any).api('admin/update-meta', {
 | 
			
		||||
				disableRegistration: this.disableRegistration,
 | 
			
		||||
				disableLocalTimeline: this.disableLocalTimeline,
 | 
			
		||||
				bannerUrl: this.bannerUrl
 | 
			
		||||
				bannerUrl: this.bannerUrl,
 | 
			
		||||
				name: this.name,
 | 
			
		||||
				description: this.description,
 | 
			
		||||
				maxNoteTextLength: parseInt(this.maxNoteTextLength, 10)
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Saved` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'success',
 | 
			
		||||
					text: '%i18n:@saved%'
 | 
			
		||||
				});
 | 
			
		||||
			}).catch(e => {
 | 
			
		||||
				//(this as any).os.apis.dialog({ text: `Failed ${e}` });
 | 
			
		||||
				this.$swal({
 | 
			
		||||
					type: 'error',
 | 
			
		||||
					text: e
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,8 @@
 | 
			
		||||
	</ol>
 | 
			
		||||
	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
 | 
			
		||||
		<li v-for="emoji in emojis" @click="complete(type, emoji.emoji)" @keydown="onKeydown" tabindex="-1">
 | 
			
		||||
			<span class="emoji" v-if="emoji.url"><img :src="emoji.url" :alt="emoji.emoji"/></span>
 | 
			
		||||
			<span class="emoji" v-if="emoji.isCustomEmoji"><img :src="emoji.url" :alt="emoji.emoji"/></span>
 | 
			
		||||
			<span class="emoji" v-else-if="!useOsDefaultEmojis"><img :src="emoji.url" :alt="emoji.emoji"/></span>
 | 
			
		||||
			<span class="emoji" v-else>{{ emoji.emoji }}</span>
 | 
			
		||||
			<span class="name" v-html="emoji.name.replace(q, `<b>${q}</b>`)"></span>
 | 
			
		||||
			<span class="alias" v-if="emoji.aliasOf">({{ emoji.aliasOf }})</span>
 | 
			
		||||
@@ -33,6 +34,7 @@ type EmojiDef = {
 | 
			
		||||
	name: string;
 | 
			
		||||
	aliasOf?: string;
 | 
			
		||||
	url?: string;
 | 
			
		||||
	isCustomEmoji?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const lib = Object.entries(emojilib.lib).filter((x: any) => {
 | 
			
		||||
@@ -42,7 +44,8 @@ const lib = Object.entries(emojilib.lib).filter((x: any) => {
 | 
			
		||||
const emjdb: EmojiDef[] = lib.map((x: any) => ({
 | 
			
		||||
	emoji: x[1].char,
 | 
			
		||||
	name: x[0],
 | 
			
		||||
	aliasOf: null
 | 
			
		||||
	aliasOf: null,
 | 
			
		||||
	url: `https://twemoji.maxcdn.com/2/svg/${x[1].char.codePointAt(0).toString(16)}.svg`
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
lib.forEach((x: any) => {
 | 
			
		||||
@@ -51,7 +54,8 @@ lib.forEach((x: any) => {
 | 
			
		||||
			emjdb.push({
 | 
			
		||||
				emoji: x[1].char,
 | 
			
		||||
				name: k,
 | 
			
		||||
				aliasOf: x[0]
 | 
			
		||||
				aliasOf: x[0],
 | 
			
		||||
				url: `https://twemoji.maxcdn.com/2/svg/${x[1].char.codePointAt(0).toString(16)}.svg`
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
@@ -77,6 +81,10 @@ export default Vue.extend({
 | 
			
		||||
	computed: {
 | 
			
		||||
		items(): HTMLCollection {
 | 
			
		||||
			return (this.$refs.suggests as Element).children;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useOsDefaultEmojis(): boolean {
 | 
			
		||||
			return this.$store.state.device.useOsDefaultEmojis;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
@@ -107,7 +115,8 @@ export default Vue.extend({
 | 
			
		||||
			emojiDefinitions.push({
 | 
			
		||||
				name: x.name,
 | 
			
		||||
				emoji: `:${x.name}:`,
 | 
			
		||||
				url: x.url
 | 
			
		||||
				url: x.url,
 | 
			
		||||
				isCustomEmoji: true
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (x.aliases) {
 | 
			
		||||
@@ -116,7 +125,8 @@ export default Vue.extend({
 | 
			
		||||
						name: alias,
 | 
			
		||||
						aliasOf: x.name,
 | 
			
		||||
						emoji: `:${x.name}:`,
 | 
			
		||||
						url: x.url
 | 
			
		||||
						url: x.url,
 | 
			
		||||
						isCustomEmoji: true
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										81
									
								
								src/client/app/common/views/components/emoji.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/client/app/common/views/components/emoji.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
<template>
 | 
			
		||||
<img v-if="customEmoji" class="fvgwvorwhxigeolkkrcderjzcawqrscl custom" :src="url" :alt="alt" :title="alt"/>
 | 
			
		||||
<img v-else-if="char && !useOsDefaultEmojis" class="fvgwvorwhxigeolkkrcderjzcawqrscl" :src="url" :alt="alt" :title="alt"/>
 | 
			
		||||
<span v-else-if="char && useOsDefaultEmojis">{{ char }}</span>
 | 
			
		||||
<span v-else>:{{ name }}:</span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { lib } from 'emojilib';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: {
 | 
			
		||||
		name: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		emoji: {
 | 
			
		||||
			type: String,
 | 
			
		||||
			required: false
 | 
			
		||||
		},
 | 
			
		||||
		customEmojis: {
 | 
			
		||||
			required: false,
 | 
			
		||||
			default: []
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			url: null,
 | 
			
		||||
			char: null,
 | 
			
		||||
			customEmoji: null
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		alt(): string {
 | 
			
		||||
			return this.customEmoji ? `:${this.customEmoji.name}:` : this.char;
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useOsDefaultEmojis(): boolean {
 | 
			
		||||
			return this.$store.state.device.useOsDefaultEmojis;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	created() {
 | 
			
		||||
		if (this.name) {
 | 
			
		||||
			const customEmoji = this.customEmojis.find(x => x.name == this.name);
 | 
			
		||||
			if (customEmoji) {
 | 
			
		||||
				this.customEmoji = customEmoji;
 | 
			
		||||
				this.url = customEmoji.url;
 | 
			
		||||
			} else {
 | 
			
		||||
				const emoji = lib[this.name];
 | 
			
		||||
				if (emoji) {
 | 
			
		||||
					this.char = emoji.char;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			this.char = this.emoji;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (this.char) {
 | 
			
		||||
			this.url = `https://twemoji.maxcdn.com/2/svg/${this.char.codePointAt(0).toString(16)}.svg`;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.fvgwvorwhxigeolkkrcderjzcawqrscl
 | 
			
		||||
	height 1em
 | 
			
		||||
 | 
			
		||||
	&.custom
 | 
			
		||||
		height 2.5em
 | 
			
		||||
		vertical-align middle
 | 
			
		||||
		transition transform 0.2s ease
 | 
			
		||||
 | 
			
		||||
		&:hover
 | 
			
		||||
			transform scale(1.2)
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										63
									
								
								src/client/app/common/views/components/github-setting.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/client/app/common/views/components/github-setting.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="mk-github-setting">
 | 
			
		||||
	<p>%i18n:@description%<a :href="`${docsUrl}/link-to-github`" target="_blank">%i18n:@detail%</a></p>
 | 
			
		||||
	<p class="account" v-if="$store.state.i.github" :title="`GitHub ID: ${$store.state.i.github.id}`">%i18n:@connected-to%: <a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 | 
			
		||||
	<p>
 | 
			
		||||
		<a :href="`${apiUrl}/connect/github`" target="_blank" @click.prevent="connect">{{ $store.state.i.github ? '%i18n:@reconnect%' : '%i18n:@connect%' }}</a>
 | 
			
		||||
		<span v-if="$store.state.i.github"> or </span>
 | 
			
		||||
		<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github" @click.prevent="disconnect">%i18n:@disconnect%</a>
 | 
			
		||||
	</p>
 | 
			
		||||
	<p class="id" v-if="$store.state.i.github">GitHub ID: {{ $store.state.i.github.id }}</p>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { apiUrl, docsUrl } from '../../../config';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			form: null,
 | 
			
		||||
			apiUrl,
 | 
			
		||||
			docsUrl
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.$watch('$store.state.i', () => {
 | 
			
		||||
			if (this.$store.state.i.github && this.form)
 | 
			
		||||
				this.form.close();
 | 
			
		||||
		}, {
 | 
			
		||||
			deep: true
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		connect() {
 | 
			
		||||
			this.form = window.open(apiUrl + '/connect/github',
 | 
			
		||||
				'github_connect_window',
 | 
			
		||||
				'height=570, width=520');
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		disconnect() {
 | 
			
		||||
			window.open(apiUrl + '/disconnect/github',
 | 
			
		||||
				'github_disconnect_window',
 | 
			
		||||
				'height=570, width=520');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.mk-github-setting
 | 
			
		||||
	.account
 | 
			
		||||
		border solid 1px #e1e8ed
 | 
			
		||||
		border-radius 4px
 | 
			
		||||
		padding 16px
 | 
			
		||||
 | 
			
		||||
		a
 | 
			
		||||
			font-weight bold
 | 
			
		||||
			color inherit
 | 
			
		||||
 | 
			
		||||
	.id
 | 
			
		||||
		color #8899a6
 | 
			
		||||
</style>
 | 
			
		||||
@@ -37,7 +37,9 @@ import messaging from './messaging.vue';
 | 
			
		||||
import messagingRoom from './messaging-room.vue';
 | 
			
		||||
import urlPreview from './url-preview.vue';
 | 
			
		||||
import twitterSetting from './twitter-setting.vue';
 | 
			
		||||
import githubSetting from './github-setting.vue';
 | 
			
		||||
import fileTypeIcon from './file-type-icon.vue';
 | 
			
		||||
import emoji from './emoji.vue';
 | 
			
		||||
import Reversi from './games/reversi/reversi.vue';
 | 
			
		||||
import welcomeTimeline from './welcome-timeline.vue';
 | 
			
		||||
import uiInput from './ui/input.vue';
 | 
			
		||||
@@ -90,7 +92,9 @@ Vue.component('mk-messaging', messaging);
 | 
			
		||||
Vue.component('mk-messaging-room', messagingRoom);
 | 
			
		||||
Vue.component('mk-url-preview', urlPreview);
 | 
			
		||||
Vue.component('mk-twitter-setting', twitterSetting);
 | 
			
		||||
Vue.component('mk-github-setting', githubSetting);
 | 
			
		||||
Vue.component('mk-file-type-icon', fileTypeIcon);
 | 
			
		||||
Vue.component('mk-emoji', emoji);
 | 
			
		||||
Vue.component('mk-reversi', Reversi);
 | 
			
		||||
Vue.component('mk-welcome-timeline', welcomeTimeline);
 | 
			
		||||
Vue.component('ui-input', uiInput);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import Vue, { VNode } from 'vue';
 | 
			
		||||
import * as emojilib from 'emojilib';
 | 
			
		||||
import { length } from 'stringz';
 | 
			
		||||
import parse from '../../../../../mfm/parse';
 | 
			
		||||
import getAcct from '../../../../../misc/acct/render';
 | 
			
		||||
@@ -188,24 +187,15 @@ export default Vue.component('misskey-flavored-markdown', {
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 'emoji': {
 | 
			
		||||
					//#region カスタム絵文字
 | 
			
		||||
					if (this.customEmojis != null) {
 | 
			
		||||
						const customEmoji = this.customEmojis.find(e => e.name == token.emoji || (e.aliases || []).includes(token.emoji));
 | 
			
		||||
						if (customEmoji) {
 | 
			
		||||
							return [createElement('img', {
 | 
			
		||||
								attrs: {
 | 
			
		||||
									src: customEmoji.url,
 | 
			
		||||
									alt: token.emoji,
 | 
			
		||||
									title: token.emoji,
 | 
			
		||||
									style: 'height: 2.5em; vertical-align: middle;'
 | 
			
		||||
								}
 | 
			
		||||
							})];
 | 
			
		||||
					return [createElement('mk-emoji', {
 | 
			
		||||
						attrs: {
 | 
			
		||||
							emoji: token.emoji,
 | 
			
		||||
							name: token.name
 | 
			
		||||
						},
 | 
			
		||||
						props: {
 | 
			
		||||
							customEmojis: this.customEmojis
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					//#endregion
 | 
			
		||||
 | 
			
		||||
					const emoji = emojilib.lib[token.emoji];
 | 
			
		||||
					return [createElement('span', emoji ? emoji.char : token.content)];
 | 
			
		||||
					})];
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				case 'search': {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,6 @@
 | 
			
		||||
<span class="mk-nav">
 | 
			
		||||
	<a :href="aboutUrl">%i18n:@about%</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a href="/stats">%i18n:@stats%</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a :href="repositoryUrl">%i18n:@repository%</a>
 | 
			
		||||
	<i>・</i>
 | 
			
		||||
	<a :href="feedbackUrl" target="_blank">%i18n:@feedback%</a>
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
	<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="number" required styl="fill"/>
 | 
			
		||||
	<ui-button type="submit" :disabled="signing">{{ signing ? '%i18n:@signing-in%' : '%i18n:@signin%' }}</ui-button>
 | 
			
		||||
	<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/twitter`">%i18n:@signin-with-twitter%</a></p>
 | 
			
		||||
	<p style="margin: 8px 0;">%i18n:@or% <a :href="`${apiUrl}/signin/github`">%i18n:@signin-with-github%</a></p>
 | 
			
		||||
</form>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,11 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	inject: ['horizonGrouped'],
 | 
			
		||||
	inject: {
 | 
			
		||||
		horizonGrouped: {
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		type: {
 | 
			
		||||
			type: String,
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,11 @@ import Vue from 'vue';
 | 
			
		||||
const getPasswordStrength = require('syuilo-password-strength');
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	inject: ['horizonGrouped'],
 | 
			
		||||
	inject: {
 | 
			
		||||
		horizonGrouped: {
 | 
			
		||||
			default: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	props: {
 | 
			
		||||
		value: {
 | 
			
		||||
			required: false
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,7 @@ class Autocomplete {
 | 
			
		||||
		} else {
 | 
			
		||||
			// サジェスト要素作成
 | 
			
		||||
			this.suggestion = new MkAutocomplete({
 | 
			
		||||
				parent: this.vm,
 | 
			
		||||
				propsData: {
 | 
			
		||||
					textarea: this.textarea,
 | 
			
		||||
					complete: this.complete,
 | 
			
		||||
@@ -222,8 +223,6 @@ class Autocomplete {
 | 
			
		||||
			const trimmedBefore = before.substring(0, before.lastIndexOf(':'));
 | 
			
		||||
			const after = source.substr(caret);
 | 
			
		||||
 | 
			
		||||
			if (value.startsWith(':')) value = value + ' ';
 | 
			
		||||
 | 
			
		||||
			// 挿入
 | 
			
		||||
			this.text = trimmedBefore + value + after;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,6 @@ import updateBanner from './api/update-banner';
 | 
			
		||||
import MkIndex from './views/pages/index.vue';
 | 
			
		||||
import MkHome from './views/pages/home.vue';
 | 
			
		||||
import MkDeck from './views/pages/deck/deck.vue';
 | 
			
		||||
import MkStats from './views/pages/stats/stats.vue';
 | 
			
		||||
import MkUser from './views/pages/user/user.vue';
 | 
			
		||||
import MkFavorites from './views/pages/favorites.vue';
 | 
			
		||||
import MkSelectDrive from './views/pages/selectdrive.vue';
 | 
			
		||||
@@ -56,7 +55,6 @@ init(async (launch) => {
 | 
			
		||||
			{ path: '/', name: 'index', component: MkIndex },
 | 
			
		||||
			{ path: '/home', name: 'home', component: MkHome },
 | 
			
		||||
			{ path: '/deck', name: 'deck', component: MkDeck },
 | 
			
		||||
			{ path: '/stats', name: 'stats', component: MkStats },
 | 
			
		||||
			{ path: '/i/customize-home', component: MkHomeCustomize },
 | 
			
		||||
			{ path: '/i/favorites', component: MkFavorites },
 | 
			
		||||
			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,42 +0,0 @@
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import { Line } from 'vue-chartjs';
 | 
			
		||||
import * as mergeOptions from 'merge-options';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	extends: Line,
 | 
			
		||||
	props: {
 | 
			
		||||
		data: {
 | 
			
		||||
			required: true
 | 
			
		||||
		},
 | 
			
		||||
		opts: {
 | 
			
		||||
			required: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	watch: {
 | 
			
		||||
		data() {
 | 
			
		||||
			this.render();
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.render();
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		render() {
 | 
			
		||||
			this.renderChart(this.data, mergeOptions({
 | 
			
		||||
				responsive: true,
 | 
			
		||||
				maintainAspectRatio: false,
 | 
			
		||||
				scales: {
 | 
			
		||||
					xAxes: [{
 | 
			
		||||
						type: 'time',
 | 
			
		||||
						distribution: 'series'
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					intersect: false,
 | 
			
		||||
					mode: 'index',
 | 
			
		||||
					position: 'nearest'
 | 
			
		||||
				}
 | 
			
		||||
			}, this.opts || {}));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
@@ -1,723 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="gkgckalzgidaygcxnugepioremxvxvpt">
 | 
			
		||||
	<header>
 | 
			
		||||
		<b>%i18n:@title%:</b>
 | 
			
		||||
		<select v-model="chartType">
 | 
			
		||||
			<optgroup label="%i18n:@federation%">
 | 
			
		||||
				<option value="federation-instances">%i18n:@charts.federation-instances%</option>
 | 
			
		||||
				<option value="federation-instances-total">%i18n:@charts.federation-instances-total%</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
			<optgroup label="%i18n:@users%">
 | 
			
		||||
				<option value="users">%i18n:@charts.users%</option>
 | 
			
		||||
				<option value="users-total">%i18n:@charts.users-total%</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
			<optgroup label="%i18n:@notes%">
 | 
			
		||||
				<option value="notes">%i18n:@charts.notes%</option>
 | 
			
		||||
				<option value="local-notes">%i18n:@charts.local-notes%</option>
 | 
			
		||||
				<option value="remote-notes">%i18n:@charts.remote-notes%</option>
 | 
			
		||||
				<option value="notes-total">%i18n:@charts.notes-total%</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
			<optgroup label="%i18n:@drive%">
 | 
			
		||||
				<option value="drive-files">%i18n:@charts.drive-files%</option>
 | 
			
		||||
				<option value="drive-files-total">%i18n:@charts.drive-files-total%</option>
 | 
			
		||||
				<option value="drive">%i18n:@charts.drive%</option>
 | 
			
		||||
				<option value="drive-total">%i18n:@charts.drive-total%</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
			<optgroup label="%i18n:@network%">
 | 
			
		||||
				<option value="network-requests">%i18n:@charts.network-requests%</option>
 | 
			
		||||
				<option value="network-time">%i18n:@charts.network-time%</option>
 | 
			
		||||
				<option value="network-usage">%i18n:@charts.network-usage%</option>
 | 
			
		||||
			</optgroup>
 | 
			
		||||
		</select>
 | 
			
		||||
		<div>
 | 
			
		||||
			<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</header>
 | 
			
		||||
	<div>
 | 
			
		||||
		<x-chart v-if="chart" :data="data[0]" :opts="data[1]"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import XChart from './charts.chart.ts';
 | 
			
		||||
 | 
			
		||||
const colors = {
 | 
			
		||||
	local: 'rgb(246, 88, 79)',
 | 
			
		||||
	remote: 'rgb(65, 221, 222)',
 | 
			
		||||
 | 
			
		||||
	localPlus: 'rgb(52, 178, 118)',
 | 
			
		||||
	remotePlus: 'rgb(158, 255, 209)',
 | 
			
		||||
	localMinus: 'rgb(255, 97, 74)',
 | 
			
		||||
	remoteMinus: 'rgb(255, 149, 134)',
 | 
			
		||||
 | 
			
		||||
	incoming: 'rgb(52, 178, 118)',
 | 
			
		||||
	outgoing: 'rgb(255, 97, 74)',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const rgba = (color: string): string => {
 | 
			
		||||
	return color.replace('rgb', 'rgba').replace(')', ', 0.1)');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const limit = 35;
 | 
			
		||||
 | 
			
		||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
 | 
			
		||||
const negate = arr => arr.map(x => -x);
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XChart
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			now: null,
 | 
			
		||||
			chart: null,
 | 
			
		||||
			chartType: 'notes',
 | 
			
		||||
			span: 'hour'
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	computed: {
 | 
			
		||||
		data(): any {
 | 
			
		||||
			if (this.chart == null) return null;
 | 
			
		||||
			switch (this.chartType) {
 | 
			
		||||
				case 'federation-instances': return this.federationInstancesChart(false);
 | 
			
		||||
				case 'federation-instances-total': return this.federationInstancesChart(true);
 | 
			
		||||
				case 'users': return this.usersChart(false);
 | 
			
		||||
				case 'users-total': return this.usersChart(true);
 | 
			
		||||
				case 'notes': return this.notesChart('combined');
 | 
			
		||||
				case 'local-notes': return this.notesChart('local');
 | 
			
		||||
				case 'remote-notes': return this.notesChart('remote');
 | 
			
		||||
				case 'notes-total': return this.notesTotalChart();
 | 
			
		||||
				case 'drive': return this.driveChart();
 | 
			
		||||
				case 'drive-total': return this.driveTotalChart();
 | 
			
		||||
				case 'drive-files': return this.driveFilesChart();
 | 
			
		||||
				case 'drive-files-total': return this.driveFilesTotalChart();
 | 
			
		||||
				case 'network-requests': return this.networkRequestsChart();
 | 
			
		||||
				case 'network-time': return this.networkTimeChart();
 | 
			
		||||
				case 'network-usage': return this.networkUsageChart();
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		stats(): any[] {
 | 
			
		||||
			const stats =
 | 
			
		||||
				this.span == 'day' ? this.chart.perDay :
 | 
			
		||||
				this.span == 'hour' ? this.chart.perHour :
 | 
			
		||||
				null;
 | 
			
		||||
 | 
			
		||||
			return stats;
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	async created() {
 | 
			
		||||
		this.now = new Date();
 | 
			
		||||
 | 
			
		||||
		const [perHour, perDay] = await Promise.all([Promise.all([
 | 
			
		||||
			(this as any).api('charts/federation', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/users', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/notes', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/drive', { limit: limit, span: 'hour' }),
 | 
			
		||||
			(this as any).api('charts/network', { limit: limit, span: 'hour' })
 | 
			
		||||
		]), Promise.all([
 | 
			
		||||
			(this as any).api('charts/federation', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/users', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/notes', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/drive', { limit: limit, span: 'day' }),
 | 
			
		||||
			(this as any).api('charts/network', { limit: limit, span: 'day' })
 | 
			
		||||
		])]);
 | 
			
		||||
 | 
			
		||||
		const chart = {
 | 
			
		||||
			perHour: {
 | 
			
		||||
				federation: perHour[0],
 | 
			
		||||
				users: perHour[1],
 | 
			
		||||
				notes: perHour[2],
 | 
			
		||||
				drive: perHour[3],
 | 
			
		||||
				network: perHour[4]
 | 
			
		||||
			},
 | 
			
		||||
			perDay: {
 | 
			
		||||
				federation: perDay[0],
 | 
			
		||||
				users: perDay[1],
 | 
			
		||||
				notes: perDay[2],
 | 
			
		||||
				drive: perDay[3],
 | 
			
		||||
				network: perDay[4]
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		this.chart = chart;
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		getDate(i: number) {
 | 
			
		||||
			const y = this.now.getFullYear();
 | 
			
		||||
			const m = this.now.getMonth();
 | 
			
		||||
			const d = this.now.getDate();
 | 
			
		||||
			const h = this.now.getHours();
 | 
			
		||||
 | 
			
		||||
			return (
 | 
			
		||||
				this.span == 'day' ? new Date(y, m, d - i) :
 | 
			
		||||
				this.span == 'hour' ? new Date(y, m, d, h - i) :
 | 
			
		||||
				null
 | 
			
		||||
			);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		format(arr) {
 | 
			
		||||
			return arr.map((v, i) => ({ t: this.getDate(i).getTime(), y: v }));
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		federationInstancesChart(total: boolean): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Instances',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.federation.instance.total
 | 
			
		||||
						: sum(this.stats.federation.instance.inc, negate(this.stats.federation.instance.dec)))
 | 
			
		||||
				}]
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		notesChart(type: string): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'All',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.inc, negate(this.stats.notes.local.dec), this.stats.notes.remote.inc, negate(this.stats.notes.remote.dec))
 | 
			
		||||
						: sum(this.stats.notes[type].inc, negate(this.stats.notes[type].dec))
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Renotes',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: 'rgba(161, 222, 65, 0.1)',
 | 
			
		||||
					borderColor: '#a1de41',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.diffs.renote, this.stats.notes.remote.diffs.renote)
 | 
			
		||||
						: this.stats.notes[type].diffs.renote
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Replies',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: 'rgba(247, 121, 108, 0.1)',
 | 
			
		||||
					borderColor: '#f7796c',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.diffs.reply, this.stats.notes.remote.diffs.reply)
 | 
			
		||||
						: this.stats.notes[type].diffs.reply
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Normal',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: 'rgba(65, 221, 222, 0.1)',
 | 
			
		||||
					borderColor: '#41ddde',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(type == 'combined'
 | 
			
		||||
						? sum(this.stats.notes.local.diffs.normal, this.stats.notes.remote.diffs.normal)
 | 
			
		||||
						: this.stats.notes[type].diffs.normal
 | 
			
		||||
					)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('number')(value);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		notesTotalChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.notes.local.total, this.stats.notes.remote.total))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.notes.local.total)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.notes.remote.total)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('number')(value);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		usersChart(total: boolean): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? sum(this.stats.users.local.total, this.stats.users.remote.total)
 | 
			
		||||
						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec), this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.users.local.total
 | 
			
		||||
						: sum(this.stats.users.local.inc, negate(this.stats.users.local.dec))
 | 
			
		||||
					)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(total
 | 
			
		||||
						? this.stats.users.remote.total
 | 
			
		||||
						: sum(this.stats.users.remote.inc, negate(this.stats.users.remote.dec))
 | 
			
		||||
					)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('number')(value);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		driveChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'All',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.incSize, negate(this.stats.drive.local.decSize), this.stats.drive.remote.incSize, negate(this.stats.drive.remote.decSize)))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.incSize)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localMinus),
 | 
			
		||||
					borderColor: colors.localMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.local.decSize))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remotePlus),
 | 
			
		||||
					borderColor: colors.remotePlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.incSize)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remoteMinus),
 | 
			
		||||
					borderColor: colors.remoteMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.remote.decSize))
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('bytes')(value, 1);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		driveTotalChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.totalSize, this.stats.drive.remote.totalSize))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.totalSize)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.totalSize)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('bytes')(value, 1);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		driveFilesChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'All',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.incCount, negate(this.stats.drive.local.decCount), this.stats.drive.remote.incCount, negate(this.stats.drive.remote.decCount)))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.incCount)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localMinus),
 | 
			
		||||
					borderColor: colors.localMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.local.decCount))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote +',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remotePlus),
 | 
			
		||||
					borderColor: colors.remotePlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.incCount)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote -',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remoteMinus),
 | 
			
		||||
					borderColor: colors.remoteMinus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(negate(this.stats.drive.remote.decCount))
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('number')(value);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		driveFilesTotalChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Combined',
 | 
			
		||||
					fill: false,
 | 
			
		||||
					borderColor: '#555',
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					borderDash: [4, 4],
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(sum(this.stats.drive.local.totalCount, this.stats.drive.remote.totalCount))
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Local',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.local),
 | 
			
		||||
					borderColor: colors.local,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.local.totalCount)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Remote',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.remote),
 | 
			
		||||
					borderColor: colors.remote,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.drive.remote.totalCount)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('number')(value);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		networkRequestsChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Incoming',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.network.incomingRequests)
 | 
			
		||||
				}]
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		networkTimeChart(): any {
 | 
			
		||||
			const data = [];
 | 
			
		||||
 | 
			
		||||
			for (let i = 0; i < limit; i++) {
 | 
			
		||||
				data.push(this.stats.network.incomingRequests[i] != 0 ? (this.stats.network.totalTime[i] / this.stats.network.incomingRequests[i]) : 0);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Avg time (ms)',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.localPlus),
 | 
			
		||||
					borderColor: colors.localPlus,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(data)
 | 
			
		||||
				}]
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		networkUsageChart(): any {
 | 
			
		||||
			return [{
 | 
			
		||||
				datasets: [{
 | 
			
		||||
					label: 'Incoming',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.incoming),
 | 
			
		||||
					borderColor: colors.incoming,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.network.incomingBytes)
 | 
			
		||||
				}, {
 | 
			
		||||
					label: 'Outgoing',
 | 
			
		||||
					fill: true,
 | 
			
		||||
					backgroundColor: rgba(colors.outgoing),
 | 
			
		||||
					borderColor: colors.outgoing,
 | 
			
		||||
					borderWidth: 2,
 | 
			
		||||
					pointBackgroundColor: '#fff',
 | 
			
		||||
					lineTension: 0,
 | 
			
		||||
					data: this.format(this.stats.network.outgoingBytes)
 | 
			
		||||
				}]
 | 
			
		||||
			}, {
 | 
			
		||||
				scales: {
 | 
			
		||||
					yAxes: [{
 | 
			
		||||
						ticks: {
 | 
			
		||||
							callback: value => {
 | 
			
		||||
								return Vue.filter('bytes')(value, 1);
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					}]
 | 
			
		||||
				},
 | 
			
		||||
				tooltips: {
 | 
			
		||||
					callbacks: {
 | 
			
		||||
						label: (tooltipItem, data) => {
 | 
			
		||||
							const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
			
		||||
							return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}];
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.gkgckalzgidaygcxnugepioremxvxvpt
 | 
			
		||||
	padding 32px
 | 
			
		||||
	background #fff
 | 
			
		||||
	box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
			
		||||
 | 
			
		||||
	*
 | 
			
		||||
		user-select none
 | 
			
		||||
 | 
			
		||||
	> header
 | 
			
		||||
		display flex
 | 
			
		||||
		margin 0 0 1em 0
 | 
			
		||||
		padding 0 0 8px 0
 | 
			
		||||
		font-size 1em
 | 
			
		||||
		color #555
 | 
			
		||||
		border-bottom solid 1px #eee
 | 
			
		||||
 | 
			
		||||
		> b
 | 
			
		||||
			margin-right 8px
 | 
			
		||||
 | 
			
		||||
		> *:last-child
 | 
			
		||||
			margin-left auto
 | 
			
		||||
 | 
			
		||||
			*
 | 
			
		||||
				&:not(.active)
 | 
			
		||||
					color var(--primary)
 | 
			
		||||
					cursor pointer
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
		> *
 | 
			
		||||
			display block
 | 
			
		||||
			height 350px
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -23,6 +23,13 @@
 | 
			
		||||
					<mk-twitter-setting/>
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
 | 
			
		||||
			<ui-card>
 | 
			
		||||
				<div slot="title">%fa:B github% %i18n:@github%</div>
 | 
			
		||||
				<section>
 | 
			
		||||
					<mk-github-setting/>
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<ui-card class="theme" v-show="page == 'theme'">
 | 
			
		||||
@@ -108,6 +115,7 @@
 | 
			
		||||
				<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion%</ui-switch>
 | 
			
		||||
				<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
 | 
			
		||||
				<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
 | 
			
		||||
				<ui-switch v-model="useOsDefaultEmojis">%i18n:common.use-os-default-emojis%</ui-switch>
 | 
			
		||||
				<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
 | 
			
		||||
			</section>
 | 
			
		||||
			<section>
 | 
			
		||||
@@ -317,6 +325,11 @@ export default Vue.extend({
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
		useOsDefaultEmojis: {
 | 
			
		||||
			get() { return this.$store.state.device.useOsDefaultEmojis; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reduceMotion: {
 | 
			
		||||
			get() { return this.$store.state.device.reduceMotion; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,65 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey">
 | 
			
		||||
	<div v-if="stats" class="stats">
 | 
			
		||||
		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
 | 
			
		||||
		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
 | 
			
		||||
		<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
 | 
			
		||||
		<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div>
 | 
			
		||||
		<x-charts/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from "vue";
 | 
			
		||||
import XCharts from "../../components/charts.vue";
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
		XCharts
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			stats: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		(this as any).api('stats').then(stats => {
 | 
			
		||||
			this.stats = stats;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.tcrwdhwpuxrwmcttxjcsehgpagpstqey
 | 
			
		||||
	width 100%
 | 
			
		||||
	padding 16px
 | 
			
		||||
 | 
			
		||||
	> .stats
 | 
			
		||||
		display flex
 | 
			
		||||
		justify-content center
 | 
			
		||||
		margin 0 auto 16px auto
 | 
			
		||||
		padding 32px
 | 
			
		||||
		background #fff
 | 
			
		||||
		box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
			
		||||
 | 
			
		||||
		> div
 | 
			
		||||
			flex 1
 | 
			
		||||
			text-align center
 | 
			
		||||
 | 
			
		||||
			> *:first-child
 | 
			
		||||
				display block
 | 
			
		||||
				color var(--primary)
 | 
			
		||||
 | 
			
		||||
			> *:last-child
 | 
			
		||||
				font-size 70%
 | 
			
		||||
 | 
			
		||||
	> div
 | 
			
		||||
		max-width 950px
 | 
			
		||||
		margin 0 auto
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										26
									
								
								src/client/app/desktop/views/pages/user/user.github.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/client/app/desktop/views/pages/user/user.github.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div class="aqooishiizumijmihokohinatamihoaz">
 | 
			
		||||
	<span>%fa:B github%<a :href="`https://github.com/${user.github.login}`" target="_blank">@{{ user.github.login }}</a></span>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	props: ['user']
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.aqooishiizumijmihokohinatamihoaz
 | 
			
		||||
	padding 32px
 | 
			
		||||
	background #171515
 | 
			
		||||
	border-radius 6px
 | 
			
		||||
	color #fff
 | 
			
		||||
 | 
			
		||||
	a
 | 
			
		||||
		margin-left 8px
 | 
			
		||||
		color #fff
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
<mk-ui>
 | 
			
		||||
	<div class="xygkxeaeontfaokvqmiblezmhvhostak" v-if="!fetching">
 | 
			
		||||
		<div class="is-suspended" v-if="user.isSuspended">%fa:exclamation-triangle% %i18n:@is-suspended%</div>
 | 
			
		||||
		<div class="is-remote" v-if="user.host != null">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
 | 
			
		||||
		<div class="is-remote" v-if="user.host">%fa:exclamation-triangle% %i18n:common.is-remote-user%<a :href="user.url || user.uri" target="_blank">%i18n:common.view-on-remote%</a></div>
 | 
			
		||||
		<main>
 | 
			
		||||
			<div class="main">
 | 
			
		||||
				<x-header :user="user"/>
 | 
			
		||||
@@ -12,14 +12,15 @@
 | 
			
		||||
			<div class="side">
 | 
			
		||||
				<div class="instance" v-if="!$store.getters.isSignedIn"><mk-instance/></div>
 | 
			
		||||
				<x-profile :user="user"/>
 | 
			
		||||
				<x-twitter :user="user" v-if="user.host === null && user.twitter"/>
 | 
			
		||||
				<x-twitter :user="user" v-if="!user.host && user.twitter"/>
 | 
			
		||||
				<x-github :user="user" v-if="!user.host && user.github"/>
 | 
			
		||||
				<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/>
 | 
			
		||||
				<mk-activity :user="user"/>
 | 
			
		||||
				<x-photos :user="user"/>
 | 
			
		||||
				<x-friends :user="user"/>
 | 
			
		||||
				<x-followers-you-know v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
 | 
			
		||||
				<div class="nav"><mk-nav/></div>
 | 
			
		||||
				<p v-if="user.host === null">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 | 
			
		||||
				<p v-if="!user.host">%i18n:@last-used-at%: <b><mk-time :time="user.lastUsedAt"/></b></p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</main>
 | 
			
		||||
	</div>
 | 
			
		||||
@@ -37,6 +38,7 @@ import XPhotos from './user.photos.vue';
 | 
			
		||||
import XFollowersYouKnow from './user.followers-you-know.vue';
 | 
			
		||||
import XFriends from './user.friends.vue';
 | 
			
		||||
import XTwitter from './user.twitter.vue';
 | 
			
		||||
import XGithub from './user.github.vue'; // ?MEM: Don't fix the intentional typo. (XGitHub -> `<x-git-hub>`)
 | 
			
		||||
 | 
			
		||||
export default Vue.extend({
 | 
			
		||||
	components: {
 | 
			
		||||
@@ -46,7 +48,8 @@ export default Vue.extend({
 | 
			
		||||
		XPhotos,
 | 
			
		||||
		XFollowersYouKnow,
 | 
			
		||||
		XFriends,
 | 
			
		||||
		XTwitter
 | 
			
		||||
		XTwitter,
 | 
			
		||||
		XGithub // ?MEM: Don't fix the intentional typo. (see L41)
 | 
			
		||||
	},
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@
 | 
			
		||||
					<ui-switch v-model="reduceMotion">%i18n:common.reduce-motion% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
 | 
			
		||||
					<ui-switch v-model="contrastedAcct">%i18n:@contrasted-acct%</ui-switch>
 | 
			
		||||
					<ui-switch v-model="showFullAcct">%i18n:common.show-full-acct%</ui-switch>
 | 
			
		||||
					<ui-switch v-model="useOsDefaultEmojis">%i18n:common.use-os-default-emojis%</ui-switch>
 | 
			
		||||
					<ui-switch v-model="iLikeSushi">%i18n:common.i-like-sushi%</ui-switch>
 | 
			
		||||
					<ui-switch v-model="disableAnimatedMfm">%i18n:common.disable-animated-mfm%</ui-switch>
 | 
			
		||||
					<ui-switch v-model="alwaysShowNsfw">%i18n:common.always-show-nsfw% (%i18n:common.this-setting-is-this-device-only%)</ui-switch>
 | 
			
		||||
@@ -125,6 +126,19 @@
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
 | 
			
		||||
			<ui-card>
 | 
			
		||||
				<div slot="title">%fa:B github% %i18n:@github%</div>
 | 
			
		||||
 | 
			
		||||
				<section>
 | 
			
		||||
					<p class="account" v-if="$store.state.i.github"><a :href="`https://github.com/${$store.state.i.github.login}`" target="_blank">@{{ $store.state.i.github.login }}</a></p>
 | 
			
		||||
					<p>
 | 
			
		||||
						<a :href="`${apiUrl}/connect/github`" target="_blank">{{ $store.state.i.github ? '%i18n:@github-reconnect%' : '%i18n:@github-connect%' }}</a>
 | 
			
		||||
						<span v-if="$store.state.i.github"> or </span>
 | 
			
		||||
						<a :href="`${apiUrl}/disconnect/github`" target="_blank" v-if="$store.state.i.github">%i18n:@github-disconnect%</a>
 | 
			
		||||
					</p>
 | 
			
		||||
				</section>
 | 
			
		||||
			</ui-card>
 | 
			
		||||
 | 
			
		||||
			<mk-api-settings />
 | 
			
		||||
 | 
			
		||||
			<ui-card>
 | 
			
		||||
@@ -186,6 +200,11 @@ export default Vue.extend({
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'darkmode', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		useOsDefaultEmojis: {
 | 
			
		||||
			get() { return this.$store.state.device.useOsDefaultEmojis; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'useOsDefaultEmojis', value }); }
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		reduceMotion: {
 | 
			
		||||
			get() { return this.$store.state.device.reduceMotion; },
 | 
			
		||||
			set(value) { this.$store.commit('device/set', { key: 'reduceMotion', value }); }
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,8 @@ const defaultDeviceSettings = {
 | 
			
		||||
	deckColumnAlign: 'center',
 | 
			
		||||
	mobileNotificationPosition: 'bottom',
 | 
			
		||||
	deckTemporaryColumn: null,
 | 
			
		||||
	deckDefault: false
 | 
			
		||||
	deckDefault: false,
 | 
			
		||||
	useOsDefaultEmojis: false
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default (os: MiOS) => new Vuex.Store({
 | 
			
		||||
 
 | 
			
		||||
@@ -49,10 +49,6 @@ export default function load() {
 | 
			
		||||
	if (config.localDriveCapacityMb == null) config.localDriveCapacityMb = 256;
 | 
			
		||||
	if (config.remoteDriveCapacityMb == null) config.remoteDriveCapacityMb = 8;
 | 
			
		||||
 | 
			
		||||
	if (config.maxNoteTextLength == null) config.maxNoteTextLength = 1000;
 | 
			
		||||
 | 
			
		||||
	if (config.name == null) config.name = 'Misskey';
 | 
			
		||||
 | 
			
		||||
	return Object.assign(config, mixin);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,6 @@ export type Source = {
 | 
			
		||||
		repository_url?: string;
 | 
			
		||||
		feedback_url?: string;
 | 
			
		||||
	};
 | 
			
		||||
	name?: string;
 | 
			
		||||
	description?: string;
 | 
			
		||||
	languages?: string[];
 | 
			
		||||
	welcome_bg_url?: string;
 | 
			
		||||
	url: string;
 | 
			
		||||
@@ -74,6 +72,10 @@ export type Source = {
 | 
			
		||||
		consumer_key: string;
 | 
			
		||||
		consumer_secret: string;
 | 
			
		||||
	};
 | 
			
		||||
	github?: {
 | 
			
		||||
		client_id: string;
 | 
			
		||||
		client_secret: string;
 | 
			
		||||
	};
 | 
			
		||||
	github_bot?: {
 | 
			
		||||
		hook_secret: string;
 | 
			
		||||
		username: string;
 | 
			
		||||
@@ -105,8 +107,6 @@ export type Source = {
 | 
			
		||||
		engine: string;
 | 
			
		||||
		timeout: number;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	maxNoteTextLength?: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -5,16 +5,29 @@
 | 
			
		||||
export type TextElementEmoji = {
 | 
			
		||||
	type: 'emoji';
 | 
			
		||||
	content: string;
 | 
			
		||||
	emoji: string;
 | 
			
		||||
	emoji?: 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) {
 | 
			
		||||
	const match = text.match(/^:([a-zA-Z0-9+_-]+):/);
 | 
			
		||||
	if (!match) return null;
 | 
			
		||||
	const emoji = match[0];
 | 
			
		||||
	return {
 | 
			
		||||
		type: 'emoji',
 | 
			
		||||
		content: emoji,
 | 
			
		||||
		emoji: match[1]
 | 
			
		||||
	} as TextElementEmoji;
 | 
			
		||||
	const name = text.match(/^:([a-zA-Z0-9+_-]+):/);
 | 
			
		||||
	if (name) {
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'emoji',
 | 
			
		||||
			content: name[0],
 | 
			
		||||
			name: name[1]
 | 
			
		||||
		} as TextElementEmoji;
 | 
			
		||||
	}
 | 
			
		||||
	const unicode = text.match(emojiRegex);
 | 
			
		||||
	if (unicode) {
 | 
			
		||||
		const [content] = unicode;
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'emoji',
 | 
			
		||||
			content,
 | 
			
		||||
			emoji: content
 | 
			
		||||
		} as TextElementEmoji;
 | 
			
		||||
	}
 | 
			
		||||
	return null;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,37 @@
 | 
			
		||||
import db from '../db/mongodb';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
 | 
			
		||||
const Meta = db.get<IMeta>('meta');
 | 
			
		||||
export default Meta;
 | 
			
		||||
 | 
			
		||||
// 後方互換性のため。
 | 
			
		||||
// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行
 | 
			
		||||
if ((config as any).name) {
 | 
			
		||||
	Meta.findOne({}).then(m => {
 | 
			
		||||
		if (m != null && m.name == null) {
 | 
			
		||||
			Meta.update({}, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					name: (config as any).name
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
if ((config as any).description) {
 | 
			
		||||
	Meta.findOne({}).then(m => {
 | 
			
		||||
		if (m != null && m.description == null) {
 | 
			
		||||
			Meta.update({}, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					description: (config as any).description
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type IMeta = {
 | 
			
		||||
	name?: string;
 | 
			
		||||
	description?: string;
 | 
			
		||||
	broadcasts?: any[];
 | 
			
		||||
	stats?: {
 | 
			
		||||
		notesCount: number;
 | 
			
		||||
@@ -15,4 +43,9 @@ export type IMeta = {
 | 
			
		||||
	disableLocalTimeline?: boolean;
 | 
			
		||||
	hidedTags?: string[];
 | 
			
		||||
	bannerUrl?: string;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Max allowed note text length in charactors
 | 
			
		||||
	 */
 | 
			
		||||
	maxNoteTextLength?: number;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ import Reaction from './note-reaction';
 | 
			
		||||
import { packMany as packFileMany, IDriveFile } from './drive-file';
 | 
			
		||||
import Favorite from './favorite';
 | 
			
		||||
import Following from './following';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import Emoji from './emoji';
 | 
			
		||||
 | 
			
		||||
const Note = db.get<INote>('notes');
 | 
			
		||||
@@ -27,10 +26,6 @@ Note.createIndex({ createdAt: -1 });
 | 
			
		||||
Note.createIndex({ score: -1 }, { sparse: true });
 | 
			
		||||
export default Note;
 | 
			
		||||
 | 
			
		||||
export function isValidText(text: string): boolean {
 | 
			
		||||
	return length(text.trim()) <= config.maxNoteTextLength && text.trim() != '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isValidCw(text: string): boolean {
 | 
			
		||||
	return length(text.trim()) <= 100;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,11 @@ export interface ILocalUser extends IUserBase {
 | 
			
		||||
		userId: string;
 | 
			
		||||
		screenName: string;
 | 
			
		||||
	};
 | 
			
		||||
	github: {
 | 
			
		||||
		accessToken: string;
 | 
			
		||||
		id: string;
 | 
			
		||||
		login: string;
 | 
			
		||||
	};
 | 
			
		||||
	line: {
 | 
			
		||||
		userId: string;
 | 
			
		||||
	};
 | 
			
		||||
@@ -280,6 +285,9 @@ export const pack = (
 | 
			
		||||
			delete _user.twitter.accessToken;
 | 
			
		||||
			delete _user.twitter.accessTokenSecret;
 | 
			
		||||
		}
 | 
			
		||||
		if (_user.github) {
 | 
			
		||||
			delete _user.github.accessToken;
 | 
			
		||||
		}
 | 
			
		||||
		delete _user.line;
 | 
			
		||||
 | 
			
		||||
		// Visible via only the official client
 | 
			
		||||
 
 | 
			
		||||
@@ -62,15 +62,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
 | 
			
		||||
		}) as IRemoteUser;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region Log
 | 
			
		||||
	publishApLogStream({
 | 
			
		||||
		direction: 'in',
 | 
			
		||||
		activity: activity.type,
 | 
			
		||||
		host: user.host,
 | 
			
		||||
		actor: user.username
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// Update activityの場合は、ここで署名検証/更新処理まで実施して終了
 | 
			
		||||
	if (activity.type === 'Update') {
 | 
			
		||||
		if (activity.object && activity.object.type === 'Person') {
 | 
			
		||||
@@ -102,6 +93,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	//#region Log
 | 
			
		||||
	publishApLogStream({
 | 
			
		||||
		direction: 'in',
 | 
			
		||||
		activity: activity.type,
 | 
			
		||||
		host: user.host,
 | 
			
		||||
		actor: user.username
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// アクティビティを処理
 | 
			
		||||
	try {
 | 
			
		||||
		await perform(user, activity);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
import parse from '../../../mfm/parse';
 | 
			
		||||
 | 
			
		||||
export default function(text: string) {
 | 
			
		||||
	if (!text) return [];
 | 
			
		||||
	return parse(text).filter(t => t.type === 'emoji').map(t => (t as any).emoji);
 | 
			
		||||
}
 | 
			
		||||
@@ -8,9 +8,7 @@ import Note, { INote } from '../../../models/note';
 | 
			
		||||
import User from '../../../models/user';
 | 
			
		||||
import toHtml from '../misc/get-note-html';
 | 
			
		||||
import parseMfm from '../../../mfm/parse';
 | 
			
		||||
import getEmojiNames from '../misc/get-emoji-names';
 | 
			
		||||
import Emoji, { IEmoji } from '../../../models/emoji';
 | 
			
		||||
import { unique } from '../../../prelude/array';
 | 
			
		||||
 | 
			
		||||
export default async function renderNote(note: INote, dive = true): Promise<any> {
 | 
			
		||||
	const promisedFiles: Promise<IDriveFile[]> = note.fileIds
 | 
			
		||||
@@ -110,8 +108,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
 | 
			
		||||
 | 
			
		||||
	const content = toHtml(Object.assign({}, note, { text }));
 | 
			
		||||
 | 
			
		||||
	const emojiNames = unique(getEmojiNames(content));
 | 
			
		||||
	const emojis = await getEmojis(emojiNames);
 | 
			
		||||
	const emojis = await getEmojis(note.emojis);
 | 
			
		||||
	const apemojis = emojis.map(emoji => renderEmoji(emoji));
 | 
			
		||||
 | 
			
		||||
	const tag = [
 | 
			
		||||
@@ -141,12 +138,10 @@ async function getEmojis(names: string[]): Promise<IEmoji[]> {
 | 
			
		||||
	if (names == null || names.length < 1) return [];
 | 
			
		||||
 | 
			
		||||
	const emojis = await Promise.all(
 | 
			
		||||
		names.map(async name => {
 | 
			
		||||
			return await Emoji.findOne({
 | 
			
		||||
				name,
 | 
			
		||||
				host: null
 | 
			
		||||
			});
 | 
			
		||||
		})
 | 
			
		||||
		names.map(name => Emoji.findOne({
 | 
			
		||||
			name,
 | 
			
		||||
			host: null
 | 
			
		||||
		}))
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	return emojis.filter(emoji => emoji != null);
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,8 @@ export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, Error] {
 | 
			
		||||
	if (defs.params == null) return [params, null];
 | 
			
		||||
 | 
			
		||||
	const x: any = {};
 | 
			
		||||
	let err: Error = null;
 | 
			
		||||
	Object.entries(defs.params).some(([k, def]) => {
 | 
			
		||||
@@ -38,7 +40,7 @@ function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, E
 | 
			
		||||
			(err as any).param = k;
 | 
			
		||||
			return true;
 | 
			
		||||
		} else {
 | 
			
		||||
			if (v === undefined && def.default) {
 | 
			
		||||
			if (v === undefined && def.hasOwnProperty('default')) {
 | 
			
		||||
				x[k] = def.default;
 | 
			
		||||
			} else {
 | 
			
		||||
				x[k] = v;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,15 +12,15 @@ export const meta = {
 | 
			
		||||
 | 
			
		||||
	params: {
 | 
			
		||||
		name: {
 | 
			
		||||
			validator: $.str
 | 
			
		||||
			validator: $.str.min(1)
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		url: {
 | 
			
		||||
			validator: $.str
 | 
			
		||||
			validator: $.str.min(1)
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		aliases: {
 | 
			
		||||
			validator: $.arr($.str).optional,
 | 
			
		||||
			validator: $.arr($.str.min(1)).optional,
 | 
			
		||||
			default: [] as string[]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,27 @@ export const meta = {
 | 
			
		||||
				'ja-JP': 'インスタンスのバナー画像URL'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		name: {
 | 
			
		||||
			validator: $.str.optional.nullable,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': 'インスタンス名'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		description: {
 | 
			
		||||
			validator: $.str.optional.nullable,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': 'インスタンスの紹介文'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		maxNoteTextLength: {
 | 
			
		||||
			validator: $.num.optional.min(1),
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '投稿の最大文字数'
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -71,6 +92,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
 | 
			
		||||
		set.bannerUrl = ps.bannerUrl;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ps.name !== undefined) {
 | 
			
		||||
		set.name = ps.name;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ps.description !== undefined) {
 | 
			
		||||
		set.description = ps.description;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (ps.maxNoteTextLength) {
 | 
			
		||||
		set.maxNoteTextLength = ps.maxNoteTextLength;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await Meta.update({}, {
 | 
			
		||||
		$set: set
 | 
			
		||||
	}, { upsert: true });
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ export const meta = {
 | 
			
		||||
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
 | 
			
		||||
	const folders = await DriveFolder
 | 
			
		||||
		.find({
 | 
			
		||||
			name: name,
 | 
			
		||||
			name: ps.name,
 | 
			
		||||
			userId: user._id,
 | 
			
		||||
			parentId: ps.parentId
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		||||
		version: pkg.version,
 | 
			
		||||
		clientVersion: client.version,
 | 
			
		||||
 | 
			
		||||
		name: config.name || 'Misskey',
 | 
			
		||||
		description: config.description,
 | 
			
		||||
		name: met.name || 'Misskey',
 | 
			
		||||
		description: met.description,
 | 
			
		||||
 | 
			
		||||
		secure: config.https != null,
 | 
			
		||||
		machine: os.hostname(),
 | 
			
		||||
@@ -62,7 +62,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		||||
		swPublickey: config.sw ? config.sw.public_key : null,
 | 
			
		||||
		hidedTags: (me && me.isAdmin) ? met.hidedTags : undefined,
 | 
			
		||||
		bannerUrl: met.bannerUrl,
 | 
			
		||||
		maxNoteTextLength: config.maxNoteTextLength,
 | 
			
		||||
		maxNoteTextLength: met.maxNoteTextLength || 1000,
 | 
			
		||||
 | 
			
		||||
		emojis: emojis,
 | 
			
		||||
 | 
			
		||||
@@ -73,6 +73,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		||||
			recaptcha: config.recaptcha ? true : false,
 | 
			
		||||
			objectStorage: config.drive && config.drive.storage === 'minio',
 | 
			
		||||
			twitter: config.twitter ? true : false,
 | 
			
		||||
			github: config.github ? true : false,
 | 
			
		||||
			serviceWorker: config.sw ? true : false,
 | 
			
		||||
			userRecommendation: config.user_recommendation ? config.user_recommendation : {}
 | 
			
		||||
		} : undefined
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,20 @@
 | 
			
		||||
import $ from 'cafy'; import ID, { transform, transformMany } from '../../../../misc/cafy-id';
 | 
			
		||||
const ms = require('ms');
 | 
			
		||||
import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
 | 
			
		||||
import { length } from 'stringz';
 | 
			
		||||
import Note, { INote, isValidCw, pack } from '../../../../models/note';
 | 
			
		||||
import User, { IUser } from '../../../../models/user';
 | 
			
		||||
import DriveFile, { IDriveFile } from '../../../../models/drive-file';
 | 
			
		||||
import create from '../../../../services/note/create';
 | 
			
		||||
import define from '../../define';
 | 
			
		||||
import Meta from '../../../../models/meta';
 | 
			
		||||
 | 
			
		||||
let maxNoteTextLength = 1000;
 | 
			
		||||
 | 
			
		||||
setInterval(() => {
 | 
			
		||||
	Meta.findOne({}).then(m => {
 | 
			
		||||
		if (m.maxNoteTextLength) maxNoteTextLength = m.maxNoteTextLength;
 | 
			
		||||
	});
 | 
			
		||||
}, 3000);
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	stability: 'stable',
 | 
			
		||||
@@ -40,7 +50,9 @@ export const meta = {
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		text: {
 | 
			
		||||
			validator: $.str.optional.nullable.pipe(isValidText),
 | 
			
		||||
			validator: $.str.optional.nullable.pipe(text =>
 | 
			
		||||
				length(text.trim()) <= maxNoteTextLength && text.trim() != ''
 | 
			
		||||
			),
 | 
			
		||||
			default: null as any,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '投稿内容'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								src/server/api/mastodon/emoji.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/server/api/mastodon/emoji.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
export type IMastodonEmoji = {
 | 
			
		||||
	shortcode: string,
 | 
			
		||||
	url: string,
 | 
			
		||||
	static_url: string,
 | 
			
		||||
	visible_in_picker: boolean
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export async function toMastodonEmojis(emoji: any): Promise<IMastodonEmoji[]> {
 | 
			
		||||
	return [{
 | 
			
		||||
		shortcode: emoji.name,
 | 
			
		||||
		url: emoji.url,
 | 
			
		||||
		static_url: emoji.url, // TODO: Implement ensuring static emoji
 | 
			
		||||
		visible_in_picker: true
 | 
			
		||||
	}, ...(emoji.aliases as string[] || []).map(x => ({
 | 
			
		||||
		shortcode: x,
 | 
			
		||||
		url: emoji.url,
 | 
			
		||||
		static_url: emoji.url,
 | 
			
		||||
		visible_in_picker: true
 | 
			
		||||
	}))];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toMisskeyEmojiSync(emoji: IMastodonEmoji) {
 | 
			
		||||
	return {
 | 
			
		||||
		name: emoji.shortcode,
 | 
			
		||||
		url: emoji.url
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function toMisskeyEmojiWithAliasesSync(emoji: IMastodonEmoji, ...aliases: string[]) {
 | 
			
		||||
	return {
 | 
			
		||||
		name: emoji.shortcode,
 | 
			
		||||
		aliases,
 | 
			
		||||
		url: emoji.url
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +1,22 @@
 | 
			
		||||
import * as Router from 'koa-router';
 | 
			
		||||
import User from '../../models/user';
 | 
			
		||||
import User from '../../../models/user';
 | 
			
		||||
import { toASCII } from 'punycode';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import Meta from '../../models/meta';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
import Meta from '../../../models/meta';
 | 
			
		||||
import { ObjectID } from 'bson';
 | 
			
		||||
const pkg = require('../../../package.json');
 | 
			
		||||
import Emoji from '../../../models/emoji';
 | 
			
		||||
import { toMastodonEmojis } from './emoji';
 | 
			
		||||
const pkg = require('../../../../package.json');
 | 
			
		||||
 | 
			
		||||
// Init router
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
router.get('/v1/custom_emojis', async ctx => ctx.body = {});
 | 
			
		||||
router.get('/v1/custom_emojis', async ctx => ctx.body =
 | 
			
		||||
	(await Emoji.find({ host: null }, {
 | 
			
		||||
		fields: {
 | 
			
		||||
			_id: false
 | 
			
		||||
		}
 | 
			
		||||
	})).map(x => toMastodonEmojis(x)));
 | 
			
		||||
 | 
			
		||||
router.get('/v1/instance', async ctx => { // TODO: This is a temporary implementation. Consider creating helper methods!
 | 
			
		||||
	const meta = await Meta.findOne() || {};
 | 
			
		||||
@@ -34,11 +41,16 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
 | 
			
		||||
		notesCount: 0
 | 
			
		||||
	};
 | 
			
		||||
	const acct = maintainer.host ? `${maintainer.username}@${maintainer.host}` : maintainer.username;
 | 
			
		||||
	const emojis = (await Emoji.find({ host: null }, {
 | 
			
		||||
		fields: {
 | 
			
		||||
			_id: false
 | 
			
		||||
		}
 | 
			
		||||
	})).map(toMastodonEmojis);
 | 
			
		||||
 | 
			
		||||
	ctx.body = {
 | 
			
		||||
		uri: config.hostname,
 | 
			
		||||
		title: config.name || 'Misskey',
 | 
			
		||||
		description: config.description || '',
 | 
			
		||||
		title: meta.name || 'Misskey',
 | 
			
		||||
		description: meta.description || '',
 | 
			
		||||
		email: config.maintainer.email || config.maintainer.url.startsWith('mailto:') ? config.maintainer.url.slice(7) : '',
 | 
			
		||||
		version: `0.0.0:compatible:misskey:${pkg.version}`, // TODO: How to tell about that this is an api for compatibility?
 | 
			
		||||
		thumbnail: meta.bannerUrl,
 | 
			
		||||
@@ -73,7 +85,7 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
 | 
			
		||||
			followers_count: maintainer.followersCount,
 | 
			
		||||
			following_count: maintainer.followingCount,
 | 
			
		||||
			statuses_count: maintainer.notesCount,
 | 
			
		||||
			emojis: [],
 | 
			
		||||
			emojis: emojis,
 | 
			
		||||
			moved: null,
 | 
			
		||||
			fields: null
 | 
			
		||||
		}
 | 
			
		||||
@@ -1,11 +1,16 @@
 | 
			
		||||
import * as EventEmitter from 'events';
 | 
			
		||||
import * as Koa from 'koa';
 | 
			
		||||
import * as Router from 'koa-router';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
const crypto = require('crypto');
 | 
			
		||||
 | 
			
		||||
import User, { IUser } from '../../../models/user';
 | 
			
		||||
import { OAuth2 } from 'oauth';
 | 
			
		||||
import User, { IUser, pack, ILocalUser } from '../../../models/user';
 | 
			
		||||
import createNote from '../../../services/note/create';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
import { publishMainStream } from '../../../stream';
 | 
			
		||||
import redis from '../../../db/redis';
 | 
			
		||||
import uuid = require('uuid');
 | 
			
		||||
import signin from '../common/signin';
 | 
			
		||||
const crypto = require('crypto');
 | 
			
		||||
 | 
			
		||||
const handler = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
@@ -28,10 +33,264 @@ const post = async (text: string, home = true) => {
 | 
			
		||||
	createNote(bot, { text, visibility: home ? 'home' : 'public' });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getUserToken(ctx: Koa.Context) {
 | 
			
		||||
	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function compareOrigin(ctx: Koa.Context) {
 | 
			
		||||
	function normalizeUrl(url: string) {
 | 
			
		||||
		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : '';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const referer = ctx.headers['referer'];
 | 
			
		||||
 | 
			
		||||
	return (normalizeUrl(referer) == normalizeUrl(config.url));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Init router
 | 
			
		||||
const router = new Router();
 | 
			
		||||
 | 
			
		||||
if (config.github_bot != null) {
 | 
			
		||||
router.get('/disconnect/github', async ctx => {
 | 
			
		||||
	if (!compareOrigin(ctx)) {
 | 
			
		||||
		ctx.throw(400, 'invalid origin');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const userToken = getUserToken(ctx);
 | 
			
		||||
	if (!userToken) {
 | 
			
		||||
		ctx.throw(400, 'signin required');
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const user = await User.findOneAndUpdate({
 | 
			
		||||
		host: null,
 | 
			
		||||
		'token': userToken
 | 
			
		||||
	}, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			'github': null
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	ctx.body = `GitHubの連携を解除しました :v:`;
 | 
			
		||||
 | 
			
		||||
	// Publish i updated event
 | 
			
		||||
	publishMainStream(user._id, 'meUpdated', await pack(user, user, {
 | 
			
		||||
		detail: true,
 | 
			
		||||
		includeSecrets: true
 | 
			
		||||
	}));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (!config.github || !redis) {
 | 
			
		||||
	router.get('/connect/github', ctx => {
 | 
			
		||||
		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	router.get('/signin/github', ctx => {
 | 
			
		||||
		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)';
 | 
			
		||||
	});
 | 
			
		||||
} else {
 | 
			
		||||
	const oauth2 = new OAuth2(
 | 
			
		||||
		config.github.client_id,
 | 
			
		||||
		config.github.client_secret,
 | 
			
		||||
		'https://github.com/',
 | 
			
		||||
		'login/oauth/authorize',
 | 
			
		||||
		'login/oauth/access_token');
 | 
			
		||||
 | 
			
		||||
	router.get('/connect/github', async ctx => {
 | 
			
		||||
		if (!compareOrigin(ctx)) {
 | 
			
		||||
			ctx.throw(400, 'invalid origin');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const userToken = getUserToken(ctx);
 | 
			
		||||
		if (!userToken) {
 | 
			
		||||
			ctx.throw(400, 'signin required');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const params = {
 | 
			
		||||
			redirect_uri: `${config.url}/api/gh/cb`,
 | 
			
		||||
			scope: ['read:user'],
 | 
			
		||||
			state: uuid()
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		redis.set(userToken, JSON.stringify(params));
 | 
			
		||||
		ctx.redirect(oauth2.getAuthorizeUrl(params));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	router.get('/signin/github', async ctx => {
 | 
			
		||||
		const sessid = uuid();
 | 
			
		||||
 | 
			
		||||
		const params = {
 | 
			
		||||
			redirect_uri: `${config.url}/api/gh/cb`,
 | 
			
		||||
			scope: ['read:user'],
 | 
			
		||||
			state: uuid()
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const expires = 1000 * 60 * 60; // 1h
 | 
			
		||||
		ctx.cookies.set('signin_with_github_session_id', sessid, {
 | 
			
		||||
			path: '/',
 | 
			
		||||
			domain: config.host,
 | 
			
		||||
			secure: config.url.startsWith('https'),
 | 
			
		||||
			httpOnly: true,
 | 
			
		||||
			expires: new Date(Date.now() + expires),
 | 
			
		||||
			maxAge: expires
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		redis.set(sessid, JSON.stringify(params));
 | 
			
		||||
		ctx.redirect(oauth2.getAuthorizeUrl(params));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	router.get('/gh/cb', async ctx => {
 | 
			
		||||
		const userToken = getUserToken(ctx);
 | 
			
		||||
 | 
			
		||||
		if (!userToken) {
 | 
			
		||||
			const sessid = ctx.cookies.get('signin_with_github_session_id');
 | 
			
		||||
 | 
			
		||||
			if (!sessid) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const code = ctx.query.code;
 | 
			
		||||
 | 
			
		||||
			if (!code) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
			
		||||
				redis.get(sessid, async (_, state) => {
 | 
			
		||||
					res(JSON.parse(state));
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (ctx.query.state !== state) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { accessToken } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				oauth2.getOAuthAccessToken(
 | 
			
		||||
					code,
 | 
			
		||||
					{ redirect_uri },
 | 
			
		||||
					(err, accessToken, refresh, result) => {
 | 
			
		||||
						if (err)
 | 
			
		||||
							rej(err);
 | 
			
		||||
						else if (result.error)
 | 
			
		||||
							rej(result.error);
 | 
			
		||||
						else
 | 
			
		||||
							res({ accessToken });
 | 
			
		||||
					}));
 | 
			
		||||
 | 
			
		||||
			const { login, id } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				request({
 | 
			
		||||
					url: 'https://api.github.com/user',
 | 
			
		||||
					headers: {
 | 
			
		||||
						'Accept': 'application/vnd.github.v3+json',
 | 
			
		||||
						'Authorization': `bearer ${accessToken}`,
 | 
			
		||||
						'User-Agent': config.user_agent
 | 
			
		||||
					}
 | 
			
		||||
				}, (err, response, body) => {
 | 
			
		||||
					if (err)
 | 
			
		||||
						rej(err);
 | 
			
		||||
					else
 | 
			
		||||
						res(JSON.parse(body));
 | 
			
		||||
				}));
 | 
			
		||||
 | 
			
		||||
			if (!login || !id) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const user = await User.findOne({
 | 
			
		||||
				host: null,
 | 
			
		||||
				'github.id': id
 | 
			
		||||
			}) as ILocalUser;
 | 
			
		||||
 | 
			
		||||
			if (!user) {
 | 
			
		||||
				ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`);
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			signin(ctx, user, true);
 | 
			
		||||
		} else {
 | 
			
		||||
			const code = ctx.query.code;
 | 
			
		||||
 | 
			
		||||
			if (!code) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { redirect_uri, state } = await new Promise<any>((res, rej) => {
 | 
			
		||||
				redis.get(userToken, async (_, state) => {
 | 
			
		||||
					res(JSON.parse(state));
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			if (ctx.query.state !== state) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const { accessToken } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				oauth2.getOAuthAccessToken(
 | 
			
		||||
					code,
 | 
			
		||||
					{ redirect_uri },
 | 
			
		||||
					(err, accessToken, refresh, result) => {
 | 
			
		||||
						if (err)
 | 
			
		||||
							rej(err);
 | 
			
		||||
						else if (result.error)
 | 
			
		||||
							rej(result.error);
 | 
			
		||||
						else
 | 
			
		||||
							res({ accessToken });
 | 
			
		||||
					}));
 | 
			
		||||
 | 
			
		||||
			const { login, id } = await new Promise<any>((res, rej) =>
 | 
			
		||||
				request({
 | 
			
		||||
					url: 'https://api.github.com/user',
 | 
			
		||||
					headers: {
 | 
			
		||||
						'Accept': 'application/vnd.github.v3+json',
 | 
			
		||||
						'Authorization': `bearer ${accessToken}`,
 | 
			
		||||
						'User-Agent': config.user_agent
 | 
			
		||||
					}
 | 
			
		||||
				}, (err, response, body) => {
 | 
			
		||||
					if (err)
 | 
			
		||||
						rej(err);
 | 
			
		||||
					else
 | 
			
		||||
						res(JSON.parse(body));
 | 
			
		||||
				}));
 | 
			
		||||
 | 
			
		||||
			if (!login || !id) {
 | 
			
		||||
				ctx.throw(400, 'invalid session');
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const user = await User.findOneAndUpdate({
 | 
			
		||||
				host: null,
 | 
			
		||||
				token: userToken
 | 
			
		||||
			}, {
 | 
			
		||||
				$set: {
 | 
			
		||||
					github: {
 | 
			
		||||
						accessToken,
 | 
			
		||||
						id,
 | 
			
		||||
						login
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`;
 | 
			
		||||
 | 
			
		||||
			// Publish i updated event
 | 
			
		||||
			publishMainStream(user._id, 'meUpdated', await pack(user, user, {
 | 
			
		||||
				detail: true,
 | 
			
		||||
				includeSecrets: true
 | 
			
		||||
			}));
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (config.github_bot) {
 | 
			
		||||
	const secret = config.github_bot.hook_secret;
 | 
			
		||||
 | 
			
		||||
	router.post('/hooks/github', ctx => {
 | 
			
		||||
 
 | 
			
		||||
@@ -456,8 +456,8 @@ function extractHashtags(tokens: ReturnType<typeof parse>): string[] {
 | 
			
		||||
function extractEmojis(tokens: ReturnType<typeof parse>): string[] {
 | 
			
		||||
	// Extract emojis
 | 
			
		||||
	const emojis = tokens
 | 
			
		||||
		.filter(t => t.type == 'emoji')
 | 
			
		||||
		.map(t => (t as TextElementEmoji).emoji)
 | 
			
		||||
		.filter(t => t.type == 'emoji' && t.name)
 | 
			
		||||
		.map(t => (t as TextElementEmoji).name)
 | 
			
		||||
		.filter(emoji => emoji.length <= 100);
 | 
			
		||||
 | 
			
		||||
	return unique(emojis);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								test/mfm.ts
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								test/mfm.ts
									
									
									
									
									
								
							@@ -16,7 +16,7 @@ describe('Text', () => {
 | 
			
		||||
			{ type: 'text', content: ' '},
 | 
			
		||||
			{ type: 'mention', content: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' },
 | 
			
		||||
			{ type: 'text', content: ' お腹ペコい ' },
 | 
			
		||||
			{ type: 'emoji', content: ':cat:', emoji: 'cat'},
 | 
			
		||||
			{ type: 'emoji', content: ':cat:', name: 'cat'},
 | 
			
		||||
			{ type: 'text', content: ' '},
 | 
			
		||||
			{ type: 'hashtag', content: '#yryr', hashtag: 'yryr' }
 | 
			
		||||
		], tokens);
 | 
			
		||||
@@ -182,15 +182,20 @@ describe('Text', () => {
 | 
			
		||||
		it('emoji', () => {
 | 
			
		||||
			const tokens1 = analyze(':cat:');
 | 
			
		||||
			assert.deepEqual([
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', emoji: 'cat'}
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', name: 'cat' }
 | 
			
		||||
			], tokens1);
 | 
			
		||||
 | 
			
		||||
			const tokens2 = analyze(':cat::cat::cat:');
 | 
			
		||||
			assert.deepEqual([
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', emoji: 'cat'},
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', emoji: 'cat'},
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', emoji: 'cat'}
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', name: 'cat' },
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', name: 'cat' },
 | 
			
		||||
				{ type: 'emoji', content: ':cat:', name: 'cat' }
 | 
			
		||||
			], tokens2);
 | 
			
		||||
 | 
			
		||||
			const tokens3 = analyze('🍎');
 | 
			
		||||
			assert.deepEqual([
 | 
			
		||||
				{ type: 'emoji', content: '🍎', emoji: '🍎' }
 | 
			
		||||
			], tokens3);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		it('block code', () => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user