Compare commits

...

150 Commits

Author SHA1 Message Date
syuilo
24ef98eb01 10.58.1 2018-11-27 01:26:07 +09:00
syuilo
7ed50b90bd [Client] Resolve #3323 2018-11-27 01:21:16 +09:00
MeiMei
b6fd5d7282 No caching /api/ (#3411) 2018-11-27 01:16:25 +09:00
Acid Chicken (硫酸鶏)
33243e7176 Fix #3409 (#3410)
* Update Dockerfile

* Update Dockerfile
2018-11-27 00:32:56 +09:00
Aya Morisawa
e8439679a5 Add yarn.lock to .gitignore (#3408) 2018-11-26 21:55:16 +09:00
MeiMei
06124dbbd5 Return 404 for undefined .well-known (#3404) 2018-11-26 04:49:24 +09:00
syuilo
857940f402 10.58.0 2018-11-26 04:33:39 +09:00
MeiMei
bcb04924ff Image for web publish (#3402)
* Image for Web

* Add comment

* Make main to original
2018-11-26 04:25:48 +09:00
syuilo
0863e5d379 🎨 2018-11-25 13:47:42 +09:00
syuilo
55dcd25df1 Improve MFM 2018-11-25 13:36:52 +09:00
syuilo
f3155ea180 [MFM] Add center syntax
Resolve #1775
2018-11-25 13:36:40 +09:00
syuilo
2c5162671c Improve MFM 2018-11-25 13:23:18 +09:00
syuilo
fc8aeb5a66 [Client] Fix bug 2018-11-25 13:21:47 +09:00
syuilo
995cf503eb Add MFM test 2018-11-25 13:21:39 +09:00
syuilo
0e49c11a4c Refactoring 2018-11-25 13:19:33 +09:00
syuilo
0367c37b0a 10.57.3 2018-11-25 05:17:45 +09:00
syuilo
e0b9fe5e5d 🎨 2018-11-25 05:16:39 +09:00
syuilo
a4726e683b 🎨 2018-11-25 05:10:48 +09:00
syuilo
3b10e93efe [MFM] Better hashtag parsing 2018-11-25 04:44:42 +09:00
syuilo
02b07c1b5b Update note-mixin.ts 2018-11-25 04:30:32 +09:00
syuilo
5e54751bd4 Refactor 2018-11-25 04:26:07 +09:00
MeiMei
93f13ffc8e Fix: url-preview (#3397) 2018-11-25 01:39:22 +09:00
syuilo
60e10d4efa 10.57.2 2018-11-24 17:31:08 +09:00
syuilo
95ba7e43b1 Fix bug: リモートユーザーのアイコンとバナーの色が取得されていない問題を修正 2018-11-24 17:29:32 +09:00
syuilo
9e5a2e5b17 Fix lint 2018-11-24 17:19:51 +09:00
syuilo
dbbc416095 [MFM] Fix hashtag detection 2018-11-24 17:18:11 +09:00
syuilo
a479ad357c Update dependency 🚀 2018-11-24 17:13:25 +09:00
syuilo
b1c12abb7c Refactor 2018-11-24 17:10:12 +09:00
syuilo
ba50156a83 10.57.1 2018-11-24 07:35:29 +09:00
syuilo
eb83ab41c0 Revert "10.57.1"
This reverts commit 8c4f0d4589.
2018-11-24 07:34:51 +09:00
syuilo
4e6a917dab Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-11-24 07:34:23 +09:00
syuilo
8c4f0d4589 10.57.1 2018-11-24 07:33:53 +09:00
syuilo
3f7738204e Merge pull request #3391 from syuilo/l10n_develop
New Crowdin translations
2018-11-24 07:31:36 +09:00
syuilo
e251a9b9fe New translations ja-JP.yml (English) 2018-11-24 07:22:10 +09:00
syuilo
01d43b9683 New translations ja-JP.yml (Norwegian) 2018-11-24 07:13:03 +09:00
syuilo
4d4a0c89a8 New translations ja-JP.yml (Dutch) 2018-11-24 07:12:58 +09:00
syuilo
0a5524e9c8 New translations ja-JP.yml (Japanese, Kansai) 2018-11-24 07:12:53 +09:00
syuilo
c8fb5746b3 New translations ja-JP.yml (Spanish) 2018-11-24 07:12:49 +09:00
syuilo
bbcc132978 New translations ja-JP.yml (Russian) 2018-11-24 07:12:44 +09:00
syuilo
d3e4f84285 New translations ja-JP.yml (Portuguese) 2018-11-24 07:12:38 +09:00
syuilo
62c470cf75 New translations ja-JP.yml (Polish) 2018-11-24 07:12:34 +09:00
syuilo
8ab31d3765 New translations ja-JP.yml (Korean) 2018-11-24 07:12:28 +09:00
syuilo
55fe1cf0a8 New translations ja-JP.yml (Italian) 2018-11-24 07:12:23 +09:00
syuilo
00cff51ff7 New translations ja-JP.yml (German) 2018-11-24 07:12:18 +09:00
syuilo
d6bc4a7aa1 New translations ja-JP.yml (French) 2018-11-24 07:12:14 +09:00
syuilo
4e57d12aea New translations ja-JP.yml (English) 2018-11-24 07:12:09 +09:00
syuilo
4a2d99c43f New translations ja-JP.yml (Chinese Simplified) 2018-11-24 07:12:03 +09:00
syuilo
217c27df86 New translations ja-JP.yml (Catalan) 2018-11-24 07:11:59 +09:00
syuilo
e6dcd438b4 Update ja-JP.yml 2018-11-24 07:05:41 +09:00
syuilo
de2b0224d6 Resolve #3158 2018-11-24 07:04:29 +09:00
syuilo
3f8a72eb88 🎨 2018-11-24 07:03:03 +09:00
syuilo
0387176e8c 🎨 2018-11-24 07:01:40 +09:00
syuilo
aa34e332f4 Update reversi.room.vue 2018-11-24 07:01:12 +09:00
syuilo
d13999d689 🎨 2018-11-24 06:56:30 +09:00
syuilo
22c4e92728 Resolve #3389 2018-11-24 06:47:51 +09:00
syuilo
df8128c0b1 Update url-preview.ts 2018-11-24 06:41:22 +09:00
syuilo
ec534a3704 New translations ja-JP.yml (Korean) 2018-11-24 00:53:34 +09:00
syuilo
366d4cd3e2 New translations ja-JP.yml (Korean) 2018-11-24 00:43:26 +09:00
Acid Chicken (硫酸鶏)
4841926df1 Update README.md [AUTOGEN] (#3393) 2018-11-23 23:12:54 +09:00
MeiMei
f2f7bdc5a9 Do not use _replyIds (#3392) 2018-11-23 23:12:28 +09:00
syuilo
fd811eb325 New translations ja-JP.yml (English) 2018-11-23 19:44:09 +09:00
syuilo
915d352505 Resolve #3366 2018-11-23 16:39:51 +09:00
syuilo
1d1024c57a [MFM] Improve hashtag detection
Resolve #3387
2018-11-23 16:02:17 +09:00
syuilo
73df6e0347 Update manage.ja.md 2018-11-23 09:26:14 +09:00
syuilo
e6d62c5a7b Update manage.en.md 2018-11-23 09:25:32 +09:00
syuilo
470e48c0a5 Delete suspend.js 2018-11-23 09:24:00 +09:00
syuilo
9235f72a2e Update manage.ja.md 2018-11-23 09:23:40 +09:00
syuilo
9fe6da79b2 Update manage.en.md 2018-11-23 09:23:19 +09:00
syuilo
1858437eb1 Delete reset-password.js 2018-11-23 09:22:45 +09:00
dependabot[bot]
c3ba0dcd32 Update @types/systeminformation requirement from 3.23.0 to 3.23.1 (#3370)
Updates the requirements on [@types/systeminformation](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 09:12:36 +09:00
dependabot[bot]
70f4b13089 Update @types/node requirement from 10.12.2 to 10.12.10 (#3369)
Updates the requirements on [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 09:12:29 +09:00
dependabot[bot]
cc57a4b671 Update @types/koa requirement from 2.0.46 to 2.0.47 (#3354)
Updates the requirements on [@types/koa](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 09:12:21 +09:00
dependabot[bot]
6902700458 Update @types/redis requirement from 2.8.7 to 2.8.8 (#3353)
Updates the requirements on [@types/redis](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 09:12:13 +09:00
dependabot[bot]
b772041547 Update @types/koa-router requirement from 7.0.33 to 7.0.35 (#3352)
Updates the requirements on [@types/koa-router](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 09:12:01 +09:00
syuilo
79174c1a19 10.57.0 2018-11-23 08:20:12 +09:00
syuilo
898850027a Merge pull request #3335 from syuilo/l10n_develop
New Crowdin translations
2018-11-23 08:19:32 +09:00
syuilo
0d272b1fb0 Resolve #3376 2018-11-23 08:13:17 +09:00
syuilo
7993a9eb90 New translations ja-JP.yml (English) 2018-11-23 08:12:53 +09:00
syuilo
42d419970d New translations ja-JP.yml (Norwegian) 2018-11-23 08:05:03 +09:00
syuilo
ad49268d8b New translations ja-JP.yml (Dutch) 2018-11-23 08:04:53 +09:00
syuilo
76c345396a New translations ja-JP.yml (Japanese, Kansai) 2018-11-23 08:04:44 +09:00
syuilo
5690ef1ebc New translations ja-JP.yml (Spanish) 2018-11-23 08:04:35 +09:00
syuilo
5616404b4d New translations ja-JP.yml (Russian) 2018-11-23 08:04:31 +09:00
syuilo
f92137f6c2 New translations ja-JP.yml (Portuguese) 2018-11-23 08:04:21 +09:00
syuilo
ca3373ba4e New translations ja-JP.yml (Polish) 2018-11-23 08:04:12 +09:00
syuilo
4e6115b414 New translations ja-JP.yml (Korean) 2018-11-23 08:04:03 +09:00
syuilo
ddf47051c9 New translations ja-JP.yml (Italian) 2018-11-23 08:03:54 +09:00
syuilo
d45478510c New translations ja-JP.yml (German) 2018-11-23 08:03:45 +09:00
syuilo
2641f89349 New translations ja-JP.yml (French) 2018-11-23 08:03:35 +09:00
syuilo
9d46d03c37 New translations ja-JP.yml (English) 2018-11-23 08:03:26 +09:00
syuilo
25b6de88a9 New translations ja-JP.yml (Chinese Simplified) 2018-11-23 08:03:18 +09:00
syuilo
a24046e46a New translations ja-JP.yml (Catalan) 2018-11-23 08:03:09 +09:00
syuilo
7e803ff9a9 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2018-11-23 08:01:29 +09:00
syuilo
246cead2b1 Improve user operations
Resolve #2197
Resolve #3367
2018-11-23 08:01:14 +09:00
dependabot[bot]
214f7f06bb Update koa requirement from 2.6.1 to 2.6.2 (#3386)
Updates the requirements on [koa](https://github.com/koajs/koa) to permit the latest version.
- [Release notes](https://github.com/koajs/koa/releases)
- [Changelog](https://github.com/koajs/koa/blob/master/History.md)
- [Commits](https://github.com/koajs/koa/commits/2.6.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 06:06:03 +09:00
dependabot[bot]
6878f73a9f Update ws requirement from 6.1.0 to 6.1.2 (#3385)
Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits/6.1.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-23 05:23:05 +09:00
MeiMei
336b45b6f7 AP quote (#3384) 2018-11-23 02:10:07 +09:00
nico
2a0b62d26d Fix #3343 (#3383)
Setting "X-Forwarded-Proto: https" in the SSL proxy is the correct way to do this
2018-11-23 02:09:04 +09:00
MeiMei
653ec0cbb0 No cache /notes/:note (#3382) 2018-11-22 23:17:58 +09:00
syuilo
120ab3f0a3 New translations ja-JP.yml (French) 2018-11-22 18:53:16 +09:00
syuilo
8bcbbbc1a3 New translations ja-JP.yml (English) 2018-11-22 09:22:41 +09:00
dependabot[bot]
13a75abc91 Update systeminformation requirement from 3.47.0 to 3.49.3 (#3374)
Updates the requirements on [systeminformation](https://github.com/sebhildebrandt/systeminformation) to permit the latest version.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-22 05:31:46 +09:00
dependabot[bot]
eace740c63 Update emojilib requirement from 2.3.0 to 2.4.0 (#3375)
Updates the requirements on [emojilib](https://github.com/muan/emojilib) to permit the latest version.
- [Release notes](https://github.com/muan/emojilib/releases)
- [Commits](https://github.com/muan/emojilib/commits/v2.4.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-22 05:31:09 +09:00
dependabot[bot]
cb3a54de00 Update apexcharts requirement from 2.2.2 to 2.2.3 (#3373)
Updates the requirements on [apexcharts](https://github.com/apexcharts/apexcharts.js) to permit the latest version.
- [Release notes](https://github.com/apexcharts/apexcharts.js/releases)
- [Changelog](https://github.com/apexcharts/apexcharts.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apexcharts/apexcharts.js/commits/v2.2.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-22 05:29:47 +09:00
dependabot[bot]
5fbc77795d Update webpack requirement from 4.25.1 to 4.26.0 (#3371)
Updates the requirements on [webpack](https://github.com/webpack/webpack) to permit the latest version.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/commits/v4.26.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-22 05:29:02 +09:00
dependabot[bot]
ce4feae731 Update vue-i18n requirement from 8.3.1 to 8.3.2 (#3372)
Updates the requirements on [vue-i18n](https://github.com/kazupon/vue-i18n) to permit the latest version.
- [Release notes](https://github.com/kazupon/vue-i18n/releases)
- [Changelog](https://github.com/kazupon/vue-i18n/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/kazupon/vue-i18n/commits/v8.3.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-11-22 05:28:39 +09:00
Aya Morisawa
08f00d4990 Remove unneccesary cast (#3355) 2018-11-22 05:07:25 +09:00
Acid Chicken (硫酸鶏)
6951f7e74a Fix #3361 (#3362)
* Update create.ts

* Update api.ts
2018-11-22 05:06:51 +09:00
syuilo
2b4d63b1bb Add some tests 2018-11-22 05:04:45 +09:00
syuilo
8cbb961493 [MFM] Improve URL parsing
Fix #3368
2018-11-22 05:02:38 +09:00
syuilo
64c795938d New translations ja-JP.yml (French) 2018-11-22 02:03:40 +09:00
syuilo
67df681a48 Refactoring 2018-11-22 01:51:26 +09:00
syuilo
9285bcf8bb New translations ja-JP.yml (Norwegian) 2018-11-21 23:55:53 +09:00
syuilo
b9b23a4b54 New translations ja-JP.yml (Dutch) 2018-11-21 23:55:48 +09:00
syuilo
2f6371b085 New translations ja-JP.yml (Japanese, Kansai) 2018-11-21 23:55:38 +09:00
syuilo
2a5c3475a7 New translations ja-JP.yml (Spanish) 2018-11-21 23:55:29 +09:00
syuilo
8a2698a5db New translations ja-JP.yml (Russian) 2018-11-21 23:55:19 +09:00
syuilo
f6919a171a New translations ja-JP.yml (Portuguese) 2018-11-21 23:55:09 +09:00
syuilo
82ebf67456 New translations ja-JP.yml (Polish) 2018-11-21 23:55:02 +09:00
syuilo
a60c8b2ee8 New translations ja-JP.yml (Korean) 2018-11-21 23:54:58 +09:00
syuilo
0a2b8ccfb6 New translations ja-JP.yml (Italian) 2018-11-21 23:54:53 +09:00
syuilo
698094b787 New translations ja-JP.yml (German) 2018-11-21 23:54:48 +09:00
syuilo
b57e111ea8 New translations ja-JP.yml (French) 2018-11-21 23:54:41 +09:00
syuilo
aa6bf2b54e New translations ja-JP.yml (English) 2018-11-21 23:54:31 +09:00
syuilo
454910d295 New translations ja-JP.yml (Chinese Simplified) 2018-11-21 23:54:21 +09:00
syuilo
d44e620769 New translations ja-JP.yml (Catalan) 2018-11-21 23:54:12 +09:00
Hakaba Hitoyo
ac14adfd3e Feature / user recommendation config in admin ui (#3357)
* add config for external user recommendation into admin ui

* debug

* correct admin ui

* switch external user recommendation to admin ui config

* debug

* debug

* debug

* Revert "debug"

This reverts commit f4a0460e5b.

* explicit parseInt radix

* add Japanese message

* change default engine to https

* remove unused settings

* debug

* nullable externalUserRecommendationTimeout
2018-11-21 23:44:59 +09:00
syuilo
928d30ee1e New translations ja-JP.yml (Norwegian) 2018-11-21 14:53:16 +09:00
syuilo
3dd9b0f347 New translations ja-JP.yml (Dutch) 2018-11-21 14:53:10 +09:00
syuilo
c57dd083c5 New translations ja-JP.yml (Japanese, Kansai) 2018-11-21 14:53:06 +09:00
syuilo
2a1d6c5406 New translations ja-JP.yml (Spanish) 2018-11-21 14:52:59 +09:00
syuilo
112e9f69bd New translations ja-JP.yml (Russian) 2018-11-21 14:52:55 +09:00
syuilo
d50e537888 New translations ja-JP.yml (Portuguese) 2018-11-21 14:52:50 +09:00
syuilo
86d14d30fa New translations ja-JP.yml (Polish) 2018-11-21 14:52:46 +09:00
syuilo
710d3689d3 New translations ja-JP.yml (Korean) 2018-11-21 14:52:39 +09:00
syuilo
3fee011369 New translations ja-JP.yml (Italian) 2018-11-21 14:52:33 +09:00
syuilo
7cd4b8ba4f New translations ja-JP.yml (German) 2018-11-21 14:52:28 +09:00
syuilo
31132de18b New translations ja-JP.yml (French) 2018-11-21 14:52:23 +09:00
syuilo
c6cb271f6f New translations ja-JP.yml (English) 2018-11-21 14:52:18 +09:00
syuilo
b7c4afd20c New translations ja-JP.yml (Chinese Simplified) 2018-11-21 14:52:14 +09:00
syuilo
70395d200a New translations ja-JP.yml (Catalan) 2018-11-21 14:52:07 +09:00
syuilo
562a5f66fc Improve usability 2018-11-21 14:44:49 +09:00
syuilo
b2f8003602 [MFM] Better inline code parse 2018-11-21 12:55:15 +09:00
MeiMei
b7b36973f7 Fix: stop in DB check (#3356) 2018-11-21 12:45:40 +09:00
syuilo
f7d5f597f3 10.56.2 2018-11-21 08:33:02 +09:00
syuilo
79c7712241 Improve MFM 2018-11-21 08:32:40 +09:00
syuilo
8f5f3985f4 [MFM] Fix hashtag parsing 2018-11-21 08:30:29 +09:00
syuilo
af00464f5b New translations ja-JP.yml (French) 2018-11-20 17:38:45 +09:00
syuilo
f522b3df91 New translations ja-JP.yml (French) 2018-11-20 17:22:49 +09:00
syuilo
e9ec4a3b84 New translations ja-JP.yml (English) 2018-11-20 06:51:54 +09:00
95 changed files with 1630 additions and 832 deletions

View File

@@ -118,12 +118,3 @@ autoAdmin: true
# Clustering # Clustering
#clusterLimit: 1 #clusterLimit: 1
# Summaly proxy
#summalyProxy: "http://example.com"
# User recommendation
#user_recommendation:
# external: true
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
# timeout: 300000

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ api-docs.json
/mongo /mongo
/elasticsearch /elasticsearch
*.code-workspace *.code-workspace
yarn.lock

View File

@@ -8,18 +8,20 @@ WORKDIR /misskey
FROM base AS builder FROM base AS builder
RUN unlink /usr/bin/free
RUN apk add --no-cache \ RUN apk add --no-cache \
gcc \
g++ \
libc-dev \
python \
autoconf \ autoconf \
automake \ automake \
file \ file \
g++ \
gcc \
libc-dev \
libtool \
make \ make \
nasm \ nasm \
pkgconfig \ pkgconfig \
libtool \ procps \
python \
zlib-dev zlib-dev
RUN npm i -g node-gyp RUN npm i -g node-gyp

View File

@@ -96,7 +96,6 @@ Please see [Contribution guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D" alt="Gargron"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D" alt="Gargron"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=VZUtwrjQa8Jml4twCjHYQQZ64wHEY4oIlGl7Kc-VYUQ%3D" alt="Nokotaro Takeda"></td> <td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=VZUtwrjQa8Jml4twCjHYQQZ64wHEY4oIlGl7Kc-VYUQ%3D" alt="Nokotaro Takeda"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td> <td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td>
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td> <td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
@@ -106,13 +105,14 @@ Please see [Contribution guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/dansup">dansup</a></td> <td><a href="https://www.patreon.com/dansup">dansup</a></td>
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td> <td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td> <td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
<table><tr> <table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
</tr><tr> </tr><tr>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table> </tr></table>
**Last updated:** Wed, 31 Oct 2018 23:21:06 UTC **Last updated:** Fri, 23 Nov 2018 14:09:04 UTC
<!-- PATREON_END --> <!-- PATREON_END -->
:four_leaf_clover: Copyright :four_leaf_clover: Copyright

View File

@@ -1,29 +0,0 @@
const mongo = require('mongodb');
const bcrypt = require('bcryptjs');
const User = require('../built/models/user').default;
const args = process.argv.slice(2);
const user = args[0];
const q = user.startsWith('@') ? {
username: user.split('@')[1],
host: user.split('@')[2] || null
} : { _id: new mongo.ObjectID(user) };
console.log(`Resetting password for ${user}...`);
const passwd = 'yo';
// Generate hash of password
const hash = bcrypt.hashSync(passwd);
User.update(q, {
$set: {
password: hash
}
}).then(() => {
console.log(`Password of ${user} is now '${passwd}'`);
}, e => {
console.error(e);
});

View File

@@ -1,23 +0,0 @@
const mongo = require('mongodb');
const User = require('../built/models/user').default;
const args = process.argv.slice(2);
const user = args[0];
const q = user.startsWith('@') ? {
username: user.split('@')[1],
host: user.split('@')[2] || null
} : { _id: new mongo.ObjectID(user) };
console.log(`Suspending ${user}...`);
User.update(q, {
$set: {
isSuspended: true
}
}).then(() => {
console.log(`Suspended ${user}`);
}, e => {
console.error(e);
});

View File

@@ -8,28 +8,11 @@ coming soon
node cli/mark-admin (User-ID or Username) node cli/mark-admin (User-ID or Username)
``` ```
## Mark as 'verified' user
``` shell
node cli/mark-verified (User-ID or Username)
```
## Suspend users
``` shell
node cli/suspend (User-ID or Username)
```
e.g. e.g.
``` shell ``` shell
# Use id # By id
node cli/suspend 57d01a501fdf2d07be417afe node cli/mark-admin 57d01a501fdf2d07be417afe
# Use username # By username
node cli/suspend @syuilo node cli/suspend @syuilo
# Use username (remote)
node cli/suspend @syuilo@misskey.xyz
```
## Reset password
``` shell
node cli/reset-password (User-ID or Username)
``` ```

View File

@@ -8,28 +8,11 @@ coming soon
node cli/mark-admin (ユーザーID または ユーザー名) node cli/mark-admin (ユーザーID または ユーザー名)
``` ```
## 'verified'ユーザーを設定する
``` shell
node cli/mark-verified (ユーザーID または ユーザー名)
```
## ユーザーを凍結する
``` shell
node cli/suspend (ユーザーID または ユーザー名)
```
例: 例:
``` shell ``` shell
# ユーザーID # ユーザーID
node cli/suspend 57d01a501fdf2d07be417afe node cli/mark-admin 57d01a501fdf2d07be417afe
# ユーザー名 # ユーザー名
node cli/suspend @syuilo node cli/mark-admin @syuilo
# ユーザー名 (リモート)
node cli/suspend @syuilo@misskey.xyz
```
## ユーザーのパスワードをリセットする
``` shell
node cli/reset-password (ユーザーID または ユーザー名)
``` ```

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -119,7 +119,7 @@ common:
reduce-motion: "Reduce motion in UI" reduce-motion: "Reduce motion in UI"
this-setting-is-this-device-only: "Only for this device" this-setting-is-this-device-only: "Only for this device"
use-os-default-emojis: "Use the OS default Emojis" use-os-default-emojis: "Use the OS default Emojis"
do-not-use-in-production: 'As this is for development, do not use this in production.' do-not-use-in-production: 'This is a development build. Do not use in production.'
is-remote-user: "This user information is copied." is-remote-user: "This user information is copied."
is-remote-post: "This post information is a copy." is-remote-post: "This post information is a copy."
view-on-remote: "View it on remote" view-on-remote: "View it on remote"
@@ -366,8 +366,8 @@ common/views/components/signin.vue:
signin: "Sign in" signin: "Sign in"
or: "Or" or: "Or"
signin-with-twitter: "Log in with Twitter" signin-with-twitter: "Log in with Twitter"
signin-with-github: "Log in with GitHub" signin-with-github: "Sign in with GitHub"
signin-with-discord: "Login with Discord" signin-with-discord: "Sign in with Discord"
login-failed: "Log in failed. Make sure you have entered your correct username and password." login-failed: "Log in failed. Make sure you have entered your correct username and password."
common/views/components/signup.vue: common/views/components/signup.vue:
invitation-code: "Invitation code" invitation-code: "Invitation code"
@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "Invite" invite: "Invite"
save: "Save" save: "Save"
saved: "Saved" saved: "Saved"
user-recommendation-config: "Recommended users"
enable-external-user-recommendation: "Enable to external user recommendation"
external-user-recommendation-engine: "Engine"
external-user-recommendation-engine-desc: "Example: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "Timeout"
external-user-recommendation-timeout-desc: "Number of milliseconds (ex. 300,000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "Chart" title: "Chart"
per-day: "per Day" per-day: "per Day"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "Response time" network-time: "Response time"
network-usage: "Traffic" network-usage: "Traffic"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "Suspend a user" operation: "Operations"
username-or-userid: "Username or user ID"
user-not-found: "User not found"
lookup: "Look up"
reset-password: "Reset password"
password-updated: "The password is now \"{password}\""
suspend: "Suspend" suspend: "Suspend"
suspended: "Successfully suspended." suspended: "Successfully suspended."
unsuspend-user: "Unsuspend users"
unsuspend: "Unsuspend" unsuspend: "Unsuspend"
unsuspended: "The user has successfully unsuspended." unsuspended: "The user has successfully unsuspended."
verify-user: "User account verification settings"
verify: "Verify account" verify: "Verify account"
verified: "The account is now being verified" verified: "The account is now being verified"
unverify-user: "User account unverification settings"
unverify: "Unverify account" unverify: "Unverify account"
unverified: "The account is now being unverified" unverified: "The account is now being unverified"
users:
title: "Users"
sort:
title: "Sort"
createdAtAsc: "Date Registered (Ascending)"
createdAtDesc: "Date Registered (Descending)"
updatedAtAsc: "Last Updated (Ascending)"
updatedAtDesc: "Last Updated (Descending)"
origin:
title: "Origin"
combined: "Local + Remote"
local: "Local"
remote: "Remote"
createdAt: "Created at"
updatedAt: "Updated at"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "Register Moderator" title: "Register Moderator"
@@ -1051,7 +1074,7 @@ admin/views/emoji.vue:
remove: "Remove" remove: "Remove"
updated: "Updated" updated: "Updated"
remove-emoji: remove-emoji:
are-you-sure: "Delete \"%1$s\"?" are-you-sure: "Delete \"$1\"?"
removed: "Deleted" removed: "Deleted"
admin/views/announcements.vue: admin/views/announcements.vue:
announcements: "Announcements" announcements: "Announcements"
@@ -1062,7 +1085,7 @@ admin/views/announcements.vue:
text: "Content" text: "Content"
saved: "Saved" saved: "Saved"
_remove: _remove:
are-you-sure: "Delete \"%1$s\"?" are-you-sure: "Delete \"$1\"?"
removed: "Deleted" removed: "Deleted"
admin/views/hashtags.vue: admin/views/hashtags.vue:
hided-tags: "Hidden Tags" hided-tags: "Hidden Tags"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -123,7 +123,7 @@ common:
is-remote-user: "Ces informations appartiennent à un·e utilisateur·rice distant·e." is-remote-user: "Ces informations appartiennent à un·e utilisateur·rice distant·e."
is-remote-post: "Ceci est une publication distante." is-remote-post: "Ceci est une publication distante."
view-on-remote: " Consulter le profil complet" view-on-remote: " Consulter le profil complet"
renoted-by: "{user}がRenote" renoted-by: "Renoté par {user}"
error: error:
title: 'Une erreur est survenue' title: 'Une erreur est survenue'
retry: 'Réessayer' retry: 'Réessayer'
@@ -432,7 +432,7 @@ common/views/components/visibility-chooser.vue:
specified-desc: "Publier uniquement aux utilisateurs·rices mentionné·e·s" specified-desc: "Publier uniquement aux utilisateurs·rices mentionné·e·s"
private: "Privé" private: "Privé"
local-public: "Local (Public)" local-public: "Local (Public)"
local-public-desc: "リモートへは公開しない" local-public-desc: "Ne pas publier pour les distants"
local-home: "Accueil (local uniquement)" local-home: "Accueil (local uniquement)"
local-followers: "Local (Abonnés)" local-followers: "Local (Abonnés)"
common/views/components/trends.vue: common/views/components/trends.vue:
@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "Inviter" invite: "Inviter"
save: "Sauvegarder" save: "Sauvegarder"
saved: "Enregistré" saved: "Enregistré"
user-recommendation-config: "Utilisateur·rice·s"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "Moteur"
external-user-recommendation-engine-desc: "Exemple: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "Délai dexpiration"
external-user-recommendation-timeout-desc: "En millisecondes (par exemple : 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "Graph" title: "Graph"
per-day: "par jour" per-day: "par jour"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "Temps de réponse" network-time: "Temps de réponse"
network-usage: "Traffic" network-usage: "Traffic"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "Suspendre un·e utilisateur·rice" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "Suspendre" suspend: "Suspendre"
suspended: "Suspendu·e avec succès." suspended: "Suspendu·e avec succès."
unsuspend-user: "Lever la suspension dutilisateur·rice·s"
unsuspend: "Suspension levée" unsuspend: "Suspension levée"
unsuspended: "La suspension de lutilisateur·rice a été levée avec succès" unsuspended: "La suspension de lutilisateur·rice a été levée avec succès"
verify-user: "Paramètres de vérification du compte utilisateur"
verify: "Vérification du compte" verify: "Vérification du compte"
verified: "Le compte a été vérifié" verified: "Le compte a été vérifié"
unverify-user: "Paramètres de non-vérification du compte utilisateur"
unverify: "Ôter la vérification du compte" unverify: "Ôter la vérification du compte"
unverified: "Ce compte n'est plus vérifié" unverified: "Ce compte n'est plus vérifié"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "Ajout dun modérateur" title: "Ajout dun modérateur"
@@ -1265,7 +1288,7 @@ mobile/views/components/ui.nav.vue:
admin: "Admin" admin: "Admin"
about: "À propos de Misskey" about: "À propos de Misskey"
mobile/views/components/user-timeline.vue: mobile/views/components/user-timeline.vue:
no-notes: "Cette utilisateur semble n'avoir rien poster pour le moment" no-notes: "Il semble que cet·te utilisateur·rice na rien publié pour le moment."
no-notes-with-media: "Aucune notes avec des médias" no-notes-with-media: "Aucune notes avec des médias"
mobile/views/components/users-list.vue: mobile/views/components/users-list.vue:
all: "Tout" all: "Tout"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -1092,17 +1092,17 @@ admin/views/instance.vue:
recaptcha-site-key: "reCAPTCHA site key" recaptcha-site-key: "reCAPTCHA site key"
recaptcha-secret-key: "reCAPTCHA secret key" recaptcha-secret-key: "reCAPTCHA secret key"
twitter-integration-config: "Twitter連携の設定" twitter-integration-config: "Twitter連携の設定"
twitter-integration-info: "コールバックURLは /api/tw/cb に設定します。" twitter-integration-info: "コールバックURLは {url} に設定します。"
enable-twitter-integration: "Twitter連携を有効にする" enable-twitter-integration: "Twitter連携を有効にする"
twitter-integration-consumer-key: "Consumer key" twitter-integration-consumer-key: "Consumer key"
twitter-integration-consumer-secret: "Consumer secret" twitter-integration-consumer-secret: "Consumer secret"
github-integration-config: "GitHub連携の設定" github-integration-config: "GitHub連携の設定"
github-integration-info: "コールバックURLは /api/gh/cb に設定します。" github-integration-info: "コールバックURLは {url} に設定します。"
enable-github-integration: "GitHub連携を有効にする" enable-github-integration: "GitHub連携を有効にする"
github-integration-client-id: "Client ID" github-integration-client-id: "Client ID"
github-integration-client-secret: "Client Secret" github-integration-client-secret: "Client Secret"
discord-integration-config: "Discord連携の設定" discord-integration-config: "Discord連携の設定"
discord-integration-info: "コールバックURLは /api/dc/cb に設定します。" discord-integration-info: "コールバックURLは {url} に設定します。"
enable-discord-integration: "Discord連携を有効にする" enable-discord-integration: "Discord連携を有効にする"
discord-integration-client-id: "Client ID" discord-integration-client-id: "Client ID"
discord-integration-client-secret: "Client Secret" discord-integration-client-secret: "Client Secret"
@@ -1117,6 +1117,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
@@ -1145,18 +1151,35 @@ admin/views/charts.vue:
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -5,12 +5,12 @@ meta:
common: common:
misskey: "연합우주의 ⭐" misskey: "연합우주의 ⭐"
about-title: "연합우주의 ⭐." about-title: "연합우주의 ⭐."
about: "Misskey를 찾아 주셔서 감사합니다. Misskey 지구에서 태어난 <b>분산 마이크로 블로그 SNS </b> 입니다. Fediverse (다양한 SNS로 구성되는 우주)에 존재하는 다른 SNS와 상호 연결되어 있습니다. 잠시 도시의 번잡함에서 벗어나 새로운 인터넷에 다이브 해 보지 않겠습니까." about: "Misskey를 찾아주셔서 감사합니다. Misskey 지구에서 태어난 <b>분산 마이크로 블로그 SNS </b> 입니다. Fediverse(다양한 SNS로 구성되는 우주)에 존재하는 다른 SNS와 상호 연결되어 있습니다. 잠시 도시의 번잡함에서 벗어나 새로운 인터넷에 다이브 해 보지 않겠습니까."
intro: intro:
title: "Misskey란?" title: "Misskey란?"
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。" about: "Misskey는 오픈소스 <b>분산형 마이크로블로그 SNS</b>입니다. 다양하고 폭넓게 커스터마이징할 수 있는 UI, 게시물에 대한 반응, 파일을 관리할 수 있는 드라이브 등의 선진적인 기능을 갖추고 있습니다. 더하여 Fediverse라고 부르는 네트워크에 연결할 수 있어 다른 SNS와도 주고받을 수 있습니다. 예를 들자면, 당신이 무언가를 게시하면, 해당 게시물은 Misskey 뿐만 아니라 다른 SNS에도 전해집니다. 살짝 어떤 행성에서 다른 행성으로 전파를 발신하고 있는 모습을 상상해주세요."
features: "특징" features: "특징"
rich-contents: "게시" rich-contents: "글 쓰기"
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。" rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
reaction: "반응" reaction: "반응"
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。" reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待" invite: "招待"
save: "保存" save: "保存"
saved: "保存しました" saved: "保存しました"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "チャート" title: "チャート"
per-day: "1日ごと" per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間" network-time: "応答時間"
network-usage: "通信量" network-usage: "通信量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "ユーザーの凍結" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "モデレーターの登録" title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "邀请" invite: "邀请"
save: "保存" save: "保存"
saved: "保存完毕" saved: "保存完毕"
user-recommendation-config: "おすすめユーザー"
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
external-user-recommendation-engine: "エンジン"
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
external-user-recommendation-timeout: "タイムアウト"
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
admin/views/charts.vue: admin/views/charts.vue:
title: "历史记录" title: "历史记录"
per-day: "每天" per-day: "每天"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "响应时间" network-time: "响应时间"
network-usage: "网络流量" network-usage: "网络流量"
admin/views/users.vue: admin/views/users.vue:
suspend-user: "冻结用户" operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "被冻结" suspend: "被冻结"
suspended: "成功冻结用户" suspended: "成功冻结用户"
unsuspend-user: "解除用户冻结"
unsuspend: "已解除冻结" unsuspend: "已解除冻结"
unsuspended: "已成功解除用户冻结" unsuspended: "已成功解除用户冻结"
verify-user: "用户账户认证设置"
verify: "认证用户" verify: "认证用户"
verified: "此账户已被认证" verified: "此账户已被认证"
unverify-user: "用户账号解除认证设置"
unverify: "解除账户认证" unverify: "解除账户认证"
unverified: "该帐户未经认证" unverified: "该帐户未经认证"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue: admin/views/moderators.vue:
add-moderator: add-moderator:
title: "注册版主" title: "注册版主"

View File

@@ -1,8 +1,8 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "10.56.1", "version": "10.58.1",
"clientVersion": "2.0.11960", "clientVersion": "2.0.12110",
"codename": "nighthike", "codename": "nighthike",
"main": "./built/index.js", "main": "./built/index.js",
"private": true, "private": true,
@@ -47,14 +47,14 @@
"@types/is-url": "1.2.28", "@types/is-url": "1.2.28",
"@types/js-yaml": "3.11.2", "@types/js-yaml": "3.11.2",
"@types/katex": "0.5.0", "@types/katex": "0.5.0",
"@types/koa": "2.0.46", "@types/koa": "2.0.47",
"@types/koa-bodyparser": "5.0.1", "@types/koa-bodyparser": "5.0.1",
"@types/koa-compress": "2.0.8", "@types/koa-compress": "2.0.8",
"@types/koa-favicon": "2.0.19", "@types/koa-favicon": "2.0.19",
"@types/koa-logger": "3.1.1", "@types/koa-logger": "3.1.1",
"@types/koa-mount": "3.0.1", "@types/koa-mount": "3.0.1",
"@types/koa-multer": "1.0.0", "@types/koa-multer": "1.0.0",
"@types/koa-router": "7.0.33", "@types/koa-router": "7.0.35",
"@types/koa-send": "4.1.1", "@types/koa-send": "4.1.1",
"@types/koa-views": "2.0.3", "@types/koa-views": "2.0.3",
"@types/koa__cors": "2.2.3", "@types/koa__cors": "2.2.3",
@@ -63,14 +63,14 @@
"@types/mocha": "5.2.5", "@types/mocha": "5.2.5",
"@types/mongodb": "3.1.14", "@types/mongodb": "3.1.14",
"@types/ms": "0.7.30", "@types/ms": "0.7.30",
"@types/node": "10.12.2", "@types/node": "10.12.10",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
"@types/parsimmon": "1.10.0", "@types/parsimmon": "1.10.0",
"@types/portscanner": "2.1.0", "@types/portscanner": "2.1.0",
"@types/pug": "2.0.4", "@types/pug": "2.0.4",
"@types/qrcode": "1.3.0", "@types/qrcode": "1.3.0",
"@types/ratelimiter": "2.1.28", "@types/ratelimiter": "2.1.28",
"@types/redis": "2.8.7", "@types/redis": "2.8.8",
"@types/request": "2.48.1", "@types/request": "2.48.1",
"@types/request-promise-native": "1.0.15", "@types/request-promise-native": "1.0.15",
"@types/rimraf": "2.0.2", "@types/rimraf": "2.0.2",
@@ -78,7 +78,7 @@
"@types/sharp": "0.21.0", "@types/sharp": "0.21.0",
"@types/showdown": "1.7.5", "@types/showdown": "1.7.5",
"@types/speakeasy": "2.0.3", "@types/speakeasy": "2.0.3",
"@types/systeminformation": "3.23.0", "@types/systeminformation": "3.23.1",
"@types/tinycolor2": "1.4.1", "@types/tinycolor2": "1.4.1",
"@types/tmp": "0.0.33", "@types/tmp": "0.0.33",
"@types/uuid": "3.4.4", "@types/uuid": "3.4.4",
@@ -87,7 +87,7 @@
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
"animejs": "2.2.0", "animejs": "2.2.0",
"apexcharts": "2.2.2", "apexcharts": "2.2.3",
"autobind-decorator": "2.2.1", "autobind-decorator": "2.2.1",
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
@@ -109,7 +109,7 @@
"diskusage": "0.2.5", "diskusage": "0.2.5",
"double-ended-queue": "2.1.0-0", "double-ended-queue": "2.1.0-0",
"elasticsearch": "15.2.0", "elasticsearch": "15.2.0",
"emojilib": "2.3.0", "emojilib": "2.4.0",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "5.8.0", "eslint": "5.8.0",
"eslint-plugin-vue": "4.7.1", "eslint-plugin-vue": "4.7.1",
@@ -143,7 +143,7 @@
"json5": "2.1.0", "json5": "2.1.0",
"json5-loader": "1.0.1", "json5-loader": "1.0.1",
"katex": "0.10.0", "katex": "0.10.0",
"koa": "2.6.1", "koa": "2.6.2",
"koa-bodyparser": "4.2.1", "koa-bodyparser": "4.2.1",
"koa-compress": "3.0.0", "koa-compress": "3.0.0",
"koa-favicon": "2.0.1", "koa-favicon": "2.0.1",
@@ -201,7 +201,7 @@
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.2.0", "summaly": "2.2.0",
"systeminformation": "3.47.0", "systeminformation": "3.49.3",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "1.1.0", "terser-webpack-plugin": "1.1.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
@@ -220,11 +220,11 @@
"vue-color": "2.7.0", "vue-color": "2.7.0",
"vue-content-loading": "1.5.3", "vue-content-loading": "1.5.3",
"vue-cropperjs": "2.2.2", "vue-cropperjs": "2.2.2",
"vue-i18n": "8.3.1", "vue-i18n": "8.3.2",
"vue-js-modal": "1.3.26", "vue-js-modal": "1.3.26",
"vue-loader": "15.4.2", "vue-loader": "15.4.2",
"vue-marquee-text-component": "1.1.0", "vue-marquee-text-component": "1.1.0",
"vue-router": "3.0.1", "vue-router": "3.0.2",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.2.2", "vue-svg-inline-loader": "1.2.2",
"vue-template-compiler": "2.5.17", "vue-template-compiler": "2.5.17",
@@ -234,10 +234,10 @@
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.3", "web-push": "3.3.3",
"webfinger.js": "2.6.6", "webfinger.js": "2.6.6",
"webpack": "4.25.1", "webpack": "4.26.0",
"webpack-cli": "3.1.2", "webpack-cli": "3.1.2",
"websocket": "1.0.28", "websocket": "1.0.28",
"ws": "6.1.0", "ws": "6.1.2",
"xev": "2.0.1" "xev": "2.0.1"
} }
} }

View File

@@ -9,7 +9,7 @@
<ui-textarea v-model="announcement.text"> <ui-textarea v-model="announcement.text">
<span>{{ $t('text') }}</span> <span>{{ $t('text') }}</span>
</ui-textarea> </ui-textarea>
<ui-horizon-group> <ui-horizon-group class="fit-bottom">
<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button> <ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button> <ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
</ui-horizon-group> </ui-horizon-group>

View File

@@ -38,7 +38,7 @@
<i slot="icon"><fa icon="link"/></i> <i slot="icon"><fa icon="link"/></i>
<span>{{ $t('add-emoji.url') }}</span> <span>{{ $t('add-emoji.url') }}</span>
</ui-input> </ui-input>
<ui-horizon-group> <ui-horizon-group class="fit-bottom">
<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button> <ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button> <ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
</ui-horizon-group> </ui-horizon-group>

View File

@@ -42,6 +42,16 @@
<section> <section>
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch> <ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
</section> </section>
<section>
<header>summaly Proxy</header>
<ui-input v-model="summalyProxy">URL</ui-input>
</section>
<section>
<header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header>
<ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch>
<ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<span slot="desc">{{ $t('external-user-recommendation-engine-desc') }}</span></ui-input>
<ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<span slot="suffix">ms</span><span slot="desc">{{ $t('external-user-recommendation-timeout-desc') }}</span></ui-input>
</section>
<section> <section>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
</section> </section>
@@ -59,7 +69,7 @@
<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</div> <div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</div>
<section> <section>
<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch> <ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
<ui-info>{{ $t('twitter-integration-info') }}</ui-info> <ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info>
<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-key') }}</ui-input> <ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-key') }}</ui-input>
<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-secret') }}</ui-input> <ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
@@ -70,7 +80,7 @@
<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</div> <div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</div>
<section> <section>
<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch> <ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
<ui-info>{{ $t('github-integration-info') }}</ui-info> <ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info>
<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-id') }}</ui-input> <ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-id') }}</ui-input>
<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-secret') }}</ui-input> <ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-secret') }}</ui-input>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
@@ -81,7 +91,7 @@
<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</div> <div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</div>
<section> <section>
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch> <ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
<ui-info>{{ $t('discord-integration-info') }}</ui-info> <ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info>
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-id') }}</ui-input> <ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-id') }}</ui-input>
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-secret') }}</ui-input> <ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-secret') }}</ui-input>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> <ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
@@ -93,15 +103,16 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import { host } from '../../config'; import { url, host } from '../../config';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import { faHeadset, faShieldAlt, faGhost } from '@fortawesome/free-solid-svg-icons'; import { faHeadset, faShieldAlt, faGhost, faUserPlus } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/instance.vue'), i18n: i18n('admin/views/instance.vue'),
data() { data() {
return { return {
url,
host: toUnicode(host), host: toUnicode(host),
maintainerName: null, maintainerName: null,
maintainerEmail: null, maintainerEmail: null,
@@ -129,7 +140,11 @@ export default Vue.extend({
discordClientSecret: null, discordClientSecret: null,
proxyAccount: null, proxyAccount: null,
inviteCode: null, inviteCode: null,
faHeadset, faShieldAlt, faGhost enableExternalUserRecommendation: false,
externalUserRecommendationEngine: null,
externalUserRecommendationTimeout: null,
summalyProxy: null,
faHeadset, faShieldAlt, faGhost, faUserPlus
}; };
}, },
@@ -158,6 +173,10 @@ export default Vue.extend({
this.enableDiscordIntegration = meta.enableDiscordIntegration; this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId; this.discordClientId = meta.discordClientId;
this.discordClientSecret = meta.discordClientSecret; this.discordClientSecret = meta.discordClientSecret;
this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation;
this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine;
this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout;
this.summalyProxy = meta.summalyProxy;
}); });
}, },
@@ -199,7 +218,11 @@ export default Vue.extend({
githubClientSecret: this.githubClientSecret, githubClientSecret: this.githubClientSecret,
enableDiscordIntegration: this.enableDiscordIntegration, enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId, discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret discordClientSecret: this.discordClientSecret,
enableExternalUserRecommendation: this.enableExternalUserRecommendation,
externalUserRecommendationEngine: this.externalUserRecommendationEngine,
externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10),
summalyProxy: this.summalyProxy
}).then(() => { }).then(() => {
this.$root.alert({ this.$root.alert({
type: 'success', type: 'success',

View File

@@ -1,42 +1,63 @@
<template> <template>
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw"> <div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
<ui-card> <ui-card>
<div slot="title">{{ $t('verify-user') }}</div> <div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
<section class="fit-top"> <section class="fit-top">
<ui-input v-model="verifyUsername" type="text"> <ui-input v-model="target" type="text">
<span slot="prefix">@</span> <span>{{ $t('username-or-userid') }}</span>
</ui-input> </ui-input>
<ui-button @click="verifyUser" :disabled="verifying">{{ $t('verify') }}</ui-button> <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
<ui-horizon-group>
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
</ui-horizon-group>
<ui-horizon-group>
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
</ui-horizon-group>
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
</section> </section>
</ui-card> </ui-card>
<ui-card> <ui-card>
<div slot="title">{{ $t('unverify-user') }}</div> <div slot="title"><fa :icon="faUsers"/> {{ $t('users.title') }}</div>
<section class="fit-top"> <section class="fit-top">
<ui-input v-model="unverifyUsername" type="text"> <ui-horizon-group inputs>
<span slot="prefix">@</span> <ui-select v-model="sort">
</ui-input> <span slot="label">{{ $t('users.sort.title') }}</span>
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button> <option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option>
</section> <option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option>
</ui-card> <option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option>
<option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option>
<ui-card> </ui-select>
<div slot="title">{{ $t('suspend-user') }}</div> <ui-select v-model="origin">
<section class="fit-top"> <span slot="label">{{ $t('users.origin.title') }}</span>
<ui-input v-model="suspendUsername" type="text"> <option value="combined">{{ $t('users.origin.combined') }}</option>
<span slot="prefix">@</span> <option value="local">{{ $t('users.origin.local') }}</option>
</ui-input> <option value="remote">{{ $t('users.origin.remote') }}</option>
<ui-button @click="suspendUser" :disabled="suspending">{{ $t('suspend') }}</ui-button> </ui-select>
</section> </ui-horizon-group>
</ui-card> <div class="kofvwchc" v-for="user in users">
<div>
<ui-card> <a :href="user | userPage(null, true)">
<div slot="title">{{ $t('unsuspend-user') }}</div> <mk-avatar class="avatar" :user="user" :disable-link="true"/>
<section class="fit-top"> </a>
<ui-input v-model="unsuspendUsername" type="text"> </div>
<span slot="prefix">@</span> <div>
</ui-input> <header>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> <b>{{ user | userName }}</b>
<span class="username">@{{ user | acct }}</span>
</header>
<div>
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</div>
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
</div> </div>
@@ -46,29 +67,89 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import parseAcct from "../../../../misc/acct/parse"; import parseAcct from "../../../../misc/acct/parse";
import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/users.vue'), i18n: i18n('admin/views/users.vue'),
data() { data() {
return { return {
verifyUsername: null, user: null,
target: null,
verifying: false, verifying: false,
unverifyUsername: null,
unverifying: false, unverifying: false,
suspendUsername: null,
suspending: false, suspending: false,
unsuspendUsername: null, unsuspending: false,
unsuspending: false sort: '+createdAt',
origin: 'combined',
limit: 10,
offset: 0,
users: [],
existMore: false,
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey
}; };
}, },
watch: {
sort() {
this.users = [];
this.offset = 0;
this.fetchUsers();
},
origin() {
this.users = [];
this.offset = 0;
this.fetchUsers();
}
},
mounted() {
this.fetchUsers();
},
methods: { methods: {
async fetchUser() {
try {
return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
} catch (e) {
if (e == 'user not found') {
this.$root.alert({
type: 'error',
text: this.$t('user-not-found')
});
} else {
this.$root.alert({
type: 'error',
text: e.toString()
});
}
}
},
async showUser() {
const user = await this.fetchUser();
this.$root.api('admin/show-user', { userId: user.id }).then(info => {
this.user = info;
});
},
async resetPassword() {
const user = await this.fetchUser();
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
this.$root.alert({
type: 'success',
text: this.$t('password-updated', { password: res.password })
});
});
},
async verifyUser() { async verifyUser() {
this.verifying = true; this.verifying = true;
const process = async () => { const process = async () => {
const user = await this.$root.api('users/show', parseAcct(this.verifyUsername)); const user = await this.fetchUser();
await this.$root.api('admin/verify-user', { userId: user.id }); await this.$root.api('admin/verify-user', { userId: user.id });
this.$root.alert({ this.$root.alert({
type: 'success', type: 'success',
@@ -90,7 +171,7 @@ export default Vue.extend({
this.unverifying = true; this.unverifying = true;
const process = async () => { const process = async () => {
const user = await this.$root.api('users/show', parseAcct(this.unverifyUsername)); const user = await this.fetchUser();
await this.$root.api('admin/unverify-user', { userId: user.id }); await this.$root.api('admin/unverify-user', { userId: user.id });
this.$root.alert({ this.$root.alert({
type: 'success', type: 'success',
@@ -112,7 +193,7 @@ export default Vue.extend({
this.suspending = true; this.suspending = true;
const process = async () => { const process = async () => {
const user = await this.$root.api('users/show', parseAcct(this.suspendUsername)); const user = await this.fetchUser();
await this.$root.api('admin/suspend-user', { userId: user.id }); await this.$root.api('admin/suspend-user', { userId: user.id });
this.$root.alert({ this.$root.alert({
type: 'success', type: 'success',
@@ -134,7 +215,7 @@ export default Vue.extend({
this.unsuspending = true; this.unsuspending = true;
const process = async () => { const process = async () => {
const user = await this.$root.api('users/show', parseAcct(this.unsuspendUsername)); const user = await this.fetchUser();
await this.$root.api('admin/unsuspend-user', { userId: user.id }); await this.$root.api('admin/unsuspend-user', { userId: user.id });
this.$root.alert({ this.$root.alert({
type: 'success', type: 'success',
@@ -150,6 +231,24 @@ export default Vue.extend({
}); });
this.unsuspending = false; this.unsuspending = false;
},
fetchUsers() {
this.$root.api('users', {
origin: this.origin,
sort: this.sort,
offset: this.offset,
limit: this.limit + 1
}).then(users => {
if (users.length == this.limit + 1) {
users.pop();
this.existMore = true;
} else {
this.existMore = false;
}
this.users = this.users.concat(users);
this.offset += this.limit;
});
} }
} }
}); });
@@ -160,4 +259,24 @@ export default Vue.extend({
@media (min-width 500px) @media (min-width 500px)
padding 16px padding 16px
.kofvwchc
display flex
padding 16px 0
border-top solid 1px var(--faceDivider)
> div:first-child
> a
> .avatar
width 64px
height 64px
> div:last-child
flex 1
padding-left 16px
> header
> .username
margin-left 8px
opacity 0.7
</style> </style>

View File

@@ -78,9 +78,10 @@ export default (opts: Opts = {}) => ({
urls(): string[] { urls(): string[] {
if (this.appearNote.text) { if (this.appearNote.text) {
const ast = parse(this.appearNote.text); const ast = parse(this.appearNote.text);
// TODO: 再帰的にURL要素がないか調べる
return unique(ast return unique(ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) .filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.props.silent))
.map(t => t.url)); .map(t => t.props.url));
} else { } else {
return null; return null;
} }

View File

@@ -5,7 +5,7 @@
<div class="icon" :class="type"><fa :icon="icon"/></div> <div class="icon" :class="type"><fa :icon="icon"/></div>
<header v-if="title" v-html="title"></header> <header v-if="title" v-html="title"></header>
<div class="body" v-if="text" v-html="text"></div> <div class="body" v-if="text" v-html="text"></div>
<ui-horizon-group no-grow class="buttons" v-if="!splash"> <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
<ui-button @click="ok" primary autofocus>OK</ui-button> <ui-button @click="ok" primary autofocus>OK</ui-button>
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button> <ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
</ui-horizon-group> </ui-horizon-group>

View File

@@ -50,15 +50,13 @@
</div> </div>
<div class="player" v-if="game.isEnded"> <div class="player" v-if="game.isEnded">
<div>
<button @click="logPos = 0" :disabled="logPos == 0"><fa icon="angle-double-left"/></button>
<button @click="logPos--" :disabled="logPos == 0"><fa icon="angle-left"/></button>
</div>
<span>{{ logPos }} / {{ logs.length }}</span> <span>{{ logPos }} / {{ logs.length }}</span>
<div> <ui-horizon-group>
<button @click="logPos++" :disabled="logPos == logs.length"><fa icon="angle-right"/></button> <ui-button @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></ui-button>
<button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa icon="angle-double-right"/></button> <ui-button @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></ui-button>
</div> <ui-button @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></ui-button>
<ui-button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></ui-button>
</ui-horizon-group>
</div> </div>
<div class="info"> <div class="info">
@@ -75,6 +73,7 @@ import i18n from '../../../../../i18n';
import * as CRC32 from 'crc-32'; import * as CRC32 from 'crc-32';
import Reversi, { Color } from '../../../../../../../games/reversi/core'; import Reversi, { Color } from '../../../../../../../games/reversi/core';
import { url } from '../../../../../config'; import { url } from '../../../../../config';
import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/games/reversi/reversi.game.vue'), i18n: i18n('common/views/components/games/reversi/reversi.game.vue'),
@@ -99,7 +98,8 @@ export default Vue.extend({
o: null as Reversi, o: null as Reversi,
logs: [], logs: [],
logPos: 0, logPos: 0,
pollingClock: null pollingClock: null,
faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight
}; };
}, },
@@ -449,7 +449,9 @@ export default Vue.extend({
padding-bottom 16px padding-bottom 16px
> .player > .player
padding-bottom 32px padding 0 16px 32px 16px
margin 0 auto
max-width 500px
> span > span
display inline-block display inline-block

View File

@@ -22,8 +22,8 @@
<div v-for="(x, i) in game.settings.map.join('')" <div v-for="(x, i) in game.settings.map.join('')"
:data-none="x == ' '" :data-none="x == ' '"
@click="onPixelClick(i, x)"> @click="onPixelClick(i, x)">
<template v-if="x == 'b'"><template v-if="$store.state.device.darkmode"><fa :icon="['far', 'circle']"/></template><template v-else><fa icon="circle"/></template></template> <fa v-if="x == 'b'" :icon="fasCircle"/>
<template v-if="x == 'w'"><template v-if="$store.state.device.darkmode"><fa :icon="['far', 'circle']"/></template><template v-else><fa icon="circle"/></template></template> <fa v-if="x == 'w'" :icon="farCircle"/>
</div> </div>
</div> </div>
</div> </div>
@@ -117,6 +117,8 @@
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../../../i18n'; import i18n from '../../../../../i18n';
import * as maps from '../../../../../../../games/reversi/maps'; import * as maps from '../../../../../../../games/reversi/maps';
import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('common/views/components/games/reversi/reversi.room.vue'), i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
@@ -129,7 +131,8 @@ export default Vue.extend({
mapName: maps.eighteight.name, mapName: maps.eighteight.name,
maps: maps, maps: maps,
form: null, form: null,
messages: [] messages: [],
fasCircle, farCircle
}; };
}, },

View File

@@ -51,8 +51,8 @@ export default Vue.extend({
if (this.message.text) { if (this.message.text) {
const ast = parse(this.message.text); const ast = parse(this.message.text);
return unique(ast return unique(ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) .filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.silent))
.map(t => t.url)); .map(t => t.props.url));
} else { } else {
return null; return null;
} }

View File

@@ -9,18 +9,18 @@ import MkGoogle from './google.vue';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import syntaxHighlight from '../../../../../mfm/syntax-highlight'; import syntaxHighlight from '../../../../../mfm/syntax-highlight';
function getText(tokens: Node[]): string { function getTextCount(tokens: Node[]): number {
let text = ''; let count = 0;
const extract = (tokens: Node[]) => { const extract = (tokens: Node[]) => {
tokens.filter(x => x.name === 'text').forEach(x => { tokens.filter(x => x.name === 'text').forEach(x => {
text += x.props.text; count += length(x.props.text);
}); });
tokens.filter(x => x.children).forEach(x => { tokens.filter(x => x.children).forEach(x => {
extract(x.children); extract(x.children);
}); });
}; };
extract(tokens); extract(tokens);
return text; return count;
} }
function getChildrenCount(tokens: Node[]): number { function getChildrenCount(tokens: Node[]): number {
@@ -98,7 +98,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'big': { case 'big': {
bigCount++; bigCount++;
const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5; const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
const isMany = bigCount > 3; const isMany = bigCount > 3;
return (createElement as any)('strong', { return (createElement as any)('strong', {
attrs: { attrs: {
@@ -111,9 +111,17 @@ export default Vue.component('misskey-flavored-markdown', {
}, genEl(token.children)); }, genEl(token.children));
} }
case 'center': {
return [createElement('div', {
attrs: {
style: 'text-align:center;'
}
}, genEl(token.children))];
}
case 'motion': { case 'motion': {
motionCount++; motionCount++;
const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5; const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
const isMany = motionCount > 3; const isMany = motionCount > 3;
return (createElement as any)('span', { return (createElement as any)('span', {
attrs: { attrs: {

View File

@@ -29,7 +29,7 @@ export default Vue.extend({
>>> .quote >>> .quote
margin 8px margin 8px
padding 6px 12px padding 6px 0 6px 12px
color var(--mfmQuote) color var(--mfmQuote)
border-left solid 3px var(--mfmQuoteLine) border-left solid 3px var(--mfmQuoteLine)
@@ -38,7 +38,7 @@ export default Vue.extend({
margin 0 0.5em margin 0 0.5em
font-size 80% font-size 80%
color #525252 color #525252
background #f8f8f8 background rgba(0, 0, 0, 0.05)
border-radius 2px border-radius 2px
>>> pre > code >>> pre > code

View File

@@ -79,6 +79,10 @@ export default Vue.extend({
* *
pointer-events none pointer-events none
user-select none
&:disabled
opacity 0.7
&:focus &:focus
&:after &:after
@@ -107,30 +111,30 @@ export default Vue.extend({
color var(--text) color var(--text)
background var(--buttonBg) background var(--buttonBg)
&:hover &:not(:disabled):hover
background var(--buttonHoverBg) background var(--buttonHoverBg)
&:active &:not(:disabled):active
background var(--buttonActiveBg) background var(--buttonActiveBg)
&.primary &.primary
color var(--primaryForeground) color var(--primaryForeground)
background var(--primary) background var(--primary)
&:hover &:not(:disabled):hover
background var(--primaryLighten5) background var(--primaryLighten5)
&:active &:not(:disabled):active
background var(--primaryDarken5) background var(--primaryDarken5)
&:not(.fill) &:not(.fill)
color var(--primary) color var(--primary)
background none background none
&:hover &:not(:disabled):hover
color var(--primaryDarken5) color var(--primaryDarken5)
&:active &:not(:disabled):active
background var(--primaryAlpha03) background var(--primaryAlpha03)
</style> </style>

View File

@@ -27,9 +27,17 @@ export default Vue.extend({
<style lang="stylus" scoped> <style lang="stylus" scoped>
.vnxwkwuf .vnxwkwuf
margin 16px 0
&.inputs &.inputs
margin 32px 0 margin 32px 0
&.fit-top
margin-top 0
&.fit-bottom
margin-bottom 0
&:not(.noGrow) &:not(.noGrow)
display flex display flex
@@ -37,5 +45,6 @@ export default Vue.extend({
flex 1 flex 1
> *:not(:last-child) > *:not(:last-child)
margin-right 16px margin-right 16px !important
</style> </style>

View File

@@ -9,27 +9,30 @@
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div> <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<template v-if="type != 'file'"> <template v-if="type != 'file'">
<input ref="input" <input ref="input"
:type="type" :type="type"
v-model="v" v-model="v"
:disabled="disabled" :disabled="disabled"
:required="required" :required="required"
:readonly="readonly" :readonly="readonly"
:pattern="pattern" :pattern="pattern"
:autocomplete="autocomplete" :autocomplete="autocomplete"
:spellcheck="spellcheck" :spellcheck="spellcheck"
@focus="focused = true" @focus="focused = true"
@blur="focused = false"> @blur="focused = false"
>
</template> </template>
<template v-else> <template v-else>
<input ref="input" <input ref="input"
type="text" type="text"
:value="placeholder" :value="placeholder"
readonly readonly
@click="chooseFile"> @click="chooseFile"
>
<input ref="file" <input ref="file"
type="file" type="file"
:value="value" :value="value"
@change="onChangeFile"> @change="onChangeFile"
>
</template> </template>
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div> <div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
</div> </div>
@@ -325,6 +328,9 @@ root(fill)
margin 6px 0 margin 6px 0
font-size 13px font-size 13px
&:empty
display none
* *
margin 0 margin 0

View File

@@ -1,15 +1,17 @@
<template> <template>
<div class="ui-select" :class="[{ focused, filled }, styl]"> <div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]">
<div class="icon" ref="icon"><slot name="icon"></slot></div> <div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input" @click="focus"> <div class="input" @click="focus">
<span class="label" ref="label"><slot name="label"></slot></span> <span class="label" ref="label"><slot name="label"></slot></span>
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div> <div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input" <select ref="input"
:value="v" :value="v"
:required="required" :required="required"
@input="$emit('input', $event.target.value)" :disabled="disabled"
@focus="focused = true" @input="$emit('input', $event.target.value)"
@blur="focused = false"> @focus="focused = true"
@blur="focused = false"
>
<slot></slot> <slot></slot>
</select> </select>
<div class="suffix"><slot name="suffix"></slot></div> <div class="suffix"><slot name="suffix"></slot></div>
@@ -22,6 +24,11 @@
import Vue from 'vue'; import Vue from 'vue';
export default Vue.extend({ export default Vue.extend({
inject: {
horizonGrouped: {
default: false
}
},
props: { props: {
value: { value: {
required: false required: false
@@ -30,11 +37,22 @@ export default Vue.extend({
type: Boolean, type: Boolean,
required: false required: false
}, },
disabled: {
type: Boolean,
required: false
},
styl: { styl: {
type: String, type: String,
required: false, required: false,
default: 'line' default: 'line'
} },
inline: {
type: Boolean,
required: false,
default(): boolean {
return this.horizonGrouped;
}
},
}, },
data() { data() {
return { return {
@@ -122,7 +140,7 @@ root(fill)
transition-duration 0.3s transition-duration 0.3s
font-size 16px font-size 16px
line-height 32px line-height 32px
color rgba(#000, 0.54) color var(--inputLabel)
pointer-events none pointer-events none
//will-change transform //will-change transform
transform-origin top left transform-origin top left
@@ -171,6 +189,9 @@ root(fill)
margin 6px 0 margin 6px 0
font-size 13px font-size 13px
&:empty
display none
* *
margin 0 margin 0
@@ -200,4 +221,14 @@ root(fill)
&:not(.fill) &:not(.fill)
root(false) root(false)
&.inline
display inline-block
margin 0
&.disabled
opacity 0.7
&, *
cursor not-allowed !important
</style> </style>

View File

@@ -1,3 +1,10 @@
import Vue from 'vue';
import * as JSON5 from 'json5';
Vue.filter('json5', x => {
return JSON5.stringify(x, null, 2);
});
require('./bytes'); require('./bytes');
require('./number'); require('./number');
require('./user'); require('./user');

View File

@@ -1,6 +1,7 @@
import Vue from 'vue'; import Vue from 'vue';
import getAcct from '../../../../../misc/acct/render'; import getAcct from '../../../../../misc/acct/render';
import getUserName from '../../../../../misc/get-user-name'; import getUserName from '../../../../../misc/get-user-name';
import { url } from '../../../config';
Vue.filter('acct', user => { Vue.filter('acct', user => {
return getAcct(user); return getAcct(user);
@@ -10,6 +11,6 @@ Vue.filter('userName', user => {
return getUserName(user); return getUserName(user);
}); });
Vue.filter('userPage', (user, path?) => { Vue.filter('userPage', (user, path?, absolute = false) => {
return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; return `${absolute ? url : ''}/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
}); });

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="mk-note-detail" :title="title"> <div class="mk-note-detail" :title="title" tabindex="-1">
<button <button
class="read-more" class="read-more"
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
@@ -63,18 +63,18 @@
<footer> <footer>
<span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span> <span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
<mk-reactions-viewer :note="appearNote"/> <mk-reactions-viewer :note="appearNote"/>
<button class="replyButton" @click="reply" :title="$t('reply')"> <button class="replyButton" @click="reply()" :title="$t('reply')">
<template v-if="appearNote.reply"><fa icon="reply-all"/></template> <template v-if="appearNote.reply"><fa icon="reply-all"/></template>
<template v-else><fa icon="reply"/></template> <template v-else><fa icon="reply"/></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button class="renoteButton" @click="renote" :title="$t('renote')"> <button class="renoteButton" @click="renote()" :title="$t('renote')">
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button> </button>
<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react" ref="reactButton" :title="$t('add-reaction')"> <button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" :title="$t('add-reaction')">
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p> <fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
</button> </button>
<button @click="menu" ref="menuButton"> <button @click="menu()" ref="menuButton">
<fa icon="ellipsis-h"/> <fa icon="ellipsis-h"/>
</button> </button>
</footer> </footer>
@@ -88,23 +88,18 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import parse from '../../../../../mfm/parse';
import MkPostFormWindow from './post-form-window.vue';
import MkRenoteFormWindow from './renote-form-window.vue';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import { sum, unique } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber'; import noteSubscriber from '../../../common/scripts/note-subscriber';
import noteMixin from '../../../common/scripts/note-mixin';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('desktop/views/components/note-detail.vue'), i18n: i18n('desktop/views/components/note-detail.vue'),
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')], mixins: [noteMixin(), noteSubscriber('note')],
props: { props: {
note: { note: {
@@ -118,47 +113,12 @@ export default Vue.extend({
data() { data() {
return { return {
showContent: false,
conversation: [], conversation: [],
conversationFetching: false, conversationFetching: false,
replies: [] replies: []
}; };
}, },
computed: {
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
appearNote(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.appearNote.reactionCounts
? sum(Object.values(this.appearNote.reactionCounts))
: 0;
},
title(): string {
return new Date(this.appearNote.createdAt).toLocaleString();
},
urls(): string[] {
if (this.appearNote.text) {
const ast = parse(this.appearNote.text);
return unique(ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
.map(t => t.url));
} else {
return null;
}
}
},
mounted() { mounted() {
// Get replies // Get replies
if (!this.compact) { if (!this.compact) {
@@ -169,24 +129,6 @@ export default Vue.extend({
this.replies = replies; this.replies = replies;
}); });
} }
// Draw map
if (this.appearNote.geo) {
const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
if (shouldShowMap) {
this.$root.os.getGoogleMaps().then(maps => {
const uluru = new maps.LatLng(this.appearNote.geo.coordinates[1], this.appearNote.geo.coordinates[0]);
const map = new maps.Map(this.$refs.map, {
center: uluru,
zoom: 15
});
new maps.Marker({
position: uluru,
map: map
});
});
}
}
}, },
methods: { methods: {
@@ -200,32 +142,6 @@ export default Vue.extend({
this.conversationFetching = false; this.conversationFetching = false;
this.conversation = conversation.reverse(); this.conversation = conversation.reverse();
}); });
},
reply() {
this.$root.new(MkPostFormWindow, {
reply: this.appearNote
});
},
renote() {
this.$root.new(MkRenoteFormWindow, {
note: this.appearNote
});
},
react() {
this.$root.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.appearNote
});
},
menu() {
this.$root.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.appearNote
});
} }
} }
}); });

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="mk-note-detail"> <div class="mk-note-detail" tabindex="-1">
<button <button
class="more" class="more"
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0" v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
@@ -61,18 +61,18 @@
</div> </div>
<footer> <footer>
<mk-reactions-viewer :note="appearNote"/> <mk-reactions-viewer :note="appearNote"/>
<button @click="reply" :title="$t('title')"> <button @click="reply()" :title="$t('title')">
<template v-if="appearNote.reply"><fa icon="reply-all"/></template> <template v-if="appearNote.reply"><fa icon="reply-all"/></template>
<template v-else><fa icon="reply"/></template> <template v-else><fa icon="reply"/></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p> <p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</button> </button>
<button @click="renote" title="Renote"> <button @click="renote()" title="Renote">
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p> <fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
</button> </button>
<button :class="{ reacted: appearNote.myReaction != null }" @click="react" ref="reactButton" :title="$t('title')"> <button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" :title="$t('title')">
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p> <fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
</button> </button>
<button @click="menu" ref="menuButton"> <button @click="menu()" ref="menuButton">
<fa icon="ellipsis-h"/> <fa icon="ellipsis-h"/>
</button> </button>
</footer> </footer>
@@ -86,21 +86,18 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
import XSub from './note.sub.vue'; import XSub from './note.sub.vue';
import { sum, unique } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber'; import noteSubscriber from '../../../common/scripts/note-subscriber';
import noteMixin from '../../../common/scripts/note-mixin';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('mobile/views/components/note-detail.vue'), i18n: i18n('mobile/views/components/note-detail.vue'),
components: { components: {
XSub XSub
}, },
mixins: [noteSubscriber('note')], mixins: [noteMixin(), noteSubscriber('note')],
props: { props: {
note: { note: {
@@ -114,43 +111,12 @@ export default Vue.extend({
data() { data() {
return { return {
showContent: false,
conversation: [], conversation: [],
conversationFetching: false, conversationFetching: false,
replies: [] replies: []
}; };
}, },
computed: {
isRenote(): boolean {
return (this.note.renote &&
this.note.text == null &&
this.note.fileIds.length == 0 &&
this.note.poll == null);
},
appearNote(): any {
return this.isRenote ? this.note.renote : this.note;
},
reactionsCount(): number {
return this.appearNote.reactionCounts
? sum(Object.values(this.appearNote.reactionCounts))
: 0;
},
urls(): string[] {
if (this.appearNote.text) {
const ast = parse(this.appearNote.text);
return unique(ast
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
.map(t => t.url));
} else {
return null;
}
}
},
mounted() { mounted() {
// Get replies // Get replies
if (!this.compact) { if (!this.compact) {
@@ -161,24 +127,6 @@ export default Vue.extend({
this.replies = replies; this.replies = replies;
}); });
} }
// Draw map
if (this.appearNote.geo) {
const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
if (shouldShowMap) {
this.$root.os.getGoogleMaps().then(maps => {
const uluru = new maps.LatLng(this.appearNote.geo.coordinates[1], this.appearNote.geo.coordinates[0]);
const map = new maps.Map(this.$refs.map, {
center: uluru,
zoom: 15
});
new maps.Marker({
position: uluru,
map: map
});
});
}
}
}, },
methods: { methods: {
@@ -192,35 +140,6 @@ export default Vue.extend({
this.conversationFetching = false; this.conversationFetching = false;
this.conversation = conversation.reverse(); this.conversation = conversation.reverse();
}); });
},
reply() {
this.$post({
reply: this.appearNote
});
},
renote() {
this.$post({
renote: this.appearNote
});
},
react() {
this.$root.new(MkReactionPicker, {
source: this.$refs.reactButton,
note: this.appearNote,
compact: true,
big: true
});
},
menu() {
this.$root.new(MkNoteMenu, {
source: this.$refs.menuButton,
note: this.appearNote,
compact: true
});
} }
} }
}); });

View File

@@ -15,23 +15,23 @@
</router-link> </router-link>
<div class="links"> <div class="links">
<ul> <ul>
<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home"/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']"/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']"/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']"/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> <li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad"/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
</ul> </ul>
<ul> <ul>
<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']"/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star"/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list"/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud"/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
</ul> </ul>
<ul> <ul>
<li><a @click="search"><i><fa icon="search"/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> <li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog"/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li> <li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li>
<li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal"/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> <li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li>
<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li> <li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon" fixed-width/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li>
</ul> </ul>
</div> </div>
<div class="announcements" v-if="announcements && announcements.length > 0"> <div class="announcements" v-if="announcements && announcements.length > 0">

View File

@@ -31,7 +31,6 @@
<x-followers-you-know :user="user"/> <x-followers-you-know :user="user"/>
</div> </div>
</section> </section>
<p v-if="user.host === null">{{ $t('last-used-at') }}: <b><mk-time :time="user.lastUsedAt"/></b></p>
</div> </div>
</template> </template>
@@ -90,7 +89,7 @@ export default Vue.extend({
@media (min-width 500px) @media (min-width 500px)
padding 10px 16px padding 10px 16px
> i > [data-icon]
margin-right 6px margin-right 6px
> .activity > .activity

View File

@@ -37,15 +37,8 @@ export type Source = {
proxy?: string; proxy?: string;
summalyProxy?: string;
accesslog?: string; accesslog?: string;
github_bot?: {
hook_secret: string;
username: string;
};
/** /**
* Service Worker * Service Worker
*/ */

View File

@@ -23,7 +23,6 @@ import notesStats from './daemons/notes-stats';
import loadConfig from './config/load'; import loadConfig from './config/load';
import { Config } from './config/types'; import { Config } from './config/types';
import { lessThan } from './prelude/array'; import { lessThan } from './prelude/array';
import { Db } from 'mongodb';
const clusterLog = debug('misskey:cluster'); const clusterLog = debug('misskey:cluster');
const ev = new Xev(); const ev = new Xev();
@@ -192,38 +191,35 @@ async function init(): Promise<Config> {
} }
// Try to connect to MongoDB // Try to connect to MongoDB
//await checkMongoDB(config); await checkMongoDB(config);
return config; return config;
} }
const requiredMongoDBVersion = [3, 6]; const requiredMongoDBVersion = [3, 6];
function checkMongoDB(config: Config): Promise<void> { function checkMongoDB(config: Config) {
const mongoDBLogger = new Logger('MongoDB'); const mongoDBLogger = new Logger('MongoDB');
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
mongoDBLogger.info(`Connecting to ${uri}`); mongoDBLogger.info(`Connecting to ${uri}`);
return mongo.then(async () => { mongo.then(() => {
mongoDBLogger.succ('Connectivity confirmed'); mongoDBLogger.succ('Connectivity confirmed');
const runningMongoDBVersion = (await nativeDbConn().then(getMongoDBVersion)).split('.').map(x => parseInt(x, 10)); nativeDbConn().then(db => db.admin().serverInfo()).then(x => x.version).then((version: string) => {
mongoDBLogger.info(`Version: ${runningMongoDBVersion.join('.')}`); mongoDBLogger.info(`Version: ${version}`);
if (lessThan(runningMongoDBVersion, requiredMongoDBVersion)) { if (lessThan(version.split('.').map(x => parseInt(x, 10)), requiredMongoDBVersion)) {
mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`); mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`);
process.exit(1); process.exit(1);
} }
});
}).catch(err => { }).catch(err => {
mongoDBLogger.error(err.message); mongoDBLogger.error(err.message);
}); });
} }
async function getMongoDBVersion(db: Db): Promise<string> {
return (await db.admin().serverInfo()).version;
}
async function spawnWorkers(limit: number = Infinity) { async function spawnWorkers(limit: number = Infinity) {
const workers = Math.min(limit, os.cpus().length); const workers = Math.min(limit, os.cpus().length);
Logger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); Logger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);

View File

@@ -45,6 +45,12 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
return pre; return pre;
}, },
center(token) {
const el = doc.createElement('div');
dive(token.children).forEach(child => el.appendChild(child));
return el;
},
emoji(token) { emoji(token) {
return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`); return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
}, },

View File

@@ -41,11 +41,12 @@ export default (source: string): Node[] => {
} }
function isBlockNode(node: Node): boolean { function isBlockNode(node: Node): boolean {
return ['blockCode', 'quote', 'title'].includes(node.name); return ['blockCode', 'center', 'quote', 'title'].includes(node.name);
} }
/** /**
* ブロック要素の前後にある改行を削除します(ブロック要素自体が改行の役割も果たすため、余計に改行されてしまうため) * ブロック要素の前後にある改行を削除します
* (ブロック要素自体が改行の役割を果たすため、余計に改行されてしまう)
* @param nodes * @param nodes
*/ */
const removeNeedlessLineBreaks = (nodes: Node[]) => { const removeNeedlessLineBreaks = (nodes: Node[]) => {

View File

@@ -29,6 +29,26 @@ function makeNodeWithChildren(name: string, children: Node[], props?: any): Node
return _makeNode(name, children, props); return _makeNode(name, children, props);
} }
function getTrailingPosition(x: string): number {
let pendingBracket = 0;
const end = x.split('').findIndex(char => {
if (char == ')') {
if (pendingBracket > 0) {
pendingBracket--;
return false;
} else {
return true;
}
} else if (char == '(') {
pendingBracket++;
return false;
} else {
return false;
}
});
return end > 0 ? end : x.length;
}
const newline = P((input, i) => { const newline = P((input, i) => {
if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') { if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') {
return P.makeSuccess(i, null); return P.makeSuccess(i, null);
@@ -53,6 +73,7 @@ const mfm = P.createLanguage({
r.math, r.math,
r.search, r.search,
r.title, r.title,
r.center,
r.text r.text
).atLeast(1), ).atLeast(1),
@@ -63,7 +84,9 @@ const mfm = P.createLanguage({
P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1) P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1)
.map(x => makeNodeWithChildren('big', P.alt( .map(x => makeNodeWithChildren('big', P.alt(
r.mention, r.mention,
r.hashtag,
r.emoji, r.emoji,
r.math,
r.text r.text
).atLeast(1).tryParse(x))), ).atLeast(1).tryParse(x))),
//#endregion //#endregion
@@ -85,11 +108,31 @@ const mfm = P.createLanguage({
P.regexp(/\*\*([\s\S]+?)\*\*/, 1) P.regexp(/\*\*([\s\S]+?)\*\*/, 1)
.map(x => makeNodeWithChildren('bold', P.alt( .map(x => makeNodeWithChildren('bold', P.alt(
r.mention, r.mention,
r.hashtag,
r.url,
r.link,
r.emoji, r.emoji,
r.text r.text
).atLeast(1).tryParse(x))), ).atLeast(1).tryParse(x))),
//#endregion //#endregion
//#region Center
center: r =>
P.regexp(/<center>([\s\S]+?)<\/center>/, 1)
.map(x => makeNodeWithChildren('center', P.alt(
r.big,
r.bold,
r.motion,
r.mention,
r.hashtag,
r.emoji,
r.math,
r.url,
r.link,
r.text
).atLeast(1).tryParse(x))),
//#endregion
//#region Emoji //#region Emoji
emoji: r => emoji: r =>
P.alt( P.alt(
@@ -110,14 +153,17 @@ const mfm = P.createLanguage({
const text = input.substr(i); const text = input.substr(i);
const match = text.match(/^#([^\s\.,!\?#]+)/i); const match = text.match(/^#([^\s\.,!\?#]+)/i);
if (!match) return P.makeFailure(i, 'not a hashtag'); if (!match) return P.makeFailure(i, 'not a hashtag');
if (input[i - 1] != ' ' && input[i - 1] != null) return P.makeFailure(i, 'require space before "#"'); let hashtag = match[1];
return P.makeSuccess(i + match[0].length, makeNode('hashtag', { hashtag: match[1] })); hashtag = hashtag.substr(0, getTrailingPosition(hashtag));
if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag');
if (!['\n', ' ', '(', null, undefined].includes(input[i - 1])) return P.makeFailure(i, 'require space before "#"');
return P.makeSuccess(i + ('#' + hashtag).length, makeNode('hashtag', { hashtag: hashtag }));
}), }),
//#endregion //#endregion
//#region Inline code //#region Inline code
inlineCode: r => inlineCode: r =>
P.regexp(/`(.+?)`/, 1) P.regexp(/`([^´\n]+?)`/, 1)
.map(x => makeNode('inlineCode', { code: x })), .map(x => makeNode('inlineCode', { code: x })),
//#endregion //#endregion
@@ -176,7 +222,11 @@ const mfm = P.createLanguage({
.map(x => makeNodeWithChildren('motion', P.alt( .map(x => makeNodeWithChildren('motion', P.alt(
r.bold, r.bold,
r.mention, r.mention,
r.hashtag,
r.emoji, r.emoji,
r.url,
r.link,
r.math,
r.text r.text
).atLeast(1).tryParse(x))), ).atLeast(1).tryParse(x))),
//#endregion //#endregion
@@ -242,11 +292,9 @@ const mfm = P.createLanguage({
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/); const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
if (!match) return P.makeFailure(i, 'not a url'); if (!match) return P.makeFailure(i, 'not a url');
let url = match[0]; let url = match[0];
const before = input[i - 1]; url = url.substr(0, getTrailingPosition(url));
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.')); if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(',')); if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
if (url.endsWith(')') && before == '(') url = url.substr(0, url.lastIndexOf(')'));
if (url.endsWith(']') && before == '[') url = url.substr(0, url.lastIndexOf(']'));
return P.makeSuccess(i + url.length, url); return P.makeSuccess(i + url.length, url);
}) })
.map(x => makeNode('url', { url: x })), .map(x => makeNode('url', { url: x })),

View File

@@ -1,4 +1,5 @@
export default (acct: string) => { export default (acct: string) => {
if (acct.startsWith('@')) acct = acct.substr(1);
const splitted = acct.split('@', 2); const splitted = acct.split('@', 2);
return { username: splitted[0], host: splitted[1] || null }; return { username: splitted[0], host: splitted[1] || null };
}; };

View File

@@ -15,7 +15,10 @@ const defaultMeta: any = {
maxNoteTextLength: 1000, maxNoteTextLength: 1000,
enableTwitterIntegration: false, enableTwitterIntegration: false,
enableGithubIntegration: false, enableGithubIntegration: false,
enableDiscordIntegration: false enableDiscordIntegration: false,
enableExternalUserRecommendation: false,
externalUserRecommendationEngine: "https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}",
externalUserRecommendationTimeout: 300000
}; };
export default async function(): Promise<IMeta> { export default async function(): Promise<IMeta> {

View File

@@ -6,15 +6,24 @@ export default function(file: IDriveFile, thumbnail = false): string {
if (file.metadata.withoutChunks) { if (file.metadata.withoutChunks) {
if (thumbnail) { if (thumbnail) {
return file.metadata.thumbnailUrl || file.metadata.url; return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || file.metadata.url;
} else { } else {
return file.metadata.url; return file.metadata.webpublicUrl || file.metadata.url;
} }
} else { } else {
if (thumbnail) { if (thumbnail) {
return `${config.drive_url}/${file._id}?thumbnail`; return `${config.drive_url}/${file._id}?thumbnail`;
} else { } else {
return `${config.drive_url}/${file._id}`; return `${config.drive_url}/${file._id}?web`;
} }
} }
} }
export function getOriginalUrl(file: IDriveFile) {
if (file.metadata && file.metadata.url) {
return file.metadata.url;
}
const accessKey = file.metadata ? file.metadata.accessKey : null;
return `${config.drive_url}/${file._id}${accessKey ? '?original=' + accessKey : ''}`;
}

View File

@@ -0,0 +1,29 @@
import * as mongo from 'mongodb';
import monkDb, { nativeDbConn } from '../db/mongodb';
const DriveFileWebpublic = monkDb.get<IDriveFileWebpublic>('driveFileWebpublics.files');
DriveFileWebpublic.createIndex('metadata.originalId', { sparse: true, unique: true });
export default DriveFileWebpublic;
export const DriveFileWebpublicChunk = monkDb.get('driveFileWebpublics.chunks');
export const getDriveFileWebpublicBucket = async (): Promise<mongo.GridFSBucket> => {
const db = await nativeDbConn();
const bucket = new mongo.GridFSBucket(db, {
bucketName: 'driveFileWebpublics'
});
return bucket;
};
export type IMetadata = {
originalId: mongo.ObjectID;
};
export type IDriveFileWebpublic = {
_id: mongo.ObjectID;
uploadDate: Date;
md5: string;
filename: string;
contentType: string;
metadata: IMetadata;
};

View File

@@ -3,7 +3,7 @@ const deepcopy = require('deepcopy');
import { pack as packFolder } from './drive-folder'; import { pack as packFolder } from './drive-folder';
import monkDb, { nativeDbConn } from '../db/mongodb'; import monkDb, { nativeDbConn } from '../db/mongodb';
import isObjectId from '../misc/is-objectid'; import isObjectId from '../misc/is-objectid';
import getDriveFileUrl from '../misc/get-drive-file-url'; import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
DriveFile.createIndex('md5'); DriveFile.createIndex('md5');
@@ -28,21 +28,48 @@ export type IMetadata = {
_user: any; _user: any;
folderId: mongo.ObjectID; folderId: mongo.ObjectID;
comment: string; comment: string;
/**
* リモートインスタンスから取得した場合の元URL
*/
uri?: string; uri?: string;
/**
* URL for web(生成されている場合) or original
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
*/
url?: string; url?: string;
/**
* URL for thumbnail (thumbnailがなければなし)
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
*/
thumbnailUrl?: string; thumbnailUrl?: string;
/**
* URL for original (web用が生成されてない場合はurlがoriginalを指す)
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
*/
webpublicUrl?: string;
accessKey?: string;
src?: string; src?: string;
deletedAt?: Date; deletedAt?: Date;
/** /**
* このファイルの中身データがMongoDB内に保存されているのか否か * このファイルの中身データがMongoDB内に保存されていないか否か
* オブジェクトストレージを利用している or リモートサーバーへの直リンクである * オブジェクトストレージを利用している or リモートサーバーへの直リンクである
* な場合は false になります * な場合は true になります
*/ */
withoutChunks?: boolean; withoutChunks?: boolean;
storage?: string; storage?: string;
storageProps?: any;
/***
* ObjectStorage の格納先の情報
*/
storageProps?: IStorageProps;
isSensitive?: boolean; isSensitive?: boolean;
/** /**
@@ -56,6 +83,25 @@ export type IMetadata = {
isRemote?: boolean; isRemote?: boolean;
}; };
export type IStorageProps = {
/**
* ObjectStorage key for original
*/
key: string;
/***
* ObjectStorage key for thumbnail (thumbnailがなければなし)
*/
thumbnailKey?: string;
/***
* ObjectStorage key for webpublic (webpublicがなければなし)
*/
webpublicKey?: string;
id?: string;
};
export type IDriveFile = { export type IDriveFile = {
_id: mongo.ObjectID; _id: mongo.ObjectID;
uploadDate: Date; uploadDate: Date;
@@ -83,7 +129,8 @@ export function validateFileName(name: string): boolean {
export const packMany = ( export const packMany = (
files: any[], files: any[],
options?: { options?: {
detail: boolean detail?: boolean
self?: boolean,
} }
) => { ) => {
return Promise.all(files.map(f => pack(f, options))); return Promise.all(files.map(f => pack(f, options)));
@@ -95,11 +142,13 @@ export const packMany = (
export const pack = ( export const pack = (
file: any, file: any,
options?: { options?: {
detail: boolean detail?: boolean,
self?: boolean,
} }
) => new Promise<any>(async (resolve, reject) => { ) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({ const opts = Object.assign({
detail: false detail: false,
self: false
}, options); }, options);
let _file: any; let _file: any;
@@ -165,5 +214,9 @@ export const pack = (
delete _target.isRemote; delete _target.isRemote;
delete _target._user; delete _target._user;
if (opts.self) {
_target.url = getOriginalUrl(_file);
}
resolve(_target); resolve(_target);
}); });

View File

@@ -125,6 +125,19 @@ if ((config as any).github) {
} }
}); });
} }
if ((config as any).user_recommendation) {
Meta.findOne({}).then(m => {
if (m != null && m.enableExternalUserRecommendation == null) {
Meta.update({}, {
$set: {
enableExternalUserRecommendation: true,
externalUserRecommendationEngine: (config as any).user_recommendation.engine,
externalUserRecommendationTimeout: (config as any).user_recommendation.timeout
}
});
}
});
}
export type IMeta = { export type IMeta = {
name?: string; name?: string;
@@ -184,6 +197,8 @@ export type IMeta = {
*/ */
maxNoteTextLength?: number; maxNoteTextLength?: number;
summalyProxy?: string;
enableTwitterIntegration?: boolean; enableTwitterIntegration?: boolean;
twitterConsumerKey?: string; twitterConsumerKey?: string;
twitterConsumerSecret?: string; twitterConsumerSecret?: string;
@@ -195,4 +210,8 @@ export type IMeta = {
enableDiscordIntegration?: boolean; enableDiscordIntegration?: boolean;
discordClientId?: string; discordClientId?: string;
discordClientSecret?: string; discordClientSecret?: string;
enableExternalUserRecommendation?: boolean;
externalUserRecommendationEngine?: string;
externalUserRecommendationTimeout?: number;
}; };

View File

@@ -18,6 +18,7 @@ Note.createIndex('uri', { sparse: true, unique: true });
Note.createIndex('userId'); Note.createIndex('userId');
Note.createIndex('mentions'); Note.createIndex('mentions');
Note.createIndex('visibleUserIds'); Note.createIndex('visibleUserIds');
Note.createIndex('replyId');
Note.createIndex('tagsLower'); Note.createIndex('tagsLower');
Note.createIndex('_user.host'); Note.createIndex('_user.host');
Note.createIndex('_files._id'); Note.createIndex('_files._id');
@@ -99,7 +100,6 @@ export type INote = {
host: string; host: string;
inbox?: string; inbox?: string;
}; };
_replyIds?: mongo.ObjectID[];
_files?: IDriveFile[]; _files?: IDriveFile[];
}; };
@@ -258,6 +258,8 @@ export const pack = async (
delete _note._reply; delete _note._reply;
delete _note._renote; delete _note._renote;
delete _note._files; delete _note._files;
delete _note._replyIds;
if (_note.geo) delete _note.geo.type; if (_note.geo) delete _note.geo.type;
// Populate user // Populate user

View File

@@ -26,6 +26,7 @@ export default User;
type IUserBase = { type IUserBase = {
_id: mongo.ObjectID; _id: mongo.ObjectID;
createdAt: Date; createdAt: Date;
updatedAt?: Date;
deletedAt?: Date; deletedAt?: Date;
followersCount: number; followersCount: number;
followingCount: number; followingCount: number;
@@ -37,6 +38,8 @@ type IUserBase = {
bannerId: mongo.ObjectID; bannerId: mongo.ObjectID;
avatarUrl?: string; avatarUrl?: string;
bannerUrl?: string; bannerUrl?: string;
avatarColor?: any;
bannerColor?: any;
wallpaperId: mongo.ObjectID; wallpaperId: mongo.ObjectID;
wallpaperUrl?: string; wallpaperUrl?: string;
data: any; data: any;
@@ -104,7 +107,6 @@ export interface ILocalUser extends IUserBase {
birthday: string; // 'YYYY-MM-DD' birthday: string; // 'YYYY-MM-DD'
tags: string[]; tags: string[];
}; };
lastUsedAt: Date;
isCat: boolean; isCat: boolean;
isAdmin?: boolean; isAdmin?: boolean;
isModerator?: boolean; isModerator?: boolean;
@@ -132,7 +134,7 @@ export interface IRemoteUser extends IUserBase {
id: string; id: string;
publicKeyPem: string; publicKeyPem: string;
}; };
updatedAt: Date; lastFetchedAt: Date;
isAdmin: false; isAdmin: false;
isModerator: false; isModerator: false;
} }

View File

@@ -96,6 +96,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// リプライ // リプライ
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
// 引用
let quote: INote;
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(() => null);
}
// テキストのパース // テキストのパース
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content); const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
@@ -104,7 +111,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
}); });
// ユーザーの情報が古かったらついでに更新しておく // ユーザーの情報が古かったらついでに更新しておく
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
updatePerson(note.attributedTo); updatePerson(note.attributedTo);
} }
@@ -112,7 +119,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
createdAt: new Date(note.published), createdAt: new Date(note.published),
files: files, files: files,
reply, reply,
renote: undefined, renote: quote,
cw: note.summary, cw: note.summary,
text: text, text: text,
viaMobile: false, viaMobile: false,

View File

@@ -143,7 +143,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
avatarId: null, avatarId: null,
bannerId: null, bannerId: null,
createdAt: Date.parse(person.published) || null, createdAt: Date.parse(person.published) || null,
updatedAt: new Date(), lastFetchedAt: new Date(),
description: htmlToMFM(person.summary), description: htmlToMFM(person.summary),
followersCount, followersCount,
followingCount, followingCount,
@@ -212,13 +212,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
const bannerId = banner ? banner._id : null; const bannerId = banner ? banner._id : null;
const avatarUrl = getDriveFileUrl(avatar, true); const avatarUrl = getDriveFileUrl(avatar, true);
const bannerUrl = getDriveFileUrl(banner, false); const bannerUrl = getDriveFileUrl(banner, false);
const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
await User.update({ _id: user._id }, { await User.update({ _id: user._id }, {
$set: { $set: {
avatarId, avatarId,
bannerId, bannerId,
avatarUrl, avatarUrl,
bannerUrl bannerUrl,
avatarColor,
bannerColor
} }
}); });
@@ -226,6 +230,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
user.bannerId = bannerId; user.bannerId = bannerId;
user.avatarUrl = avatarUrl; user.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl; user.bannerUrl = bannerUrl;
user.avatarColor = avatarColor;
user.bannerColor = bannerColor;
//#endregion //#endregion
await updateFeatured(user._id).catch(err => console.log(err)); await updateFeatured(user._id).catch(err => console.log(err));
@@ -298,7 +304,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
// Update user // Update user
await User.update({ _id: exist._id }, { await User.update({ _id: exist._id }, {
$set: { $set: {
updatedAt: new Date(), lastFetchedAt: new Date(),
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox, sharedInbox: person.sharedInbox,
featured: person.featured, featured: person.featured,
@@ -306,6 +312,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
bannerId: banner ? banner._id : null, bannerId: banner ? banner._id : null,
avatarUrl: getDriveFileUrl(avatar, true), avatarUrl: getDriveFileUrl(avatar, true),
bannerUrl: getDriveFileUrl(banner, false), bannerUrl: getDriveFileUrl(banner, false),
avatarColor: avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null,
bannerColor: banner && banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null,
description: htmlToMFM(person.summary), description: htmlToMFM(person.summary),
followersCount, followersCount,
followingCount, followingCount,

View File

@@ -42,6 +42,18 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
inReplyTo = null; inReplyTo = null;
} }
let quote;
if (note.renoteId) {
const renote = await Note.findOne({
_id: note.renoteId,
});
if (renote) {
quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`;
}
}
const user = await User.findOne({ const user = await User.findOne({
_id: note.userId _id: note.userId
}); });
@@ -112,6 +124,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
summary: note.cw, summary: note.cw,
content, content,
_misskey_content: text, _misskey_content: text,
_misskey_quote: quote,
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),
to, to,
cc, cc,

View File

@@ -41,6 +41,7 @@ export interface IOrderedCollection extends IObject {
export interface INote extends IObject { export interface INote extends IObject {
type: 'Note'; type: 'Note';
_misskey_content: string; _misskey_content: string;
_misskey_quote: string;
} }
export interface IPerson extends IObject { export interface IPerson extends IObject {

View File

@@ -76,7 +76,7 @@ router.get('/notes/:note', async (ctx, next) => {
} }
ctx.body = pack(await renderNote(note, false)); ctx.body = pack(await renderNote(note, false));
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx); setResponseType(ctx);
}); });

View File

@@ -9,8 +9,8 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
path: '/', path: '/',
domain: config.hostname, domain: config.hostname,
// SEE: https://github.com/koajs/koa/issues/974 // SEE: https://github.com/koajs/koa/issues/974
//secure: config.url.startsWith('https'), // When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
secure: false, secure: config.url.startsWith('https'),
httpOnly: false, httpOnly: false,
expires: new Date(Date.now() + expires), expires: new Date(Date.now() + expires),
maxAge: expires maxAge: expires

View File

@@ -0,0 +1,57 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import User from '../../../../models/user';
import * as bcrypt from 'bcryptjs';
import rndstr from 'rndstr';
export const meta = {
desc: {
'ja-JP': '指定したユーザーのパスワードをリセットします。',
},
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
}
},
}
};
export default define(meta, (ps) => new Promise(async (res, rej) => {
const user = await User.findOne({
_id: ps.userId
});
if (user == null) {
return rej('user not found');
}
if (user.isAdmin) {
return rej('cannot reset password of admin');
}
const passwd = rndstr('a-zA-Z0-9', 8);
// Generate hash of password
const hash = bcrypt.hashSync(passwd);
await User.findOneAndUpdate({
_id: user._id
}, {
$set: {
password: hash
}
});
res({
password: passwd
});
}));

View File

@@ -0,0 +1,40 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import User from '../../../../models/user';
export const meta = {
desc: {
'ja-JP': '指定したユーザーの情報を取得します。',
},
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to suspend'
}
},
}
};
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
const user = await User.findOne({
_id: ps.userId
});
if (user == null) {
return rej('user not found');
}
if (me.isModerator && user.isAdmin) {
return rej('cannot show info of admin');
}
res(user);
}));

View File

@@ -139,6 +139,13 @@ export const meta = {
} }
}, },
summalyProxy: {
validator: $.str.optional.nullable,
desc: {
'ja-JP': 'summalyプロキシURL'
}
},
enableTwitterIntegration: { enableTwitterIntegration: {
validator: $.bool.optional, validator: $.bool.optional,
desc: { desc: {
@@ -200,6 +207,27 @@ export const meta = {
desc: { desc: {
'ja-JP': 'DiscordアプリのClient Secret' 'ja-JP': 'DiscordアプリのClient Secret'
} }
},
enableExternalUserRecommendation: {
validator: $.bool.optional,
desc: {
'ja-JP': '外部ユーザーレコメンデーションを有効にする'
}
},
externalUserRecommendationEngine: {
validator: $.str.optional.nullable,
desc: {
'ja-JP': '外部ユーザーレコメンデーションのサードパーティエンジン'
}
},
externalUserRecommendationTimeout: {
validator: $.num.optional.nullable.min(0),
desc: {
'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)'
}
} }
} }
}; };
@@ -279,6 +307,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
set.langs = ps.langs; set.langs = ps.langs;
} }
if (ps.summalyProxy !== undefined) {
set.summalyProxy = ps.summalyProxy;
}
if (ps.enableTwitterIntegration !== undefined) { if (ps.enableTwitterIntegration !== undefined) {
set.enableTwitterIntegration = ps.enableTwitterIntegration; set.enableTwitterIntegration = ps.enableTwitterIntegration;
} }
@@ -315,6 +347,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
set.discordClientSecret = ps.discordClientSecret; set.discordClientSecret = ps.discordClientSecret;
} }
if (ps.enableExternalUserRecommendation !== undefined) {
set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation;
}
if (ps.externalUserRecommendationEngine !== undefined) {
set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine;
}
if (ps.externalUserRecommendationTimeout !== undefined) {
set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout;
}
await Meta.update({}, { await Meta.update({}, {
$set: set $set: set
}, { upsert: true }); }, { upsert: true });

View File

@@ -77,5 +77,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
sort: sort sort: sort
}); });
res(await packMany(files)); res(await packMany(files, { detail: false, self: true }));
})); }));

View File

@@ -32,6 +32,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
if (file === null) { if (file === null) {
res({ file: null }); res({ file: null });
} else { } else {
res({ file: await pack(file) }); res({ file: await pack(file, { self: true }) });
} }
})); }));

View File

@@ -74,7 +74,7 @@ export default define(meta, (ps, user, app, file, cleanup) => new Promise(async
cleanup(); cleanup();
res(pack(driveFile)); res(pack(driveFile, { self: true }));
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -31,5 +31,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
'metadata.folderId': ps.folderId 'metadata.folderId': ps.folderId
}); });
res(await Promise.all(files.map(file => pack(file)))); res(await Promise.all(files.map(file => pack(file, { self: true }))));
})); }));

View File

@@ -41,7 +41,8 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
// Serialize // Serialize
const _file = await pack(file, { const _file = await pack(file, {
detail: true detail: true,
self: true
}); });
res(_file); res(_file);

View File

@@ -111,7 +111,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
}); });
// Serialize // Serialize
const fileObj = await pack(file); const fileObj = await pack(file, { self: true });
// Response // Response
res(fileObj); res(fileObj);

View File

@@ -26,7 +26,7 @@ export const meta = {
folderId: { folderId: {
validator: $.type(ID).optional.nullable, validator: $.type(ID).optional.nullable,
default: null as any as any, default: null as any,
transform: transform transform: transform
}, },
@@ -50,5 +50,5 @@ export const meta = {
}; };
export default define(meta, (ps, user) => new Promise(async (res, rej) => { export default define(meta, (ps, user) => new Promise(async (res, rej) => {
res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force))); res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }));
})); }));

View File

@@ -65,5 +65,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
sort: sort sort: sort
}); });
res(await packMany(files)); res(await packMany(files, { self: true }));
})); }));

View File

@@ -1,4 +1,4 @@
import User, { pack } from '../../../models/user'; import { pack } from '../../../models/user';
import define from '../define'; import define from '../define';
export const meta = { export const meta = {
@@ -27,11 +27,4 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
includeHasUnreadNotes: true, includeHasUnreadNotes: true,
includeSecrets: isSecure includeSecrets: isSecure
})); }));
// Update lastUsedAt
User.update({ _id: user._id }, {
$set: {
lastUsedAt: new Date()
}
});
})); }));

View File

@@ -72,6 +72,10 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
enableTwitterIntegration: instance.enableTwitterIntegration, enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration, enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration, enableDiscordIntegration: instance.enableDiscordIntegration,
enableExternalUserRecommendation: instance.enableExternalUserRecommendation,
externalUserRecommendationEngine: instance.externalUserRecommendationEngine,
externalUserRecommendationTimeout: instance.externalUserRecommendationTimeout
}; };
if (ps.detail) { if (ps.detail) {
@@ -85,7 +89,11 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
github: instance.enableGithubIntegration, github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration, discord: instance.enableDiscordIntegration,
serviceWorker: config.sw ? true : false, serviceWorker: config.sw ? true : false,
userRecommendation: config.user_recommendation ? config.user_recommendation : {} userRecommendation: {
external: instance.enableExternalUserRecommendation,
engine: instance.externalUserRecommendationEngine,
timeout: instance.externalUserRecommendationTimeout
}
}; };
} }
@@ -99,6 +107,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
response.githubClientSecret = instance.githubClientSecret; response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId; response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret; response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
} }
res(response); res(response);

View File

@@ -219,7 +219,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
} }
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
if ((ps.text == null) && files === null && renote === null && ps.poll == null) { if (!(ps.text || files.length || renote || ps.poll)) {
return rej('text, fileIds, renoteId or poll is required'); return rej('text, fileIds, renoteId or poll is required');
} }

View File

@@ -33,16 +33,13 @@ export const meta = {
}; };
export default define(meta, (ps, user) => new Promise(async (res, rej) => { export default define(meta, (ps, user) => new Promise(async (res, rej) => {
// Lookup note
const note = await Note.findOne({
_id: ps.noteId
});
if (note === null) { const notes = await Note.find({
return rej('note not found'); replyId: ps.noteId
} }, {
limit: ps.limit,
skip: ps.offset
});
const ids = (note._replyIds || []).slice(ps.offset, ps.offset + ps.limit); res(await packMany(notes, user));
res(await packMany(ids, user));
})); }));

View File

@@ -17,7 +17,23 @@ export const meta = {
}, },
sort: { sort: {
validator: $.str.optional.or('+follower|-follower'), validator: $.str.optional.or([
'+follower',
'-follower',
'+createdAt',
'-createdAt',
'+updatedAt',
'-updatedAt',
]),
},
origin: {
validator: $.str.optional.or([
'combined',
'local',
'remote',
]),
default: 'local'
} }
} }
}; };
@@ -33,6 +49,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
_sort = { _sort = {
followersCount: 1 followersCount: 1
}; };
} else if (ps.sort == '+createdAt') {
_sort = {
createdAt: -1
};
} else if (ps.sort == '+updatedAt') {
_sort = {
updatedAt: -1
};
} else if (ps.sort == '-createdAt') {
_sort = {
createdAt: 1
};
} else if (ps.sort == '-updatedAt') {
_sort = {
updatedAt: 1
};
} }
} else { } else {
_sort = { _sort = {
@@ -40,14 +72,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
}; };
} }
const q =
ps.origin == 'local' ? { host: null } :
ps.origin == 'remote' ? { host: { $ne: null } } :
{};
const users = await User const users = await User
.find({ .find(q, {
host: null
}, {
limit: ps.limit, limit: ps.limit,
sort: _sort, sort: _sort,
skip: ps.offset skip: ps.offset
}); });
res(await Promise.all(users.map(user => pack(user, me)))); res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
})); }));

View File

@@ -6,6 +6,8 @@ import Mute from '../../../../models/mute';
import * as request from 'request'; import * as request from 'request';
import config from '../../../../config'; import config from '../../../../config';
import define from '../../define'; import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
export const meta = { export const meta = {
desc: { desc: {
@@ -30,13 +32,15 @@ export const meta = {
}; };
export default define(meta, (ps, me) => new Promise(async (res, rej) => { export default define(meta, (ps, me) => new Promise(async (res, rej) => {
if (config.user_recommendation && config.user_recommendation.external) { const instance = await fetchMeta();
if (instance.enableExternalUserRecommendation) {
const userName = me.username; const userName = me.username;
const hostName = config.hostname; const hostName = config.hostname;
const limit = ps.limit; const limit = ps.limit;
const offset = ps.offset; const offset = ps.offset;
const timeout = config.user_recommendation.timeout; const timeout = instance.externalUserRecommendationTimeout;
const engine = config.user_recommendation.engine; const engine = instance.externalUserRecommendationEngine;
const url = engine const url = engine
.replace('{{host}}', hostName) .replace('{{host}}', hostName)
.replace('{{user}}', userName) .replace('{{user}}', userName)
@@ -72,7 +76,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
$nin: followingIds.concat(mutedUserIds) $nin: followingIds.concat(mutedUserIds)
}, },
isLocked: { $ne: true }, isLocked: { $ne: true },
lastUsedAt: { updatedAt: {
$gte: new Date(Date.now() - ms('7days')) $gte: new Date(Date.now() - ms('7days'))
}, },
host: null host: null

View File

@@ -80,7 +80,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
})); }));
if (isRemoteUser(user)) { if (isRemoteUser(user)) {
if (user.updatedAt == null || Date.now() - user.updatedAt.getTime() > 1000 * 60 * 60 * 24) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
resolveRemoteUser(ps.username, ps.host, { }, true); resolveRemoteUser(ps.username, ps.host, { }, true);
} }
} }

View File

@@ -19,6 +19,12 @@ app.use(cors({
origin: '*' origin: '*'
})); }));
// No caching
app.use(async (ctx, next) => {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
await next();
});
app.use(bodyParser({ app.use(bodyParser({
// リクエストが multipart/form-data でない限りはJSONだと見なす // リクエストが multipart/form-data でない限りはJSONだと見なす
detectJSON: ctx => !ctx.is('multipart/form-data') detectJSON: ctx => !ctx.is('multipart/form-data')
@@ -45,7 +51,6 @@ router.post('/signin', require('./private/signin').default);
router.use(require('./service/discord').routes()); router.use(require('./service/discord').routes());
router.use(require('./service/github').routes()); router.use(require('./service/github').routes());
router.use(require('./service/github-bot').routes());
router.use(require('./service/twitter').routes()); router.use(require('./service/twitter').routes());
router.use(require('./mastodon').routes()); router.use(require('./mastodon').routes());

View File

@@ -1,163 +0,0 @@
import * as EventEmitter from 'events';
import * as Router from 'koa-router';
import * as request from 'request';
import User, { IUser } from '../../../models/user';
import createNote from '../../../services/note/create';
import config from '../../../config';
const crypto = require('crypto');
const handler = new EventEmitter();
let bot: IUser;
const post = async (text: string, home = true) => {
if (bot == null) {
const account = await User.findOne({
usernameLower: config.github_bot.username.toLowerCase()
});
if (account == null) {
console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`);
return;
} else {
bot = account;
}
}
createNote(bot, { text, visibility: home ? 'home' : 'public' });
};
// Init router
const router = new Router();
if (config.github_bot) {
const secret = config.github_bot.hook_secret;
router.post('/hooks/github', ctx => {
const body = JSON.stringify(ctx.request.body);
const hash = crypto.createHmac('sha1', secret).update(body).digest('hex');
const sig1 = new Buffer(ctx.headers['x-hub-signature']);
const sig2 = new Buffer(`sha1=${hash}`);
// シグネチャ比較
if (sig1.equals(sig2)) {
handler.emit(ctx.headers['x-github-event'], ctx.request.body);
ctx.status = 204;
} else {
ctx.status = 400;
}
});
}
module.exports = router;
handler.on('status', event => {
const state = event.state;
switch (state) {
case 'error':
case 'failure':
const commit = event.commit;
const parent = commit.parents[0];
// Fetch parent status
request({
url: `${parent.url}/statuses`,
proxy: config.proxy,
headers: {
'User-Agent': 'misskey'
}
}, (err, res, body) => {
if (err) {
console.error(err);
return;
}
const parentStatuses = JSON.parse(body);
const parentState = parentStatuses[0].state;
const stillFailed = parentState == 'failure' || parentState == 'error';
if (stillFailed) {
post(`⚠️**BUILD STILL FAILED**⚠️: ?[${commit.commit.message}](${commit.html_url})`);
} else {
post(`🚨**BUILD FAILED**🚨: →→→?[${commit.commit.message}](${commit.html_url})←←←`);
}
});
break;
}
});
handler.on('push', event => {
const ref = event.ref;
switch (ref) {
case 'refs/heads/develop':
const pusher = event.pusher;
const compare = event.compare;
const commits: any[] = event.commits;
post([
`🆕 Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`,
commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'),
].join('\n'));
break;
}
});
handler.on('issues', event => {
const issue = event.issue;
const action = event.action;
let title: string;
switch (action) {
case 'opened': title = '💥 Issue opened'; break;
case 'closed': title = '💮 Issue closed'; break;
case 'reopened': title = '🔥 Issue reopened'; break;
default: return;
}
post(`${title}: <${issue.number}>「${issue.title}\n${issue.html_url}`);
});
handler.on('issue_comment', event => {
const issue = event.issue;
const comment = event.comment;
const action = event.action;
let text: string;
switch (action) {
case 'created': text = `💬 Commented to「${issue.title}」:${comment.user.login}${comment.body}\n${comment.html_url}`; break;
default: return;
}
post(text);
});
handler.on('release', event => {
const action = event.action;
const release = event.release;
let text: string;
switch (action) {
case 'published': text = `🎁 **NEW RELEASE**: [${release.tag_name}](${release.html_url}) is out now. Enjoy!`; break;
default: return;
}
post(text);
});
handler.on('watch', event => {
const sender = event.sender;
post(`(((⭐️))) Starred by **${sender.login}** (((⭐️)))`, false);
});
handler.on('fork', event => {
const repo = event.forkee;
post(`🍴 Forked:\n${repo.html_url} 🍴`);
});
handler.on('pull_request', event => {
const pr = event.pull_request;
const action = event.action;
let text: string;
switch (action) {
case 'opened': text = `📦 New Pull Request:「${pr.title}\n${pr.html_url}`; break;
case 'reopened': text = `🗿 Pull Request Reopened:「${pr.title}\n${pr.html_url}`; break;
case 'closed':
text = pr.merged
? `💯 Pull Request Merged!:「${pr.title}\n${pr.html_url}`
: `🚫 Pull Request Closed:「${pr.title}\n${pr.html_url}`;
break;
default: return;
}
post(text);
});

View File

@@ -46,7 +46,6 @@ export default class Connection {
switch (type) { switch (type) {
case 'api': this.onApiRequest(body); break; case 'api': this.onApiRequest(body); break;
case 'alive': this.onAlive(); break;
case 'readNotification': this.onReadNotification(body); break; case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break; case 'subNote': this.onSubscribeNote(body); break;
case 'sn': this.onSubscribeNote(body); break; // alias case 'sn': this.onSubscribeNote(body); break; // alias
@@ -77,16 +76,6 @@ export default class Connection {
}); });
} }
@autobind
private onAlive() {
// Update lastUsedAt
User.update({ _id: this.user._id }, {
$set: {
'lastUsedAt': new Date()
}
});
}
@autobind @autobind
private onReadNotification(payload: any) { private onReadNotification(payload: any) {
if (!payload.id) return; if (!payload.id) return;

View File

@@ -3,6 +3,7 @@ import * as send from 'koa-send';
import * as mongodb from 'mongodb'; import * as mongodb from 'mongodb';
import DriveFile, { getDriveFileBucket } from '../../models/drive-file'; import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
const assets = `${__dirname}/../../server/file/assets/`; const assets = `${__dirname}/../../server/file/assets/`;
@@ -41,6 +42,11 @@ export default async function(ctx: Koa.Context) {
} }
const sendRaw = async () => { const sendRaw = async () => {
if (file.metadata && file.metadata.accessKey && file.metadata.accessKey != ctx.query['original']) {
ctx.status = 403;
return;
}
const bucket = await getDriveFileBucket(); const bucket = await getDriveFileBucket();
const readable = bucket.openDownloadStream(fileId); const readable = bucket.openDownloadStream(fileId);
readable.on('error', commonReadableHandlerGenerator(ctx)); readable.on('error', commonReadableHandlerGenerator(ctx));
@@ -60,6 +66,19 @@ export default async function(ctx: Koa.Context) {
} else { } else {
await sendRaw(); await sendRaw();
} }
} else if ('web' in ctx.query) {
const web = await DriveFileWebpublic.findOne({
'metadata.originalId': fileId
});
if (web != null) {
ctx.set('Content-Type', file.contentType);
const bucket = await getDriveFileWebpublicBucket();
ctx.body = bucket.openDownloadStream(web._id);
} else {
await sendRaw();
}
} else { } else {
if ('download' in ctx.query) { if ('download' in ctx.query) {
ctx.set('Content-Disposition', 'attachment'); ctx.set('Content-Disposition', 'attachment');

View File

@@ -59,6 +59,11 @@ const router = new Router();
router.use(activityPub.routes()); router.use(activityPub.routes());
router.use(webFinger.routes()); router.use(webFinger.routes());
// Return 404 for other .well-known
router.all('/.well-known/*', async ctx => {
ctx.status = 404;
});
// Register router // Register router
app.use(router.routes()); app.use(router.routes());

View File

@@ -111,7 +111,7 @@ router.get('/notes/:note', async ctx => {
note: _note, note: _note,
summary: getNoteSummary(_note) summary: getNoteSummary(_note)
}); });
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
} else { } else {
ctx.status = 404; ctx.status = 404;
} }

View File

@@ -1,13 +1,14 @@
import * as Koa from 'koa'; import * as Koa from 'koa';
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
import summaly from 'summaly'; import summaly from 'summaly';
import config from '../../config'; import fetchMeta from '../../misc/fetch-meta';
module.exports = async (ctx: Koa.Context) => { module.exports = async (ctx: Koa.Context) => {
const meta = await fetchMeta();
try { try {
const summary = config.summalyProxy ? await request.get({ const summary = meta.summalyProxy ? await request.get({
url: config.summalyProxy, url: meta.summalyProxy,
proxy: config.proxy,
qs: { qs: {
url: ctx.query.url url: ctx.query.url
}, },

View File

@@ -16,6 +16,7 @@ import { publishMainStream, publishDriveStream } from '../../stream';
import { isLocalUser, IUser, IRemoteUser } from '../../models/user'; import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import delFile from './delete-file'; import delFile from './delete-file';
import config from '../../config'; import config from '../../config';
import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
import driveChart from '../../chart/drive'; import driveChart from '../../chart/drive';
import perUserDriveChart from '../../chart/per-user-drive'; import perUserDriveChart from '../../chart/per-user-drive';
@@ -23,7 +24,71 @@ import fetchMeta from '../../misc/fetch-meta';
const log = debug('misskey:drive:add-file'); const log = debug('misskey:drive:add-file');
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> { /***
* Save file
* @param path Path for original
* @param name Name for original
* @param type Content-Type for original
* @param hash Hash for original
* @param size Size for original
* @param metadata
*/
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> {
// #region webpublic
let webpublic: Buffer;
let webpublicExt = 'jpg';
let webpublicType = 'image/jpeg';
if (!metadata.uri) { // from local instance
log(`creating web image`);
if (['image/jpeg'].includes(type)) {
webpublic = await sharp(path)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.rotate()
.jpeg({
quality: 85,
progressive: true
})
.toBuffer();
} else if (['image/webp'].includes(type)) {
webpublic = await sharp(path)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.rotate()
.webp({
quality: 85
})
.toBuffer();
webpublicExt = 'webp';
webpublicType = 'image/webp';
} else if (['image/png'].includes(type)) {
webpublic = await sharp(path)
.resize(2048, 2048, {
fit: 'inside',
withoutEnlargement: true
})
.rotate()
.png()
.toBuffer();
webpublicExt = 'png';
webpublicType = 'image/png';
} else {
log(`web image not created (not an image)`);
}
} else {
log(`web image not created (from remote)`);
}
// #endregion webpublic
// #region thumbnail
let thumbnail: Buffer; let thumbnail: Buffer;
let thumbnailExt = 'jpg'; let thumbnailExt = 'jpg';
let thumbnailType = 'image/jpeg'; let thumbnailType = 'image/jpeg';
@@ -53,10 +118,9 @@ async function save(path: string, name: string, type: string, hash: string, size
thumbnailExt = 'png'; thumbnailExt = 'png';
thumbnailType = 'image/png'; thumbnailType = 'image/png';
} }
// #endregion thumbnail
if (config.drive && config.drive.storage == 'minio') { if (config.drive && config.drive.storage == 'minio') {
const minio = new Minio.Client(config.drive.config);
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']); let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
if (ext === '') { if (ext === '') {
@@ -66,33 +130,41 @@ async function save(path: string, name: string, type: string, hash: string, size
} }
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`; const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;
const webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${webpublicExt}`;
const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${thumbnailExt}`; const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${thumbnailExt}`;
log(`uploading original: ${key}`);
const uploads = [
upload(key, fs.createReadStream(path), type)
];
if (webpublic) {
log(`uploading webpublic: ${webpublicKey}`);
uploads.push(upload(webpublicKey, webpublic, webpublicType));
}
if (thumbnail) {
log(`uploading thumbnail: ${thumbnailKey}`);
uploads.push(upload(thumbnailKey, thumbnail, thumbnailType));
}
await Promise.all(uploads);
const baseUrl = config.drive.baseUrl const baseUrl = config.drive.baseUrl
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
await minio.putObject(config.drive.bucket, key, fs.createReadStream(path), size, {
'Content-Type': type,
'Cache-Control': 'max-age=31536000, immutable'
});
if (thumbnail) {
await minio.putObject(config.drive.bucket, thumbnailKey, thumbnail, size, {
'Content-Type': thumbnailType,
'Cache-Control': 'max-age=31536000, immutable'
});
}
Object.assign(metadata, { Object.assign(metadata, {
withoutChunks: true, withoutChunks: true,
storage: 'minio', storage: 'minio',
storageProps: { storageProps: {
key: key, key: key,
thumbnailKey: thumbnailKey webpublicKey: webpublic ? webpublicKey : null,
thumbnailKey: thumbnail ? thumbnailKey : null,
}, },
url: `${ baseUrl }/${ key }`, url: `${ baseUrl }/${ key }`,
webpublicUrl: webpublic ? `${ baseUrl }/${ webpublicKey }` : null,
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null
}); } as IMetadata);
const file = await DriveFile.insert({ const file = await DriveFile.insert({
length: size, length: size,
@@ -105,29 +177,55 @@ async function save(path: string, name: string, type: string, hash: string, size
return file; return file;
} else { } else {
// Get MongoDB GridFS bucket // #region store original
const bucket = await getDriveFileBucket(); const originalDst = await getDriveFileBucket();
const file = await new Promise<IDriveFile>((resolve, reject) => { // web用(Exif削除済み)がある場合はオリジナルにアクセス制限
const writeStream = bucket.openUploadStream(name, { if (webpublic) metadata.accessKey = uuid.v4();
const originalFile = await new Promise<IDriveFile>((resolve, reject) => {
const writeStream = originalDst.openUploadStream(name, {
contentType: type, contentType: type,
metadata metadata
}); });
writeStream.once('finish', resolve); writeStream.once('finish', resolve);
writeStream.on('error', reject); writeStream.on('error', reject);
fs.createReadStream(path).pipe(writeStream); fs.createReadStream(path).pipe(writeStream);
}); });
log(`original stored to ${originalFile._id}`);
// #endregion store original
// #region store webpublic
if (webpublic) {
const webDst = await getDriveFileWebpublicBucket();
const webFile = await new Promise<IDriveFile>((resolve, reject) => {
const writeStream = webDst.openUploadStream(name, {
contentType: webpublicType,
metadata: {
originalId: originalFile._id
}
});
writeStream.once('finish', resolve);
writeStream.on('error', reject);
writeStream.end(webpublic);
});
log(`web stored ${webFile._id}`);
}
// #endregion store webpublic
if (thumbnail) { if (thumbnail) {
const thumbnailBucket = await getDriveFileThumbnailBucket(); const thumbnailBucket = await getDriveFileThumbnailBucket();
await new Promise<IDriveFile>((resolve, reject) => { const tuhmFile = await new Promise<IDriveFile>((resolve, reject) => {
const writeStream = thumbnailBucket.openUploadStream(name, { const writeStream = thumbnailBucket.openUploadStream(name, {
contentType: thumbnailType, contentType: thumbnailType,
metadata: { metadata: {
originalId: file._id originalId: originalFile._id
} }
}); });
@@ -135,12 +233,23 @@ async function save(path: string, name: string, type: string, hash: string, size
writeStream.on('error', reject); writeStream.on('error', reject);
writeStream.end(thumbnail); writeStream.end(thumbnail);
}); });
log(`thumbnail stored ${tuhmFile._id}`);
} }
return file; return originalFile;
} }
} }
async function upload(key: string, stream: fs.ReadStream | Buffer, type: string) {
const minio = new Minio.Client(config.drive.config);
await minio.putObject(config.drive.bucket, key, stream, null, {
'Content-Type': type,
'Cache-Control': 'max-age=31536000, immutable'
});
}
async function deleteOldFile(user: IRemoteUser) { async function deleteOldFile(user: IRemoteUser) {
const oldFile = await DriveFile.findOne({ const oldFile = await DriveFile.findOne({
_id: { _id: {

View File

@@ -4,6 +4,7 @@ import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-
import config from '../../config'; import config from '../../config';
import driveChart from '../../chart/drive'; import driveChart from '../../chart/drive';
import perUserDriveChart from '../../chart/per-user-drive'; import perUserDriveChart from '../../chart/per-user-drive';
import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
export default async function(file: IDriveFile, isExpired = false) { export default async function(file: IDriveFile, isExpired = false) {
if (file.metadata.storage == 'minio') { if (file.metadata.storage == 'minio') {
@@ -20,6 +21,11 @@ export default async function(file: IDriveFile, isExpired = false) {
const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`; const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
await minio.removeObject(config.drive.bucket, thumbnailObj); await minio.removeObject(config.drive.bucket, thumbnailObj);
} }
if (file.metadata.webpublicUrl) {
const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`;
await minio.removeObject(config.drive.bucket, webpublicObj);
}
} }
// チャンクをすべて削除 // チャンクをすべて削除
@@ -48,6 +54,20 @@ export default async function(file: IDriveFile, isExpired = false) {
} }
//#endregion //#endregion
//#region Web公開用もあれば削除
const webpublic = await DriveFileWebpublic.findOne({
'metadata.originalId': file._id
});
if (webpublic) {
await DriveFileWebpublicChunk.remove({
files_id: webpublic._id
});
await DriveFileWebpublic.remove({ _id: webpublic._id });
}
//#endregion
// 統計を更新 // 統計を更新
driveChart.update(file, false); driveChart.update(file, false);
perUserDriveChart.update(file, false); perUserDriveChart.update(file, false);

View File

@@ -622,9 +622,6 @@ function saveQuote(renote: INote, note: INote) {
function saveReply(reply: INote, note: INote) { function saveReply(reply: INote, note: INote) {
Note.update({ _id: reply._id }, { Note.update({ _id: reply._id }, {
$push: {
_replyIds: note._id
},
$inc: { $inc: {
repliesCount: 1 repliesCount: 1
} }
@@ -633,6 +630,9 @@ function saveReply(reply: INote, note: INote) {
function incNotesCountOfUser(user: IUser) { function incNotesCountOfUser(user: IUser) {
User.update({ _id: user._id }, { User.update({ _id: user._id }, {
$set: {
updatedAt: new Date()
},
$inc: { $inc: {
notesCount: 1 notesCount: 1
} }

View File

@@ -314,6 +314,7 @@ describe('API', () => {
const file = await uploadFile(bob); const file = await uploadFile(bob);
const res = await request('/notes/create', { const res = await request('/notes/create', {
text: 'test',
fileIds: [file.id] fileIds: [file.id]
}, me); }, me);
@@ -327,6 +328,7 @@ describe('API', () => {
const me = await signup(); const me = await signup();
const res = await request('/notes/create', { const res = await request('/notes/create', {
text: 'test',
fileIds: ['000000000000000000000000'] fileIds: ['000000000000000000000000']
}, me); }, me);

View File

@@ -162,27 +162,87 @@ describe('Text', () => {
}); });
}); });
it('hashtag', () => { describe('hashtag', () => {
const tokens1 = analyze('Strawberry Pasta #alice'); it('simple', () => {
assert.deepEqual([ const tokens = analyze('#alice');
text('Strawberry Pasta '), assert.deepEqual([
node('hashtag', { hashtag: 'alice' }) node('hashtag', { hashtag: 'alice' })
], tokens1); ], tokens);
});
const tokens2 = analyze('Foo #bar, baz #piyo.'); it('after line break', () => {
assert.deepEqual([ const tokens = analyze('foo\n#alice');
text('Foo '), assert.deepEqual([
node('hashtag', { hashtag: 'bar' }), text('foo\n'),
text(', baz '), node('hashtag', { hashtag: 'alice' })
node('hashtag', { hashtag: 'piyo' }), ], tokens);
text('.'), });
], tokens2);
const tokens3 = analyze('#Foo!'); it('with text', () => {
assert.deepEqual([ const tokens = analyze('Strawberry Pasta #alice');
node('hashtag', { hashtag: 'Foo' }), assert.deepEqual([
text('!'), text('Strawberry Pasta '),
], tokens3); node('hashtag', { hashtag: 'alice' })
], tokens);
});
it('ignore comma and period', () => {
const tokens = analyze('Foo #bar, baz #piyo.');
assert.deepEqual([
text('Foo '),
node('hashtag', { hashtag: 'bar' }),
text(', baz '),
node('hashtag', { hashtag: 'piyo' }),
text('.'),
], tokens);
});
it('ignore exclamation mark', () => {
const tokens = analyze('#Foo!');
assert.deepEqual([
node('hashtag', { hashtag: 'Foo' }),
text('!'),
], tokens);
});
it('allow including number', () => {
const tokens = analyze('#foo123');
assert.deepEqual([
node('hashtag', { hashtag: 'foo123' }),
], tokens);
});
it('with brackets', () => {
const tokens = analyze('(#foo)');
assert.deepEqual([
text('('),
node('hashtag', { hashtag: 'foo' }),
text(')'),
], tokens);
});
it('with brackets (space before)', () => {
const tokens = analyze('(bar #foo)');
assert.deepEqual([
text('(bar '),
node('hashtag', { hashtag: 'foo' }),
text(')'),
], tokens);
});
it('disallow number only', () => {
const tokens = analyze('#123');
assert.deepEqual([
text('#123'),
], tokens);
});
it('disallow number only (with brackets)', () => {
const tokens = analyze('(#123)');
assert.deepEqual([
text('(#123)'),
], tokens);
});
}); });
describe('quote', () => { describe('quote', () => {
@@ -360,6 +420,15 @@ describe('Text', () => {
], tokens); ], tokens);
}); });
it('ignore parent brackets 2', () => {
const tokens = analyze('(foo https://example.com/foo)');
assert.deepEqual([
text('(foo '),
node('url', { url: 'https://example.com/foo' }),
text(')')
], tokens);
});
it('ignore parent brackets with internal brackets', () => { it('ignore parent brackets with internal brackets', () => {
const tokens = analyze('(https://example.com/foo(bar))'); const tokens = analyze('(https://example.com/foo(bar))');
assert.deepEqual([ assert.deepEqual([
@@ -370,13 +439,55 @@ describe('Text', () => {
}); });
}); });
it('link', () => { describe('link', () => {
const tokens = analyze('[foo](https://example.com)'); it('simple', () => {
assert.deepEqual([ const tokens = analyze('[foo](https://example.com)');
nodeWithChildren('link', [ assert.deepEqual([
text('foo') nodeWithChildren('link', [
], { url: 'https://example.com', silent: false }) text('foo')
], tokens); ], { url: 'https://example.com', silent: false })
], tokens);
});
it('simple (with silent flag)', () => {
const tokens = analyze('?[foo](https://example.com)');
assert.deepEqual([
nodeWithChildren('link', [
text('foo')
], { url: 'https://example.com', silent: true })
], tokens);
});
it('in text', () => {
const tokens = analyze('before[foo](https://example.com)after');
assert.deepEqual([
text('before'),
nodeWithChildren('link', [
text('foo')
], { url: 'https://example.com', silent: false }),
text('after'),
], tokens);
});
it('with brackets', () => {
const tokens = analyze('[foo](https://example.com/foo(bar))');
assert.deepEqual([
nodeWithChildren('link', [
text('foo')
], { url: 'https://example.com/foo(bar)', silent: false })
], tokens);
});
it('with parent brackets', () => {
const tokens = analyze('([foo](https://example.com/foo(bar)))');
assert.deepEqual([
text('('),
nodeWithChildren('link', [
text('foo')
], { url: 'https://example.com/foo(bar)', silent: false }),
text(')')
], tokens);
});
}); });
it('emoji', () => { it('emoji', () => {
@@ -448,11 +559,27 @@ describe('Text', () => {
}); });
}); });
it('inline code', () => { describe('inline code', () => {
const tokens = analyze('`var x = "Strawberry Pasta";`'); it('simple', () => {
assert.deepEqual([ const tokens = analyze('`var x = "Strawberry Pasta";`');
node('inlineCode', { code: 'var x = "Strawberry Pasta";' }) assert.deepEqual([
], tokens); node('inlineCode', { code: 'var x = "Strawberry Pasta";' })
], tokens);
});
it('disallow line break', () => {
const tokens = analyze('`foo\nbar`');
assert.deepEqual([
text('`foo\nbar`')
], tokens);
});
it('disallow ´', () => {
const tokens = analyze('`foo´bar`');
assert.deepEqual([
text('`foo´bar`')
], tokens);
});
}); });
it('math', () => { it('math', () => {
@@ -514,6 +641,17 @@ describe('Text', () => {
], tokens); ], tokens);
}); });
}); });
describe('center', () => {
it('simple', () => {
const tokens = analyze('<center>foo</center>');
assert.deepEqual([
nodeWithChildren('center', [
text('foo')
]),
], tokens);
});
});
}); });
describe('toHtml', () => { describe('toHtml', () => {

View File

@@ -5,21 +5,22 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as webpack from 'webpack'; import * as webpack from 'webpack';
import chalk from 'chalk'; import chalk from 'chalk';
import rndstr from 'rndstr';
const { VueLoaderPlugin } = require('vue-loader'); const { VueLoaderPlugin } = require('vue-loader');
const WebpackOnBuildPlugin = require('on-build-webpack'); const WebpackOnBuildPlugin = require('on-build-webpack');
//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); //const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin'); const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const isProduction = process.env.NODE_ENV == 'production';
const constants = require('./src/const.json'); const constants = require('./src/const.json');
const locales = require('./locales'); const locales = require('./locales');
const meta = require('./package.json'); const meta = require('./package.json');
const version = meta.clientVersion; const version = isProduction ? meta.clientVersion : meta.clientVersion + '-' + rndstr({ length: 8, chars: '0-9a-z' });
const codename = meta.codename; const codename = meta.codename;
const isProduction = process.env.NODE_ENV == 'production';
const postcss = { const postcss = {
loader: 'postcss-loader', loader: 'postcss-loader',
options: { options: {