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