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') {
|
||||
@ -101,6 +92,15 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
//#region Log
|
||||
publishApLogStream({
|
||||
direction: 'in',
|
||||
activity: activity.type,
|
||||
host: user.host,
|
||||
actor: user.username
|
||||
});
|
||||
//#endregion
|
||||
|
||||
// アクティビティを処理
|
||||
try {
|
||||
|
@ -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