Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24ef98eb01 | ||
|
|
7ed50b90bd | ||
|
|
b6fd5d7282 | ||
|
|
33243e7176 | ||
|
|
e8439679a5 | ||
|
|
06124dbbd5 | ||
|
|
857940f402 | ||
|
|
bcb04924ff | ||
|
|
0863e5d379 | ||
|
|
55dcd25df1 | ||
|
|
f3155ea180 | ||
|
|
2c5162671c | ||
|
|
fc8aeb5a66 | ||
|
|
995cf503eb | ||
|
|
0e49c11a4c | ||
|
|
0367c37b0a | ||
|
|
e0b9fe5e5d | ||
|
|
a4726e683b | ||
|
|
3b10e93efe | ||
|
|
02b07c1b5b | ||
|
|
5e54751bd4 | ||
|
|
93f13ffc8e | ||
|
|
60e10d4efa | ||
|
|
95ba7e43b1 | ||
|
|
9e5a2e5b17 | ||
|
|
dbbc416095 | ||
|
|
a479ad357c | ||
|
|
b1c12abb7c | ||
|
|
ba50156a83 | ||
|
|
eb83ab41c0 | ||
|
|
4e6a917dab | ||
|
|
8c4f0d4589 | ||
|
|
3f7738204e | ||
|
|
e251a9b9fe | ||
|
|
01d43b9683 | ||
|
|
4d4a0c89a8 | ||
|
|
0a5524e9c8 | ||
|
|
c8fb5746b3 | ||
|
|
bbcc132978 | ||
|
|
d3e4f84285 | ||
|
|
62c470cf75 | ||
|
|
8ab31d3765 | ||
|
|
55fe1cf0a8 | ||
|
|
00cff51ff7 | ||
|
|
d6bc4a7aa1 | ||
|
|
4e57d12aea | ||
|
|
4a2d99c43f | ||
|
|
217c27df86 | ||
|
|
e6dcd438b4 | ||
|
|
de2b0224d6 | ||
|
|
3f8a72eb88 | ||
|
|
0387176e8c | ||
|
|
aa34e332f4 | ||
|
|
d13999d689 | ||
|
|
22c4e92728 | ||
|
|
df8128c0b1 | ||
|
|
ec534a3704 | ||
|
|
366d4cd3e2 | ||
|
|
4841926df1 | ||
|
|
f2f7bdc5a9 | ||
|
|
fd811eb325 | ||
|
|
915d352505 | ||
|
|
1d1024c57a | ||
|
|
73df6e0347 | ||
|
|
e6d62c5a7b | ||
|
|
470e48c0a5 | ||
|
|
9235f72a2e | ||
|
|
9fe6da79b2 | ||
|
|
1858437eb1 | ||
|
|
c3ba0dcd32 | ||
|
|
70f4b13089 | ||
|
|
cc57a4b671 | ||
|
|
6902700458 | ||
|
|
b772041547 | ||
|
|
79174c1a19 | ||
|
|
898850027a | ||
|
|
0d272b1fb0 | ||
|
|
7993a9eb90 | ||
|
|
42d419970d | ||
|
|
ad49268d8b | ||
|
|
76c345396a | ||
|
|
5690ef1ebc | ||
|
|
5616404b4d | ||
|
|
f92137f6c2 | ||
|
|
ca3373ba4e | ||
|
|
4e6115b414 | ||
|
|
ddf47051c9 | ||
|
|
d45478510c | ||
|
|
2641f89349 | ||
|
|
9d46d03c37 | ||
|
|
25b6de88a9 | ||
|
|
a24046e46a | ||
|
|
7e803ff9a9 | ||
|
|
246cead2b1 | ||
|
|
214f7f06bb | ||
|
|
6878f73a9f | ||
|
|
336b45b6f7 | ||
|
|
2a0b62d26d | ||
|
|
653ec0cbb0 | ||
|
|
120ab3f0a3 | ||
|
|
8bcbbbc1a3 | ||
|
|
13a75abc91 | ||
|
|
eace740c63 | ||
|
|
cb3a54de00 | ||
|
|
5fbc77795d | ||
|
|
ce4feae731 | ||
|
|
08f00d4990 | ||
|
|
6951f7e74a | ||
|
|
2b4d63b1bb | ||
|
|
8cbb961493 | ||
|
|
64c795938d | ||
|
|
67df681a48 | ||
|
|
9285bcf8bb | ||
|
|
b9b23a4b54 | ||
|
|
2f6371b085 | ||
|
|
2a5c3475a7 | ||
|
|
8a2698a5db | ||
|
|
f6919a171a | ||
|
|
82ebf67456 | ||
|
|
a60c8b2ee8 | ||
|
|
0a2b8ccfb6 | ||
|
|
698094b787 | ||
|
|
b57e111ea8 | ||
|
|
aa6bf2b54e | ||
|
|
454910d295 | ||
|
|
d44e620769 | ||
|
|
ac14adfd3e | ||
|
|
928d30ee1e | ||
|
|
3dd9b0f347 | ||
|
|
c57dd083c5 | ||
|
|
2a1d6c5406 | ||
|
|
112e9f69bd | ||
|
|
d50e537888 | ||
|
|
86d14d30fa | ||
|
|
710d3689d3 | ||
|
|
3fee011369 | ||
|
|
7cd4b8ba4f | ||
|
|
31132de18b | ||
|
|
c6cb271f6f | ||
|
|
b7c4afd20c | ||
|
|
70395d200a | ||
|
|
562a5f66fc | ||
|
|
b2f8003602 | ||
|
|
b7b36973f7 | ||
|
|
f7d5f597f3 | ||
|
|
79c7712241 | ||
|
|
8f5f3985f4 | ||
|
|
af00464f5b | ||
|
|
f522b3df91 | ||
|
|
e9ec4a3b84 |
@@ -118,12 +118,3 @@ autoAdmin: true
|
|||||||
|
|
||||||
# Clustering
|
# Clustering
|
||||||
#clusterLimit: 1
|
#clusterLimit: 1
|
||||||
|
|
||||||
# Summaly proxy
|
|
||||||
#summalyProxy: "http://example.com"
|
|
||||||
|
|
||||||
# User recommendation
|
|
||||||
#user_recommendation:
|
|
||||||
# external: true
|
|
||||||
# engine: http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}
|
|
||||||
# timeout: 300000
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,3 +17,4 @@ api-docs.json
|
|||||||
/mongo
|
/mongo
|
||||||
/elasticsearch
|
/elasticsearch
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
|
yarn.lock
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -8,18 +8,20 @@ WORKDIR /misskey
|
|||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
|
||||||
|
RUN unlink /usr/bin/free
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
gcc \
|
|
||||||
g++ \
|
|
||||||
libc-dev \
|
|
||||||
python \
|
|
||||||
autoconf \
|
autoconf \
|
||||||
automake \
|
automake \
|
||||||
file \
|
file \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
libc-dev \
|
||||||
|
libtool \
|
||||||
make \
|
make \
|
||||||
nasm \
|
nasm \
|
||||||
pkgconfig \
|
pkgconfig \
|
||||||
libtool \
|
procps \
|
||||||
|
python \
|
||||||
zlib-dev
|
zlib-dev
|
||||||
RUN npm i -g node-gyp
|
RUN npm i -g node-gyp
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ Please see [Contribution guide](./CONTRIBUTING.md).
|
|||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=S1zP0QyLU52Dqq6dtc9qNYyWfW86XrYHiR4NMbeOrnA%3D" alt="dansup"></td>
|
||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D" alt="Gargron"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D" alt="Gargron"></td>
|
||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=VZUtwrjQa8Jml4twCjHYQQZ64wHEY4oIlGl7Kc-VYUQ%3D" alt="Nokotaro Takeda"></td>
|
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=VZUtwrjQa8Jml4twCjHYQQZ64wHEY4oIlGl7Kc-VYUQ%3D" alt="Nokotaro Takeda"></td>
|
||||||
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
|
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td>
|
<td><a href="https://www.patreon.com/user?u=13039004">nemu</a></td>
|
||||||
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
|
<td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td>
|
||||||
@@ -106,13 +105,14 @@ Please see [Contribution guide](./CONTRIBUTING.md).
|
|||||||
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
<td><a href="https://www.patreon.com/dansup">dansup</a></td>
|
||||||
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
|
<td><a href="https://www.patreon.com/mastodon">Gargron</a></td>
|
||||||
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
|
<td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td>
|
||||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
|
||||||
</tr></table>
|
</tr></table>
|
||||||
<table><tr>
|
<table><tr>
|
||||||
|
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=tMosUojzUYJCH_3t--tvYA-SMCyrS__hzSndyaRSnbo%3D" alt="Takashi Shibuya"></td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
|
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
|
|
||||||
**Last updated:** Wed, 31 Oct 2018 23:21:06 UTC
|
**Last updated:** Fri, 23 Nov 2018 14:09:04 UTC
|
||||||
<!-- PATREON_END -->
|
<!-- PATREON_END -->
|
||||||
|
|
||||||
:four_leaf_clover: Copyright
|
:four_leaf_clover: Copyright
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -8,28 +8,11 @@ coming soon
|
|||||||
node cli/mark-admin (User-ID or Username)
|
node cli/mark-admin (User-ID or Username)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mark as 'verified' user
|
|
||||||
``` shell
|
|
||||||
node cli/mark-verified (User-ID or Username)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Suspend users
|
|
||||||
``` shell
|
|
||||||
node cli/suspend (User-ID or Username)
|
|
||||||
```
|
|
||||||
e.g.
|
e.g.
|
||||||
``` shell
|
``` shell
|
||||||
# Use id
|
# By id
|
||||||
node cli/suspend 57d01a501fdf2d07be417afe
|
node cli/mark-admin 57d01a501fdf2d07be417afe
|
||||||
|
|
||||||
# Use username
|
# By username
|
||||||
node cli/suspend @syuilo
|
node cli/suspend @syuilo
|
||||||
|
|
||||||
# Use username (remote)
|
|
||||||
node cli/suspend @syuilo@misskey.xyz
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reset password
|
|
||||||
``` shell
|
|
||||||
node cli/reset-password (User-ID or Username)
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,28 +8,11 @@ coming soon
|
|||||||
node cli/mark-admin (ユーザーID または ユーザー名)
|
node cli/mark-admin (ユーザーID または ユーザー名)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 'verified'ユーザーを設定する
|
|
||||||
``` shell
|
|
||||||
node cli/mark-verified (ユーザーID または ユーザー名)
|
|
||||||
```
|
|
||||||
|
|
||||||
## ユーザーを凍結する
|
|
||||||
``` shell
|
|
||||||
node cli/suspend (ユーザーID または ユーザー名)
|
|
||||||
```
|
|
||||||
例:
|
例:
|
||||||
``` shell
|
``` shell
|
||||||
# ユーザーID
|
# ユーザーID
|
||||||
node cli/suspend 57d01a501fdf2d07be417afe
|
node cli/mark-admin 57d01a501fdf2d07be417afe
|
||||||
|
|
||||||
# ユーザー名
|
# ユーザー名
|
||||||
node cli/suspend @syuilo
|
node cli/mark-admin @syuilo
|
||||||
|
|
||||||
# ユーザー名 (リモート)
|
|
||||||
node cli/suspend @syuilo@misskey.xyz
|
|
||||||
```
|
|
||||||
|
|
||||||
## ユーザーのパスワードをリセットする
|
|
||||||
``` shell
|
|
||||||
node cli/reset-password (ユーザーID または ユーザー名)
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ common:
|
|||||||
reduce-motion: "Reduce motion in UI"
|
reduce-motion: "Reduce motion in UI"
|
||||||
this-setting-is-this-device-only: "Only for this device"
|
this-setting-is-this-device-only: "Only for this device"
|
||||||
use-os-default-emojis: "Use the OS default Emojis"
|
use-os-default-emojis: "Use the OS default Emojis"
|
||||||
do-not-use-in-production: 'As this is for development, do not use this in production.'
|
do-not-use-in-production: 'This is a development build. Do not use in production.'
|
||||||
is-remote-user: "This user information is copied."
|
is-remote-user: "This user information is copied."
|
||||||
is-remote-post: "This post information is a copy."
|
is-remote-post: "This post information is a copy."
|
||||||
view-on-remote: "View it on remote"
|
view-on-remote: "View it on remote"
|
||||||
@@ -366,8 +366,8 @@ common/views/components/signin.vue:
|
|||||||
signin: "Sign in"
|
signin: "Sign in"
|
||||||
or: "Or"
|
or: "Or"
|
||||||
signin-with-twitter: "Log in with Twitter"
|
signin-with-twitter: "Log in with Twitter"
|
||||||
signin-with-github: "Log in with GitHub"
|
signin-with-github: "Sign in with GitHub"
|
||||||
signin-with-discord: "Login with Discord"
|
signin-with-discord: "Sign in with Discord"
|
||||||
login-failed: "Log in failed. Make sure you have entered your correct username and password."
|
login-failed: "Log in failed. Make sure you have entered your correct username and password."
|
||||||
common/views/components/signup.vue:
|
common/views/components/signup.vue:
|
||||||
invitation-code: "Invitation code"
|
invitation-code: "Invitation code"
|
||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "Invite"
|
invite: "Invite"
|
||||||
save: "Save"
|
save: "Save"
|
||||||
saved: "Saved"
|
saved: "Saved"
|
||||||
|
user-recommendation-config: "Recommended users"
|
||||||
|
enable-external-user-recommendation: "Enable to external user recommendation"
|
||||||
|
external-user-recommendation-engine: "Engine"
|
||||||
|
external-user-recommendation-engine-desc: "Example : https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "Timeout"
|
||||||
|
external-user-recommendation-timeout-desc: "Number of milliseconds (ex. 300,000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "Chart"
|
title: "Chart"
|
||||||
per-day: "per Day"
|
per-day: "per Day"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "Response time"
|
network-time: "Response time"
|
||||||
network-usage: "Traffic"
|
network-usage: "Traffic"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "Suspend a user"
|
operation: "Operations"
|
||||||
|
username-or-userid: "Username or user ID"
|
||||||
|
user-not-found: "User not found"
|
||||||
|
lookup: "Look up"
|
||||||
|
reset-password: "Reset password"
|
||||||
|
password-updated: "The password is now \"{password}\""
|
||||||
suspend: "Suspend"
|
suspend: "Suspend"
|
||||||
suspended: "Successfully suspended."
|
suspended: "Successfully suspended."
|
||||||
unsuspend-user: "Unsuspend users"
|
|
||||||
unsuspend: "Unsuspend"
|
unsuspend: "Unsuspend"
|
||||||
unsuspended: "The user has successfully unsuspended."
|
unsuspended: "The user has successfully unsuspended."
|
||||||
verify-user: "User account verification settings"
|
|
||||||
verify: "Verify account"
|
verify: "Verify account"
|
||||||
verified: "The account is now being verified"
|
verified: "The account is now being verified"
|
||||||
unverify-user: "User account unverification settings"
|
|
||||||
unverify: "Unverify account"
|
unverify: "Unverify account"
|
||||||
unverified: "The account is now being unverified"
|
unverified: "The account is now being unverified"
|
||||||
|
users:
|
||||||
|
title: "Users"
|
||||||
|
sort:
|
||||||
|
title: "Sort"
|
||||||
|
createdAtAsc: "Date Registered (Ascending)"
|
||||||
|
createdAtDesc: "Date Registered (Descending)"
|
||||||
|
updatedAtAsc: "Last Updated (Ascending)"
|
||||||
|
updatedAtDesc: "Last Updated (Descending)"
|
||||||
|
origin:
|
||||||
|
title: "Origin"
|
||||||
|
combined: "Local + Remote"
|
||||||
|
local: "Local"
|
||||||
|
remote: "Remote"
|
||||||
|
createdAt: "Created at"
|
||||||
|
updatedAt: "Updated at"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "Register Moderator"
|
title: "Register Moderator"
|
||||||
@@ -1051,7 +1074,7 @@ admin/views/emoji.vue:
|
|||||||
remove: "Remove"
|
remove: "Remove"
|
||||||
updated: "Updated"
|
updated: "Updated"
|
||||||
remove-emoji:
|
remove-emoji:
|
||||||
are-you-sure: "Delete \"%1$s\"?"
|
are-you-sure: "Delete \"$1\"?"
|
||||||
removed: "Deleted"
|
removed: "Deleted"
|
||||||
admin/views/announcements.vue:
|
admin/views/announcements.vue:
|
||||||
announcements: "Announcements"
|
announcements: "Announcements"
|
||||||
@@ -1062,7 +1085,7 @@ admin/views/announcements.vue:
|
|||||||
text: "Content"
|
text: "Content"
|
||||||
saved: "Saved"
|
saved: "Saved"
|
||||||
_remove:
|
_remove:
|
||||||
are-you-sure: "Delete \"%1$s\"?"
|
are-you-sure: "Delete \"$1\"?"
|
||||||
removed: "Deleted"
|
removed: "Deleted"
|
||||||
admin/views/hashtags.vue:
|
admin/views/hashtags.vue:
|
||||||
hided-tags: "Hidden Tags"
|
hided-tags: "Hidden Tags"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ common:
|
|||||||
is-remote-user: "Ces informations appartiennent à un·e utilisateur·rice distant·e."
|
is-remote-user: "Ces informations appartiennent à un·e utilisateur·rice distant·e."
|
||||||
is-remote-post: "Ceci est une publication distante."
|
is-remote-post: "Ceci est une publication distante."
|
||||||
view-on-remote: " Consulter le profil complet"
|
view-on-remote: " Consulter le profil complet"
|
||||||
renoted-by: "{user}がRenote"
|
renoted-by: "Renoté par {user}"
|
||||||
error:
|
error:
|
||||||
title: 'Une erreur est survenue'
|
title: 'Une erreur est survenue'
|
||||||
retry: 'Réessayer'
|
retry: 'Réessayer'
|
||||||
@@ -432,7 +432,7 @@ common/views/components/visibility-chooser.vue:
|
|||||||
specified-desc: "Publier uniquement aux utilisateurs·rices mentionné·e·s"
|
specified-desc: "Publier uniquement aux utilisateurs·rices mentionné·e·s"
|
||||||
private: "Privé"
|
private: "Privé"
|
||||||
local-public: "Local (Public)"
|
local-public: "Local (Public)"
|
||||||
local-public-desc: "リモートへは公開しない"
|
local-public-desc: "Ne pas publier pour les distants"
|
||||||
local-home: "Accueil (local uniquement)"
|
local-home: "Accueil (local uniquement)"
|
||||||
local-followers: "Local (Abonnés)"
|
local-followers: "Local (Abonnés)"
|
||||||
common/views/components/trends.vue:
|
common/views/components/trends.vue:
|
||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "Inviter"
|
invite: "Inviter"
|
||||||
save: "Sauvegarder"
|
save: "Sauvegarder"
|
||||||
saved: "Enregistré"
|
saved: "Enregistré"
|
||||||
|
user-recommendation-config: "Utilisateur·rice·s"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "Moteur"
|
||||||
|
external-user-recommendation-engine-desc: "Exemple : https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "Délai d’expiration"
|
||||||
|
external-user-recommendation-timeout-desc: "En millisecondes (par exemple : 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "Graph"
|
title: "Graph"
|
||||||
per-day: "par jour"
|
per-day: "par jour"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "Temps de réponse"
|
network-time: "Temps de réponse"
|
||||||
network-usage: "Traffic"
|
network-usage: "Traffic"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "Suspendre un·e utilisateur·rice"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "Suspendre"
|
suspend: "Suspendre"
|
||||||
suspended: "Suspendu·e avec succès."
|
suspended: "Suspendu·e avec succès."
|
||||||
unsuspend-user: "Lever la suspension d’utilisateur·rice·s"
|
|
||||||
unsuspend: "Suspension levée"
|
unsuspend: "Suspension levée"
|
||||||
unsuspended: "La suspension de l’utilisateur·rice a été levée avec succès"
|
unsuspended: "La suspension de l’utilisateur·rice a été levée avec succès"
|
||||||
verify-user: "Paramètres de vérification du compte utilisateur"
|
|
||||||
verify: "Vérification du compte"
|
verify: "Vérification du compte"
|
||||||
verified: "Le compte a été vérifié"
|
verified: "Le compte a été vérifié"
|
||||||
unverify-user: "Paramètres de non-vérification du compte utilisateur"
|
|
||||||
unverify: "Ôter la vérification du compte"
|
unverify: "Ôter la vérification du compte"
|
||||||
unverified: "Ce compte n'est plus vérifié"
|
unverified: "Ce compte n'est plus vérifié"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "Ajout d’un modérateur"
|
title: "Ajout d’un modérateur"
|
||||||
@@ -1265,7 +1288,7 @@ mobile/views/components/ui.nav.vue:
|
|||||||
admin: "Admin"
|
admin: "Admin"
|
||||||
about: "À propos de Misskey"
|
about: "À propos de Misskey"
|
||||||
mobile/views/components/user-timeline.vue:
|
mobile/views/components/user-timeline.vue:
|
||||||
no-notes: "Cette utilisateur semble n'avoir rien poster pour le moment"
|
no-notes: "Il semble que cet·te utilisateur·rice n’a rien publié pour le moment."
|
||||||
no-notes-with-media: "Aucune notes avec des médias"
|
no-notes-with-media: "Aucune notes avec des médias"
|
||||||
mobile/views/components/users-list.vue:
|
mobile/views/components/users-list.vue:
|
||||||
all: "Tout"
|
all: "Tout"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -1092,17 +1092,17 @@ admin/views/instance.vue:
|
|||||||
recaptcha-site-key: "reCAPTCHA site key"
|
recaptcha-site-key: "reCAPTCHA site key"
|
||||||
recaptcha-secret-key: "reCAPTCHA secret key"
|
recaptcha-secret-key: "reCAPTCHA secret key"
|
||||||
twitter-integration-config: "Twitter連携の設定"
|
twitter-integration-config: "Twitter連携の設定"
|
||||||
twitter-integration-info: "コールバックURLは /api/tw/cb に設定します。"
|
twitter-integration-info: "コールバックURLは {url} に設定します。"
|
||||||
enable-twitter-integration: "Twitter連携を有効にする"
|
enable-twitter-integration: "Twitter連携を有効にする"
|
||||||
twitter-integration-consumer-key: "Consumer key"
|
twitter-integration-consumer-key: "Consumer key"
|
||||||
twitter-integration-consumer-secret: "Consumer secret"
|
twitter-integration-consumer-secret: "Consumer secret"
|
||||||
github-integration-config: "GitHub連携の設定"
|
github-integration-config: "GitHub連携の設定"
|
||||||
github-integration-info: "コールバックURLは /api/gh/cb に設定します。"
|
github-integration-info: "コールバックURLは {url} に設定します。"
|
||||||
enable-github-integration: "GitHub連携を有効にする"
|
enable-github-integration: "GitHub連携を有効にする"
|
||||||
github-integration-client-id: "Client ID"
|
github-integration-client-id: "Client ID"
|
||||||
github-integration-client-secret: "Client Secret"
|
github-integration-client-secret: "Client Secret"
|
||||||
discord-integration-config: "Discord連携の設定"
|
discord-integration-config: "Discord連携の設定"
|
||||||
discord-integration-info: "コールバックURLは /api/dc/cb に設定します。"
|
discord-integration-info: "コールバックURLは {url} に設定します。"
|
||||||
enable-discord-integration: "Discord連携を有効にする"
|
enable-discord-integration: "Discord連携を有効にする"
|
||||||
discord-integration-client-id: "Client ID"
|
discord-integration-client-id: "Client ID"
|
||||||
discord-integration-client-secret: "Client Secret"
|
discord-integration-client-secret: "Client Secret"
|
||||||
@@ -1117,6 +1117,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
|
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
@@ -1145,18 +1151,35 @@ admin/views/charts.vue:
|
|||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
|
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
|
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ meta:
|
|||||||
common:
|
common:
|
||||||
misskey: "연합우주의 ⭐"
|
misskey: "연합우주의 ⭐"
|
||||||
about-title: "연합우주의 ⭐."
|
about-title: "연합우주의 ⭐."
|
||||||
about: "Misskey를 찾아 주셔서 감사합니다. Misskey은 지구에서 태어난 <b>분산 마이크로 블로그 SNS </b> 입니다. Fediverse (다양한 SNS로 구성되는 우주)에 존재하는 다른 SNS와 상호 연결되어 있습니다. 잠시 도시의 번잡함에서 벗어나 새로운 인터넷에 다이브 해 보지 않겠습니까."
|
about: "Misskey를 찾아주셔서 감사합니다. Misskey는 지구에서 태어난 <b>분산 마이크로 블로그 SNS </b> 입니다. Fediverse(다양한 SNS로 구성되는 우주)에 존재하는 다른 SNS와 상호 연결되어 있습니다. 잠시 도시의 번잡함에서 벗어나 새로운 인터넷에 다이브 해 보지 않겠습니까."
|
||||||
intro:
|
intro:
|
||||||
title: "Misskey란?"
|
title: "Misskey란?"
|
||||||
about: "Misskeyはオープンソースの<b>分散型マイクロブログSNS</b>です。リッチで高度にカスタマイズできるUI、投稿へのリアクション、ファイルを一元管理できるドライブなど、先進的な機能を揃えています。また、Fediverseと呼ばれるネットワークに接続できるため、他のSNSともやり取りできます。例えば、あなたが何か投稿すると、その投稿はMisskeyだけでなく他のSNSにも伝わります。ちょうどある惑星から他の惑星に電波を発信している様子をイメージしてください。"
|
about: "Misskey는 오픈소스 <b>분산형 마이크로블로그 SNS</b>입니다. 다양하고 폭넓게 커스터마이징할 수 있는 UI, 게시물에 대한 반응, 파일을 관리할 수 있는 드라이브 등의 선진적인 기능을 갖추고 있습니다. 더하여 Fediverse라고 부르는 네트워크에 연결할 수 있어 다른 SNS와도 주고받을 수 있습니다. 예를 들자면, 당신이 무언가를 게시하면, 해당 게시물은 Misskey 뿐만 아니라 다른 SNS에도 전해집니다. 살짝 어떤 행성에서 다른 행성으로 전파를 발신하고 있는 모습을 상상해주세요."
|
||||||
features: "특징"
|
features: "특징"
|
||||||
rich-contents: "게시"
|
rich-contents: "글 쓰기"
|
||||||
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
|
rich-contents-desc: "自分の考え、話題の出来事、皆と共有したいことについて発信してください。必要であれば、様々な構文を使って投稿を装飾したり、好きな画像、動画などのファイルやアンケートを添付することもできます。"
|
||||||
reaction: "반응"
|
reaction: "반응"
|
||||||
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
reaction-desc: "あなたの気持ちを伝える最も簡単な方法です。Misskeyは、他のユーザーの投稿に様々なリアクションを付けることができます。いちどMisskeyのリアクション機能を体験してしまうと、もう「いいね」の概念しか存在しないSNSには戻れなくなるかもしれません。"
|
||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "招待"
|
invite: "招待"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存しました"
|
saved: "保存しました"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "チャート"
|
title: "チャート"
|
||||||
per-day: "1日ごと"
|
per-day: "1日ごと"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "応答時間"
|
network-time: "応答時間"
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend-user: "ユーザーの凍結の解除"
|
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify-user: "ユーザーの公式アカウント解除"
|
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "モデレーターの登録"
|
title: "モデレーターの登録"
|
||||||
|
|||||||
@@ -991,6 +991,12 @@ admin/views/instance.vue:
|
|||||||
invite: "邀请"
|
invite: "邀请"
|
||||||
save: "保存"
|
save: "保存"
|
||||||
saved: "保存完毕"
|
saved: "保存完毕"
|
||||||
|
user-recommendation-config: "おすすめユーザー"
|
||||||
|
enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする"
|
||||||
|
external-user-recommendation-engine: "エンジン"
|
||||||
|
external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}"
|
||||||
|
external-user-recommendation-timeout: "タイムアウト"
|
||||||
|
external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)"
|
||||||
admin/views/charts.vue:
|
admin/views/charts.vue:
|
||||||
title: "历史记录"
|
title: "历史记录"
|
||||||
per-day: "每天"
|
per-day: "每天"
|
||||||
@@ -1017,18 +1023,35 @@ admin/views/charts.vue:
|
|||||||
network-time: "响应时间"
|
network-time: "响应时间"
|
||||||
network-usage: "网络流量"
|
network-usage: "网络流量"
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "冻结用户"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "被冻结"
|
suspend: "被冻结"
|
||||||
suspended: "成功冻结用户"
|
suspended: "成功冻结用户"
|
||||||
unsuspend-user: "解除用户冻结"
|
|
||||||
unsuspend: "已解除冻结"
|
unsuspend: "已解除冻结"
|
||||||
unsuspended: "已成功解除用户冻结"
|
unsuspended: "已成功解除用户冻结"
|
||||||
verify-user: "用户账户认证设置"
|
|
||||||
verify: "认证用户"
|
verify: "认证用户"
|
||||||
verified: "此账户已被认证"
|
verified: "此账户已被认证"
|
||||||
unverify-user: "用户账号解除认证设置"
|
|
||||||
unverify: "解除账户认证"
|
unverify: "解除账户认证"
|
||||||
unverified: "该帐户未经认证"
|
unverified: "该帐户未经认证"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "更新日時が古い順"
|
||||||
|
updatedAtDesc: "更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
title: "注册版主"
|
title: "注册版主"
|
||||||
|
|||||||
30
package.json
30
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "10.56.1",
|
"version": "10.58.1",
|
||||||
"clientVersion": "2.0.11960",
|
"clientVersion": "2.0.12110",
|
||||||
"codename": "nighthike",
|
"codename": "nighthike",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -47,14 +47,14 @@
|
|||||||
"@types/is-url": "1.2.28",
|
"@types/is-url": "1.2.28",
|
||||||
"@types/js-yaml": "3.11.2",
|
"@types/js-yaml": "3.11.2",
|
||||||
"@types/katex": "0.5.0",
|
"@types/katex": "0.5.0",
|
||||||
"@types/koa": "2.0.46",
|
"@types/koa": "2.0.47",
|
||||||
"@types/koa-bodyparser": "5.0.1",
|
"@types/koa-bodyparser": "5.0.1",
|
||||||
"@types/koa-compress": "2.0.8",
|
"@types/koa-compress": "2.0.8",
|
||||||
"@types/koa-favicon": "2.0.19",
|
"@types/koa-favicon": "2.0.19",
|
||||||
"@types/koa-logger": "3.1.1",
|
"@types/koa-logger": "3.1.1",
|
||||||
"@types/koa-mount": "3.0.1",
|
"@types/koa-mount": "3.0.1",
|
||||||
"@types/koa-multer": "1.0.0",
|
"@types/koa-multer": "1.0.0",
|
||||||
"@types/koa-router": "7.0.33",
|
"@types/koa-router": "7.0.35",
|
||||||
"@types/koa-send": "4.1.1",
|
"@types/koa-send": "4.1.1",
|
||||||
"@types/koa-views": "2.0.3",
|
"@types/koa-views": "2.0.3",
|
||||||
"@types/koa__cors": "2.2.3",
|
"@types/koa__cors": "2.2.3",
|
||||||
@@ -63,14 +63,14 @@
|
|||||||
"@types/mocha": "5.2.5",
|
"@types/mocha": "5.2.5",
|
||||||
"@types/mongodb": "3.1.14",
|
"@types/mongodb": "3.1.14",
|
||||||
"@types/ms": "0.7.30",
|
"@types/ms": "0.7.30",
|
||||||
"@types/node": "10.12.2",
|
"@types/node": "10.12.10",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
"@types/parsimmon": "1.10.0",
|
"@types/parsimmon": "1.10.0",
|
||||||
"@types/portscanner": "2.1.0",
|
"@types/portscanner": "2.1.0",
|
||||||
"@types/pug": "2.0.4",
|
"@types/pug": "2.0.4",
|
||||||
"@types/qrcode": "1.3.0",
|
"@types/qrcode": "1.3.0",
|
||||||
"@types/ratelimiter": "2.1.28",
|
"@types/ratelimiter": "2.1.28",
|
||||||
"@types/redis": "2.8.7",
|
"@types/redis": "2.8.8",
|
||||||
"@types/request": "2.48.1",
|
"@types/request": "2.48.1",
|
||||||
"@types/request-promise-native": "1.0.15",
|
"@types/request-promise-native": "1.0.15",
|
||||||
"@types/rimraf": "2.0.2",
|
"@types/rimraf": "2.0.2",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"@types/sharp": "0.21.0",
|
"@types/sharp": "0.21.0",
|
||||||
"@types/showdown": "1.7.5",
|
"@types/showdown": "1.7.5",
|
||||||
"@types/speakeasy": "2.0.3",
|
"@types/speakeasy": "2.0.3",
|
||||||
"@types/systeminformation": "3.23.0",
|
"@types/systeminformation": "3.23.1",
|
||||||
"@types/tinycolor2": "1.4.1",
|
"@types/tinycolor2": "1.4.1",
|
||||||
"@types/tmp": "0.0.33",
|
"@types/tmp": "0.0.33",
|
||||||
"@types/uuid": "3.4.4",
|
"@types/uuid": "3.4.4",
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
"@types/websocket": "0.0.40",
|
"@types/websocket": "0.0.40",
|
||||||
"@types/ws": "6.0.1",
|
"@types/ws": "6.0.1",
|
||||||
"animejs": "2.2.0",
|
"animejs": "2.2.0",
|
||||||
"apexcharts": "2.2.2",
|
"apexcharts": "2.2.3",
|
||||||
"autobind-decorator": "2.2.1",
|
"autobind-decorator": "2.2.1",
|
||||||
"autosize": "4.0.2",
|
"autosize": "4.0.2",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"diskusage": "0.2.5",
|
"diskusage": "0.2.5",
|
||||||
"double-ended-queue": "2.1.0-0",
|
"double-ended-queue": "2.1.0-0",
|
||||||
"elasticsearch": "15.2.0",
|
"elasticsearch": "15.2.0",
|
||||||
"emojilib": "2.3.0",
|
"emojilib": "2.4.0",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"eslint": "5.8.0",
|
"eslint": "5.8.0",
|
||||||
"eslint-plugin-vue": "4.7.1",
|
"eslint-plugin-vue": "4.7.1",
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
"json5": "2.1.0",
|
"json5": "2.1.0",
|
||||||
"json5-loader": "1.0.1",
|
"json5-loader": "1.0.1",
|
||||||
"katex": "0.10.0",
|
"katex": "0.10.0",
|
||||||
"koa": "2.6.1",
|
"koa": "2.6.2",
|
||||||
"koa-bodyparser": "4.2.1",
|
"koa-bodyparser": "4.2.1",
|
||||||
"koa-compress": "3.0.0",
|
"koa-compress": "3.0.0",
|
||||||
"koa-favicon": "2.0.1",
|
"koa-favicon": "2.0.1",
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
"stylus": "0.54.5",
|
"stylus": "0.54.5",
|
||||||
"stylus-loader": "3.0.2",
|
"stylus-loader": "3.0.2",
|
||||||
"summaly": "2.2.0",
|
"summaly": "2.2.0",
|
||||||
"systeminformation": "3.47.0",
|
"systeminformation": "3.49.3",
|
||||||
"syuilo-password-strength": "0.0.1",
|
"syuilo-password-strength": "0.0.1",
|
||||||
"terser-webpack-plugin": "1.1.0",
|
"terser-webpack-plugin": "1.1.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
@@ -220,11 +220,11 @@
|
|||||||
"vue-color": "2.7.0",
|
"vue-color": "2.7.0",
|
||||||
"vue-content-loading": "1.5.3",
|
"vue-content-loading": "1.5.3",
|
||||||
"vue-cropperjs": "2.2.2",
|
"vue-cropperjs": "2.2.2",
|
||||||
"vue-i18n": "8.3.1",
|
"vue-i18n": "8.3.2",
|
||||||
"vue-js-modal": "1.3.26",
|
"vue-js-modal": "1.3.26",
|
||||||
"vue-loader": "15.4.2",
|
"vue-loader": "15.4.2",
|
||||||
"vue-marquee-text-component": "1.1.0",
|
"vue-marquee-text-component": "1.1.0",
|
||||||
"vue-router": "3.0.1",
|
"vue-router": "3.0.2",
|
||||||
"vue-style-loader": "4.1.2",
|
"vue-style-loader": "4.1.2",
|
||||||
"vue-svg-inline-loader": "1.2.2",
|
"vue-svg-inline-loader": "1.2.2",
|
||||||
"vue-template-compiler": "2.5.17",
|
"vue-template-compiler": "2.5.17",
|
||||||
@@ -234,10 +234,10 @@
|
|||||||
"vuex-persistedstate": "2.5.4",
|
"vuex-persistedstate": "2.5.4",
|
||||||
"web-push": "3.3.3",
|
"web-push": "3.3.3",
|
||||||
"webfinger.js": "2.6.6",
|
"webfinger.js": "2.6.6",
|
||||||
"webpack": "4.25.1",
|
"webpack": "4.26.0",
|
||||||
"webpack-cli": "3.1.2",
|
"webpack-cli": "3.1.2",
|
||||||
"websocket": "1.0.28",
|
"websocket": "1.0.28",
|
||||||
"ws": "6.1.0",
|
"ws": "6.1.2",
|
||||||
"xev": "2.0.1"
|
"xev": "2.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<ui-textarea v-model="announcement.text">
|
<ui-textarea v-model="announcement.text">
|
||||||
<span>{{ $t('text') }}</span>
|
<span>{{ $t('text') }}</span>
|
||||||
</ui-textarea>
|
</ui-textarea>
|
||||||
<ui-horizon-group>
|
<ui-horizon-group class="fit-bottom">
|
||||||
<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
|
<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
|
||||||
<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
|
<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<i slot="icon"><fa icon="link"/></i>
|
<i slot="icon"><fa icon="link"/></i>
|
||||||
<span>{{ $t('add-emoji.url') }}</span>
|
<span>{{ $t('add-emoji.url') }}</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
<ui-horizon-group>
|
<ui-horizon-group class="fit-bottom">
|
||||||
<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
|
<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
|
||||||
<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
|
<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
|||||||
@@ -42,6 +42,16 @@
|
|||||||
<section>
|
<section>
|
||||||
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
|
<ui-switch v-model="disableLocalTimeline">{{ $t('disable-local-timeline') }}</ui-switch>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<header>summaly Proxy</header>
|
||||||
|
<ui-input v-model="summalyProxy">URL</ui-input>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header>
|
||||||
|
<ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch>
|
||||||
|
<ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<span slot="desc">{{ $t('external-user-recommendation-engine-desc') }}</span></ui-input>
|
||||||
|
<ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<span slot="suffix">ms</span><span slot="desc">{{ $t('external-user-recommendation-timeout-desc') }}</span></ui-input>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
||||||
</section>
|
</section>
|
||||||
@@ -59,7 +69,7 @@
|
|||||||
<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</div>
|
<div slot="title"><fa :icon="['fab', 'twitter']"/> {{ $t('twitter-integration-config') }}</div>
|
||||||
<section>
|
<section>
|
||||||
<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
|
<ui-switch v-model="enableTwitterIntegration">{{ $t('enable-twitter-integration') }}</ui-switch>
|
||||||
<ui-info>{{ $t('twitter-integration-info') }}</ui-info>
|
<ui-info>{{ $t('twitter-integration-info', { url: `${url}/api/tw/cb` }) }}</ui-info>
|
||||||
<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-key') }}</ui-input>
|
<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-key') }}</ui-input>
|
||||||
<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
|
<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('twitter-integration-consumer-secret') }}</ui-input>
|
||||||
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
||||||
@@ -70,7 +80,7 @@
|
|||||||
<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</div>
|
<div slot="title"><fa :icon="['fab', 'github']"/> {{ $t('github-integration-config') }}</div>
|
||||||
<section>
|
<section>
|
||||||
<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
|
<ui-switch v-model="enableGithubIntegration">{{ $t('enable-github-integration') }}</ui-switch>
|
||||||
<ui-info>{{ $t('github-integration-info') }}</ui-info>
|
<ui-info>{{ $t('github-integration-info', { url: `${url}/api/gh/cb` }) }}</ui-info>
|
||||||
<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-id') }}</ui-input>
|
<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-id') }}</ui-input>
|
||||||
<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-secret') }}</ui-input>
|
<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('github-integration-client-secret') }}</ui-input>
|
||||||
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
||||||
@@ -81,7 +91,7 @@
|
|||||||
<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</div>
|
<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</div>
|
||||||
<section>
|
<section>
|
||||||
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
|
<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch>
|
||||||
<ui-info>{{ $t('discord-integration-info') }}</ui-info>
|
<ui-info>{{ $t('discord-integration-info', { url: `${url}/api/dc/cb` }) }}</ui-info>
|
||||||
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-id') }}</ui-input>
|
<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-id') }}</ui-input>
|
||||||
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-secret') }}</ui-input>
|
<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-secret') }}</ui-input>
|
||||||
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
<ui-button @click="updateMeta">{{ $t('save') }}</ui-button>
|
||||||
@@ -93,15 +103,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import { host } from '../../config';
|
import { url, host } from '../../config';
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import { faHeadset, faShieldAlt, faGhost } from '@fortawesome/free-solid-svg-icons';
|
import { faHeadset, faShieldAlt, faGhost, faUserPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('admin/views/instance.vue'),
|
i18n: i18n('admin/views/instance.vue'),
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
url,
|
||||||
host: toUnicode(host),
|
host: toUnicode(host),
|
||||||
maintainerName: null,
|
maintainerName: null,
|
||||||
maintainerEmail: null,
|
maintainerEmail: null,
|
||||||
@@ -129,7 +140,11 @@ export default Vue.extend({
|
|||||||
discordClientSecret: null,
|
discordClientSecret: null,
|
||||||
proxyAccount: null,
|
proxyAccount: null,
|
||||||
inviteCode: null,
|
inviteCode: null,
|
||||||
faHeadset, faShieldAlt, faGhost
|
enableExternalUserRecommendation: false,
|
||||||
|
externalUserRecommendationEngine: null,
|
||||||
|
externalUserRecommendationTimeout: null,
|
||||||
|
summalyProxy: null,
|
||||||
|
faHeadset, faShieldAlt, faGhost, faUserPlus
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -158,6 +173,10 @@ export default Vue.extend({
|
|||||||
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||||
this.discordClientId = meta.discordClientId;
|
this.discordClientId = meta.discordClientId;
|
||||||
this.discordClientSecret = meta.discordClientSecret;
|
this.discordClientSecret = meta.discordClientSecret;
|
||||||
|
this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation;
|
||||||
|
this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine;
|
||||||
|
this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout;
|
||||||
|
this.summalyProxy = meta.summalyProxy;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -199,7 +218,11 @@ export default Vue.extend({
|
|||||||
githubClientSecret: this.githubClientSecret,
|
githubClientSecret: this.githubClientSecret,
|
||||||
enableDiscordIntegration: this.enableDiscordIntegration,
|
enableDiscordIntegration: this.enableDiscordIntegration,
|
||||||
discordClientId: this.discordClientId,
|
discordClientId: this.discordClientId,
|
||||||
discordClientSecret: this.discordClientSecret
|
discordClientSecret: this.discordClientSecret,
|
||||||
|
enableExternalUserRecommendation: this.enableExternalUserRecommendation,
|
||||||
|
externalUserRecommendationEngine: this.externalUserRecommendationEngine,
|
||||||
|
externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10),
|
||||||
|
summalyProxy: this.summalyProxy
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|||||||
@@ -1,42 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
|
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">{{ $t('verify-user') }}</div>
|
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
<ui-input v-model="verifyUsername" type="text">
|
<ui-input v-model="target" type="text">
|
||||||
<span slot="prefix">@</span>
|
<span>{{ $t('username-or-userid') }}</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
<ui-button @click="verifyUser" :disabled="verifying">{{ $t('verify') }}</ui-button>
|
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
|
||||||
|
<ui-horizon-group>
|
||||||
|
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
|
||||||
|
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
|
||||||
|
</ui-horizon-group>
|
||||||
|
<ui-horizon-group>
|
||||||
|
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
|
||||||
|
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
|
||||||
|
</ui-horizon-group>
|
||||||
|
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
|
||||||
|
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title">{{ $t('unverify-user') }}</div>
|
<div slot="title"><fa :icon="faUsers"/> {{ $t('users.title') }}</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
<ui-input v-model="unverifyUsername" type="text">
|
<ui-horizon-group inputs>
|
||||||
<span slot="prefix">@</span>
|
<ui-select v-model="sort">
|
||||||
</ui-input>
|
<span slot="label">{{ $t('users.sort.title') }}</span>
|
||||||
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
|
<option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option>
|
||||||
</section>
|
<option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option>
|
||||||
</ui-card>
|
<option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option>
|
||||||
|
<option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option>
|
||||||
<ui-card>
|
</ui-select>
|
||||||
<div slot="title">{{ $t('suspend-user') }}</div>
|
<ui-select v-model="origin">
|
||||||
<section class="fit-top">
|
<span slot="label">{{ $t('users.origin.title') }}</span>
|
||||||
<ui-input v-model="suspendUsername" type="text">
|
<option value="combined">{{ $t('users.origin.combined') }}</option>
|
||||||
<span slot="prefix">@</span>
|
<option value="local">{{ $t('users.origin.local') }}</option>
|
||||||
</ui-input>
|
<option value="remote">{{ $t('users.origin.remote') }}</option>
|
||||||
<ui-button @click="suspendUser" :disabled="suspending">{{ $t('suspend') }}</ui-button>
|
</ui-select>
|
||||||
</section>
|
</ui-horizon-group>
|
||||||
</ui-card>
|
<div class="kofvwchc" v-for="user in users">
|
||||||
|
<div>
|
||||||
<ui-card>
|
<a :href="user | userPage(null, true)">
|
||||||
<div slot="title">{{ $t('unsuspend-user') }}</div>
|
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
|
||||||
<section class="fit-top">
|
</a>
|
||||||
<ui-input v-model="unsuspendUsername" type="text">
|
</div>
|
||||||
<span slot="prefix">@</span>
|
<div>
|
||||||
</ui-input>
|
<header>
|
||||||
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
|
<b>{{ user | userName }}</b>
|
||||||
|
<span class="username">@{{ user | acct }}</span>
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,29 +67,89 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import parseAcct from "../../../../misc/acct/parse";
|
import parseAcct from "../../../../misc/acct/parse";
|
||||||
|
import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('admin/views/users.vue'),
|
i18n: i18n('admin/views/users.vue'),
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
verifyUsername: null,
|
user: null,
|
||||||
|
target: null,
|
||||||
verifying: false,
|
verifying: false,
|
||||||
unverifyUsername: null,
|
|
||||||
unverifying: false,
|
unverifying: false,
|
||||||
suspendUsername: null,
|
|
||||||
suspending: false,
|
suspending: false,
|
||||||
unsuspendUsername: null,
|
unsuspending: false,
|
||||||
unsuspending: false
|
sort: '+createdAt',
|
||||||
|
origin: 'combined',
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
users: [],
|
||||||
|
existMore: false,
|
||||||
|
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
sort() {
|
||||||
|
this.users = [];
|
||||||
|
this.offset = 0;
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
origin() {
|
||||||
|
this.users = [];
|
||||||
|
this.offset = 0;
|
||||||
|
this.fetchUsers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async fetchUser() {
|
||||||
|
try {
|
||||||
|
return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
|
||||||
|
} catch (e) {
|
||||||
|
if (e == 'user not found') {
|
||||||
|
this.$root.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: this.$t('user-not-found')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async showUser() {
|
||||||
|
const user = await this.fetchUser();
|
||||||
|
this.$root.api('admin/show-user', { userId: user.id }).then(info => {
|
||||||
|
this.user = info;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async resetPassword() {
|
||||||
|
const user = await this.fetchUser();
|
||||||
|
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
|
||||||
|
this.$root.alert({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('password-updated', { password: res.password })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async verifyUser() {
|
async verifyUser() {
|
||||||
this.verifying = true;
|
this.verifying = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.verifyUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/verify-user', { userId: user.id });
|
await this.$root.api('admin/verify-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -90,7 +171,7 @@ export default Vue.extend({
|
|||||||
this.unverifying = true;
|
this.unverifying = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.unverifyUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/unverify-user', { userId: user.id });
|
await this.$root.api('admin/unverify-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -112,7 +193,7 @@ export default Vue.extend({
|
|||||||
this.suspending = true;
|
this.suspending = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.suspendUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/suspend-user', { userId: user.id });
|
await this.$root.api('admin/suspend-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -134,7 +215,7 @@ export default Vue.extend({
|
|||||||
this.unsuspending = true;
|
this.unsuspending = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.unsuspendUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/unsuspend-user', { userId: user.id });
|
await this.$root.api('admin/unsuspend-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -150,6 +231,24 @@ export default Vue.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.unsuspending = false;
|
this.unsuspending = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchUsers() {
|
||||||
|
this.$root.api('users', {
|
||||||
|
origin: this.origin,
|
||||||
|
sort: this.sort,
|
||||||
|
offset: this.offset,
|
||||||
|
limit: this.limit + 1
|
||||||
|
}).then(users => {
|
||||||
|
if (users.length == this.limit + 1) {
|
||||||
|
users.pop();
|
||||||
|
this.existMore = true;
|
||||||
|
} else {
|
||||||
|
this.existMore = false;
|
||||||
|
}
|
||||||
|
this.users = this.users.concat(users);
|
||||||
|
this.offset += this.limit;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -160,4 +259,24 @@ export default Vue.extend({
|
|||||||
@media (min-width 500px)
|
@media (min-width 500px)
|
||||||
padding 16px
|
padding 16px
|
||||||
|
|
||||||
|
.kofvwchc
|
||||||
|
display flex
|
||||||
|
padding 16px 0
|
||||||
|
border-top solid 1px var(--faceDivider)
|
||||||
|
|
||||||
|
> div:first-child
|
||||||
|
> a
|
||||||
|
> .avatar
|
||||||
|
width 64px
|
||||||
|
height 64px
|
||||||
|
|
||||||
|
> div:last-child
|
||||||
|
flex 1
|
||||||
|
padding-left 16px
|
||||||
|
|
||||||
|
> header
|
||||||
|
> .username
|
||||||
|
margin-left 8px
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ export default (opts: Opts = {}) => ({
|
|||||||
urls(): string[] {
|
urls(): string[] {
|
||||||
if (this.appearNote.text) {
|
if (this.appearNote.text) {
|
||||||
const ast = parse(this.appearNote.text);
|
const ast = parse(this.appearNote.text);
|
||||||
|
// TODO: 再帰的にURL要素がないか調べる
|
||||||
return unique(ast
|
return unique(ast
|
||||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.props.silent))
|
||||||
.map(t => t.url));
|
.map(t => t.props.url));
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="icon" :class="type"><fa :icon="icon"/></div>
|
<div class="icon" :class="type"><fa :icon="icon"/></div>
|
||||||
<header v-if="title" v-html="title"></header>
|
<header v-if="title" v-html="title"></header>
|
||||||
<div class="body" v-if="text" v-html="text"></div>
|
<div class="body" v-if="text" v-html="text"></div>
|
||||||
<ui-horizon-group no-grow class="buttons" v-if="!splash">
|
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
|
||||||
<ui-button @click="ok" primary autofocus>OK</ui-button>
|
<ui-button @click="ok" primary autofocus>OK</ui-button>
|
||||||
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
|
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
|||||||
@@ -50,15 +50,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="player" v-if="game.isEnded">
|
<div class="player" v-if="game.isEnded">
|
||||||
<div>
|
|
||||||
<button @click="logPos = 0" :disabled="logPos == 0"><fa icon="angle-double-left"/></button>
|
|
||||||
<button @click="logPos--" :disabled="logPos == 0"><fa icon="angle-left"/></button>
|
|
||||||
</div>
|
|
||||||
<span>{{ logPos }} / {{ logs.length }}</span>
|
<span>{{ logPos }} / {{ logs.length }}</span>
|
||||||
<div>
|
<ui-horizon-group>
|
||||||
<button @click="logPos++" :disabled="logPos == logs.length"><fa icon="angle-right"/></button>
|
<ui-button @click="logPos = 0" :disabled="logPos == 0"><fa :icon="faAngleDoubleLeft"/></ui-button>
|
||||||
<button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa icon="angle-double-right"/></button>
|
<ui-button @click="logPos--" :disabled="logPos == 0"><fa :icon="faAngleLeft"/></ui-button>
|
||||||
</div>
|
<ui-button @click="logPos++" :disabled="logPos == logs.length"><fa :icon="faAngleRight"/></ui-button>
|
||||||
|
<ui-button @click="logPos = logs.length" :disabled="logPos == logs.length"><fa :icon="faAngleDoubleRight"/></ui-button>
|
||||||
|
</ui-horizon-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
@@ -75,6 +73,7 @@ import i18n from '../../../../../i18n';
|
|||||||
import * as CRC32 from 'crc-32';
|
import * as CRC32 from 'crc-32';
|
||||||
import Reversi, { Color } from '../../../../../../../games/reversi/core';
|
import Reversi, { Color } from '../../../../../../../games/reversi/core';
|
||||||
import { url } from '../../../../../config';
|
import { url } from '../../../../../config';
|
||||||
|
import { faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/games/reversi/reversi.game.vue'),
|
i18n: i18n('common/views/components/games/reversi/reversi.game.vue'),
|
||||||
@@ -99,7 +98,8 @@ export default Vue.extend({
|
|||||||
o: null as Reversi,
|
o: null as Reversi,
|
||||||
logs: [],
|
logs: [],
|
||||||
logPos: 0,
|
logPos: 0,
|
||||||
pollingClock: null
|
pollingClock: null,
|
||||||
|
faAngleDoubleLeft, faAngleLeft, faAngleRight, faAngleDoubleRight
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -449,7 +449,9 @@ export default Vue.extend({
|
|||||||
padding-bottom 16px
|
padding-bottom 16px
|
||||||
|
|
||||||
> .player
|
> .player
|
||||||
padding-bottom 32px
|
padding 0 16px 32px 16px
|
||||||
|
margin 0 auto
|
||||||
|
max-width 500px
|
||||||
|
|
||||||
> span
|
> span
|
||||||
display inline-block
|
display inline-block
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
<div v-for="(x, i) in game.settings.map.join('')"
|
<div v-for="(x, i) in game.settings.map.join('')"
|
||||||
:data-none="x == ' '"
|
:data-none="x == ' '"
|
||||||
@click="onPixelClick(i, x)">
|
@click="onPixelClick(i, x)">
|
||||||
<template v-if="x == 'b'"><template v-if="$store.state.device.darkmode"><fa :icon="['far', 'circle']"/></template><template v-else><fa icon="circle"/></template></template>
|
<fa v-if="x == 'b'" :icon="fasCircle"/>
|
||||||
<template v-if="x == 'w'"><template v-if="$store.state.device.darkmode"><fa :icon="['far', 'circle']"/></template><template v-else><fa icon="circle"/></template></template>
|
<fa v-if="x == 'w'" :icon="farCircle"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,6 +117,8 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../../../i18n';
|
import i18n from '../../../../../i18n';
|
||||||
import * as maps from '../../../../../../../games/reversi/maps';
|
import * as maps from '../../../../../../../games/reversi/maps';
|
||||||
|
import { faCircle as fasCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { faCircle as farCircle } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
|
i18n: i18n('common/views/components/games/reversi/reversi.room.vue'),
|
||||||
@@ -129,7 +131,8 @@ export default Vue.extend({
|
|||||||
mapName: maps.eighteight.name,
|
mapName: maps.eighteight.name,
|
||||||
maps: maps,
|
maps: maps,
|
||||||
form: null,
|
form: null,
|
||||||
messages: []
|
messages: [],
|
||||||
|
fasCircle, farCircle
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export default Vue.extend({
|
|||||||
if (this.message.text) {
|
if (this.message.text) {
|
||||||
const ast = parse(this.message.text);
|
const ast = parse(this.message.text);
|
||||||
return unique(ast
|
return unique(ast
|
||||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
.filter(t => ((t.name == 'url' || t.name == 'link') && t.props.url && !t.silent))
|
||||||
.map(t => t.url));
|
.map(t => t.props.url));
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ import MkGoogle from './google.vue';
|
|||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
||||||
|
|
||||||
function getText(tokens: Node[]): string {
|
function getTextCount(tokens: Node[]): number {
|
||||||
let text = '';
|
let count = 0;
|
||||||
const extract = (tokens: Node[]) => {
|
const extract = (tokens: Node[]) => {
|
||||||
tokens.filter(x => x.name === 'text').forEach(x => {
|
tokens.filter(x => x.name === 'text').forEach(x => {
|
||||||
text += x.props.text;
|
count += length(x.props.text);
|
||||||
});
|
});
|
||||||
tokens.filter(x => x.children).forEach(x => {
|
tokens.filter(x => x.children).forEach(x => {
|
||||||
extract(x.children);
|
extract(x.children);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
extract(tokens);
|
extract(tokens);
|
||||||
return text;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChildrenCount(tokens: Node[]): number {
|
function getChildrenCount(tokens: Node[]): number {
|
||||||
@@ -98,7 +98,7 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
|
|
||||||
case 'big': {
|
case 'big': {
|
||||||
bigCount++;
|
bigCount++;
|
||||||
const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5;
|
const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
|
||||||
const isMany = bigCount > 3;
|
const isMany = bigCount > 3;
|
||||||
return (createElement as any)('strong', {
|
return (createElement as any)('strong', {
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -111,9 +111,17 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||||||
}, genEl(token.children));
|
}, genEl(token.children));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'center': {
|
||||||
|
return [createElement('div', {
|
||||||
|
attrs: {
|
||||||
|
style: 'text-align:center;'
|
||||||
|
}
|
||||||
|
}, genEl(token.children))];
|
||||||
|
}
|
||||||
|
|
||||||
case 'motion': {
|
case 'motion': {
|
||||||
motionCount++;
|
motionCount++;
|
||||||
const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5;
|
const isLong = getTextCount(token.children) > 10 || getChildrenCount(token.children) > 5;
|
||||||
const isMany = motionCount > 3;
|
const isMany = motionCount > 3;
|
||||||
return (createElement as any)('span', {
|
return (createElement as any)('span', {
|
||||||
attrs: {
|
attrs: {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default Vue.extend({
|
|||||||
|
|
||||||
>>> .quote
|
>>> .quote
|
||||||
margin 8px
|
margin 8px
|
||||||
padding 6px 12px
|
padding 6px 0 6px 12px
|
||||||
color var(--mfmQuote)
|
color var(--mfmQuote)
|
||||||
border-left solid 3px var(--mfmQuoteLine)
|
border-left solid 3px var(--mfmQuoteLine)
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export default Vue.extend({
|
|||||||
margin 0 0.5em
|
margin 0 0.5em
|
||||||
font-size 80%
|
font-size 80%
|
||||||
color #525252
|
color #525252
|
||||||
background #f8f8f8
|
background rgba(0, 0, 0, 0.05)
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
|
||||||
>>> pre > code
|
>>> pre > code
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ export default Vue.extend({
|
|||||||
|
|
||||||
*
|
*
|
||||||
pointer-events none
|
pointer-events none
|
||||||
|
user-select none
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
&:focus
|
&:focus
|
||||||
&:after
|
&:after
|
||||||
@@ -107,30 +111,30 @@ export default Vue.extend({
|
|||||||
color var(--text)
|
color var(--text)
|
||||||
background var(--buttonBg)
|
background var(--buttonBg)
|
||||||
|
|
||||||
&:hover
|
&:not(:disabled):hover
|
||||||
background var(--buttonHoverBg)
|
background var(--buttonHoverBg)
|
||||||
|
|
||||||
&:active
|
&:not(:disabled):active
|
||||||
background var(--buttonActiveBg)
|
background var(--buttonActiveBg)
|
||||||
|
|
||||||
&.primary
|
&.primary
|
||||||
color var(--primaryForeground)
|
color var(--primaryForeground)
|
||||||
background var(--primary)
|
background var(--primary)
|
||||||
|
|
||||||
&:hover
|
&:not(:disabled):hover
|
||||||
background var(--primaryLighten5)
|
background var(--primaryLighten5)
|
||||||
|
|
||||||
&:active
|
&:not(:disabled):active
|
||||||
background var(--primaryDarken5)
|
background var(--primaryDarken5)
|
||||||
|
|
||||||
&:not(.fill)
|
&:not(.fill)
|
||||||
color var(--primary)
|
color var(--primary)
|
||||||
background none
|
background none
|
||||||
|
|
||||||
&:hover
|
&:not(:disabled):hover
|
||||||
color var(--primaryDarken5)
|
color var(--primaryDarken5)
|
||||||
|
|
||||||
&:active
|
&:not(:disabled):active
|
||||||
background var(--primaryAlpha03)
|
background var(--primaryAlpha03)
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,9 +27,17 @@ export default Vue.extend({
|
|||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.vnxwkwuf
|
.vnxwkwuf
|
||||||
|
margin 16px 0
|
||||||
|
|
||||||
&.inputs
|
&.inputs
|
||||||
margin 32px 0
|
margin 32px 0
|
||||||
|
|
||||||
|
&.fit-top
|
||||||
|
margin-top 0
|
||||||
|
|
||||||
|
&.fit-bottom
|
||||||
|
margin-bottom 0
|
||||||
|
|
||||||
&:not(.noGrow)
|
&:not(.noGrow)
|
||||||
display flex
|
display flex
|
||||||
|
|
||||||
@@ -37,5 +45,6 @@ export default Vue.extend({
|
|||||||
flex 1
|
flex 1
|
||||||
|
|
||||||
> *:not(:last-child)
|
> *:not(:last-child)
|
||||||
margin-right 16px
|
margin-right 16px !important
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,27 +9,30 @@
|
|||||||
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
<template v-if="type != 'file'">
|
<template v-if="type != 'file'">
|
||||||
<input ref="input"
|
<input ref="input"
|
||||||
:type="type"
|
:type="type"
|
||||||
v-model="v"
|
v-model="v"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:required="required"
|
:required="required"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:pattern="pattern"
|
:pattern="pattern"
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false">
|
@blur="focused = false"
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<input ref="input"
|
<input ref="input"
|
||||||
type="text"
|
type="text"
|
||||||
:value="placeholder"
|
:value="placeholder"
|
||||||
readonly
|
readonly
|
||||||
@click="chooseFile">
|
@click="chooseFile"
|
||||||
|
>
|
||||||
<input ref="file"
|
<input ref="file"
|
||||||
type="file"
|
type="file"
|
||||||
:value="value"
|
:value="value"
|
||||||
@change="onChangeFile">
|
@change="onChangeFile"
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,6 +328,9 @@ root(fill)
|
|||||||
margin 6px 0
|
margin 6px 0
|
||||||
font-size 13px
|
font-size 13px
|
||||||
|
|
||||||
|
&:empty
|
||||||
|
display none
|
||||||
|
|
||||||
*
|
*
|
||||||
margin 0
|
margin 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ui-select" :class="[{ focused, filled }, styl]">
|
<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]">
|
||||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
<div class="input" @click="focus">
|
<div class="input" @click="focus">
|
||||||
<span class="label" ref="label"><slot name="label"></slot></span>
|
<span class="label" ref="label"><slot name="label"></slot></span>
|
||||||
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
|
||||||
<select ref="input"
|
<select ref="input"
|
||||||
:value="v"
|
:value="v"
|
||||||
:required="required"
|
:required="required"
|
||||||
@input="$emit('input', $event.target.value)"
|
:disabled="disabled"
|
||||||
@focus="focused = true"
|
@input="$emit('input', $event.target.value)"
|
||||||
@blur="focused = false">
|
@focus="focused = true"
|
||||||
|
@blur="focused = false"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</select>
|
</select>
|
||||||
<div class="suffix"><slot name="suffix"></slot></div>
|
<div class="suffix"><slot name="suffix"></slot></div>
|
||||||
@@ -22,6 +24,11 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
inject: {
|
||||||
|
horizonGrouped: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
required: false
|
required: false
|
||||||
@@ -30,11 +37,22 @@ export default Vue.extend({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
styl: {
|
styl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'line'
|
default: 'line'
|
||||||
}
|
},
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default(): boolean {
|
||||||
|
return this.horizonGrouped;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -122,7 +140,7 @@ root(fill)
|
|||||||
transition-duration 0.3s
|
transition-duration 0.3s
|
||||||
font-size 16px
|
font-size 16px
|
||||||
line-height 32px
|
line-height 32px
|
||||||
color rgba(#000, 0.54)
|
color var(--inputLabel)
|
||||||
pointer-events none
|
pointer-events none
|
||||||
//will-change transform
|
//will-change transform
|
||||||
transform-origin top left
|
transform-origin top left
|
||||||
@@ -171,6 +189,9 @@ root(fill)
|
|||||||
margin 6px 0
|
margin 6px 0
|
||||||
font-size 13px
|
font-size 13px
|
||||||
|
|
||||||
|
&:empty
|
||||||
|
display none
|
||||||
|
|
||||||
*
|
*
|
||||||
margin 0
|
margin 0
|
||||||
|
|
||||||
@@ -200,4 +221,14 @@ root(fill)
|
|||||||
&:not(.fill)
|
&:not(.fill)
|
||||||
root(false)
|
root(false)
|
||||||
|
|
||||||
|
&.inline
|
||||||
|
display inline-block
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
|
&, *
|
||||||
|
cursor not-allowed !important
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import * as JSON5 from 'json5';
|
||||||
|
|
||||||
|
Vue.filter('json5', x => {
|
||||||
|
return JSON5.stringify(x, null, 2);
|
||||||
|
});
|
||||||
|
|
||||||
require('./bytes');
|
require('./bytes');
|
||||||
require('./number');
|
require('./number');
|
||||||
require('./user');
|
require('./user');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import getAcct from '../../../../../misc/acct/render';
|
import getAcct from '../../../../../misc/acct/render';
|
||||||
import getUserName from '../../../../../misc/get-user-name';
|
import getUserName from '../../../../../misc/get-user-name';
|
||||||
|
import { url } from '../../../config';
|
||||||
|
|
||||||
Vue.filter('acct', user => {
|
Vue.filter('acct', user => {
|
||||||
return getAcct(user);
|
return getAcct(user);
|
||||||
@@ -10,6 +11,6 @@ Vue.filter('userName', user => {
|
|||||||
return getUserName(user);
|
return getUserName(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
Vue.filter('userPage', (user, path?) => {
|
Vue.filter('userPage', (user, path?, absolute = false) => {
|
||||||
return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
|
return `${absolute ? url : ''}/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-note-detail" :title="title">
|
<div class="mk-note-detail" :title="title" tabindex="-1">
|
||||||
<button
|
<button
|
||||||
class="read-more"
|
class="read-more"
|
||||||
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
|
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
|
||||||
@@ -63,18 +63,18 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
|
<span class="app" v-if="note.app && $store.state.settings.showVia">via <b>{{ note.app.name }}</b></span>
|
||||||
<mk-reactions-viewer :note="appearNote"/>
|
<mk-reactions-viewer :note="appearNote"/>
|
||||||
<button class="replyButton" @click="reply" :title="$t('reply')">
|
<button class="replyButton" @click="reply()" :title="$t('reply')">
|
||||||
<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
|
<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
|
||||||
<template v-else><fa icon="reply"/></template>
|
<template v-else><fa icon="reply"/></template>
|
||||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="renoteButton" @click="renote" :title="$t('renote')">
|
<button class="renoteButton" @click="renote()" :title="$t('renote')">
|
||||||
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react" ref="reactButton" :title="$t('add-reaction')">
|
<button class="reactionButton" :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" :title="$t('add-reaction')">
|
||||||
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button @click="menu" ref="menuButton">
|
<button @click="menu()" ref="menuButton">
|
||||||
<fa icon="ellipsis-h"/>
|
<fa icon="ellipsis-h"/>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -88,23 +88,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import parse from '../../../../../mfm/parse';
|
|
||||||
|
|
||||||
import MkPostFormWindow from './post-form-window.vue';
|
|
||||||
import MkRenoteFormWindow from './renote-form-window.vue';
|
|
||||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
|
||||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
|
||||||
import XSub from './note.sub.vue';
|
import XSub from './note.sub.vue';
|
||||||
import { sum, unique } from '../../../../../prelude/array';
|
|
||||||
import noteSubscriber from '../../../common/scripts/note-subscriber';
|
import noteSubscriber from '../../../common/scripts/note-subscriber';
|
||||||
|
import noteMixin from '../../../common/scripts/note-mixin';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('desktop/views/components/note-detail.vue'),
|
i18n: i18n('desktop/views/components/note-detail.vue'),
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
XSub
|
XSub
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [noteSubscriber('note')],
|
mixins: [noteMixin(), noteSubscriber('note')],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
@@ -118,47 +113,12 @@ export default Vue.extend({
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showContent: false,
|
|
||||||
conversation: [],
|
conversation: [],
|
||||||
conversationFetching: false,
|
conversationFetching: false,
|
||||||
replies: []
|
replies: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
isRenote(): boolean {
|
|
||||||
return (this.note.renote &&
|
|
||||||
this.note.text == null &&
|
|
||||||
this.note.fileIds.length == 0 &&
|
|
||||||
this.note.poll == null);
|
|
||||||
},
|
|
||||||
|
|
||||||
appearNote(): any {
|
|
||||||
return this.isRenote ? this.note.renote : this.note;
|
|
||||||
},
|
|
||||||
|
|
||||||
reactionsCount(): number {
|
|
||||||
return this.appearNote.reactionCounts
|
|
||||||
? sum(Object.values(this.appearNote.reactionCounts))
|
|
||||||
: 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
title(): string {
|
|
||||||
return new Date(this.appearNote.createdAt).toLocaleString();
|
|
||||||
},
|
|
||||||
|
|
||||||
urls(): string[] {
|
|
||||||
if (this.appearNote.text) {
|
|
||||||
const ast = parse(this.appearNote.text);
|
|
||||||
return unique(ast
|
|
||||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
|
||||||
.map(t => t.url));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// Get replies
|
// Get replies
|
||||||
if (!this.compact) {
|
if (!this.compact) {
|
||||||
@@ -169,24 +129,6 @@ export default Vue.extend({
|
|||||||
this.replies = replies;
|
this.replies = replies;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw map
|
|
||||||
if (this.appearNote.geo) {
|
|
||||||
const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
|
|
||||||
if (shouldShowMap) {
|
|
||||||
this.$root.os.getGoogleMaps().then(maps => {
|
|
||||||
const uluru = new maps.LatLng(this.appearNote.geo.coordinates[1], this.appearNote.geo.coordinates[0]);
|
|
||||||
const map = new maps.Map(this.$refs.map, {
|
|
||||||
center: uluru,
|
|
||||||
zoom: 15
|
|
||||||
});
|
|
||||||
new maps.Marker({
|
|
||||||
position: uluru,
|
|
||||||
map: map
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -200,32 +142,6 @@ export default Vue.extend({
|
|||||||
this.conversationFetching = false;
|
this.conversationFetching = false;
|
||||||
this.conversation = conversation.reverse();
|
this.conversation = conversation.reverse();
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
reply() {
|
|
||||||
this.$root.new(MkPostFormWindow, {
|
|
||||||
reply: this.appearNote
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renote() {
|
|
||||||
this.$root.new(MkRenoteFormWindow, {
|
|
||||||
note: this.appearNote
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
react() {
|
|
||||||
this.$root.new(MkReactionPicker, {
|
|
||||||
source: this.$refs.reactButton,
|
|
||||||
note: this.appearNote
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
menu() {
|
|
||||||
this.$root.new(MkNoteMenu, {
|
|
||||||
source: this.$refs.menuButton,
|
|
||||||
note: this.appearNote
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mk-note-detail">
|
<div class="mk-note-detail" tabindex="-1">
|
||||||
<button
|
<button
|
||||||
class="more"
|
class="more"
|
||||||
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
|
v-if="appearNote.reply && appearNote.reply.replyId && conversation.length == 0"
|
||||||
@@ -61,18 +61,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<mk-reactions-viewer :note="appearNote"/>
|
<mk-reactions-viewer :note="appearNote"/>
|
||||||
<button @click="reply" :title="$t('title')">
|
<button @click="reply()" :title="$t('title')">
|
||||||
<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
|
<template v-if="appearNote.reply"><fa icon="reply-all"/></template>
|
||||||
<template v-else><fa icon="reply"/></template>
|
<template v-else><fa icon="reply"/></template>
|
||||||
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
<p class="count" v-if="appearNote.repliesCount > 0">{{ appearNote.repliesCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button @click="renote" title="Renote">
|
<button @click="renote()" title="Renote">
|
||||||
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
<fa icon="retweet"/><p class="count" v-if="appearNote.renoteCount > 0">{{ appearNote.renoteCount }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button :class="{ reacted: appearNote.myReaction != null }" @click="react" ref="reactButton" :title="$t('title')">
|
<button :class="{ reacted: appearNote.myReaction != null }" @click="react()" ref="reactButton" :title="$t('title')">
|
||||||
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
<fa icon="plus"/><p class="count" v-if="appearNote.reactions_count > 0">{{ appearNote.reactions_count }}</p>
|
||||||
</button>
|
</button>
|
||||||
<button @click="menu" ref="menuButton">
|
<button @click="menu()" ref="menuButton">
|
||||||
<fa icon="ellipsis-h"/>
|
<fa icon="ellipsis-h"/>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -86,21 +86,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import parse from '../../../../../mfm/parse';
|
|
||||||
|
|
||||||
import MkNoteMenu from '../../../common/views/components/note-menu.vue';
|
|
||||||
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
|
|
||||||
import XSub from './note.sub.vue';
|
import XSub from './note.sub.vue';
|
||||||
import { sum, unique } from '../../../../../prelude/array';
|
|
||||||
import noteSubscriber from '../../../common/scripts/note-subscriber';
|
import noteSubscriber from '../../../common/scripts/note-subscriber';
|
||||||
|
import noteMixin from '../../../common/scripts/note-mixin';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
i18n: i18n('mobile/views/components/note-detail.vue'),
|
i18n: i18n('mobile/views/components/note-detail.vue'),
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
XSub
|
XSub
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [noteSubscriber('note')],
|
mixins: [noteMixin(), noteSubscriber('note')],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
note: {
|
note: {
|
||||||
@@ -114,43 +111,12 @@ export default Vue.extend({
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showContent: false,
|
|
||||||
conversation: [],
|
conversation: [],
|
||||||
conversationFetching: false,
|
conversationFetching: false,
|
||||||
replies: []
|
replies: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
isRenote(): boolean {
|
|
||||||
return (this.note.renote &&
|
|
||||||
this.note.text == null &&
|
|
||||||
this.note.fileIds.length == 0 &&
|
|
||||||
this.note.poll == null);
|
|
||||||
},
|
|
||||||
|
|
||||||
appearNote(): any {
|
|
||||||
return this.isRenote ? this.note.renote : this.note;
|
|
||||||
},
|
|
||||||
|
|
||||||
reactionsCount(): number {
|
|
||||||
return this.appearNote.reactionCounts
|
|
||||||
? sum(Object.values(this.appearNote.reactionCounts))
|
|
||||||
: 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
urls(): string[] {
|
|
||||||
if (this.appearNote.text) {
|
|
||||||
const ast = parse(this.appearNote.text);
|
|
||||||
return unique(ast
|
|
||||||
.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
|
|
||||||
.map(t => t.url));
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// Get replies
|
// Get replies
|
||||||
if (!this.compact) {
|
if (!this.compact) {
|
||||||
@@ -161,24 +127,6 @@ export default Vue.extend({
|
|||||||
this.replies = replies;
|
this.replies = replies;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw map
|
|
||||||
if (this.appearNote.geo) {
|
|
||||||
const shouldShowMap = this.$store.getters.isSignedIn ? this.$store.state.settings.showMaps : true;
|
|
||||||
if (shouldShowMap) {
|
|
||||||
this.$root.os.getGoogleMaps().then(maps => {
|
|
||||||
const uluru = new maps.LatLng(this.appearNote.geo.coordinates[1], this.appearNote.geo.coordinates[0]);
|
|
||||||
const map = new maps.Map(this.$refs.map, {
|
|
||||||
center: uluru,
|
|
||||||
zoom: 15
|
|
||||||
});
|
|
||||||
new maps.Marker({
|
|
||||||
position: uluru,
|
|
||||||
map: map
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -192,35 +140,6 @@ export default Vue.extend({
|
|||||||
this.conversationFetching = false;
|
this.conversationFetching = false;
|
||||||
this.conversation = conversation.reverse();
|
this.conversation = conversation.reverse();
|
||||||
});
|
});
|
||||||
},
|
|
||||||
|
|
||||||
reply() {
|
|
||||||
this.$post({
|
|
||||||
reply: this.appearNote
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renote() {
|
|
||||||
this.$post({
|
|
||||||
renote: this.appearNote
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
react() {
|
|
||||||
this.$root.new(MkReactionPicker, {
|
|
||||||
source: this.$refs.reactButton,
|
|
||||||
note: this.appearNote,
|
|
||||||
compact: true,
|
|
||||||
big: true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
menu() {
|
|
||||||
this.$root.new(MkNoteMenu, {
|
|
||||||
source: this.$refs.menuButton,
|
|
||||||
note: this.appearNote,
|
|
||||||
compact: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,23 +15,23 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home"/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/" :data-active="$route.name == 'index'"><i><fa icon="home" fixed-width/></i>{{ $t('timeline') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']"/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'"><i><fa :icon="['far', 'bell']" fixed-width/></i>{{ $t('notifications') }}<i v-if="hasUnreadNotification" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']"/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'"><i><fa :icon="['far', 'comments']" fixed-width/></i>{{ $t('@.messaging') }}<i v-if="hasUnreadMessagingMessage" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']"/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
<li v-if="$store.getters.isSignedIn && ($store.state.i.isLocked || $store.state.i.carefulBot)"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'"><i><fa :icon="['far', 'envelope']" fixed-width/></i>{{ $t('follow-requests') }}<i v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li><router-link to="/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad"/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/reversi" :data-active="$route.name == 'reversi'"><i><fa icon="gamepad" fixed-width/></i>{{ $t('game') }}<i v-if="hasGameInvitation" class="circle"><fa icon="circle"/></i><i><fa icon="angle-right"/></i></router-link></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']"/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/widgets" :data-active="$route.name == 'widgets'"><i><fa :icon="['far', 'calendar-alt']" fixed-width/></i>{{ $t('widgets') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star"/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/favorites" :data-active="$route.name == 'favorites'"><i><fa icon="star" fixed-width/></i>{{ $t('favorites') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list"/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/lists" :data-active="$route.name == 'user-lists'"><i><fa icon="list" fixed-width/></i>{{ $t('user-lists') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud"/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/drive" :data-active="$route.name == 'drive'"><i><fa icon="cloud" fixed-width/></i>{{ $t('@.drive') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a @click="search"><i><fa icon="search"/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
|
<li><a @click="search"><i><fa icon="search" fixed-width/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li>
|
||||||
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog"/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li>
|
<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog" fixed-width/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li>
|
||||||
<li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal"/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li>
|
<li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal" fixed-width/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li>
|
||||||
<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li>
|
<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon" fixed-width/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="announcements" v-if="announcements && announcements.length > 0">
|
<div class="announcements" v-if="announcements && announcements.length > 0">
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
<x-followers-you-know :user="user"/>
|
<x-followers-you-know :user="user"/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<p v-if="user.host === null">{{ $t('last-used-at') }}: <b><mk-time :time="user.lastUsedAt"/></b></p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ export default Vue.extend({
|
|||||||
@media (min-width 500px)
|
@media (min-width 500px)
|
||||||
padding 10px 16px
|
padding 10px 16px
|
||||||
|
|
||||||
> i
|
> [data-icon]
|
||||||
margin-right 6px
|
margin-right 6px
|
||||||
|
|
||||||
> .activity
|
> .activity
|
||||||
|
|||||||
@@ -37,15 +37,8 @@ export type Source = {
|
|||||||
|
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
|
|
||||||
summalyProxy?: string;
|
|
||||||
|
|
||||||
accesslog?: string;
|
accesslog?: string;
|
||||||
|
|
||||||
github_bot?: {
|
|
||||||
hook_secret: string;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service Worker
|
* Service Worker
|
||||||
*/
|
*/
|
||||||
|
|||||||
24
src/index.ts
24
src/index.ts
@@ -23,7 +23,6 @@ import notesStats from './daemons/notes-stats';
|
|||||||
import loadConfig from './config/load';
|
import loadConfig from './config/load';
|
||||||
import { Config } from './config/types';
|
import { Config } from './config/types';
|
||||||
import { lessThan } from './prelude/array';
|
import { lessThan } from './prelude/array';
|
||||||
import { Db } from 'mongodb';
|
|
||||||
|
|
||||||
const clusterLog = debug('misskey:cluster');
|
const clusterLog = debug('misskey:cluster');
|
||||||
const ev = new Xev();
|
const ev = new Xev();
|
||||||
@@ -192,38 +191,35 @@ async function init(): Promise<Config> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to connect to MongoDB
|
// Try to connect to MongoDB
|
||||||
//await checkMongoDB(config);
|
await checkMongoDB(config);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredMongoDBVersion = [3, 6];
|
const requiredMongoDBVersion = [3, 6];
|
||||||
|
|
||||||
function checkMongoDB(config: Config): Promise<void> {
|
function checkMongoDB(config: Config) {
|
||||||
const mongoDBLogger = new Logger('MongoDB');
|
const mongoDBLogger = new Logger('MongoDB');
|
||||||
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
|
const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null;
|
||||||
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
|
const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null;
|
||||||
const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`;
|
||||||
mongoDBLogger.info(`Connecting to ${uri}`);
|
mongoDBLogger.info(`Connecting to ${uri}`);
|
||||||
|
|
||||||
return mongo.then(async () => {
|
mongo.then(() => {
|
||||||
mongoDBLogger.succ('Connectivity confirmed');
|
mongoDBLogger.succ('Connectivity confirmed');
|
||||||
|
|
||||||
const runningMongoDBVersion = (await nativeDbConn().then(getMongoDBVersion)).split('.').map(x => parseInt(x, 10));
|
nativeDbConn().then(db => db.admin().serverInfo()).then(x => x.version).then((version: string) => {
|
||||||
mongoDBLogger.info(`Version: ${runningMongoDBVersion.join('.')}`);
|
mongoDBLogger.info(`Version: ${version}`);
|
||||||
if (lessThan(runningMongoDBVersion, requiredMongoDBVersion)) {
|
if (lessThan(version.split('.').map(x => parseInt(x, 10)), requiredMongoDBVersion)) {
|
||||||
mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`);
|
mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
mongoDBLogger.error(err.message);
|
mongoDBLogger.error(err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMongoDBVersion(db: Db): Promise<string> {
|
|
||||||
return (await db.admin().serverInfo()).version;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function spawnWorkers(limit: number = Infinity) {
|
async function spawnWorkers(limit: number = Infinity) {
|
||||||
const workers = Math.min(limit, os.cpus().length);
|
const workers = Math.min(limit, os.cpus().length);
|
||||||
Logger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
Logger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUser
|
|||||||
return pre;
|
return pre;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
center(token) {
|
||||||
|
const el = doc.createElement('div');
|
||||||
|
dive(token.children).forEach(child => el.appendChild(child));
|
||||||
|
return el;
|
||||||
|
},
|
||||||
|
|
||||||
emoji(token) {
|
emoji(token) {
|
||||||
return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
|
return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,11 +41,12 @@ export default (source: string): Node[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isBlockNode(node: Node): boolean {
|
function isBlockNode(node: Node): boolean {
|
||||||
return ['blockCode', 'quote', 'title'].includes(node.name);
|
return ['blockCode', 'center', 'quote', 'title'].includes(node.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ブロック要素の前後にある改行を削除します(ブロック要素自体が改行の役割も果たすため、余計に改行されてしまうため)
|
* ブロック要素の前後にある改行を削除します
|
||||||
|
* (ブロック要素自体が改行の役割を果たすため、余計に改行されてしまう)
|
||||||
* @param nodes
|
* @param nodes
|
||||||
*/
|
*/
|
||||||
const removeNeedlessLineBreaks = (nodes: Node[]) => {
|
const removeNeedlessLineBreaks = (nodes: Node[]) => {
|
||||||
|
|||||||
@@ -29,6 +29,26 @@ function makeNodeWithChildren(name: string, children: Node[], props?: any): Node
|
|||||||
return _makeNode(name, children, props);
|
return _makeNode(name, children, props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTrailingPosition(x: string): number {
|
||||||
|
let pendingBracket = 0;
|
||||||
|
const end = x.split('').findIndex(char => {
|
||||||
|
if (char == ')') {
|
||||||
|
if (pendingBracket > 0) {
|
||||||
|
pendingBracket--;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (char == '(') {
|
||||||
|
pendingBracket++;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return end > 0 ? end : x.length;
|
||||||
|
}
|
||||||
|
|
||||||
const newline = P((input, i) => {
|
const newline = P((input, i) => {
|
||||||
if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') {
|
if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') {
|
||||||
return P.makeSuccess(i, null);
|
return P.makeSuccess(i, null);
|
||||||
@@ -53,6 +73,7 @@ const mfm = P.createLanguage({
|
|||||||
r.math,
|
r.math,
|
||||||
r.search,
|
r.search,
|
||||||
r.title,
|
r.title,
|
||||||
|
r.center,
|
||||||
r.text
|
r.text
|
||||||
).atLeast(1),
|
).atLeast(1),
|
||||||
|
|
||||||
@@ -63,7 +84,9 @@ const mfm = P.createLanguage({
|
|||||||
P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1)
|
P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1)
|
||||||
.map(x => makeNodeWithChildren('big', P.alt(
|
.map(x => makeNodeWithChildren('big', P.alt(
|
||||||
r.mention,
|
r.mention,
|
||||||
|
r.hashtag,
|
||||||
r.emoji,
|
r.emoji,
|
||||||
|
r.math,
|
||||||
r.text
|
r.text
|
||||||
).atLeast(1).tryParse(x))),
|
).atLeast(1).tryParse(x))),
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -85,11 +108,31 @@ const mfm = P.createLanguage({
|
|||||||
P.regexp(/\*\*([\s\S]+?)\*\*/, 1)
|
P.regexp(/\*\*([\s\S]+?)\*\*/, 1)
|
||||||
.map(x => makeNodeWithChildren('bold', P.alt(
|
.map(x => makeNodeWithChildren('bold', P.alt(
|
||||||
r.mention,
|
r.mention,
|
||||||
|
r.hashtag,
|
||||||
|
r.url,
|
||||||
|
r.link,
|
||||||
r.emoji,
|
r.emoji,
|
||||||
r.text
|
r.text
|
||||||
).atLeast(1).tryParse(x))),
|
).atLeast(1).tryParse(x))),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region Center
|
||||||
|
center: r =>
|
||||||
|
P.regexp(/<center>([\s\S]+?)<\/center>/, 1)
|
||||||
|
.map(x => makeNodeWithChildren('center', P.alt(
|
||||||
|
r.big,
|
||||||
|
r.bold,
|
||||||
|
r.motion,
|
||||||
|
r.mention,
|
||||||
|
r.hashtag,
|
||||||
|
r.emoji,
|
||||||
|
r.math,
|
||||||
|
r.url,
|
||||||
|
r.link,
|
||||||
|
r.text
|
||||||
|
).atLeast(1).tryParse(x))),
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region Emoji
|
//#region Emoji
|
||||||
emoji: r =>
|
emoji: r =>
|
||||||
P.alt(
|
P.alt(
|
||||||
@@ -110,14 +153,17 @@ const mfm = P.createLanguage({
|
|||||||
const text = input.substr(i);
|
const text = input.substr(i);
|
||||||
const match = text.match(/^#([^\s\.,!\?#]+)/i);
|
const match = text.match(/^#([^\s\.,!\?#]+)/i);
|
||||||
if (!match) return P.makeFailure(i, 'not a hashtag');
|
if (!match) return P.makeFailure(i, 'not a hashtag');
|
||||||
if (input[i - 1] != ' ' && input[i - 1] != null) return P.makeFailure(i, 'require space before "#"');
|
let hashtag = match[1];
|
||||||
return P.makeSuccess(i + match[0].length, makeNode('hashtag', { hashtag: match[1] }));
|
hashtag = hashtag.substr(0, getTrailingPosition(hashtag));
|
||||||
|
if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag');
|
||||||
|
if (!['\n', ' ', '(', null, undefined].includes(input[i - 1])) return P.makeFailure(i, 'require space before "#"');
|
||||||
|
return P.makeSuccess(i + ('#' + hashtag).length, makeNode('hashtag', { hashtag: hashtag }));
|
||||||
}),
|
}),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Inline code
|
//#region Inline code
|
||||||
inlineCode: r =>
|
inlineCode: r =>
|
||||||
P.regexp(/`(.+?)`/, 1)
|
P.regexp(/`([^´\n]+?)`/, 1)
|
||||||
.map(x => makeNode('inlineCode', { code: x })),
|
.map(x => makeNode('inlineCode', { code: x })),
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
@@ -176,7 +222,11 @@ const mfm = P.createLanguage({
|
|||||||
.map(x => makeNodeWithChildren('motion', P.alt(
|
.map(x => makeNodeWithChildren('motion', P.alt(
|
||||||
r.bold,
|
r.bold,
|
||||||
r.mention,
|
r.mention,
|
||||||
|
r.hashtag,
|
||||||
r.emoji,
|
r.emoji,
|
||||||
|
r.url,
|
||||||
|
r.link,
|
||||||
|
r.math,
|
||||||
r.text
|
r.text
|
||||||
).atLeast(1).tryParse(x))),
|
).atLeast(1).tryParse(x))),
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -242,11 +292,9 @@ const mfm = P.createLanguage({
|
|||||||
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
|
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
|
||||||
if (!match) return P.makeFailure(i, 'not a url');
|
if (!match) return P.makeFailure(i, 'not a url');
|
||||||
let url = match[0];
|
let url = match[0];
|
||||||
const before = input[i - 1];
|
url = url.substr(0, getTrailingPosition(url));
|
||||||
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
|
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
|
||||||
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
|
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
|
||||||
if (url.endsWith(')') && before == '(') url = url.substr(0, url.lastIndexOf(')'));
|
|
||||||
if (url.endsWith(']') && before == '[') url = url.substr(0, url.lastIndexOf(']'));
|
|
||||||
return P.makeSuccess(i + url.length, url);
|
return P.makeSuccess(i + url.length, url);
|
||||||
})
|
})
|
||||||
.map(x => makeNode('url', { url: x })),
|
.map(x => makeNode('url', { url: x })),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export default (acct: string) => {
|
export default (acct: string) => {
|
||||||
|
if (acct.startsWith('@')) acct = acct.substr(1);
|
||||||
const splitted = acct.split('@', 2);
|
const splitted = acct.split('@', 2);
|
||||||
return { username: splitted[0], host: splitted[1] || null };
|
return { username: splitted[0], host: splitted[1] || null };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ const defaultMeta: any = {
|
|||||||
maxNoteTextLength: 1000,
|
maxNoteTextLength: 1000,
|
||||||
enableTwitterIntegration: false,
|
enableTwitterIntegration: false,
|
||||||
enableGithubIntegration: false,
|
enableGithubIntegration: false,
|
||||||
enableDiscordIntegration: false
|
enableDiscordIntegration: false,
|
||||||
|
enableExternalUserRecommendation: false,
|
||||||
|
externalUserRecommendationEngine: "https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}",
|
||||||
|
externalUserRecommendationTimeout: 300000
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function(): Promise<IMeta> {
|
export default async function(): Promise<IMeta> {
|
||||||
|
|||||||
@@ -6,15 +6,24 @@ export default function(file: IDriveFile, thumbnail = false): string {
|
|||||||
|
|
||||||
if (file.metadata.withoutChunks) {
|
if (file.metadata.withoutChunks) {
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
return file.metadata.thumbnailUrl || file.metadata.url;
|
return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || file.metadata.url;
|
||||||
} else {
|
} else {
|
||||||
return file.metadata.url;
|
return file.metadata.webpublicUrl || file.metadata.url;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
return `${config.drive_url}/${file._id}?thumbnail`;
|
return `${config.drive_url}/${file._id}?thumbnail`;
|
||||||
} else {
|
} else {
|
||||||
return `${config.drive_url}/${file._id}`;
|
return `${config.drive_url}/${file._id}?web`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getOriginalUrl(file: IDriveFile) {
|
||||||
|
if (file.metadata && file.metadata.url) {
|
||||||
|
return file.metadata.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessKey = file.metadata ? file.metadata.accessKey : null;
|
||||||
|
return `${config.drive_url}/${file._id}${accessKey ? '?original=' + accessKey : ''}`;
|
||||||
|
}
|
||||||
|
|||||||
29
src/models/drive-file-webpublic.ts
Normal file
29
src/models/drive-file-webpublic.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ const deepcopy = require('deepcopy');
|
|||||||
import { pack as packFolder } from './drive-folder';
|
import { pack as packFolder } from './drive-folder';
|
||||||
import monkDb, { nativeDbConn } from '../db/mongodb';
|
import monkDb, { nativeDbConn } from '../db/mongodb';
|
||||||
import isObjectId from '../misc/is-objectid';
|
import isObjectId from '../misc/is-objectid';
|
||||||
import getDriveFileUrl from '../misc/get-drive-file-url';
|
import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url';
|
||||||
|
|
||||||
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
|
const DriveFile = monkDb.get<IDriveFile>('driveFiles.files');
|
||||||
DriveFile.createIndex('md5');
|
DriveFile.createIndex('md5');
|
||||||
@@ -28,21 +28,48 @@ export type IMetadata = {
|
|||||||
_user: any;
|
_user: any;
|
||||||
folderId: mongo.ObjectID;
|
folderId: mongo.ObjectID;
|
||||||
comment: string;
|
comment: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* リモートインスタンスから取得した場合の元URL
|
||||||
|
*/
|
||||||
uri?: string;
|
uri?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL for web(生成されている場合) or original
|
||||||
|
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
|
||||||
|
*/
|
||||||
url?: string;
|
url?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL for thumbnail (thumbnailがなければなし)
|
||||||
|
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
|
||||||
|
*/
|
||||||
thumbnailUrl?: string;
|
thumbnailUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL for original (web用が生成されてない場合はurlがoriginalを指す)
|
||||||
|
* * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ
|
||||||
|
*/
|
||||||
|
webpublicUrl?: string;
|
||||||
|
|
||||||
|
accessKey?: string;
|
||||||
|
|
||||||
src?: string;
|
src?: string;
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* このファイルの中身データがMongoDB内に保存されているのか否か
|
* このファイルの中身データがMongoDB内に保存されていないか否か
|
||||||
* オブジェクトストレージを利用している or リモートサーバーへの直リンクである
|
* オブジェクトストレージを利用している or リモートサーバーへの直リンクである
|
||||||
* な場合は false になります
|
* な場合は true になります
|
||||||
*/
|
*/
|
||||||
withoutChunks?: boolean;
|
withoutChunks?: boolean;
|
||||||
|
|
||||||
storage?: string;
|
storage?: string;
|
||||||
storageProps?: any;
|
|
||||||
|
/***
|
||||||
|
* ObjectStorage の格納先の情報
|
||||||
|
*/
|
||||||
|
storageProps?: IStorageProps;
|
||||||
isSensitive?: boolean;
|
isSensitive?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,6 +83,25 @@ export type IMetadata = {
|
|||||||
isRemote?: boolean;
|
isRemote?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type IStorageProps = {
|
||||||
|
/**
|
||||||
|
* ObjectStorage key for original
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/***
|
||||||
|
* ObjectStorage key for thumbnail (thumbnailがなければなし)
|
||||||
|
*/
|
||||||
|
thumbnailKey?: string;
|
||||||
|
|
||||||
|
/***
|
||||||
|
* ObjectStorage key for webpublic (webpublicがなければなし)
|
||||||
|
*/
|
||||||
|
webpublicKey?: string;
|
||||||
|
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type IDriveFile = {
|
export type IDriveFile = {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
uploadDate: Date;
|
uploadDate: Date;
|
||||||
@@ -83,7 +129,8 @@ export function validateFileName(name: string): boolean {
|
|||||||
export const packMany = (
|
export const packMany = (
|
||||||
files: any[],
|
files: any[],
|
||||||
options?: {
|
options?: {
|
||||||
detail: boolean
|
detail?: boolean
|
||||||
|
self?: boolean,
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
return Promise.all(files.map(f => pack(f, options)));
|
return Promise.all(files.map(f => pack(f, options)));
|
||||||
@@ -95,11 +142,13 @@ export const packMany = (
|
|||||||
export const pack = (
|
export const pack = (
|
||||||
file: any,
|
file: any,
|
||||||
options?: {
|
options?: {
|
||||||
detail: boolean
|
detail?: boolean,
|
||||||
|
self?: boolean,
|
||||||
}
|
}
|
||||||
) => new Promise<any>(async (resolve, reject) => {
|
) => new Promise<any>(async (resolve, reject) => {
|
||||||
const opts = Object.assign({
|
const opts = Object.assign({
|
||||||
detail: false
|
detail: false,
|
||||||
|
self: false
|
||||||
}, options);
|
}, options);
|
||||||
|
|
||||||
let _file: any;
|
let _file: any;
|
||||||
@@ -165,5 +214,9 @@ export const pack = (
|
|||||||
delete _target.isRemote;
|
delete _target.isRemote;
|
||||||
delete _target._user;
|
delete _target._user;
|
||||||
|
|
||||||
|
if (opts.self) {
|
||||||
|
_target.url = getOriginalUrl(_file);
|
||||||
|
}
|
||||||
|
|
||||||
resolve(_target);
|
resolve(_target);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -125,6 +125,19 @@ if ((config as any).github) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if ((config as any).user_recommendation) {
|
||||||
|
Meta.findOne({}).then(m => {
|
||||||
|
if (m != null && m.enableExternalUserRecommendation == null) {
|
||||||
|
Meta.update({}, {
|
||||||
|
$set: {
|
||||||
|
enableExternalUserRecommendation: true,
|
||||||
|
externalUserRecommendationEngine: (config as any).user_recommendation.engine,
|
||||||
|
externalUserRecommendationTimeout: (config as any).user_recommendation.timeout
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export type IMeta = {
|
export type IMeta = {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -184,6 +197,8 @@ export type IMeta = {
|
|||||||
*/
|
*/
|
||||||
maxNoteTextLength?: number;
|
maxNoteTextLength?: number;
|
||||||
|
|
||||||
|
summalyProxy?: string;
|
||||||
|
|
||||||
enableTwitterIntegration?: boolean;
|
enableTwitterIntegration?: boolean;
|
||||||
twitterConsumerKey?: string;
|
twitterConsumerKey?: string;
|
||||||
twitterConsumerSecret?: string;
|
twitterConsumerSecret?: string;
|
||||||
@@ -195,4 +210,8 @@ export type IMeta = {
|
|||||||
enableDiscordIntegration?: boolean;
|
enableDiscordIntegration?: boolean;
|
||||||
discordClientId?: string;
|
discordClientId?: string;
|
||||||
discordClientSecret?: string;
|
discordClientSecret?: string;
|
||||||
|
|
||||||
|
enableExternalUserRecommendation?: boolean;
|
||||||
|
externalUserRecommendationEngine?: string;
|
||||||
|
externalUserRecommendationTimeout?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ Note.createIndex('uri', { sparse: true, unique: true });
|
|||||||
Note.createIndex('userId');
|
Note.createIndex('userId');
|
||||||
Note.createIndex('mentions');
|
Note.createIndex('mentions');
|
||||||
Note.createIndex('visibleUserIds');
|
Note.createIndex('visibleUserIds');
|
||||||
|
Note.createIndex('replyId');
|
||||||
Note.createIndex('tagsLower');
|
Note.createIndex('tagsLower');
|
||||||
Note.createIndex('_user.host');
|
Note.createIndex('_user.host');
|
||||||
Note.createIndex('_files._id');
|
Note.createIndex('_files._id');
|
||||||
@@ -99,7 +100,6 @@ export type INote = {
|
|||||||
host: string;
|
host: string;
|
||||||
inbox?: string;
|
inbox?: string;
|
||||||
};
|
};
|
||||||
_replyIds?: mongo.ObjectID[];
|
|
||||||
_files?: IDriveFile[];
|
_files?: IDriveFile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,6 +258,8 @@ export const pack = async (
|
|||||||
delete _note._reply;
|
delete _note._reply;
|
||||||
delete _note._renote;
|
delete _note._renote;
|
||||||
delete _note._files;
|
delete _note._files;
|
||||||
|
delete _note._replyIds;
|
||||||
|
|
||||||
if (_note.geo) delete _note.geo.type;
|
if (_note.geo) delete _note.geo.type;
|
||||||
|
|
||||||
// Populate user
|
// Populate user
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default User;
|
|||||||
type IUserBase = {
|
type IUserBase = {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
followersCount: number;
|
followersCount: number;
|
||||||
followingCount: number;
|
followingCount: number;
|
||||||
@@ -37,6 +38,8 @@ type IUserBase = {
|
|||||||
bannerId: mongo.ObjectID;
|
bannerId: mongo.ObjectID;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
bannerUrl?: string;
|
bannerUrl?: string;
|
||||||
|
avatarColor?: any;
|
||||||
|
bannerColor?: any;
|
||||||
wallpaperId: mongo.ObjectID;
|
wallpaperId: mongo.ObjectID;
|
||||||
wallpaperUrl?: string;
|
wallpaperUrl?: string;
|
||||||
data: any;
|
data: any;
|
||||||
@@ -104,7 +107,6 @@ export interface ILocalUser extends IUserBase {
|
|||||||
birthday: string; // 'YYYY-MM-DD'
|
birthday: string; // 'YYYY-MM-DD'
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
lastUsedAt: Date;
|
|
||||||
isCat: boolean;
|
isCat: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
isModerator?: boolean;
|
isModerator?: boolean;
|
||||||
@@ -132,7 +134,7 @@ export interface IRemoteUser extends IUserBase {
|
|||||||
id: string;
|
id: string;
|
||||||
publicKeyPem: string;
|
publicKeyPem: string;
|
||||||
};
|
};
|
||||||
updatedAt: Date;
|
lastFetchedAt: Date;
|
||||||
isAdmin: false;
|
isAdmin: false;
|
||||||
isModerator: false;
|
isModerator: false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||||||
// リプライ
|
// リプライ
|
||||||
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
|
const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null;
|
||||||
|
|
||||||
|
// 引用
|
||||||
|
let quote: INote;
|
||||||
|
|
||||||
|
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
|
||||||
|
quote = await resolveNote(note._misskey_quote).catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
// テキストのパース
|
// テキストのパース
|
||||||
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
|
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
|
||||||
|
|
||||||
@@ -104,7 +111,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ユーザーの情報が古かったらついでに更新しておく
|
// ユーザーの情報が古かったらついでに更新しておく
|
||||||
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
updatePerson(note.attributedTo);
|
updatePerson(note.attributedTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +119,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||||||
createdAt: new Date(note.published),
|
createdAt: new Date(note.published),
|
||||||
files: files,
|
files: files,
|
||||||
reply,
|
reply,
|
||||||
renote: undefined,
|
renote: quote,
|
||||||
cw: note.summary,
|
cw: note.summary,
|
||||||
text: text,
|
text: text,
|
||||||
viaMobile: false,
|
viaMobile: false,
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
|||||||
avatarId: null,
|
avatarId: null,
|
||||||
bannerId: null,
|
bannerId: null,
|
||||||
createdAt: Date.parse(person.published) || null,
|
createdAt: Date.parse(person.published) || null,
|
||||||
updatedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
description: htmlToMFM(person.summary),
|
description: htmlToMFM(person.summary),
|
||||||
followersCount,
|
followersCount,
|
||||||
followingCount,
|
followingCount,
|
||||||
@@ -212,13 +212,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
|||||||
const bannerId = banner ? banner._id : null;
|
const bannerId = banner ? banner._id : null;
|
||||||
const avatarUrl = getDriveFileUrl(avatar, true);
|
const avatarUrl = getDriveFileUrl(avatar, true);
|
||||||
const bannerUrl = getDriveFileUrl(banner, false);
|
const bannerUrl = getDriveFileUrl(banner, false);
|
||||||
|
const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
|
||||||
|
const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
|
||||||
|
|
||||||
await User.update({ _id: user._id }, {
|
await User.update({ _id: user._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
avatarId,
|
avatarId,
|
||||||
bannerId,
|
bannerId,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
bannerUrl
|
bannerUrl,
|
||||||
|
avatarColor,
|
||||||
|
bannerColor
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,6 +230,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
|||||||
user.bannerId = bannerId;
|
user.bannerId = bannerId;
|
||||||
user.avatarUrl = avatarUrl;
|
user.avatarUrl = avatarUrl;
|
||||||
user.bannerUrl = bannerUrl;
|
user.bannerUrl = bannerUrl;
|
||||||
|
user.avatarColor = avatarColor;
|
||||||
|
user.bannerColor = bannerColor;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
await updateFeatured(user._id).catch(err => console.log(err));
|
await updateFeatured(user._id).catch(err => console.log(err));
|
||||||
@@ -298,7 +304,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
|||||||
// Update user
|
// Update user
|
||||||
await User.update({ _id: exist._id }, {
|
await User.update({ _id: exist._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
updatedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox,
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
@@ -306,6 +312,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
|||||||
bannerId: banner ? banner._id : null,
|
bannerId: banner ? banner._id : null,
|
||||||
avatarUrl: getDriveFileUrl(avatar, true),
|
avatarUrl: getDriveFileUrl(avatar, true),
|
||||||
bannerUrl: getDriveFileUrl(banner, false),
|
bannerUrl: getDriveFileUrl(banner, false),
|
||||||
|
avatarColor: avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null,
|
||||||
|
bannerColor: banner && banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null,
|
||||||
description: htmlToMFM(person.summary),
|
description: htmlToMFM(person.summary),
|
||||||
followersCount,
|
followersCount,
|
||||||
followingCount,
|
followingCount,
|
||||||
|
|||||||
@@ -42,6 +42,18 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
|||||||
inReplyTo = null;
|
inReplyTo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let quote;
|
||||||
|
|
||||||
|
if (note.renoteId) {
|
||||||
|
const renote = await Note.findOne({
|
||||||
|
_id: note.renoteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (renote) {
|
||||||
|
quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
_id: note.userId
|
_id: note.userId
|
||||||
});
|
});
|
||||||
@@ -112,6 +124,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
|||||||
summary: note.cw,
|
summary: note.cw,
|
||||||
content,
|
content,
|
||||||
_misskey_content: text,
|
_misskey_content: text,
|
||||||
|
_misskey_quote: quote,
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
to,
|
to,
|
||||||
cc,
|
cc,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface IOrderedCollection extends IObject {
|
|||||||
export interface INote extends IObject {
|
export interface INote extends IObject {
|
||||||
type: 'Note';
|
type: 'Note';
|
||||||
_misskey_content: string;
|
_misskey_content: string;
|
||||||
|
_misskey_quote: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPerson extends IObject {
|
export interface IPerson extends IObject {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ router.get('/notes/:note', async (ctx, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = pack(await renderNote(note, false));
|
ctx.body = pack(await renderNote(note, false));
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
setResponseType(ctx);
|
setResponseType(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
|
|||||||
path: '/',
|
path: '/',
|
||||||
domain: config.hostname,
|
domain: config.hostname,
|
||||||
// SEE: https://github.com/koajs/koa/issues/974
|
// SEE: https://github.com/koajs/koa/issues/974
|
||||||
//secure: config.url.startsWith('https'),
|
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||||
secure: false,
|
secure: config.url.startsWith('https'),
|
||||||
httpOnly: false,
|
httpOnly: false,
|
||||||
expires: new Date(Date.now() + expires),
|
expires: new Date(Date.now() + expires),
|
||||||
maxAge: expires
|
maxAge: expires
|
||||||
|
|||||||
57
src/server/api/endpoints/admin/reset-password.ts
Normal file
57
src/server/api/endpoints/admin/reset-password.ts
Normal 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
|
||||||
|
});
|
||||||
|
}));
|
||||||
40
src/server/api/endpoints/admin/show-user.ts
Normal file
40
src/server/api/endpoints/admin/show-user.ts
Normal 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);
|
||||||
|
}));
|
||||||
@@ -139,6 +139,13 @@ export const meta = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
summalyProxy: {
|
||||||
|
validator: $.str.optional.nullable,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': 'summalyプロキシURL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
enableTwitterIntegration: {
|
enableTwitterIntegration: {
|
||||||
validator: $.bool.optional,
|
validator: $.bool.optional,
|
||||||
desc: {
|
desc: {
|
||||||
@@ -200,6 +207,27 @@ export const meta = {
|
|||||||
desc: {
|
desc: {
|
||||||
'ja-JP': 'DiscordアプリのClient Secret'
|
'ja-JP': 'DiscordアプリのClient Secret'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enableExternalUserRecommendation: {
|
||||||
|
validator: $.bool.optional,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '外部ユーザーレコメンデーションを有効にする'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
externalUserRecommendationEngine: {
|
||||||
|
validator: $.str.optional.nullable,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '外部ユーザーレコメンデーションのサードパーティエンジン'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
externalUserRecommendationTimeout: {
|
||||||
|
validator: $.num.optional.nullable.min(0),
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -279,6 +307,10 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
|
|||||||
set.langs = ps.langs;
|
set.langs = ps.langs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.summalyProxy !== undefined) {
|
||||||
|
set.summalyProxy = ps.summalyProxy;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.enableTwitterIntegration !== undefined) {
|
if (ps.enableTwitterIntegration !== undefined) {
|
||||||
set.enableTwitterIntegration = ps.enableTwitterIntegration;
|
set.enableTwitterIntegration = ps.enableTwitterIntegration;
|
||||||
}
|
}
|
||||||
@@ -315,6 +347,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => {
|
|||||||
set.discordClientSecret = ps.discordClientSecret;
|
set.discordClientSecret = ps.discordClientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableExternalUserRecommendation !== undefined) {
|
||||||
|
set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.externalUserRecommendationEngine !== undefined) {
|
||||||
|
set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.externalUserRecommendationTimeout !== undefined) {
|
||||||
|
set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
await Meta.update({}, {
|
await Meta.update({}, {
|
||||||
$set: set
|
$set: set
|
||||||
}, { upsert: true });
|
}, { upsert: true });
|
||||||
|
|||||||
@@ -77,5 +77,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
sort: sort
|
sort: sort
|
||||||
});
|
});
|
||||||
|
|
||||||
res(await packMany(files));
|
res(await packMany(files, { detail: false, self: true }));
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
if (file === null) {
|
if (file === null) {
|
||||||
res({ file: null });
|
res({ file: null });
|
||||||
} else {
|
} else {
|
||||||
res({ file: await pack(file) });
|
res({ file: await pack(file, { self: true }) });
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default define(meta, (ps, user, app, file, cleanup) => new Promise(async
|
|||||||
|
|
||||||
cleanup();
|
cleanup();
|
||||||
|
|
||||||
res(pack(driveFile));
|
res(pack(driveFile, { self: true }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
|
|||||||
@@ -31,5 +31,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
'metadata.folderId': ps.folderId
|
'metadata.folderId': ps.folderId
|
||||||
});
|
});
|
||||||
|
|
||||||
res(await Promise.all(files.map(file => pack(file))));
|
res(await Promise.all(files.map(file => pack(file, { self: true }))));
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
const _file = await pack(file, {
|
const _file = await pack(file, {
|
||||||
detail: true
|
detail: true,
|
||||||
|
self: true
|
||||||
});
|
});
|
||||||
|
|
||||||
res(_file);
|
res(_file);
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Serialize
|
// Serialize
|
||||||
const fileObj = await pack(file);
|
const fileObj = await pack(file, { self: true });
|
||||||
|
|
||||||
// Response
|
// Response
|
||||||
res(fileObj);
|
res(fileObj);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const meta = {
|
|||||||
|
|
||||||
folderId: {
|
folderId: {
|
||||||
validator: $.type(ID).optional.nullable,
|
validator: $.type(ID).optional.nullable,
|
||||||
default: null as any as any,
|
default: null as any,
|
||||||
transform: transform
|
transform: transform
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -50,5 +50,5 @@ export const meta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
||||||
res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force)));
|
res(pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }));
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -65,5 +65,5 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
|||||||
sort: sort
|
sort: sort
|
||||||
});
|
});
|
||||||
|
|
||||||
res(await packMany(files));
|
res(await packMany(files, { self: true }));
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import User, { pack } from '../../../models/user';
|
import { pack } from '../../../models/user';
|
||||||
import define from '../define';
|
import define from '../define';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
@@ -27,11 +27,4 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
|
|||||||
includeHasUnreadNotes: true,
|
includeHasUnreadNotes: true,
|
||||||
includeSecrets: isSecure
|
includeSecrets: isSecure
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Update lastUsedAt
|
|
||||||
User.update({ _id: user._id }, {
|
|
||||||
$set: {
|
|
||||||
lastUsedAt: new Date()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
enableTwitterIntegration: instance.enableTwitterIntegration,
|
enableTwitterIntegration: instance.enableTwitterIntegration,
|
||||||
enableGithubIntegration: instance.enableGithubIntegration,
|
enableGithubIntegration: instance.enableGithubIntegration,
|
||||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||||
|
|
||||||
|
enableExternalUserRecommendation: instance.enableExternalUserRecommendation,
|
||||||
|
externalUserRecommendationEngine: instance.externalUserRecommendationEngine,
|
||||||
|
externalUserRecommendationTimeout: instance.externalUserRecommendationTimeout
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ps.detail) {
|
if (ps.detail) {
|
||||||
@@ -85,7 +89,11 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
github: instance.enableGithubIntegration,
|
github: instance.enableGithubIntegration,
|
||||||
discord: instance.enableDiscordIntegration,
|
discord: instance.enableDiscordIntegration,
|
||||||
serviceWorker: config.sw ? true : false,
|
serviceWorker: config.sw ? true : false,
|
||||||
userRecommendation: config.user_recommendation ? config.user_recommendation : {}
|
userRecommendation: {
|
||||||
|
external: instance.enableExternalUserRecommendation,
|
||||||
|
engine: instance.externalUserRecommendationEngine,
|
||||||
|
timeout: instance.externalUserRecommendationTimeout
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +107,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
response.githubClientSecret = instance.githubClientSecret;
|
response.githubClientSecret = instance.githubClientSecret;
|
||||||
response.discordClientId = instance.discordClientId;
|
response.discordClientId = instance.discordClientId;
|
||||||
response.discordClientSecret = instance.discordClientSecret;
|
response.discordClientSecret = instance.discordClientSecret;
|
||||||
|
response.summalyProxy = instance.summalyProxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
res(response);
|
res(response);
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
|
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
|
||||||
if ((ps.text == null) && files === null && renote === null && ps.poll == null) {
|
if (!(ps.text || files.length || renote || ps.poll)) {
|
||||||
return rej('text, fileIds, renoteId or poll is required');
|
return rej('text, fileIds, renoteId or poll is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,13 @@ export const meta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
||||||
// Lookup note
|
|
||||||
const note = await Note.findOne({
|
|
||||||
_id: ps.noteId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (note === null) {
|
const notes = await Note.find({
|
||||||
return rej('note not found');
|
replyId: ps.noteId
|
||||||
}
|
}, {
|
||||||
|
limit: ps.limit,
|
||||||
|
skip: ps.offset
|
||||||
|
});
|
||||||
|
|
||||||
const ids = (note._replyIds || []).slice(ps.offset, ps.offset + ps.limit);
|
res(await packMany(notes, user));
|
||||||
|
|
||||||
res(await packMany(ids, user));
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -17,7 +17,23 @@ export const meta = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
sort: {
|
sort: {
|
||||||
validator: $.str.optional.or('+follower|-follower'),
|
validator: $.str.optional.or([
|
||||||
|
'+follower',
|
||||||
|
'-follower',
|
||||||
|
'+createdAt',
|
||||||
|
'-createdAt',
|
||||||
|
'+updatedAt',
|
||||||
|
'-updatedAt',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
|
||||||
|
origin: {
|
||||||
|
validator: $.str.optional.or([
|
||||||
|
'combined',
|
||||||
|
'local',
|
||||||
|
'remote',
|
||||||
|
]),
|
||||||
|
default: 'local'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -33,6 +49,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
_sort = {
|
_sort = {
|
||||||
followersCount: 1
|
followersCount: 1
|
||||||
};
|
};
|
||||||
|
} else if (ps.sort == '+createdAt') {
|
||||||
|
_sort = {
|
||||||
|
createdAt: -1
|
||||||
|
};
|
||||||
|
} else if (ps.sort == '+updatedAt') {
|
||||||
|
_sort = {
|
||||||
|
updatedAt: -1
|
||||||
|
};
|
||||||
|
} else if (ps.sort == '-createdAt') {
|
||||||
|
_sort = {
|
||||||
|
createdAt: 1
|
||||||
|
};
|
||||||
|
} else if (ps.sort == '-updatedAt') {
|
||||||
|
_sort = {
|
||||||
|
updatedAt: 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_sort = {
|
_sort = {
|
||||||
@@ -40,14 +72,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const q =
|
||||||
|
ps.origin == 'local' ? { host: null } :
|
||||||
|
ps.origin == 'remote' ? { host: { $ne: null } } :
|
||||||
|
{};
|
||||||
|
|
||||||
const users = await User
|
const users = await User
|
||||||
.find({
|
.find(q, {
|
||||||
host: null
|
|
||||||
}, {
|
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
sort: _sort,
|
sort: _sort,
|
||||||
skip: ps.offset
|
skip: ps.offset
|
||||||
});
|
});
|
||||||
|
|
||||||
res(await Promise.all(users.map(user => pack(user, me))));
|
res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import Mute from '../../../../models/mute';
|
|||||||
import * as request from 'request';
|
import * as request from 'request';
|
||||||
import config from '../../../../config';
|
import config from '../../../../config';
|
||||||
import define from '../../define';
|
import define from '../../define';
|
||||||
|
import fetchMeta from '../../../../misc/fetch-meta';
|
||||||
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
@@ -30,13 +32,15 @@ export const meta = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
if (config.user_recommendation && config.user_recommendation.external) {
|
const instance = await fetchMeta();
|
||||||
|
|
||||||
|
if (instance.enableExternalUserRecommendation) {
|
||||||
const userName = me.username;
|
const userName = me.username;
|
||||||
const hostName = config.hostname;
|
const hostName = config.hostname;
|
||||||
const limit = ps.limit;
|
const limit = ps.limit;
|
||||||
const offset = ps.offset;
|
const offset = ps.offset;
|
||||||
const timeout = config.user_recommendation.timeout;
|
const timeout = instance.externalUserRecommendationTimeout;
|
||||||
const engine = config.user_recommendation.engine;
|
const engine = instance.externalUserRecommendationEngine;
|
||||||
const url = engine
|
const url = engine
|
||||||
.replace('{{host}}', hostName)
|
.replace('{{host}}', hostName)
|
||||||
.replace('{{user}}', userName)
|
.replace('{{user}}', userName)
|
||||||
@@ -72,7 +76,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
$nin: followingIds.concat(mutedUserIds)
|
$nin: followingIds.concat(mutedUserIds)
|
||||||
},
|
},
|
||||||
isLocked: { $ne: true },
|
isLocked: { $ne: true },
|
||||||
lastUsedAt: {
|
updatedAt: {
|
||||||
$gte: new Date(Date.now() - ms('7days'))
|
$gte: new Date(Date.now() - ms('7days'))
|
||||||
},
|
},
|
||||||
host: null
|
host: null
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
if (isRemoteUser(user)) {
|
if (isRemoteUser(user)) {
|
||||||
if (user.updatedAt == null || Date.now() - user.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
resolveRemoteUser(ps.username, ps.host, { }, true);
|
resolveRemoteUser(ps.username, ps.host, { }, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ app.use(cors({
|
|||||||
origin: '*'
|
origin: '*'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// No caching
|
||||||
|
app.use(async (ctx, next) => {
|
||||||
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
|
||||||
app.use(bodyParser({
|
app.use(bodyParser({
|
||||||
// リクエストが multipart/form-data でない限りはJSONだと見なす
|
// リクエストが multipart/form-data でない限りはJSONだと見なす
|
||||||
detectJSON: ctx => !ctx.is('multipart/form-data')
|
detectJSON: ctx => !ctx.is('multipart/form-data')
|
||||||
@@ -45,7 +51,6 @@ router.post('/signin', require('./private/signin').default);
|
|||||||
|
|
||||||
router.use(require('./service/discord').routes());
|
router.use(require('./service/discord').routes());
|
||||||
router.use(require('./service/github').routes());
|
router.use(require('./service/github').routes());
|
||||||
router.use(require('./service/github-bot').routes());
|
|
||||||
router.use(require('./service/twitter').routes());
|
router.use(require('./service/twitter').routes());
|
||||||
|
|
||||||
router.use(require('./mastodon').routes());
|
router.use(require('./mastodon').routes());
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -46,7 +46,6 @@ export default class Connection {
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'api': this.onApiRequest(body); break;
|
case 'api': this.onApiRequest(body); break;
|
||||||
case 'alive': this.onAlive(); break;
|
|
||||||
case 'readNotification': this.onReadNotification(body); break;
|
case 'readNotification': this.onReadNotification(body); break;
|
||||||
case 'subNote': this.onSubscribeNote(body); break;
|
case 'subNote': this.onSubscribeNote(body); break;
|
||||||
case 'sn': this.onSubscribeNote(body); break; // alias
|
case 'sn': this.onSubscribeNote(body); break; // alias
|
||||||
@@ -77,16 +76,6 @@ export default class Connection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
|
||||||
private onAlive() {
|
|
||||||
// Update lastUsedAt
|
|
||||||
User.update({ _id: this.user._id }, {
|
|
||||||
$set: {
|
|
||||||
'lastUsedAt': new Date()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
private onReadNotification(payload: any) {
|
private onReadNotification(payload: any) {
|
||||||
if (!payload.id) return;
|
if (!payload.id) return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as send from 'koa-send';
|
|||||||
import * as mongodb from 'mongodb';
|
import * as mongodb from 'mongodb';
|
||||||
import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
|
import DriveFile, { getDriveFileBucket } from '../../models/drive-file';
|
||||||
import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||||
|
import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
|
||||||
|
|
||||||
const assets = `${__dirname}/../../server/file/assets/`;
|
const assets = `${__dirname}/../../server/file/assets/`;
|
||||||
|
|
||||||
@@ -41,6 +42,11 @@ export default async function(ctx: Koa.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendRaw = async () => {
|
const sendRaw = async () => {
|
||||||
|
if (file.metadata && file.metadata.accessKey && file.metadata.accessKey != ctx.query['original']) {
|
||||||
|
ctx.status = 403;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bucket = await getDriveFileBucket();
|
const bucket = await getDriveFileBucket();
|
||||||
const readable = bucket.openDownloadStream(fileId);
|
const readable = bucket.openDownloadStream(fileId);
|
||||||
readable.on('error', commonReadableHandlerGenerator(ctx));
|
readable.on('error', commonReadableHandlerGenerator(ctx));
|
||||||
@@ -60,6 +66,19 @@ export default async function(ctx: Koa.Context) {
|
|||||||
} else {
|
} else {
|
||||||
await sendRaw();
|
await sendRaw();
|
||||||
}
|
}
|
||||||
|
} else if ('web' in ctx.query) {
|
||||||
|
const web = await DriveFileWebpublic.findOne({
|
||||||
|
'metadata.originalId': fileId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (web != null) {
|
||||||
|
ctx.set('Content-Type', file.contentType);
|
||||||
|
|
||||||
|
const bucket = await getDriveFileWebpublicBucket();
|
||||||
|
ctx.body = bucket.openDownloadStream(web._id);
|
||||||
|
} else {
|
||||||
|
await sendRaw();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if ('download' in ctx.query) {
|
if ('download' in ctx.query) {
|
||||||
ctx.set('Content-Disposition', 'attachment');
|
ctx.set('Content-Disposition', 'attachment');
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ const router = new Router();
|
|||||||
router.use(activityPub.routes());
|
router.use(activityPub.routes());
|
||||||
router.use(webFinger.routes());
|
router.use(webFinger.routes());
|
||||||
|
|
||||||
|
// Return 404 for other .well-known
|
||||||
|
router.all('/.well-known/*', async ctx => {
|
||||||
|
ctx.status = 404;
|
||||||
|
});
|
||||||
|
|
||||||
// Register router
|
// Register router
|
||||||
app.use(router.routes());
|
app.use(router.routes());
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ router.get('/notes/:note', async ctx => {
|
|||||||
note: _note,
|
note: _note,
|
||||||
summary: getNoteSummary(_note)
|
summary: getNoteSummary(_note)
|
||||||
});
|
});
|
||||||
ctx.set('Cache-Control', 'public, max-age=180');
|
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import * as Koa from 'koa';
|
import * as Koa from 'koa';
|
||||||
import * as request from 'request-promise-native';
|
import * as request from 'request-promise-native';
|
||||||
import summaly from 'summaly';
|
import summaly from 'summaly';
|
||||||
import config from '../../config';
|
import fetchMeta from '../../misc/fetch-meta';
|
||||||
|
|
||||||
module.exports = async (ctx: Koa.Context) => {
|
module.exports = async (ctx: Koa.Context) => {
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const summary = config.summalyProxy ? await request.get({
|
const summary = meta.summalyProxy ? await request.get({
|
||||||
url: config.summalyProxy,
|
url: meta.summalyProxy,
|
||||||
proxy: config.proxy,
|
|
||||||
qs: {
|
qs: {
|
||||||
url: ctx.query.url
|
url: ctx.query.url
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { publishMainStream, publishDriveStream } from '../../stream';
|
|||||||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
||||||
import delFile from './delete-file';
|
import delFile from './delete-file';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
|
import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
|
||||||
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||||
import driveChart from '../../chart/drive';
|
import driveChart from '../../chart/drive';
|
||||||
import perUserDriveChart from '../../chart/per-user-drive';
|
import perUserDriveChart from '../../chart/per-user-drive';
|
||||||
@@ -23,7 +24,71 @@ import fetchMeta from '../../misc/fetch-meta';
|
|||||||
|
|
||||||
const log = debug('misskey:drive:add-file');
|
const log = debug('misskey:drive:add-file');
|
||||||
|
|
||||||
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: any): Promise<IDriveFile> {
|
/***
|
||||||
|
* Save file
|
||||||
|
* @param path Path for original
|
||||||
|
* @param name Name for original
|
||||||
|
* @param type Content-Type for original
|
||||||
|
* @param hash Hash for original
|
||||||
|
* @param size Size for original
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> {
|
||||||
|
// #region webpublic
|
||||||
|
let webpublic: Buffer;
|
||||||
|
let webpublicExt = 'jpg';
|
||||||
|
let webpublicType = 'image/jpeg';
|
||||||
|
|
||||||
|
if (!metadata.uri) { // from local instance
|
||||||
|
log(`creating web image`);
|
||||||
|
|
||||||
|
if (['image/jpeg'].includes(type)) {
|
||||||
|
webpublic = await sharp(path)
|
||||||
|
.resize(2048, 2048, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.rotate()
|
||||||
|
.jpeg({
|
||||||
|
quality: 85,
|
||||||
|
progressive: true
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
} else if (['image/webp'].includes(type)) {
|
||||||
|
webpublic = await sharp(path)
|
||||||
|
.resize(2048, 2048, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.rotate()
|
||||||
|
.webp({
|
||||||
|
quality: 85
|
||||||
|
})
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
webpublicExt = 'webp';
|
||||||
|
webpublicType = 'image/webp';
|
||||||
|
} else if (['image/png'].includes(type)) {
|
||||||
|
webpublic = await sharp(path)
|
||||||
|
.resize(2048, 2048, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
})
|
||||||
|
.rotate()
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
webpublicExt = 'png';
|
||||||
|
webpublicType = 'image/png';
|
||||||
|
} else {
|
||||||
|
log(`web image not created (not an image)`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`web image not created (from remote)`);
|
||||||
|
}
|
||||||
|
// #endregion webpublic
|
||||||
|
|
||||||
|
// #region thumbnail
|
||||||
let thumbnail: Buffer;
|
let thumbnail: Buffer;
|
||||||
let thumbnailExt = 'jpg';
|
let thumbnailExt = 'jpg';
|
||||||
let thumbnailType = 'image/jpeg';
|
let thumbnailType = 'image/jpeg';
|
||||||
@@ -53,10 +118,9 @@ async function save(path: string, name: string, type: string, hash: string, size
|
|||||||
thumbnailExt = 'png';
|
thumbnailExt = 'png';
|
||||||
thumbnailType = 'image/png';
|
thumbnailType = 'image/png';
|
||||||
}
|
}
|
||||||
|
// #endregion thumbnail
|
||||||
|
|
||||||
if (config.drive && config.drive.storage == 'minio') {
|
if (config.drive && config.drive.storage == 'minio') {
|
||||||
const minio = new Minio.Client(config.drive.config);
|
|
||||||
|
|
||||||
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
|
let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) || ['']);
|
||||||
|
|
||||||
if (ext === '') {
|
if (ext === '') {
|
||||||
@@ -66,33 +130,41 @@ async function save(path: string, name: string, type: string, hash: string, size
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;
|
const key = `${config.drive.prefix}/${uuid.v4()}${ext}`;
|
||||||
|
const webpublicKey = `${config.drive.prefix}/${uuid.v4()}.${webpublicExt}`;
|
||||||
const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${thumbnailExt}`;
|
const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}.${thumbnailExt}`;
|
||||||
|
|
||||||
|
log(`uploading original: ${key}`);
|
||||||
|
const uploads = [
|
||||||
|
upload(key, fs.createReadStream(path), type)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (webpublic) {
|
||||||
|
log(`uploading webpublic: ${webpublicKey}`);
|
||||||
|
uploads.push(upload(webpublicKey, webpublic, webpublicType));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thumbnail) {
|
||||||
|
log(`uploading thumbnail: ${thumbnailKey}`);
|
||||||
|
uploads.push(upload(thumbnailKey, thumbnail, thumbnailType));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(uploads);
|
||||||
|
|
||||||
const baseUrl = config.drive.baseUrl
|
const baseUrl = config.drive.baseUrl
|
||||||
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
|
|| `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`;
|
||||||
|
|
||||||
await minio.putObject(config.drive.bucket, key, fs.createReadStream(path), size, {
|
|
||||||
'Content-Type': type,
|
|
||||||
'Cache-Control': 'max-age=31536000, immutable'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (thumbnail) {
|
|
||||||
await minio.putObject(config.drive.bucket, thumbnailKey, thumbnail, size, {
|
|
||||||
'Content-Type': thumbnailType,
|
|
||||||
'Cache-Control': 'max-age=31536000, immutable'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(metadata, {
|
Object.assign(metadata, {
|
||||||
withoutChunks: true,
|
withoutChunks: true,
|
||||||
storage: 'minio',
|
storage: 'minio',
|
||||||
storageProps: {
|
storageProps: {
|
||||||
key: key,
|
key: key,
|
||||||
thumbnailKey: thumbnailKey
|
webpublicKey: webpublic ? webpublicKey : null,
|
||||||
|
thumbnailKey: thumbnail ? thumbnailKey : null,
|
||||||
},
|
},
|
||||||
url: `${ baseUrl }/${ key }`,
|
url: `${ baseUrl }/${ key }`,
|
||||||
|
webpublicUrl: webpublic ? `${ baseUrl }/${ webpublicKey }` : null,
|
||||||
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null
|
thumbnailUrl: thumbnail ? `${ baseUrl }/${ thumbnailKey }` : null
|
||||||
});
|
} as IMetadata);
|
||||||
|
|
||||||
const file = await DriveFile.insert({
|
const file = await DriveFile.insert({
|
||||||
length: size,
|
length: size,
|
||||||
@@ -105,29 +177,55 @@ async function save(path: string, name: string, type: string, hash: string, size
|
|||||||
|
|
||||||
return file;
|
return file;
|
||||||
} else {
|
} else {
|
||||||
// Get MongoDB GridFS bucket
|
// #region store original
|
||||||
const bucket = await getDriveFileBucket();
|
const originalDst = await getDriveFileBucket();
|
||||||
|
|
||||||
const file = await new Promise<IDriveFile>((resolve, reject) => {
|
// web用(Exif削除済み)がある場合はオリジナルにアクセス制限
|
||||||
const writeStream = bucket.openUploadStream(name, {
|
if (webpublic) metadata.accessKey = uuid.v4();
|
||||||
|
|
||||||
|
const originalFile = await new Promise<IDriveFile>((resolve, reject) => {
|
||||||
|
const writeStream = originalDst.openUploadStream(name, {
|
||||||
contentType: type,
|
contentType: type,
|
||||||
metadata
|
metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
writeStream.once('finish', resolve);
|
writeStream.once('finish', resolve);
|
||||||
writeStream.on('error', reject);
|
writeStream.on('error', reject);
|
||||||
|
|
||||||
fs.createReadStream(path).pipe(writeStream);
|
fs.createReadStream(path).pipe(writeStream);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log(`original stored to ${originalFile._id}`);
|
||||||
|
// #endregion store original
|
||||||
|
|
||||||
|
// #region store webpublic
|
||||||
|
if (webpublic) {
|
||||||
|
const webDst = await getDriveFileWebpublicBucket();
|
||||||
|
|
||||||
|
const webFile = await new Promise<IDriveFile>((resolve, reject) => {
|
||||||
|
const writeStream = webDst.openUploadStream(name, {
|
||||||
|
contentType: webpublicType,
|
||||||
|
metadata: {
|
||||||
|
originalId: originalFile._id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
writeStream.once('finish', resolve);
|
||||||
|
writeStream.on('error', reject);
|
||||||
|
writeStream.end(webpublic);
|
||||||
|
});
|
||||||
|
|
||||||
|
log(`web stored ${webFile._id}`);
|
||||||
|
}
|
||||||
|
// #endregion store webpublic
|
||||||
|
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
const thumbnailBucket = await getDriveFileThumbnailBucket();
|
const thumbnailBucket = await getDriveFileThumbnailBucket();
|
||||||
|
|
||||||
await new Promise<IDriveFile>((resolve, reject) => {
|
const tuhmFile = await new Promise<IDriveFile>((resolve, reject) => {
|
||||||
const writeStream = thumbnailBucket.openUploadStream(name, {
|
const writeStream = thumbnailBucket.openUploadStream(name, {
|
||||||
contentType: thumbnailType,
|
contentType: thumbnailType,
|
||||||
metadata: {
|
metadata: {
|
||||||
originalId: file._id
|
originalId: originalFile._id
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,12 +233,23 @@ async function save(path: string, name: string, type: string, hash: string, size
|
|||||||
writeStream.on('error', reject);
|
writeStream.on('error', reject);
|
||||||
writeStream.end(thumbnail);
|
writeStream.end(thumbnail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log(`thumbnail stored ${tuhmFile._id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return file;
|
return originalFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function upload(key: string, stream: fs.ReadStream | Buffer, type: string) {
|
||||||
|
const minio = new Minio.Client(config.drive.config);
|
||||||
|
|
||||||
|
await minio.putObject(config.drive.bucket, key, stream, null, {
|
||||||
|
'Content-Type': type,
|
||||||
|
'Cache-Control': 'max-age=31536000, immutable'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteOldFile(user: IRemoteUser) {
|
async function deleteOldFile(user: IRemoteUser) {
|
||||||
const oldFile = await DriveFile.findOne({
|
const oldFile = await DriveFile.findOne({
|
||||||
_id: {
|
_id: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import driveChart from '../../chart/drive';
|
import driveChart from '../../chart/drive';
|
||||||
import perUserDriveChart from '../../chart/per-user-drive';
|
import perUserDriveChart from '../../chart/per-user-drive';
|
||||||
|
import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
|
||||||
|
|
||||||
export default async function(file: IDriveFile, isExpired = false) {
|
export default async function(file: IDriveFile, isExpired = false) {
|
||||||
if (file.metadata.storage == 'minio') {
|
if (file.metadata.storage == 'minio') {
|
||||||
@@ -20,6 +21,11 @@ export default async function(file: IDriveFile, isExpired = false) {
|
|||||||
const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
|
const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`;
|
||||||
await minio.removeObject(config.drive.bucket, thumbnailObj);
|
await minio.removeObject(config.drive.bucket, thumbnailObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (file.metadata.webpublicUrl) {
|
||||||
|
const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`;
|
||||||
|
await minio.removeObject(config.drive.bucket, webpublicObj);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// チャンクをすべて削除
|
// チャンクをすべて削除
|
||||||
@@ -48,6 +54,20 @@ export default async function(file: IDriveFile, isExpired = false) {
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region Web公開用もあれば削除
|
||||||
|
const webpublic = await DriveFileWebpublic.findOne({
|
||||||
|
'metadata.originalId': file._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (webpublic) {
|
||||||
|
await DriveFileWebpublicChunk.remove({
|
||||||
|
files_id: webpublic._id
|
||||||
|
});
|
||||||
|
|
||||||
|
await DriveFileWebpublic.remove({ _id: webpublic._id });
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
// 統計を更新
|
// 統計を更新
|
||||||
driveChart.update(file, false);
|
driveChart.update(file, false);
|
||||||
perUserDriveChart.update(file, false);
|
perUserDriveChart.update(file, false);
|
||||||
|
|||||||
@@ -622,9 +622,6 @@ function saveQuote(renote: INote, note: INote) {
|
|||||||
|
|
||||||
function saveReply(reply: INote, note: INote) {
|
function saveReply(reply: INote, note: INote) {
|
||||||
Note.update({ _id: reply._id }, {
|
Note.update({ _id: reply._id }, {
|
||||||
$push: {
|
|
||||||
_replyIds: note._id
|
|
||||||
},
|
|
||||||
$inc: {
|
$inc: {
|
||||||
repliesCount: 1
|
repliesCount: 1
|
||||||
}
|
}
|
||||||
@@ -633,6 +630,9 @@ function saveReply(reply: INote, note: INote) {
|
|||||||
|
|
||||||
function incNotesCountOfUser(user: IUser) {
|
function incNotesCountOfUser(user: IUser) {
|
||||||
User.update({ _id: user._id }, {
|
User.update({ _id: user._id }, {
|
||||||
|
$set: {
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
$inc: {
|
$inc: {
|
||||||
notesCount: 1
|
notesCount: 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ describe('API', () => {
|
|||||||
const file = await uploadFile(bob);
|
const file = await uploadFile(bob);
|
||||||
|
|
||||||
const res = await request('/notes/create', {
|
const res = await request('/notes/create', {
|
||||||
|
text: 'test',
|
||||||
fileIds: [file.id]
|
fileIds: [file.id]
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
@@ -327,6 +328,7 @@ describe('API', () => {
|
|||||||
const me = await signup();
|
const me = await signup();
|
||||||
|
|
||||||
const res = await request('/notes/create', {
|
const res = await request('/notes/create', {
|
||||||
|
text: 'test',
|
||||||
fileIds: ['000000000000000000000000']
|
fileIds: ['000000000000000000000000']
|
||||||
}, me);
|
}, me);
|
||||||
|
|
||||||
|
|||||||
200
test/mfm.ts
200
test/mfm.ts
@@ -162,27 +162,87 @@ describe('Text', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hashtag', () => {
|
describe('hashtag', () => {
|
||||||
const tokens1 = analyze('Strawberry Pasta #alice');
|
it('simple', () => {
|
||||||
assert.deepEqual([
|
const tokens = analyze('#alice');
|
||||||
text('Strawberry Pasta '),
|
assert.deepEqual([
|
||||||
node('hashtag', { hashtag: 'alice' })
|
node('hashtag', { hashtag: 'alice' })
|
||||||
], tokens1);
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
const tokens2 = analyze('Foo #bar, baz #piyo.');
|
it('after line break', () => {
|
||||||
assert.deepEqual([
|
const tokens = analyze('foo\n#alice');
|
||||||
text('Foo '),
|
assert.deepEqual([
|
||||||
node('hashtag', { hashtag: 'bar' }),
|
text('foo\n'),
|
||||||
text(', baz '),
|
node('hashtag', { hashtag: 'alice' })
|
||||||
node('hashtag', { hashtag: 'piyo' }),
|
], tokens);
|
||||||
text('.'),
|
});
|
||||||
], tokens2);
|
|
||||||
|
|
||||||
const tokens3 = analyze('#Foo!');
|
it('with text', () => {
|
||||||
assert.deepEqual([
|
const tokens = analyze('Strawberry Pasta #alice');
|
||||||
node('hashtag', { hashtag: 'Foo' }),
|
assert.deepEqual([
|
||||||
text('!'),
|
text('Strawberry Pasta '),
|
||||||
], tokens3);
|
node('hashtag', { hashtag: 'alice' })
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignore comma and period', () => {
|
||||||
|
const tokens = analyze('Foo #bar, baz #piyo.');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('Foo '),
|
||||||
|
node('hashtag', { hashtag: 'bar' }),
|
||||||
|
text(', baz '),
|
||||||
|
node('hashtag', { hashtag: 'piyo' }),
|
||||||
|
text('.'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignore exclamation mark', () => {
|
||||||
|
const tokens = analyze('#Foo!');
|
||||||
|
assert.deepEqual([
|
||||||
|
node('hashtag', { hashtag: 'Foo' }),
|
||||||
|
text('!'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allow including number', () => {
|
||||||
|
const tokens = analyze('#foo123');
|
||||||
|
assert.deepEqual([
|
||||||
|
node('hashtag', { hashtag: 'foo123' }),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with brackets', () => {
|
||||||
|
const tokens = analyze('(#foo)');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('('),
|
||||||
|
node('hashtag', { hashtag: 'foo' }),
|
||||||
|
text(')'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with brackets (space before)', () => {
|
||||||
|
const tokens = analyze('(bar #foo)');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('(bar '),
|
||||||
|
node('hashtag', { hashtag: 'foo' }),
|
||||||
|
text(')'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallow number only', () => {
|
||||||
|
const tokens = analyze('#123');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('#123'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallow number only (with brackets)', () => {
|
||||||
|
const tokens = analyze('(#123)');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('(#123)'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('quote', () => {
|
describe('quote', () => {
|
||||||
@@ -360,6 +420,15 @@ describe('Text', () => {
|
|||||||
], tokens);
|
], tokens);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('ignore parent brackets 2', () => {
|
||||||
|
const tokens = analyze('(foo https://example.com/foo)');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('(foo '),
|
||||||
|
node('url', { url: 'https://example.com/foo' }),
|
||||||
|
text(')')
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
it('ignore parent brackets with internal brackets', () => {
|
it('ignore parent brackets with internal brackets', () => {
|
||||||
const tokens = analyze('(https://example.com/foo(bar))');
|
const tokens = analyze('(https://example.com/foo(bar))');
|
||||||
assert.deepEqual([
|
assert.deepEqual([
|
||||||
@@ -370,13 +439,55 @@ describe('Text', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('link', () => {
|
describe('link', () => {
|
||||||
const tokens = analyze('[foo](https://example.com)');
|
it('simple', () => {
|
||||||
assert.deepEqual([
|
const tokens = analyze('[foo](https://example.com)');
|
||||||
nodeWithChildren('link', [
|
assert.deepEqual([
|
||||||
text('foo')
|
nodeWithChildren('link', [
|
||||||
], { url: 'https://example.com', silent: false })
|
text('foo')
|
||||||
], tokens);
|
], { url: 'https://example.com', silent: false })
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('simple (with silent flag)', () => {
|
||||||
|
const tokens = analyze('?[foo](https://example.com)');
|
||||||
|
assert.deepEqual([
|
||||||
|
nodeWithChildren('link', [
|
||||||
|
text('foo')
|
||||||
|
], { url: 'https://example.com', silent: true })
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('in text', () => {
|
||||||
|
const tokens = analyze('before[foo](https://example.com)after');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('before'),
|
||||||
|
nodeWithChildren('link', [
|
||||||
|
text('foo')
|
||||||
|
], { url: 'https://example.com', silent: false }),
|
||||||
|
text('after'),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with brackets', () => {
|
||||||
|
const tokens = analyze('[foo](https://example.com/foo(bar))');
|
||||||
|
assert.deepEqual([
|
||||||
|
nodeWithChildren('link', [
|
||||||
|
text('foo')
|
||||||
|
], { url: 'https://example.com/foo(bar)', silent: false })
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with parent brackets', () => {
|
||||||
|
const tokens = analyze('([foo](https://example.com/foo(bar)))');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('('),
|
||||||
|
nodeWithChildren('link', [
|
||||||
|
text('foo')
|
||||||
|
], { url: 'https://example.com/foo(bar)', silent: false }),
|
||||||
|
text(')')
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emoji', () => {
|
it('emoji', () => {
|
||||||
@@ -448,11 +559,27 @@ describe('Text', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('inline code', () => {
|
describe('inline code', () => {
|
||||||
const tokens = analyze('`var x = "Strawberry Pasta";`');
|
it('simple', () => {
|
||||||
assert.deepEqual([
|
const tokens = analyze('`var x = "Strawberry Pasta";`');
|
||||||
node('inlineCode', { code: 'var x = "Strawberry Pasta";' })
|
assert.deepEqual([
|
||||||
], tokens);
|
node('inlineCode', { code: 'var x = "Strawberry Pasta";' })
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallow line break', () => {
|
||||||
|
const tokens = analyze('`foo\nbar`');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('`foo\nbar`')
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallow ´', () => {
|
||||||
|
const tokens = analyze('`foo´bar`');
|
||||||
|
assert.deepEqual([
|
||||||
|
text('`foo´bar`')
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('math', () => {
|
it('math', () => {
|
||||||
@@ -514,6 +641,17 @@ describe('Text', () => {
|
|||||||
], tokens);
|
], tokens);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('center', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
const tokens = analyze('<center>foo</center>');
|
||||||
|
assert.deepEqual([
|
||||||
|
nodeWithChildren('center', [
|
||||||
|
text('foo')
|
||||||
|
]),
|
||||||
|
], tokens);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toHtml', () => {
|
describe('toHtml', () => {
|
||||||
|
|||||||
@@ -5,21 +5,22 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as webpack from 'webpack';
|
import * as webpack from 'webpack';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
import rndstr from 'rndstr';
|
||||||
const { VueLoaderPlugin } = require('vue-loader');
|
const { VueLoaderPlugin } = require('vue-loader');
|
||||||
const WebpackOnBuildPlugin = require('on-build-webpack');
|
const WebpackOnBuildPlugin = require('on-build-webpack');
|
||||||
//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
//const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
|
||||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
|
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV == 'production';
|
||||||
|
|
||||||
const constants = require('./src/const.json');
|
const constants = require('./src/const.json');
|
||||||
|
|
||||||
const locales = require('./locales');
|
const locales = require('./locales');
|
||||||
const meta = require('./package.json');
|
const meta = require('./package.json');
|
||||||
const version = meta.clientVersion;
|
const version = isProduction ? meta.clientVersion : meta.clientVersion + '-' + rndstr({ length: 8, chars: '0-9a-z' });
|
||||||
const codename = meta.codename;
|
const codename = meta.codename;
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV == 'production';
|
|
||||||
|
|
||||||
const postcss = {
|
const postcss = {
|
||||||
loader: 'postcss-loader',
|
loader: 'postcss-loader',
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
Reference in New Issue
Block a user