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
#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
/elasticsearch
*.code-workspace
yarn.lock

View File

@@ -8,18 +8,20 @@ WORKDIR /misskey
FROM base AS builder
RUN unlink /usr/bin/free
RUN apk add --no-cache \
gcc \
g++ \
libc-dev \
python \
autoconf \
automake \
file \
g++ \
gcc \
libc-dev \
libtool \
make \
nasm \
pkgconfig \
libtool \
procps \
python \
zlib-dev
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/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/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
</tr><tr>
<td><a href="https://www.patreon.com/user?u=13039004">nemu</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/mastodon">Gargron</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>
<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>
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Wed, 31 Oct 2018 23:21:06 UTC
**Last updated:** Fri, 23 Nov 2018 14:09:04 UTC
<!-- PATREON_END -->
: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)
```
## 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.
``` shell
# Use id
node cli/suspend 57d01a501fdf2d07be417afe
# By id
node cli/mark-admin 57d01a501fdf2d07be417afe
# Use username
# By username
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 または ユーザー名)
```
## 'verified'ユーザーを設定する
``` shell
node cli/mark-verified (ユーザーID または ユーザー名)
```
## ユーザーを凍結する
``` shell
node cli/suspend (ユーザーID または ユーザー名)
```
例:
``` shell
# ユーザーID
node cli/suspend 57d01a501fdf2d07be417afe
node cli/mark-admin 57d01a501fdf2d07be417afe
# ユーザー名
node cli/suspend @syuilo
# ユーザー名 (リモート)
node cli/suspend @syuilo@misskey.xyz
```
## ユーザーのパスワードをリセットする
``` shell
node cli/reset-password (ユーザーID または ユーザー名)
node cli/mark-admin @syuilo
```

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -119,7 +119,7 @@ common:
reduce-motion: "Reduce motion in UI"
this-setting-is-this-device-only: "Only for this device"
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-post: "This post information is a copy."
view-on-remote: "View it on remote"
@@ -366,8 +366,8 @@ common/views/components/signin.vue:
signin: "Sign in"
or: "Or"
signin-with-twitter: "Log in with Twitter"
signin-with-github: "Log in with GitHub"
signin-with-discord: "Login with Discord"
signin-with-github: "Sign in with GitHub"
signin-with-discord: "Sign in with Discord"
login-failed: "Log in failed. Make sure you have entered your correct username and password."
common/views/components/signup.vue:
invitation-code: "Invitation code"
@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "Invite"
save: "Save"
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:
title: "Chart"
per-day: "per Day"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "Response time"
network-usage: "Traffic"
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"
suspended: "Successfully suspended."
unsuspend-user: "Unsuspend users"
unsuspend: "Unsuspend"
unsuspended: "The user has successfully unsuspended."
verify-user: "User account verification settings"
verify: "Verify account"
verified: "The account is now being verified"
unverify-user: "User account unverification settings"
unverify: "Unverify account"
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:
add-moderator:
title: "Register Moderator"
@@ -1051,7 +1074,7 @@ admin/views/emoji.vue:
remove: "Remove"
updated: "Updated"
remove-emoji:
are-you-sure: "Delete \"%1$s\"?"
are-you-sure: "Delete \"$1\"?"
removed: "Deleted"
admin/views/announcements.vue:
announcements: "Announcements"
@@ -1062,7 +1085,7 @@ admin/views/announcements.vue:
text: "Content"
saved: "Saved"
_remove:
are-you-sure: "Delete \"%1$s\"?"
are-you-sure: "Delete \"$1\"?"
removed: "Deleted"
admin/views/hashtags.vue:
hided-tags: "Hidden Tags"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -123,7 +123,7 @@ common:
is-remote-user: "Ces informations appartiennent à un·e utilisateur·rice distant·e."
is-remote-post: "Ceci est une publication distante."
view-on-remote: " Consulter le profil complet"
renoted-by: "{user}がRenote"
renoted-by: "Renoté par {user}"
error:
title: 'Une erreur est survenue'
retry: 'Réessayer'
@@ -432,7 +432,7 @@ common/views/components/visibility-chooser.vue:
specified-desc: "Publier uniquement aux utilisateurs·rices mentionné·e·s"
private: "Privé"
local-public: "Local (Public)"
local-public-desc: "リモートへは公開しない"
local-public-desc: "Ne pas publier pour les distants"
local-home: "Accueil (local uniquement)"
local-followers: "Local (Abonnés)"
common/views/components/trends.vue:
@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "Inviter"
save: "Sauvegarder"
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:
title: "Graph"
per-day: "par jour"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "Temps de réponse"
network-usage: "Traffic"
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"
suspended: "Suspendu·e avec succès."
unsuspend-user: "Lever la suspension dutilisateur·rice·s"
unsuspend: "Suspension levée"
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"
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"
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:
add-moderator:
title: "Ajout dun modérateur"
@@ -1265,7 +1288,7 @@ mobile/views/components/ui.nav.vue:
admin: "Admin"
about: "À propos de Misskey"
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"
mobile/views/components/users-list.vue:
all: "Tout"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -1092,17 +1092,17 @@ admin/views/instance.vue:
recaptcha-site-key: "reCAPTCHA site key"
recaptcha-secret-key: "reCAPTCHA secret key"
twitter-integration-config: "Twitter連携の設定"
twitter-integration-info: "コールバックURLは /api/tw/cb に設定します。"
twitter-integration-info: "コールバックURLは {url} に設定します。"
enable-twitter-integration: "Twitter連携を有効にする"
twitter-integration-consumer-key: "Consumer key"
twitter-integration-consumer-secret: "Consumer secret"
github-integration-config: "GitHub連携の設定"
github-integration-info: "コールバックURLは /api/gh/cb に設定します。"
github-integration-info: "コールバックURLは {url} に設定します。"
enable-github-integration: "GitHub連携を有効にする"
github-integration-client-id: "Client ID"
github-integration-client-secret: "Client Secret"
discord-integration-config: "Discord連携の設定"
discord-integration-info: "コールバックURLは /api/dc/cb に設定します。"
discord-integration-info: "コールバックURLは {url} に設定します。"
enable-discord-integration: "Discord連携を有効にする"
discord-integration-client-id: "Client ID"
discord-integration-client-secret: "Client Secret"
@@ -1117,6 +1117,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
@@ -1145,18 +1151,35 @@ admin/views/charts.vue:
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

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

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "招待"
save: "保存"
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:
title: "チャート"
per-day: "1日ごと"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "応答時間"
network-usage: "通信量"
admin/views/users.vue:
suspend-user: "ユーザーの凍結"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "凍結"
suspended: "凍結しました"
unsuspend-user: "ユーザーの凍結の解除"
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
unverify-user: "ユーザーの公式アカウント解除"
unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"

View File

@@ -991,6 +991,12 @@ admin/views/instance.vue:
invite: "邀请"
save: "保存"
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:
title: "历史记录"
per-day: "每天"
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
network-time: "响应时间"
network-usage: "网络流量"
admin/views/users.vue:
suspend-user: "冻结用户"
operation: "操作"
username-or-userid: "ユーザー名またはユーザーID"
user-not-found: "ユーザーが見つかりません"
lookup: "照会"
reset-password: "パスワードをリセット"
password-updated: "パスワードは現在「{password}」です"
suspend: "被冻结"
suspended: "成功冻结用户"
unsuspend-user: "解除用户冻结"
unsuspend: "已解除冻结"
unsuspended: "已成功解除用户冻结"
verify-user: "用户账户认证设置"
verify: "认证用户"
verified: "此账户已被认证"
unverify-user: "用户账号解除认证设置"
unverify: "解除账户认证"
unverified: "该帐户未经认证"
users:
title: "ユーザー"
sort:
title: "ソート"
createdAtAsc: "登録日時が古い順"
createdAtDesc: "登録日時が新しい順"
updatedAtAsc: "更新日時が古い順"
updatedAtDesc: "更新日時が新しい順"
origin:
title: "オリジン"
combined: "ローカル+リモート"
local: "ローカル"
remote: "リモート"
createdAt: "登録日時"
updatedAt: "更新日時"
admin/views/moderators.vue:
add-moderator:
title: "注册版主"

View File

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

View File

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

View File

@@ -38,7 +38,7 @@
<i slot="icon"><fa icon="link"/></i>
<span>{{ $t('add-emoji.url') }}</span>
</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="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
</ui-horizon-group>

View File

@@ -42,6 +42,16 @@
<section>
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
</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>
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
</section>
@@ -59,7 +69,7 @@
<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</div>
<section>
<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="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>
@@ -70,7 +80,7 @@
<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</div>
<section>
<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="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>
@@ -81,7 +91,7 @@
<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</div>
<section>
<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="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>
@@ -93,15 +103,16 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { host } from '../../config';
import { url, host } from '../../config';
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({
i18n: i18n('admin/views/instance.vue'),
data() {
return {
url,
host: toUnicode(host),
maintainerName: null,
maintainerEmail: null,
@@ -129,7 +140,11 @@ export default Vue.extend({
discordClientSecret: null,
proxyAccount: 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.discordClientId = meta.discordClientId;
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,
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret
discordClientSecret: this.discordClientSecret,
enableExternalUserRecommendation: this.enableExternalUserRecommendation,
externalUserRecommendationEngine: this.externalUserRecommendationEngine,
externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10),
summalyProxy: this.summalyProxy
}).then(() => {
this.$root.alert({
type: 'success',

View File

@@ -1,42 +1,63 @@
<template>
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
<ui-card>
<div slot="title">{{ $t('verify-user') }}</div>
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
<section class="fit-top">
<ui-input v-model="verifyUsername" type="text">
<span slot="prefix">@</span>
<ui-input v-model="target" type="text">
<span>{{ $t('username-or-userid') }}</span>
</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>
</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">
<ui-input v-model="unverifyUsername" type="text">
<span slot="prefix">@</span>
</ui-input>
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
</section>
</ui-card>
<ui-card>
<div slot="title">{{ $t('suspend-user') }}</div>
<section class="fit-top">
<ui-input v-model="suspendUsername" type="text">
<span slot="prefix">@</span>
</ui-input>
<ui-button @click="suspendUser" :disabled="suspending">{{ $t('suspend') }}</ui-button>
</section>
</ui-card>
<ui-card>
<div slot="title">{{ $t('unsuspend-user') }}</div>
<section class="fit-top">
<ui-input v-model="unsuspendUsername" type="text">
<span slot="prefix">@</span>
</ui-input>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
<ui-horizon-group inputs>
<ui-select v-model="sort">
<span slot="label">{{ $t('users.sort.title') }}</span>
<option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option>
<option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option>
<option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option>
<option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option>
</ui-select>
<ui-select v-model="origin">
<span slot="label">{{ $t('users.origin.title') }}</span>
<option value="combined">{{ $t('users.origin.combined') }}</option>
<option value="local">{{ $t('users.origin.local') }}</option>
<option value="remote">{{ $t('users.origin.remote') }}</option>
</ui-select>
</ui-horizon-group>
<div class="kofvwchc" v-for="user in users">
<div>
<a :href="user | userPage(null, true)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<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>
</ui-card>
</div>
@@ -46,29 +67,89 @@
import Vue from 'vue';
import i18n from '../../i18n';
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({
i18n: i18n('admin/views/users.vue'),
data() {
return {
verifyUsername: null,
user: null,
target: null,
verifying: false,
unverifyUsername: null,
unverifying: false,
suspendUsername: null,
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: {
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() {
this.verifying = true;
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 });
this.$root.alert({
type: 'success',
@@ -90,7 +171,7 @@ export default Vue.extend({
this.unverifying = true;
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 });
this.$root.alert({
type: 'success',
@@ -112,7 +193,7 @@ export default Vue.extend({
this.suspending = true;
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 });
this.$root.alert({
type: 'success',
@@ -134,7 +215,7 @@ export default Vue.extend({
this.unsuspending = true;
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 });
this.$root.alert({
type: 'success',
@@ -150,6 +231,24 @@ export default Vue.extend({
});
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)
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>

View File

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

View File

@@ -5,7 +5,7 @@
<div class="icon" :class="type"><fa :icon="icon"/></div>
<header v-if="title" v-html="title"></header>
<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="cancel" v-if="showCancelButton">Cancel</ui-button>
</ui-horizon-group>

View File

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

View File

@@ -22,8 +22,8 @@
<div v-for="(x, i) in game.settings.map.join('')"
:data-none="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>
<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 == 'b'" :icon="fasCircle"/>
<fa v-if="x == 'w'" :icon="farCircle"/>
</div>
</div>
</div>
@@ -117,6 +117,8 @@
import Vue from 'vue';
import i18n from '../../../../../i18n';
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({
i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
@@ -129,7 +131,8 @@ export default Vue.extend({
mapName: maps.eighteight.name,
maps: maps,
form: null,
messages: []
messages: [],
fasCircle, farCircle
};
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
<template>
<div class="mk-note-detail" :title="title">
<div class="mk-note-detail" :title="title" tabindex="-1">
<button
class="read-more"
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
@@ -63,18 +63,18 @@
<footer>
<span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
<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-else><fa icon="reply"/></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</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>
</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>
</button>
<button @click="menu" ref="menuButton">
<button @click="menu()" ref="menuButton">
<fa icon="ellipsis-h"/>
</button>
</footer>
@@ -88,23 +88,18 @@
<script lang="ts">
import Vue from 'vue';
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 { sum, unique } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber';
import noteMixin from '../../../common/scripts/note-mixin';
export default Vue.extend({
i18n: i18n('desktop/views/components/note-detail.vue'),
components: {
XSub
},
mixins: [noteSubscriber('note')],
mixins: [noteMixin(), noteSubscriber('note')],
props: {
note: {
@@ -118,47 +113,12 @@ export default Vue.extend({
data() {
return {
showContent: false,
conversation: [],
conversationFetching: false,
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() {
// Get replies
if (!this.compact) {
@@ -169,24 +129,6 @@ export default Vue.extend({
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: {
@@ -200,32 +142,6 @@ export default Vue.extend({
this.conversationFetching = false;
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>
<div class="mk-note-detail">
<div class="mk-note-detail" tabindex="-1">
<button
class="more"
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
@@ -61,18 +61,18 @@
</div>
<footer>
<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-else><fa icon="reply"/></template>
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
</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>
</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>
</button>
<button @click="menu" ref="menuButton">
<button @click="menu()" ref="menuButton">
<fa icon="ellipsis-h"/>
</button>
</footer>
@@ -86,21 +86,18 @@
<script lang="ts">
import Vue from 'vue';
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 { sum, unique } from '../../../../../prelude/array';
import noteSubscriber from '../../../common/scripts/note-subscriber';
import noteMixin from '../../../common/scripts/note-mixin';
export default Vue.extend({
i18n: i18n('mobile/views/components/note-detail.vue'),
components: {
XSub
},
mixins: [noteSubscriber('note')],
mixins: [noteMixin(), noteSubscriber('note')],
props: {
note: {
@@ -114,43 +111,12 @@ export default Vue.extend({
data() {
return {
showContent: false,
conversation: [],
conversationFetching: false,
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() {
// Get replies
if (!this.compact) {
@@ -161,24 +127,6 @@ export default Vue.extend({
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: {
@@ -192,35 +140,6 @@ export default Vue.extend({
this.conversationFetching = false;
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>
<div class="links">
<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="/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/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 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><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="/" :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']" 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']" 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']" 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" 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>
<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/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/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/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/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" 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" 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" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
</ul>
<ul>
<li><a @click="search"><i><fa icon="search"/></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 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 @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><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" 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" 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" fixed-width/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li>
</ul>
</div>
<div class="announcements" v-if="announcements && announcements.length > 0">

View File

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

View File

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

View File

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

View File

@@ -45,6 +45,12 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
return pre;
},
center(token) {
const el = doc.createElement('div');
dive(token.children).forEach(child => el.appendChild(child));
return el;
},
emoji(token) {
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 {
return ['blockCode', 'quote', 'title'].includes(node.name);
return ['blockCode', 'center', 'quote', 'title'].includes(node.name);
}
/**
* ブロック要素の前後にある改行を削除します(ブロック要素自体が改行の役割も果たすため、余計に改行されてしまうため)
* ブロック要素の前後にある改行を削除します
* (ブロック要素自体が改行の役割を果たすため、余計に改行されてしまう)
* @param nodes
*/
const removeNeedlessLineBreaks = (nodes: Node[]) => {

View File

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

View File

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

View File

@@ -15,7 +15,10 @@ const defaultMeta: any = {
maxNoteTextLength: 1000,
enableTwitterIntegration: 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> {

View File

@@ -6,15 +6,24 @@ export default function(file: IDriveFile, thumbnail = false): string {
if (file.metadata.withoutChunks) {
if (thumbnail) {
return file.metadata.thumbnailUrl || file.metadata.url;
return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || file.metadata.url;
} else {
return file.metadata.url;
return file.metadata.webpublicUrl || file.metadata.url;
}
} else {
if (thumbnail) {
return `${config.drive_url}/${file._id}?thumbnail`;
} 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 monkDb, { nativeDbConn } from '../db/mongodb';
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');
DriveFile.createIndex('md5');
@@ -28,21 +28,48 @@ export type IMetadata = {
_user: any;
folderId: mongo.ObjectID;
comment: string;
/**
* リモートインスタンスから取得した場合の元URL
*/
uri?: string;
/**
* URL for web(生成されている場合) or original
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
*/
url?: string;
/**
* URL for thumbnail (thumbnailがなければなし)
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
*/
thumbnailUrl?: string;
/**
* URL for original (web用が生成されてない場合はurlがoriginalを指す)
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
*/
webpublicUrl?: string;
accessKey?: string;
src?: string;
deletedAt?: Date;
/**
* このファイルの中身データがMongoDB内に保存されているのか否か
* このファイルの中身データがMongoDB内に保存されていないか否か
* オブジェクトストレージを利用している or リモートサーバーへの直リンクである
* な場合は false になります
* な場合は true になります
*/
withoutChunks?: boolean;
storage?: string;
storageProps?: any;
/***
* ObjectStorage の格納先の情報
*/
storageProps?: IStorageProps;
isSensitive?: boolean;
/**
@@ -56,6 +83,25 @@ export type IMetadata = {
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 = {
_id: mongo.ObjectID;
uploadDate: Date;
@@ -83,7 +129,8 @@ export function validateFileName(name: string): boolean {
export const packMany = (
files: any[],
options?: {
detail: boolean
detail?: boolean
self?: boolean,
}
) => {
return Promise.all(files.map(f => pack(f, options)));
@@ -95,11 +142,13 @@ export const packMany = (
export const pack = (
file: any,
options?: {
detail: boolean
detail?: boolean,
self?: boolean,
}
) => new Promise<any>(async (resolve, reject) => {
const opts = Object.assign({
detail: false
detail: false,
self: false
}, options);
let _file: any;
@@ -165,5 +214,9 @@ export const pack = (
delete _target.isRemote;
delete _target._user;
if (opts.self) {
_target.url = getOriginalUrl(_file);
}
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 = {
name?: string;
@@ -184,6 +197,8 @@ export type IMeta = {
*/
maxNoteTextLength?: number;
summalyProxy?: string;
enableTwitterIntegration?: boolean;
twitterConsumerKey?: string;
twitterConsumerSecret?: string;
@@ -195,4 +210,8 @@ export type IMeta = {
enableDiscordIntegration?: boolean;
discordClientId?: 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('mentions');
Note.createIndex('visibleUserIds');
Note.createIndex('replyId');
Note.createIndex('tagsLower');
Note.createIndex('_user.host');
Note.createIndex('_files._id');
@@ -99,7 +100,6 @@ export type INote = {
host: string;
inbox?: string;
};
_replyIds?: mongo.ObjectID[];
_files?: IDriveFile[];
};
@@ -258,6 +258,8 @@ export const pack = async (
delete _note._reply;
delete _note._renote;
delete _note._files;
delete _note._replyIds;
if (_note.geo) delete _note.geo.type;
// Populate user

View File

@@ -26,6 +26,7 @@ export default User;
type IUserBase = {
_id: mongo.ObjectID;
createdAt: Date;
updatedAt?: Date;
deletedAt?: Date;
followersCount: number;
followingCount: number;
@@ -37,6 +38,8 @@ type IUserBase = {
bannerId: mongo.ObjectID;
avatarUrl?: string;
bannerUrl?: string;
avatarColor?: any;
bannerColor?: any;
wallpaperId: mongo.ObjectID;
wallpaperUrl?: string;
data: any;
@@ -104,7 +107,6 @@ export interface ILocalUser extends IUserBase {
birthday: string; // 'YYYY-MM-DD'
tags: string[];
};
lastUsedAt: Date;
isCat: boolean;
isAdmin?: boolean;
isModerator?: boolean;
@@ -132,7 +134,7 @@ export interface IRemoteUser extends IUserBase {
id: string;
publicKeyPem: string;
};
updatedAt: Date;
lastFetchedAt: Date;
isAdmin: 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;
// 引用
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);
@@ -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);
}
@@ -112,7 +119,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
createdAt: new Date(note.published),
files: files,
reply,
renote: undefined,
renote: quote,
cw: note.summary,
text: text,
viaMobile: false,

View File

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

View File

@@ -42,6 +42,18 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
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({
_id: note.userId
});
@@ -112,6 +124,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
summary: note.cw,
content,
_misskey_content: text,
_misskey_quote: quote,
published: note.createdAt.toISOString(),
to,
cc,

View File

@@ -41,6 +41,7 @@ export interface IOrderedCollection extends IObject {
export interface INote extends IObject {
type: 'Note';
_misskey_content: string;
_misskey_quote: string;
}
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.set('Cache-Control', 'public, max-age=180');
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
});

View File

@@ -9,8 +9,8 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
path: '/',
domain: config.hostname,
// SEE: https://github.com/koajs/koa/issues/974
//secure: config.url.startsWith('https'),
secure: false,
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
secure: config.url.startsWith('https'),
httpOnly: false,
expires: new Date(Date.now() + 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: {
validator: $.bool.optional,
desc: {
@@ -200,6 +207,27 @@ export const meta = {
desc: {
'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;
}
if (ps.summalyProxy !== undefined) {
set.summalyProxy = ps.summalyProxy;
}
if (ps.enableTwitterIntegration !== undefined) {
set.enableTwitterIntegration = ps.enableTwitterIntegration;
}
@@ -315,6 +347,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
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({}, {
$set: set
}, { upsert: true });

View File

@@ -77,5 +77,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
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) {
res({ file: null });
} 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();
res(pack(driveFile));
res(pack(driveFile, { self: true }));
} catch (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
});
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
const _file = await pack(file, {
detail: true
detail: true,
self: true
});
res(_file);

View File

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

View File

@@ -26,7 +26,7 @@ export const meta = {
folderId: {
validator: $.type(ID).optional.nullable,
default: null as any as any,
default: null as any,
transform: transform
},
@@ -50,5 +50,5 @@ export const meta = {
};
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
});
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';
export const meta = {
@@ -27,11 +27,4 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
includeHasUnreadNotes: true,
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,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableExternalUserRecommendation: instance.enableExternalUserRecommendation,
externalUserRecommendationEngine: instance.externalUserRecommendationEngine,
externalUserRecommendationTimeout: instance.externalUserRecommendationTimeout
};
if (ps.detail) {
@@ -85,7 +89,11 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
github: instance.enableGithubIntegration,
discord: instance.enableDiscordIntegration,
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.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
}
res(response);

View File

@@ -219,7 +219,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
}
// テキストが無いかつ添付ファイルが無いかつ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');
}

View File

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

View File

@@ -17,7 +17,23 @@ export const meta = {
},
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 = {
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 {
_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
.find({
host: null
}, {
.find(q, {
limit: ps.limit,
sort: _sort,
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 config from '../../../../config';
import define from '../../define';
import fetchMeta from '../../../../misc/fetch-meta';
export const meta = {
desc: {
@@ -30,13 +32,15 @@ export const meta = {
};
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 hostName = config.hostname;
const limit = ps.limit;
const offset = ps.offset;
const timeout = config.user_recommendation.timeout;
const engine = config.user_recommendation.engine;
const timeout = instance.externalUserRecommendationTimeout;
const engine = instance.externalUserRecommendationEngine;
const url = engine
.replace('{{host}}', hostName)
.replace('{{user}}', userName)
@@ -72,7 +76,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
$nin: followingIds.concat(mutedUserIds)
},
isLocked: { $ne: true },
lastUsedAt: {
updatedAt: {
$gte: new Date(Date.now() - ms('7days'))
},
host: null

View File

@@ -80,7 +80,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
}));
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);
}
}

View File

@@ -19,6 +19,12 @@ app.use(cors({
origin: '*'
}));
// No caching
app.use(async (ctx, next) => {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
await next();
});
app.use(bodyParser({
// リクエストが multipart/form-data でない限りはJSONだと見なす
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/github').routes());
router.use(require('./service/github-bot').routes());
router.use(require('./service/twitter').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) {
case 'api': this.onApiRequest(body); break;
case 'alive': this.onAlive(); break;
case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break;
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
private onReadNotification(payload: any) {
if (!payload.id) return;

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import { publishMainStream, publishDriveStream } from '../../stream';
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
import delFile from './delete-file';
import config from '../../config';
import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
import driveChart from '../../chart/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');
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 thumbnailExt = 'jpg';
let thumbnailType = 'image/jpeg';
@@ -53,10 +118,9 @@ async function save(path: string, name: string, type: string, hash: string, size
thumbnailExt = 'png';
thumbnailType = 'image/png';
}
// #endregion thumbnail
if (config.drive && config.drive.storage == 'minio') {
const minio = new Minio.Client(config.drive.config);
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
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 webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${webpublicExt}`;
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
|| `${ 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, {
withoutChunks: true,
storage: 'minio',
storageProps: {
key: key,
thumbnailKey: thumbnailKey
webpublicKey: webpublic ? webpublicKey : null,
thumbnailKey: thumbnail ? thumbnailKey : null,
},
url: `${ baseUrl }/${ key }`,
webpublicUrl: webpublic ? `${ baseUrl }/${ webpublicKey }` : null,
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null
});
} as IMetadata);
const file = await DriveFile.insert({
length: size,
@@ -105,29 +177,55 @@ async function save(path: string, name: string, type: string, hash: string, size
return file;
} else {
// Get MongoDB GridFS bucket
const bucket = await getDriveFileBucket();
// #region store original
const originalDst = await getDriveFileBucket();
const file = await new Promise<IDriveFile>((resolve, reject) => {
const writeStream = bucket.openUploadStream(name, {
// web用(Exif削除済み)がある場合はオリジナルにアクセス制限
if (webpublic) metadata.accessKey = uuid.v4();
const originalFile = await new Promise<IDriveFile>((resolve, reject) => {
const writeStream = originalDst.openUploadStream(name, {
contentType: type,
metadata
});
writeStream.once('finish', resolve);
writeStream.on('error', reject);
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) {
const thumbnailBucket = await getDriveFileThumbnailBucket();
await new Promise<IDriveFile>((resolve, reject) => {
const tuhmFile = await new Promise<IDriveFile>((resolve, reject) => {
const writeStream = thumbnailBucket.openUploadStream(name, {
contentType: thumbnailType,
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.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) {
const oldFile = await DriveFile.findOne({
_id: {

View File

@@ -4,6 +4,7 @@ import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-
import config from '../../config';
import driveChart from '../../chart/drive';
import perUserDriveChart from '../../chart/per-user-drive';
import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
export default async function(file: IDriveFile, isExpired = false) {
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`;
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
//#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);
perUserDriveChart.update(file, false);

View File

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

View File

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

View File

@@ -162,27 +162,87 @@ describe('Text', () => {
});
});
it('hashtag', () => {
const tokens1 = analyze('Strawberry Pasta #alice');
assert.deepEqual([
text('Strawberry Pasta '),
node('hashtag', { hashtag: 'alice' })
], tokens1);
describe('hashtag', () => {
it('simple', () => {
const tokens = analyze('#alice');
assert.deepEqual([
node('hashtag', { hashtag: 'alice' })
], tokens);
});
const tokens2 = analyze('Foo #bar, baz #piyo.');
assert.deepEqual([
text('Foo '),
node('hashtag', { hashtag: 'bar' }),
text(', baz '),
node('hashtag', { hashtag: 'piyo' }),
text('.'),
], tokens2);
it('after line break', () => {
const tokens = analyze('foo\n#alice');
assert.deepEqual([
text('foo\n'),
node('hashtag', { hashtag: 'alice' })
], tokens);
});
const tokens3 = analyze('#Foo!');
assert.deepEqual([
node('hashtag', { hashtag: 'Foo' }),
text('!'),
], tokens3);
it('with text', () => {
const tokens = analyze('Strawberry Pasta #alice');
assert.deepEqual([
text('Strawberry Pasta '),
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', () => {
@@ -360,6 +420,15 @@ describe('Text', () => {
], 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', () => {
const tokens = analyze('(https://example.com/foo(bar))');
assert.deepEqual([
@@ -370,13 +439,55 @@ describe('Text', () => {
});
});
it('link', () => {
const tokens = analyze('[foo](https://example.com)');
assert.deepEqual([
nodeWithChildren('link', [
text('foo')
], { url: 'https://example.com', silent: false })
], tokens);
describe('link', () => {
it('simple', () => {
const tokens = analyze('[foo](https://example.com)');
assert.deepEqual([
nodeWithChildren('link', [
text('foo')
], { 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', () => {
@@ -448,11 +559,27 @@ describe('Text', () => {
});
});
it('inline code', () => {
const tokens = analyze('`var x = "Strawberry Pasta";`');
assert.deepEqual([
node('inlineCode', { code: 'var x = "Strawberry Pasta";' })
], tokens);
describe('inline code', () => {
it('simple', () => {
const tokens = analyze('`var x = "Strawberry Pasta";`');
assert.deepEqual([
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', () => {
@@ -514,6 +641,17 @@ describe('Text', () => {
], tokens);
});
});
describe('center', () => {
it('simple', () => {
const tokens = analyze('<center>foo</center>');
assert.deepEqual([
nodeWithChildren('center', [
text('foo')
]),
], tokens);
});
});
});
describe('toHtml', () => {

View File

@@ -5,21 +5,22 @@
import * as fs from 'fs';
import * as webpack from 'webpack';
import chalk from 'chalk';
import rndstr from 'rndstr';
const { VueLoaderPlugin } = require('vue-loader');
const WebpackOnBuildPlugin = require('on-build-webpack');
//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const isProduction = process.env.NODE_ENV == 'production';
const constants = require('./src/const.json');
const locales = require('./locales');
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 isProduction = process.env.NODE_ENV == 'production';
const postcss = {
loader: 'postcss-loader',
options: {