Compare commits
442 Commits
13.10.0
...
13.11.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1aa8f09b95 | ||
|
|
65584f21bb | ||
|
|
b697f948df | ||
|
|
551b00302b | ||
|
|
05aa0fa11a | ||
|
|
008e3fb37e | ||
|
|
68e8892f61 | ||
|
|
25ebb73756 | ||
|
|
fa67fb42b1 | ||
|
|
40b3041608 | ||
|
|
e3b8e8746e | ||
|
|
33bce49ee8 | ||
|
|
44a4faebc0 | ||
|
|
79f198e4f1 | ||
|
|
eb30976ae6 | ||
|
|
abda3b6c8c | ||
|
|
191ed3c814 | ||
|
|
7da2ca1862 | ||
|
|
cb39db100a | ||
|
|
e3f4c9bcf6 | ||
|
|
ed4a100e96 | ||
|
|
1377ea4178 | ||
|
|
6e1ae7b242 | ||
|
|
930724f9de | ||
|
|
3bbeec70ec | ||
|
|
69828e8dac | ||
|
|
58c8d21348 | ||
|
|
1e3fb5531b | ||
|
|
239d3f2dbf | ||
|
|
ff6d9d2860 | ||
|
|
f4588f3907 | ||
|
|
a561b83070 | ||
|
|
88f22da052 | ||
|
|
89c12948a1 | ||
|
|
f8cdab6d02 | ||
|
|
521eb95dbc | ||
|
|
c29f21ac56 | ||
|
|
de9d136a3f | ||
|
|
a0d5e2469d | ||
|
|
b38811af7c | ||
|
|
bc5be83e29 | ||
|
|
52011610c6 | ||
|
|
a2e022631d | ||
|
|
a574d0fbcb | ||
|
|
e9165a0d95 | ||
|
|
73203a3d72 | ||
|
|
437de6417e | ||
|
|
fd05826187 | ||
|
|
bd142b8fe5 | ||
|
|
384a3bed09 | ||
|
|
3c04fef8d1 | ||
|
|
9629929977 | ||
|
|
e76d3e72db | ||
|
|
19349b930a | ||
|
|
10c3c15f06 | ||
|
|
3b3f683f8c | ||
|
|
516a791bf4 | ||
|
|
2650a7a5b8 | ||
|
|
712c60106a | ||
|
|
41aafdea00 | ||
|
|
0bb882c9d4 | ||
|
|
3f3eca7d3a | ||
|
|
62efe2b4d5 | ||
|
|
6798effbab | ||
|
|
d739aeee32 | ||
|
|
a093c373c4 | ||
|
|
288cce4a22 | ||
|
|
28647de196 | ||
|
|
cf6a302f8f | ||
|
|
1c3d9a5df0 | ||
|
|
f44504097c | ||
|
|
625fed8838 | ||
|
|
3f39fc90fc | ||
|
|
7bd0001e76 | ||
|
|
ecaf152b4a | ||
|
|
7f3afac0a2 | ||
|
|
a5f76c064e | ||
|
|
d6e04da20b | ||
|
|
55652a7a18 | ||
|
|
7e845ebbc2 | ||
|
|
6954a6300c | ||
|
|
30d6992684 | ||
|
|
c25d89ef9c | ||
|
|
38d0b62167 | ||
|
|
b91d831e82 | ||
|
|
8a0201fe9c | ||
|
|
ce1fb06ce8 | ||
|
|
14591aa8f2 | ||
|
|
38b9fc4f25 | ||
|
|
32149f5122 | ||
|
|
211682d7ec | ||
|
|
b3b64d8525 | ||
|
|
2acb655c0f | ||
|
|
8d90e88e16 | ||
|
|
9b004248a9 | ||
|
|
b303a80491 | ||
|
|
b1b871aebf | ||
|
|
58f3a2ee94 | ||
|
|
8a9847b06a | ||
|
|
da6b30c5dd | ||
|
|
92ddebb3fd | ||
|
|
b53d6c7f8c | ||
|
|
c032dd1214 | ||
|
|
4f9f625e65 | ||
|
|
a46bfc7c14 | ||
|
|
4f7ba294d9 | ||
|
|
902d7c5538 | ||
|
|
3877b366b3 | ||
|
|
423a67fb06 | ||
|
|
3ab2a008f2 | ||
|
|
a61fe9023f | ||
|
|
36e8988ff7 | ||
|
|
a0c3dede8e | ||
|
|
dfb6df018a | ||
|
|
d6b1e5be2f | ||
|
|
ddb1635a46 | ||
|
|
9946ba0f08 | ||
|
|
29c63adfb4 | ||
|
|
d57a504cac | ||
|
|
e4f6fac460 | ||
|
|
f9693d1fad | ||
|
|
252fe24e8d | ||
|
|
5143f3372c | ||
|
|
5ab02cbeea | ||
|
|
faf827a50a | ||
|
|
a217344315 | ||
|
|
388ecfb1ea | ||
|
|
9982ead9e6 | ||
|
|
903d0bd3e2 | ||
|
|
edeabac8f7 | ||
|
|
3f4d93a287 | ||
|
|
b9289c397b | ||
|
|
97e7f7efcc | ||
|
|
fc76829eb4 | ||
|
|
8b30a0d641 | ||
|
|
97731705ba | ||
|
|
b0fa9f3809 | ||
|
|
20fb266a93 | ||
|
|
7256b1f8d3 | ||
|
|
0f334f900c | ||
|
|
88981e9d5b | ||
|
|
ec8905f81c | ||
|
|
65f20de821 | ||
|
|
e7a15b5604 | ||
|
|
4521e61ae2 | ||
|
|
f39b5be064 | ||
|
|
0960d61ce2 | ||
|
|
1521bb088c | ||
|
|
38b153ca94 | ||
|
|
f5fd59d656 | ||
|
|
f7bc442ba5 | ||
|
|
139dade62c | ||
|
|
3f1a87d1ad | ||
|
|
f3a29cbb0d | ||
|
|
f246699f38 | ||
|
|
bc5e457a63 | ||
|
|
9224b6635f | ||
|
|
5667f0f839 | ||
|
|
d300f4a87b | ||
|
|
f16a7cbcf2 | ||
|
|
c6d12c21db | ||
|
|
2b831da1d1 | ||
|
|
7a8a756789 | ||
|
|
a47f4b8955 | ||
|
|
c823cbe63b | ||
|
|
5605b03ce4 | ||
|
|
671ce6a531 | ||
|
|
14977ed2b7 | ||
|
|
13a406b6cf | ||
|
|
a9f0bea60c | ||
|
|
152247bfda | ||
|
|
6a4d56c81c | ||
|
|
85f50f3875 | ||
|
|
558d73b21a | ||
|
|
bf9846a269 | ||
|
|
70fc25aac1 | ||
|
|
9bc5d52e41 | ||
|
|
f0a70a70c3 | ||
|
|
3cb0cc7989 | ||
|
|
5d94062581 | ||
|
|
a23b25bff7 | ||
|
|
98fd6b5879 | ||
|
|
5393436403 | ||
|
|
d1c5556724 | ||
|
|
3db0147392 | ||
|
|
26068a3a8f | ||
|
|
0398787520 | ||
|
|
dccf03ffc2 | ||
|
|
6d78162323 | ||
|
|
74e0824b1d | ||
|
|
231b54a889 | ||
|
|
d7fbef0a55 | ||
|
|
ea6f9f1e7d | ||
|
|
ffbe896f28 | ||
|
|
e9769c937b | ||
|
|
e03d5a86a4 | ||
|
|
bb2b33f2c9 | ||
|
|
0fa9f945cf | ||
|
|
5c80ad8fe9 | ||
|
|
cee1d5e2d0 | ||
|
|
49d4c538fe | ||
|
|
c1d9e23171 | ||
|
|
8963e36aa2 | ||
|
|
4d3f0be419 | ||
|
|
07cfab0489 | ||
|
|
453574644c | ||
|
|
a529b0e5a3 | ||
|
|
bc6d576a4e | ||
|
|
356fd72603 | ||
|
|
225a8e11a9 | ||
|
|
4a0995a175 | ||
|
|
6fe12b52fd | ||
|
|
31cc188bbd | ||
|
|
772e05e835 | ||
|
|
abdb43c440 | ||
|
|
efb5e1d1cc | ||
|
|
3b524f32bf | ||
|
|
899c6241c3 | ||
|
|
8ca9621d42 | ||
|
|
b56d173f8a | ||
|
|
2bce064ada | ||
|
|
5be54451a8 | ||
|
|
458b9feb62 | ||
|
|
8332c03bd8 | ||
|
|
723cff0205 | ||
|
|
bd7cc6ac38 | ||
|
|
e7fc998459 | ||
|
|
da2483f7fe | ||
|
|
31f3f5f0f0 | ||
|
|
09a846a45c | ||
|
|
4edc7d8956 | ||
|
|
6a29b182ee | ||
|
|
46f99755db | ||
|
|
05cba6d357 | ||
|
|
c4e536ddbf | ||
|
|
d42c35cae7 | ||
|
|
8a1ccd123d | ||
|
|
cabed6098d | ||
|
|
5f52b13325 | ||
|
|
e438091113 | ||
|
|
4a17906530 | ||
|
|
ab26a8016d | ||
|
|
972963913b | ||
|
|
f01e6ef6bf | ||
|
|
7f9a41b05c | ||
|
|
41d1b1b8c9 | ||
|
|
48a97d25e5 | ||
|
|
c2d8759812 | ||
|
|
658901a47f | ||
|
|
050787238b | ||
|
|
fb42721160 | ||
|
|
573634706d | ||
|
|
c6fc9af77a | ||
|
|
2474c7f4cb | ||
|
|
d2f9798894 | ||
|
|
38815f6214 | ||
|
|
640ad17854 | ||
|
|
24a3a0bbff | ||
|
|
bf5fff879f | ||
|
|
1d0ca7eecf | ||
|
|
5ced8aef27 | ||
|
|
49f8bb4974 | ||
|
|
cc3fbefc83 | ||
|
|
bf527cd6e7 | ||
|
|
0ac4d744fd | ||
|
|
bdbbb92ff6 | ||
|
|
2eecb8e876 | ||
|
|
2ec608fdba | ||
|
|
ca7d2081a2 | ||
|
|
3907cb51d6 | ||
|
|
be7b71a2f5 | ||
|
|
6e21147ff6 | ||
|
|
a19c560e7b | ||
|
|
0dcb99df19 | ||
|
|
91e0e498eb | ||
|
|
1dfcca7b9c | ||
|
|
13a2d16eab | ||
|
|
4922cb92e0 | ||
|
|
cc6a2670c8 | ||
|
|
4f62f6f288 | ||
|
|
fc921d8c1d | ||
|
|
c89374c321 | ||
|
|
6a87f4ade9 | ||
|
|
c17c42fa05 | ||
|
|
5531a1fdf2 | ||
|
|
78ee600752 | ||
|
|
bc9525c665 | ||
|
|
b41c1fe3af | ||
|
|
84dffdf510 | ||
|
|
8ab9b92f60 | ||
|
|
287f595239 | ||
|
|
cbce1242b9 | ||
|
|
f3640add23 | ||
|
|
a05f47d5cc | ||
|
|
6c990f3ce7 | ||
|
|
a4b14d54f2 | ||
|
|
2c079a9608 | ||
|
|
233ffe46d5 | ||
|
|
231ac8e323 | ||
|
|
755189f6f3 | ||
|
|
d2b634c349 | ||
|
|
4ff49b0a3f | ||
|
|
2bf63b158c | ||
|
|
de81437248 | ||
|
|
043ffe3a2e | ||
|
|
43d9149c8a | ||
|
|
f08a840d7d | ||
|
|
714753d001 | ||
|
|
7f3943f794 | ||
|
|
80ef07222d | ||
|
|
510a1408a2 | ||
|
|
34afa60aff | ||
|
|
71f35d90c5 | ||
|
|
8ba56df54e | ||
|
|
227e603194 | ||
|
|
4021c3c61a | ||
|
|
d549879cd4 | ||
|
|
b7fd9e8c55 | ||
|
|
f278390012 | ||
|
|
fa98dabeec | ||
|
|
1243e6a655 | ||
|
|
7a148e9ffa | ||
|
|
a526f75a3e | ||
|
|
b1b90185c4 | ||
|
|
ab391870ed | ||
|
|
3e6ad67d59 | ||
|
|
39668a21d7 | ||
|
|
85eb00b0a6 | ||
|
|
0c61d052d2 | ||
|
|
e6e339cc3e | ||
|
|
8ea124b999 | ||
|
|
7ee3b4536c | ||
|
|
99fa6e3cd0 | ||
|
|
32ff71a67d | ||
|
|
2ffff0fa61 | ||
|
|
51ca78d496 | ||
|
|
677ed40b40 | ||
|
|
0616534af7 | ||
|
|
7a41dc2a7a | ||
|
|
65f0b195a2 | ||
|
|
170e84360c | ||
|
|
42b611de9b | ||
|
|
015d169423 | ||
|
|
56958d2397 | ||
|
|
b11c00ccdc | ||
|
|
dfc8a09f44 | ||
|
|
127a1320a8 | ||
|
|
4e93f2f360 | ||
|
|
76d0a9cc5d | ||
|
|
99dc38c6e0 | ||
|
|
fe1c8b882a | ||
|
|
0acaa2f3b0 | ||
|
|
78861f8a31 | ||
|
|
05a902f3a5 | ||
|
|
0bab055441 | ||
|
|
adb168dfdc | ||
|
|
8bc7daa3f3 | ||
|
|
b6df8cc1f5 | ||
|
|
b95775041c | ||
|
|
36bfaeba12 | ||
|
|
e40f0800a0 | ||
|
|
154e0fe94a | ||
|
|
c6285944a1 | ||
|
|
037f68858d | ||
|
|
873f24618c | ||
|
|
17c9f6eace | ||
|
|
2a84e5034e | ||
|
|
bc3b3bb1c0 | ||
|
|
b729000687 | ||
|
|
64cf49c378 | ||
|
|
f894d978df | ||
|
|
ce12fc2b67 | ||
|
|
091ef2e240 | ||
|
|
1d17516aa8 | ||
|
|
b5a01217b1 | ||
|
|
b9056a9fd9 | ||
|
|
aa9182bd50 | ||
|
|
c75ff55bbc | ||
|
|
1e7219b043 | ||
|
|
5431f4fb86 | ||
|
|
1cdf9e19a0 | ||
|
|
5e6e1084cf | ||
|
|
a1f2dd7846 | ||
|
|
b2549d98f9 | ||
|
|
b1934d95f2 | ||
|
|
d9fbddc26c | ||
|
|
b818da3b1c | ||
|
|
509dc30ae6 | ||
|
|
8c08be47cb | ||
|
|
691f0d07b9 | ||
|
|
a61507e971 | ||
|
|
70aaf34094 | ||
|
|
d311f74349 | ||
|
|
893fa691a7 | ||
|
|
39270c782a | ||
|
|
adae7cf2ec | ||
|
|
6ae5f76250 | ||
|
|
ca655c0628 | ||
|
|
8d3f9d7e34 | ||
|
|
e7eac5baa7 | ||
|
|
08f673802f | ||
|
|
bdb592539e | ||
|
|
3557741f13 | ||
|
|
9d89314afa | ||
|
|
83b86c745d | ||
|
|
c6088a1cb3 | ||
|
|
88c1874943 | ||
|
|
ec50d657bb | ||
|
|
e8c856924c | ||
|
|
e1317ce098 | ||
|
|
a385bd0227 | ||
|
|
99276028ae | ||
|
|
d7d02cd2bc | ||
|
|
bd52ff905f | ||
|
|
49a655f78e | ||
|
|
c27340eddc | ||
|
|
57c6e7f823 | ||
|
|
6c938905f9 | ||
|
|
767df5967a | ||
|
|
2f8f6431d1 | ||
|
|
aace7a1900 | ||
|
|
00bbd9c7bb | ||
|
|
1bd75619dd | ||
|
|
c7d5da4ec5 | ||
|
|
1cfa30b982 | ||
|
|
f5fcc130bf | ||
|
|
c7bfdea17b | ||
|
|
460e23c2e7 | ||
|
|
a4d5faa789 | ||
|
|
148730dac3 | ||
|
|
e43a8bf88a | ||
|
|
502ccea980 | ||
|
|
deab5bce91 | ||
|
|
6dc01cd109 | ||
|
|
eacb5fea9f | ||
|
|
92c170d540 | ||
|
|
32b0679472 | ||
|
|
71d89fb33e | ||
|
|
81289762bc | ||
|
|
cb0e275db9 | ||
|
|
b53a6bfe0c | ||
|
|
8d7e6ee769 |
@@ -62,6 +62,22 @@ redis:
|
|||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
|
#redisForPubsub:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForJobQueue:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌─────────────────────────────┐
|
# ┌─────────────────────────────┐
|
||||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,22 @@ redis:
|
|||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
|
#redisForPubsub:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForJobQueue:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌─────────────────────────────┐
|
# ┌─────────────────────────────┐
|
||||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,22 @@ redis:
|
|||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
|
#redisForPubsub:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForJobQueue:
|
||||||
|
# host: redis
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌─────────────────────────────┐
|
# ┌─────────────────────────────┐
|
||||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|||||||
36
.github/workflows/api-misskey-js.yml
vendored
Normal file
36
.github/workflows/api-misskey-js.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: API report (misskey.js)
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
report:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3.6.0
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm --filter misskey-js build
|
||||||
|
|
||||||
|
- name: Check files
|
||||||
|
run: ls packages/misskey-js/built
|
||||||
|
|
||||||
|
- name: API report
|
||||||
|
run: pnpm --filter misskey-js api-prod
|
||||||
|
|
||||||
|
- name: Show report
|
||||||
|
if: always()
|
||||||
|
run: cat packages/misskey-js/temp/misskey-js.api.md
|
||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -36,6 +36,7 @@ jobs:
|
|||||||
- backend
|
- backend
|
||||||
- frontend
|
- frontend
|
||||||
- sw
|
- sw
|
||||||
|
- misskey-js
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.3.0
|
- uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
@@ -61,6 +62,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
workspace:
|
workspace:
|
||||||
- backend
|
- backend
|
||||||
|
- misskey-js
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3.3.0
|
- uses: actions/checkout@v3.3.0
|
||||||
with:
|
with:
|
||||||
|
|||||||
70
.github/workflows/storybook.yml
vendored
Normal file
70
.github/workflows/storybook.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Storybook
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- l10n_develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max_old_space_size=7168"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3.3.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: true
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 7
|
||||||
|
run_install: false
|
||||||
|
- name: Use Node.js 18.x
|
||||||
|
uses: actions/setup-node@v3.6.0
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: corepack enable
|
||||||
|
- run: pnpm i --frozen-lockfile
|
||||||
|
- name: Check pnpm-lock.yaml
|
||||||
|
run: git diff --exit-code pnpm-lock.yaml
|
||||||
|
- name: Build misskey-js
|
||||||
|
run: pnpm --filter misskey-js build
|
||||||
|
- name: Build storybook
|
||||||
|
run: pnpm --filter frontend build-storybook
|
||||||
|
- name: Publish to Chromatic
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
run: pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static
|
||||||
|
env:
|
||||||
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
- name: Publish to Chromatic
|
||||||
|
if: github.ref != 'refs/heads/master'
|
||||||
|
id: chromatic
|
||||||
|
run: |
|
||||||
|
CHROMATIC_PARAMETER="$(node packages/frontend/.storybook/changes.js $(git diff-tree --no-commit-id --name-only -r ${{ github.event.before }} HEAD | xargs))"
|
||||||
|
if [ "$CHROMATIC_PARAMETER" = " --skip" ]; then
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
pnpm --filter frontend chromatic --exit-once-uploaded -d storybook-static $(echo "$CHROMATIC_PARAMETER")
|
||||||
|
env:
|
||||||
|
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
- name: Notify that Chromatic will skip testing
|
||||||
|
uses: actions/github-script@v6.4.0
|
||||||
|
if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/develop' && steps.chromatic.outputs.skip == 'true'
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
github.rest.repos.createCommitComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
commit_sha: context.sha,
|
||||||
|
body: 'Chromatic will skip testing but you may still have to [review the changes on Chromatic](https://www.chromatic.com/pullrequests?appId=6428f7d7b962f0b79f97d6e4).'
|
||||||
|
})
|
||||||
|
- name: Upload Artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: storybook
|
||||||
|
path: packages/frontend/storybook-static
|
||||||
2
.github/workflows/test-backend.yml
vendored
2
.github/workflows/test-backend.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
redis:
|
||||||
image: redis:6
|
image: redis:7
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/test-frontend.yml
vendored
2
.github/workflows/test-frontend.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
POSTGRES_DB: test-misskey
|
POSTGRES_DB: test-misskey
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
redis:
|
redis:
|
||||||
image: redis:6
|
image: redis:7
|
||||||
ports:
|
ports:
|
||||||
- 56312:6379
|
- 56312:6379
|
||||||
|
|
||||||
|
|||||||
52
.github/workflows/test-misskey-js.yml
vendored
Normal file
52
.github/workflows/test-misskey-js.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Test (misskey.js)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3.3.0
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3.6.0
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check pnpm-lock.yaml
|
||||||
|
run: git diff --exit-code pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm --filter misskey-js build
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pnpm --filter misskey-js test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Upload Coverage
|
||||||
|
uses: codecov/codecov-action@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: ./packages/misskey-js/coverage/coverage-final.json
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -55,6 +55,8 @@ api-docs.json
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/files
|
/files
|
||||||
ormconfig.json
|
ormconfig.json
|
||||||
|
temp
|
||||||
|
/packages/frontend/src/**/*.stories.ts
|
||||||
|
|
||||||
# blender backups
|
# blender backups
|
||||||
*.blend1
|
*.blend1
|
||||||
|
|||||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -12,6 +12,76 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## 13.x.x (unreleased)
|
||||||
|
|
||||||
|
### NOTE
|
||||||
|
- このバージョンからRedis 7.xが必要です。
|
||||||
|
- アップデートを行うと全ての通知はリセットされます。
|
||||||
|
|
||||||
|
### General
|
||||||
|
- チャンネルをお気に入りに登録できるように
|
||||||
|
- チャンネルにノートをピン留めできるように
|
||||||
|
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
|
||||||
|
- ノートのリアクションを大きく表示するオプションを追加
|
||||||
|
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
|
||||||
|
- オブジェクトストレージの設定画面を分かりやすく
|
||||||
|
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
|
||||||
|
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
|
||||||
|
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
|
||||||
|
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
|
||||||
|
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
|
||||||
|
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
|
||||||
|
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
|
||||||
|
- Add Minimizing ("folding") of windows
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- イベント用Redisを別サーバーに分離できるように
|
||||||
|
- ジョブキュー用Redisを別サーバーに分離できるように
|
||||||
|
- サーバーの全体的なパフォーマンスを向上
|
||||||
|
- ノート作成時のパフォーマンスを向上
|
||||||
|
- アンテナのタイムライン取得時のパフォーマンスを向上
|
||||||
|
- チャンネルのタイムライン取得時のパフォーマンスを向上
|
||||||
|
- 通知に関する全体的なパフォーマンスを向上
|
||||||
|
- webhookがcontent-type text/plain;charset=UTF-8 で飛んでくる問題を修正
|
||||||
|
|
||||||
|
## 13.10.3
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- オブジェクトストレージのリージョン指定が必須になりました
|
||||||
|
- リージョンの指定の無いサービスは us-east-1 を設定してください
|
||||||
|
- 値が空の場合は設定ファイルまたは環境変数の使用を試みます
|
||||||
|
- e.g. ~/aws/config, AWS_REGION
|
||||||
|
|
||||||
|
### General
|
||||||
|
- コンディショナルロールの条件に「投稿数が~以下」「投稿数が~以上」を追加
|
||||||
|
- リアクション非対応AP実装からのLikeアクティビティの解釈を👍から♥に
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- クリップボタンをノートアクションに追加できるように
|
||||||
|
- センシティブワードの一覧にピン留めユーザーのIDが表示される問題を修正
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- リモートユーザーのチャート生成を無効にするオプションを追加
|
||||||
|
- リモートサーバーのチャート生成を無効にするオプションを追加
|
||||||
|
- ドライブのチャートはローカルユーザーのみ生成するように
|
||||||
|
- 空のアンテナが作成できるのを修正
|
||||||
|
|
||||||
|
## 13.10.2
|
||||||
|
|
||||||
|
### Server
|
||||||
|
- 絵文字を編集すると保存できないことがある問題を修正
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- ドライブファイルのメニューが正常に動作しない問題を修正
|
||||||
|
|
||||||
|
## 13.10.1
|
||||||
|
|
||||||
|
### Client
|
||||||
|
- Misskey PlayのPlayボタンを押した時にエラーが発生する問題を修正
|
||||||
|
|
||||||
## 13.10.0
|
## 13.10.0
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|||||||
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
|||||||
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
|
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
|
||||||
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
|
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
|
||||||
|
|
||||||
|
## Storybook
|
||||||
|
|
||||||
|
Misskey uses [Storybook](https://storybook.js.org/) for UI development.
|
||||||
|
|
||||||
|
### Setup & Run
|
||||||
|
|
||||||
|
#### Universal
|
||||||
|
|
||||||
|
##### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter misskey-js build
|
||||||
|
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### macOS & Linux
|
||||||
|
|
||||||
|
##### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter misskey-js build
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter frontend storybook-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
When you create a new component (in this example, `MyComponent.vue`), the story file (`MyComponent.stories.ts`) will be automatically generated by the `.storybook/generate.js` script.
|
||||||
|
You can override the default story by creating a impl story file (`MyComponent.stories.impl.ts`).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
/* eslint-disable import/no-duplicates */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import MyComponent from './MyComponent.vue';
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MyComponent,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MyComponent v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkAvatar>;
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to opt-out from the automatic generation, create a `MyComponent.stories.impl.ts` file and add the following line to the file.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import MyComponent from './MyComponent.vue';
|
||||||
|
void MyComponent;
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override the component meta by creating a meta story file (`MyComponent.stories.meta.ts`).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const argTypes = {
|
||||||
|
scale: {
|
||||||
|
control: {
|
||||||
|
type: 'range',
|
||||||
|
min: 1,
|
||||||
|
max: 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, you can use msw to mock API requests in the storybook. Creating a `MyComponent.stories.msw.ts` file to define the mock handlers.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { rest } from 'msw';
|
||||||
|
export const handlers = [
|
||||||
|
rest.post('/api/notes/timeline', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.json([]),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to re-run the `.storybook/generate.js` script after adding, editing, or removing the above files.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
### How to resolve conflictions occurred at pnpm-lock.yaml?
|
### How to resolve conflictions occurred at pnpm-lock.yaml?
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ COPY --link ["scripts", "./scripts"]
|
|||||||
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
COPY --link ["packages/backend/package.json", "./packages/backend/"]
|
||||||
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
|
||||||
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
COPY --link ["packages/sw/package.json", "./packages/sw/"]
|
||||||
|
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
pnpm i --frozen-lockfile --aggregate-output
|
pnpm i --frozen-lockfile --aggregate-output
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -54,6 +54,17 @@ With Misskey's built in drive, you get cloud storage right in your social media,
|
|||||||
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
|
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
|
<a class="rss3" title="RSS3" href="https://rss3.io/" target="_blank"><img src="https://rss3.mypinata.cloud/ipfs/QmUG6H3Z7D5P511shn7sB4CPmpjH5uZWu4m5mWX7U3Gqbu" alt="RSS3" height="60"></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
|
||||||
|
|
||||||
|
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
|
||||||
|
|
||||||
|
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" width="117" height="30" alt="Docker" /></a>
|
||||||
|
|
||||||
|
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.
|
||||||
|
|||||||
@@ -78,10 +78,27 @@ db:
|
|||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
#pass: example-pass
|
#pass: example-pass
|
||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
||||||
|
#redisForPubsub:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
|
#redisForJobQueue:
|
||||||
|
# host: localhost
|
||||||
|
# port: 6379
|
||||||
|
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
|
# #pass: example-pass
|
||||||
|
# #prefix: example-prefix
|
||||||
|
# #db: 1
|
||||||
|
|
||||||
# ┌─────────────────────────────┐
|
# ┌─────────────────────────────┐
|
||||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
|
|||||||
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
|
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
|
||||||
notificationType: "أنواع الإشعارات"
|
notificationType: "أنواع الإشعارات"
|
||||||
edit: "التعديل"
|
edit: "التعديل"
|
||||||
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
|
|
||||||
emailServer: "خادم البريد الإلكتروني"
|
emailServer: "خادم البريد الإلكتروني"
|
||||||
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
|
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
|
||||||
email: "البريد الإلكتروني "
|
email: "البريد الإلكتروني "
|
||||||
@@ -1275,3 +1274,7 @@ _deck:
|
|||||||
channel: "القنوات"
|
channel: "القنوات"
|
||||||
mentions: "الإشارات"
|
mentions: "الإشارات"
|
||||||
direct: "مباشرة"
|
direct: "مباشرة"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "الإسم"
|
||||||
|
active: "مفعّل"
|
||||||
|
|
||||||
|
|||||||
@@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
|
|||||||
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
|
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
|
||||||
notificationType: "বিজ্ঞপ্তির ধরন"
|
notificationType: "বিজ্ঞপ্তির ধরন"
|
||||||
edit: "সম্পাদনা"
|
edit: "সম্পাদনা"
|
||||||
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
|
|
||||||
emailServer: "ইমেইল সার্ভার"
|
emailServer: "ইমেইল সার্ভার"
|
||||||
enableEmail: "ইমেইল বিতরণ চালু করুন"
|
enableEmail: "ইমেইল বিতরণ চালু করুন"
|
||||||
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
|
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
|
||||||
@@ -1354,3 +1353,7 @@ _deck:
|
|||||||
channel: "চ্যানেলগুলি"
|
channel: "চ্যানেলগুলি"
|
||||||
mentions: "উল্লেখসমূহ"
|
mentions: "উল্লেখসমূহ"
|
||||||
direct: "ডাইরেক্ট নোটগুলি"
|
direct: "ডাইরেক্ট নোটগুলি"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "নাম"
|
||||||
|
active: "চালু"
|
||||||
|
|
||||||
|
|||||||
@@ -460,3 +460,4 @@ _deck:
|
|||||||
list: "Llistes"
|
list: "Llistes"
|
||||||
mentions: "Mencions"
|
mentions: "Mencions"
|
||||||
direct: "Publicacions directes"
|
direct: "Publicacions directes"
|
||||||
|
|
||||||
|
|||||||
@@ -776,3 +776,7 @@ _deck:
|
|||||||
list: "Seznamy"
|
list: "Seznamy"
|
||||||
channel: "Kanály"
|
channel: "Kanály"
|
||||||
mentions: "Zmínění"
|
mentions: "Zmínění"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Jméno"
|
||||||
|
active: "Zapnuto"
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
---
|
---
|
||||||
_lang_: "Dansk"
|
_lang_: "Dansk"
|
||||||
|
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ aboutX: "Über {x}"
|
|||||||
emojiStyle: "Emoji-Stil"
|
emojiStyle: "Emoji-Stil"
|
||||||
native: "Nativ"
|
native: "Nativ"
|
||||||
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
|
disableDrawer: "Keine ausfahrbaren Menüs verwenden"
|
||||||
showNoteActionsOnlyHover: "Aktionen für Notizen nur bei Mouseover anzeigen"
|
showNoteActionsOnlyHover: "Notizmenü nur bei Mouseover anzeigen"
|
||||||
noHistory: "Kein Verlauf gefunden"
|
noHistory: "Kein Verlauf gefunden"
|
||||||
signinHistory: "Anmeldungsverlauf"
|
signinHistory: "Anmeldungsverlauf"
|
||||||
enableAdvancedMfm: "Erweitertes MFM aktivieren"
|
enableAdvancedMfm: "Erweitertes MFM aktivieren"
|
||||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein
|
|||||||
objectStorageUseProxy: "Über Proxy verbinden"
|
objectStorageUseProxy: "Über Proxy verbinden"
|
||||||
objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst"
|
objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst"
|
||||||
objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
|
objectStorageSetPublicRead: "Bei Upload auf \"public-read\" stellen"
|
||||||
|
s3ForcePathStyleDesc: "Ist s3ForcePathStyle aktiviert, so muss der Bucketname nicht im Hostnamen der URL, sondern im Pfad der URL angeben werden. Diese Option muss eventuell aktiviert werden, wenn Dienste wie z.B. eine selbstbetriebene Minio-Instanz verwendet werden."
|
||||||
serverLogs: "Serverprotokolle"
|
serverLogs: "Serverprotokolle"
|
||||||
deleteAll: "Alle löschen"
|
deleteAll: "Alle löschen"
|
||||||
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
|
showFixedPostForm: "Bereich zum Schreiben neuer Notizen am Anfang der Chronik anzeigen"
|
||||||
@@ -594,7 +595,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
|
|||||||
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
|
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
|
||||||
notificationType: "Art der Benachrichtigung"
|
notificationType: "Art der Benachrichtigung"
|
||||||
edit: "Bearbeiten"
|
edit: "Bearbeiten"
|
||||||
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
|
|
||||||
emailServer: "Email-Server"
|
emailServer: "Email-Server"
|
||||||
enableEmail: "Email-Versand aktivieren"
|
enableEmail: "Email-Versand aktivieren"
|
||||||
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"
|
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"
|
||||||
@@ -961,7 +961,9 @@ copyErrorInfo: "Fehlerdetails kopieren"
|
|||||||
joinThisServer: "Bei dieser Instanz registrieren"
|
joinThisServer: "Bei dieser Instanz registrieren"
|
||||||
exploreOtherServers: "Eine andere Instanz finden"
|
exploreOtherServers: "Eine andere Instanz finden"
|
||||||
letsLookAtTimeline: "Die Chronik durchstöbern"
|
letsLookAtTimeline: "Die Chronik durchstöbern"
|
||||||
disableFederationWarn: "Dies deaktiviert Föderation, aber alle Notizen bleiben, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird diese Option nicht benötigt."
|
disableFederationConfirm: "Föderation wirklich deaktivieren?"
|
||||||
|
disableFederationConfirmWarn: "Auch mit deaktivierter Föderation bleiben Notizen, sofern nicht umgestellt, öffentlich. In den meisten Fällen wird dies nicht benötigt."
|
||||||
|
disableFederationOk: "Deaktivieren"
|
||||||
invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren."
|
invitationRequiredToRegister: "Diese Instanz ist einladungsbasiert. Du musst einen validen Einladungscode eingeben, um dich zu registrieren."
|
||||||
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
|
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
|
||||||
postToTheChannel: "In Kanal senden"
|
postToTheChannel: "In Kanal senden"
|
||||||
@@ -978,6 +980,14 @@ license: "Lizenz"
|
|||||||
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
|
unfavoriteConfirm: "Wirklich aus Favoriten entfernen?"
|
||||||
myClips: "Meine Clips"
|
myClips: "Meine Clips"
|
||||||
drivecleaner: "Drive-Reiniger"
|
drivecleaner: "Drive-Reiniger"
|
||||||
|
retryAllQueuesNow: "Sofort Warteschlangen erneut ausführen"
|
||||||
|
retryAllQueuesConfirmTitle: "Wirklich erneut versuchen?"
|
||||||
|
retryAllQueuesConfirmText: "Dies wird zu einer temporären Erhöhung der Serverlast führen."
|
||||||
|
enableChartsForRemoteUser: "Diagramme für Nutzer fremder Instanzen erstellen"
|
||||||
|
enableChartsForFederatedInstances: "Diagramme für fremde Instanzen erstellen"
|
||||||
|
showClipButtonInNoteFooter: "\"Clip\" zum Notizmenu hinzufügen"
|
||||||
|
largeNoteReactions: "Reaktionen vergrößert anzeigen"
|
||||||
|
noteIdOrUrl: "Notiz-ID oder URL"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Freigeschaltet am"
|
earnedAt: "Freigeschaltet am"
|
||||||
_types:
|
_types:
|
||||||
@@ -1274,6 +1284,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "Hat X oder mehr Follower"
|
followersMoreThanOrEq: "Hat X oder mehr Follower"
|
||||||
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
|
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
|
||||||
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
|
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
|
||||||
|
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
|
||||||
|
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
|
||||||
and: "UND-Bedingung"
|
and: "UND-Bedingung"
|
||||||
or: "ODER-Bedingung"
|
or: "ODER-Bedingung"
|
||||||
not: "NICHT-Bedingung"
|
not: "NICHT-Bedingung"
|
||||||
@@ -1872,3 +1884,18 @@ _disabledTimeline:
|
|||||||
_drivecleaner:
|
_drivecleaner:
|
||||||
orderBySizeDesc: "Absteigende Dateigrößen"
|
orderBySizeDesc: "Absteigende Dateigrößen"
|
||||||
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
|
orderByCreatedAtAsc: "Aufsteigendes Erstelldatum"
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "Webhook erstellen"
|
||||||
|
name: "Name"
|
||||||
|
secret: "Secret"
|
||||||
|
events: "Webhook-Ereignisse"
|
||||||
|
active: "Aktiviert"
|
||||||
|
_events:
|
||||||
|
follow: "Wenn du jemandem folgst"
|
||||||
|
followed: "Wenn dir jemand folgt"
|
||||||
|
note: "Wenn du eine Notiz schickst"
|
||||||
|
reply: "Wenn du eine Antwort erhältst"
|
||||||
|
renote: "Wenn du ein Renote erhältst"
|
||||||
|
reaction: "Wenn du eine Reaktion erhältst"
|
||||||
|
mention: "Wenn du erwähnt wirst"
|
||||||
|
|
||||||
|
|||||||
@@ -392,3 +392,6 @@ _deck:
|
|||||||
antenna: "Αντένες"
|
antenna: "Αντένες"
|
||||||
list: "Λίστα"
|
list: "Λίστα"
|
||||||
mentions: "Επισημάνσεις"
|
mentions: "Επισημάνσεις"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Όνομα"
|
||||||
|
|
||||||
|
|||||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Turn this off if you are not going to use HTTPS for AP
|
|||||||
objectStorageUseProxy: "Connect over Proxy"
|
objectStorageUseProxy: "Connect over Proxy"
|
||||||
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
|
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
|
||||||
objectStorageSetPublicRead: "Set \"public-read\" on upload"
|
objectStorageSetPublicRead: "Set \"public-read\" on upload"
|
||||||
|
s3ForcePathStyleDesc: "If s3ForcePathStyle is enabled, the bucket name has to included in the path of the URL as opposed to the hostname of the URL. You may need to enable this setting when using services such as a self-hosted Minio instance."
|
||||||
serverLogs: "Server logs"
|
serverLogs: "Server logs"
|
||||||
deleteAll: "Delete all"
|
deleteAll: "Delete all"
|
||||||
showFixedPostForm: "Display the posting form at the top of the timeline"
|
showFixedPostForm: "Display the posting form at the top of the timeline"
|
||||||
@@ -594,7 +595,6 @@ tokenRequested: "Grant access to account"
|
|||||||
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
|
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
|
||||||
notificationType: "Notification type"
|
notificationType: "Notification type"
|
||||||
edit: "Edit"
|
edit: "Edit"
|
||||||
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
|
|
||||||
emailServer: "Email server"
|
emailServer: "Email server"
|
||||||
enableEmail: "Enable email distribution"
|
enableEmail: "Enable email distribution"
|
||||||
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
|
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
|
||||||
@@ -961,7 +961,9 @@ copyErrorInfo: "Copy error details"
|
|||||||
joinThisServer: "Sign up at this instance"
|
joinThisServer: "Sign up at this instance"
|
||||||
exploreOtherServers: "Look for another instance"
|
exploreOtherServers: "Look for another instance"
|
||||||
letsLookAtTimeline: "Have a look at the timeline"
|
letsLookAtTimeline: "Have a look at the timeline"
|
||||||
disableFederationWarn: "This will disable federation, but posts will continue to be public unless set otherwise. You usually do not need to use this setting."
|
disableFederationConfirm: "Really disable federation?"
|
||||||
|
disableFederationConfirmWarn: "Even if defederated, posts will continue to be public unless set otherwise. You usually do not need to do this."
|
||||||
|
disableFederationOk: "Disable"
|
||||||
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
|
invitationRequiredToRegister: "This instance is invite-only. You must enter a valid invite code sign up."
|
||||||
emailNotSupported: "This instance does not support sending emails"
|
emailNotSupported: "This instance does not support sending emails"
|
||||||
postToTheChannel: "Post to channel"
|
postToTheChannel: "Post to channel"
|
||||||
@@ -978,6 +980,24 @@ license: "License"
|
|||||||
unfavoriteConfirm: "Really remove from favorites?"
|
unfavoriteConfirm: "Really remove from favorites?"
|
||||||
myClips: "My clips"
|
myClips: "My clips"
|
||||||
drivecleaner: "Drive Cleaner"
|
drivecleaner: "Drive Cleaner"
|
||||||
|
retryAllQueuesNow: "Retry running all queues"
|
||||||
|
retryAllQueuesConfirmTitle: "Really retry all?"
|
||||||
|
retryAllQueuesConfirmText: "This will temporarily increase the server load."
|
||||||
|
enableChartsForRemoteUser: "Generate remote user data charts"
|
||||||
|
enableChartsForFederatedInstances: "Generate remote instance data charts"
|
||||||
|
showClipButtonInNoteFooter: "Add \"Clip\" to note action menu"
|
||||||
|
largeNoteReactions: "Enlargen displayed reactions"
|
||||||
|
noteIdOrUrl: "Note ID or URL"
|
||||||
|
migration: "Migration"
|
||||||
|
moveTo: "Move current account to new account"
|
||||||
|
moveToLabel: "Account you're moving to:"
|
||||||
|
moveAccountDescription: "This process is irreversible. Make sure you've set up an alias for this account on your new account before moving. Please enter the tag of the account formatted like @person@instance.com"
|
||||||
|
moveFrom: "Move to this account from an older account"
|
||||||
|
moveFromLabel: "Account you're moving from:"
|
||||||
|
moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com"
|
||||||
|
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from."
|
||||||
|
accountMoved: "User has moved to a new account:"
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Unlocked at"
|
earnedAt: "Unlocked at"
|
||||||
_types:
|
_types:
|
||||||
@@ -1274,6 +1294,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "Has X or more followers"
|
followersMoreThanOrEq: "Has X or more followers"
|
||||||
followingLessThanOrEq: "Follows X or fewer accounts"
|
followingLessThanOrEq: "Follows X or fewer accounts"
|
||||||
followingMoreThanOrEq: "Follows X or more accounts"
|
followingMoreThanOrEq: "Follows X or more accounts"
|
||||||
|
notesLessThanOrEq: "Post count is less than/equal to"
|
||||||
|
notesMoreThanOrEq: "Post count is greater than/equal to"
|
||||||
and: "AND-Condition"
|
and: "AND-Condition"
|
||||||
or: "OR-Condition"
|
or: "OR-Condition"
|
||||||
not: "NOT-Condition"
|
not: "NOT-Condition"
|
||||||
@@ -1872,3 +1894,17 @@ _disabledTimeline:
|
|||||||
_drivecleaner:
|
_drivecleaner:
|
||||||
orderBySizeDesc: "Descending Filesizes"
|
orderBySizeDesc: "Descending Filesizes"
|
||||||
orderByCreatedAtAsc: "Ascending Dates"
|
orderByCreatedAtAsc: "Ascending Dates"
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "Create Webhook"
|
||||||
|
name: "Name"
|
||||||
|
secret: "Secret"
|
||||||
|
events: "Webhook Events"
|
||||||
|
active: "Enabled"
|
||||||
|
_events:
|
||||||
|
follow: "When following a user"
|
||||||
|
followed: "When being followed"
|
||||||
|
note: "When posting a note"
|
||||||
|
reply: "When receiving a reply"
|
||||||
|
renote: "When renoted"
|
||||||
|
reaction: "When receiving a reaction"
|
||||||
|
mention: "When being mentioned"
|
||||||
|
|||||||
@@ -594,7 +594,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
|
|||||||
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
|
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
|
||||||
notificationType: "Tipo de notificación"
|
notificationType: "Tipo de notificación"
|
||||||
edit: "Editar"
|
edit: "Editar"
|
||||||
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
|
|
||||||
emailServer: "Servidor de correo"
|
emailServer: "Servidor de correo"
|
||||||
enableEmail: "Activar el envío de correos electrónicos"
|
enableEmail: "Activar el envío de correos electrónicos"
|
||||||
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
|
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
|
||||||
@@ -961,7 +960,6 @@ copyErrorInfo: "Copiar detalles del error"
|
|||||||
joinThisServer: "Registrarse en esta instancia"
|
joinThisServer: "Registrarse en esta instancia"
|
||||||
exploreOtherServers: "Buscar otra instancia"
|
exploreOtherServers: "Buscar otra instancia"
|
||||||
letsLookAtTimeline: "Mirar la línea de tiempo local"
|
letsLookAtTimeline: "Mirar la línea de tiempo local"
|
||||||
disableFederationWarn: "Esto desactivará la federación, pero las publicaciones segurán siendo públicas al menos que se configure diferente. Usualmente no necesitas usar esta configuración."
|
|
||||||
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
|
invitationRequiredToRegister: "Esta instancia está configurada sólo por invitación, tienes que ingresar un código de invitación válido."
|
||||||
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
|
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
|
||||||
postToTheChannel: "Publicar en el canal"
|
postToTheChannel: "Publicar en el canal"
|
||||||
@@ -973,6 +971,14 @@ rolesAssignedToMe: "Roles asignados a mí"
|
|||||||
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
|
resetPasswordConfirm: "¿Realmente quieres cambiar la contraseña?"
|
||||||
sensitiveWords: "Palabras sensibles"
|
sensitiveWords: "Palabras sensibles"
|
||||||
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
|
sensitiveWordsDescription: "La visibilidad de todas las notas que contienen cualquiera de las palabras configuradas serán puestas en \"Inicio\" automáticamente. Puedes enumerás varias separándolas con saltos de línea"
|
||||||
|
notesSearchNotAvailable: "No se puede buscar una nota"
|
||||||
|
license: "Licencia"
|
||||||
|
unfavoriteConfirm: "¿Desea quitar de favoritos?"
|
||||||
|
myClips: "Mis clips"
|
||||||
|
drivecleaner: "Limpiador del Drive"
|
||||||
|
retryAllQueuesNow: "Reintentar inmediatamente todas las colas"
|
||||||
|
retryAllQueuesConfirmTitle: "Desea ¿reintentar inmediatamente todas las colas?"
|
||||||
|
retryAllQueuesConfirmText: "La carga del servidor está incrementándose temporalmente "
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Desbloqueado el"
|
earnedAt: "Desbloqueado el"
|
||||||
_types:
|
_types:
|
||||||
@@ -1864,3 +1870,10 @@ _dialog:
|
|||||||
_disabledTimeline:
|
_disabledTimeline:
|
||||||
title: "Línea de tiempo deshabilitada"
|
title: "Línea de tiempo deshabilitada"
|
||||||
description: "No puedes usar esta línea de tiempo con tus roles actuales."
|
description: "No puedes usar esta línea de tiempo con tus roles actuales."
|
||||||
|
_drivecleaner:
|
||||||
|
orderBySizeDesc: "Más grandes"
|
||||||
|
orderByCreatedAtAsc: "Más antiguos"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Nombre"
|
||||||
|
active: "Activado"
|
||||||
|
|
||||||
|
|||||||
@@ -575,7 +575,6 @@ tokenRequested: "Autoriser l'accès au compte"
|
|||||||
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
|
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
|
||||||
notificationType: "Type de notifications"
|
notificationType: "Type de notifications"
|
||||||
edit: "Editer"
|
edit: "Editer"
|
||||||
useStarForReactionFallback: "Utiliser ★ comme alternative si l’émoji de réaction est inconnu"
|
|
||||||
emailServer: "Serveur mail"
|
emailServer: "Serveur mail"
|
||||||
enableEmail: "Activer la distribution de courriel"
|
enableEmail: "Activer la distribution de courriel"
|
||||||
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli."
|
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli."
|
||||||
@@ -1468,3 +1467,7 @@ _deck:
|
|||||||
channel: "Canaux"
|
channel: "Canaux"
|
||||||
mentions: "Mentions"
|
mentions: "Mentions"
|
||||||
direct: "Direct"
|
direct: "Direct"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Nom"
|
||||||
|
active: "Activé"
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -579,7 +579,6 @@ tokenRequested: "Berikan ijin akses ke akun"
|
|||||||
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
|
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
|
||||||
notificationType: "Jenis pemberitahuan"
|
notificationType: "Jenis pemberitahuan"
|
||||||
edit: "Sunting"
|
edit: "Sunting"
|
||||||
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
|
|
||||||
emailServer: "Peladen surel"
|
emailServer: "Peladen surel"
|
||||||
enableEmail: "Nyalakan distribusi surel"
|
enableEmail: "Nyalakan distribusi surel"
|
||||||
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
|
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
|
||||||
@@ -1804,3 +1803,7 @@ _deck:
|
|||||||
channel: "Kanal"
|
channel: "Kanal"
|
||||||
mentions: "Sebutan"
|
mentions: "Sebutan"
|
||||||
direct: "Langsung"
|
direct: "Langsung"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Nama"
|
||||||
|
active: "Aktif"
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
|
|||||||
host: "Server remoto"
|
host: "Server remoto"
|
||||||
selectUser: "Seleziona profilo"
|
selectUser: "Seleziona profilo"
|
||||||
recipient: "Destinatario"
|
recipient: "Destinatario"
|
||||||
annotation: "Descrizione"
|
annotation: "Annotazione"
|
||||||
federation: "Federazione"
|
federation: "Federazione"
|
||||||
instances: "Istanza"
|
instances: "Istanza"
|
||||||
registeredAt: "Registrato presso"
|
registeredAt: "Registrato presso"
|
||||||
@@ -212,7 +212,7 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a
|
|||||||
done: "Fine"
|
done: "Fine"
|
||||||
processing: "In elaborazione"
|
processing: "In elaborazione"
|
||||||
preview: "Anteprima"
|
preview: "Anteprima"
|
||||||
default: "Medio"
|
default: "Predefinito"
|
||||||
defaultValueIs: "Predefinito: {value}"
|
defaultValueIs: "Predefinito: {value}"
|
||||||
noCustomEmojis: "Nessun emoji"
|
noCustomEmojis: "Nessun emoji"
|
||||||
noJobs: "Nessun lavoro"
|
noJobs: "Nessun lavoro"
|
||||||
@@ -237,14 +237,14 @@ more: "Di più!"
|
|||||||
featured: "Tendenze"
|
featured: "Tendenze"
|
||||||
usernameOrUserId: "Nome utente o ID utente"
|
usernameOrUserId: "Nome utente o ID utente"
|
||||||
noSuchUser: "Nessun utente trovato"
|
noSuchUser: "Nessun utente trovato"
|
||||||
lookup: "Cerca"
|
lookup: "Ricerca remota"
|
||||||
announcements: "Annunci"
|
announcements: "Annunci"
|
||||||
imageUrl: "URL dell'immagine"
|
imageUrl: "URL dell'immagine"
|
||||||
remove: "Elimina"
|
remove: "Elimina"
|
||||||
removed: "Eliminato con successo"
|
removed: "Eliminato con successo"
|
||||||
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
|
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
|
||||||
deleteAreYouSure: "Eliminare \"{x}\"?"
|
deleteAreYouSure: "Eliminare \"{x}\"?"
|
||||||
resetAreYouSure: "Reimposta"
|
resetAreYouSure: "Ripristinare?"
|
||||||
saved: "Salvato"
|
saved: "Salvato"
|
||||||
messaging: "Messaggi"
|
messaging: "Messaggi"
|
||||||
upload: "Carica"
|
upload: "Carica"
|
||||||
@@ -409,7 +409,7 @@ lastUsedAt: "Uso più recente: {t}"
|
|||||||
unregister: "Annulla l'iscrizione"
|
unregister: "Annulla l'iscrizione"
|
||||||
passwordLessLogin: "Accedi senza password"
|
passwordLessLogin: "Accedi senza password"
|
||||||
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
|
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
|
||||||
resetPassword: "Reimposta password"
|
resetPassword: "Ripristina la password"
|
||||||
newPasswordIs: "La tua nuova password è「{password}」"
|
newPasswordIs: "La tua nuova password è「{password}」"
|
||||||
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
|
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
|
||||||
share: "Condividi"
|
share: "Condividi"
|
||||||
@@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
|
|||||||
visibility: "Visibilità"
|
visibility: "Visibilità"
|
||||||
poll: "Sondaggio"
|
poll: "Sondaggio"
|
||||||
useCw: "Nascondere media"
|
useCw: "Nascondere media"
|
||||||
enablePlayer: "Apri in lettore video"
|
enablePlayer: "Visualizza"
|
||||||
disablePlayer: "Chiudi il lettore"
|
disablePlayer: "Chiudi"
|
||||||
expandTweet: "Espandi tweet"
|
expandTweet: "Espandi tweet"
|
||||||
themeEditor: "Editor di temi"
|
themeEditor: "Editor di temi"
|
||||||
description: "Descrizione"
|
description: "Descrizione"
|
||||||
@@ -594,10 +594,9 @@ tokenRequested: "Autorizza accesso al profilo"
|
|||||||
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
|
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
|
||||||
notificationType: "Tipo di notifiche"
|
notificationType: "Tipo di notifiche"
|
||||||
edit: "Modifica"
|
edit: "Modifica"
|
||||||
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
|
|
||||||
emailServer: "Server email"
|
emailServer: "Server email"
|
||||||
enableEmail: "Abilita consegna email"
|
enableEmail: "Abilita consegna email"
|
||||||
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
|
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per ripristinare la password"
|
||||||
email: "Email"
|
email: "Email"
|
||||||
emailAddress: "Indirizzo di posta elettronica"
|
emailAddress: "Indirizzo di posta elettronica"
|
||||||
smtpConfig: "Impostazioni del server SMTP"
|
smtpConfig: "Impostazioni del server SMTP"
|
||||||
@@ -961,22 +960,30 @@ copyErrorInfo: "Copia le informazioni sull'errore"
|
|||||||
joinThisServer: "Registrati su questa istanza"
|
joinThisServer: "Registrati su questa istanza"
|
||||||
exploreOtherServers: "Trova altre istanze"
|
exploreOtherServers: "Trova altre istanze"
|
||||||
letsLookAtTimeline: "Sbircia la timeline"
|
letsLookAtTimeline: "Sbircia la timeline"
|
||||||
disableFederationWarn: "Disabilita la federazione. Questo cambiamento non rende le pubblicazioni private. Di solito non è necessario abilitare questa opzione."
|
invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione."
|
||||||
invitationRequiredToRegister: "L'accesso a questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore."
|
|
||||||
emailNotSupported: "L'istanza non supporta l'invio di email"
|
emailNotSupported: "L'istanza non supporta l'invio di email"
|
||||||
postToTheChannel: "Pubblica sul canale"
|
postToTheChannel: "Pubblica nel canale"
|
||||||
cannotBeChangedLater: "Non sarà più modificabile"
|
cannotBeChangedLater: "Non sarà più modificabile"
|
||||||
reactionAcceptance: "Accettazione reazioni"
|
reactionAcceptance: "Accettazione reazioni"
|
||||||
likeOnly: "Solo i Like"
|
likeOnly: "Solo i Like"
|
||||||
likeOnlyForRemote: "Solo Like remoti"
|
likeOnlyForRemote: "Solo Like remoti"
|
||||||
rolesAssignedToMe: "I miei ruoli"
|
rolesAssignedToMe: "I miei ruoli"
|
||||||
resetPasswordConfirm: "Vuoi reimpostare la password?"
|
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
|
||||||
sensitiveWords: "Parole sensibili"
|
sensitiveWords: "Parole sensibili"
|
||||||
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
|
sensitiveWordsDescription: "Imposta automaticamente \"Home\" alla visibilità delle Note che contengono una qualsiasi parola tra queste configurate. Puoi separarle per riga."
|
||||||
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
|
notesSearchNotAvailable: "Non è possibile cercare tra le Note."
|
||||||
license: "Licenza"
|
license: "Licenza"
|
||||||
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
|
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
|
||||||
myClips: "Le mie Clip"
|
myClips: "Le mie Clip"
|
||||||
|
drivecleaner: "Drive cleaner"
|
||||||
|
retryAllQueuesNow: "Ritenta di consumare tutte le code"
|
||||||
|
retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?"
|
||||||
|
retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente."
|
||||||
|
enableChartsForRemoteUser: "Abilita i grafici per i profili remoti"
|
||||||
|
enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate"
|
||||||
|
showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
|
||||||
|
largeNoteReactions: "Ingrandisci le reazioni"
|
||||||
|
noteIdOrUrl: "ID della Nota o URL"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Data di conseguimento"
|
earnedAt: "Data di conseguimento"
|
||||||
_types:
|
_types:
|
||||||
@@ -1273,6 +1280,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "Ha più di N follower"
|
followersMoreThanOrEq: "Ha più di N follower"
|
||||||
followingLessThanOrEq: "Segue N profili o meno"
|
followingLessThanOrEq: "Segue N profili o meno"
|
||||||
followingMoreThanOrEq: "Segue N profili o più"
|
followingMoreThanOrEq: "Segue N profili o più"
|
||||||
|
notesLessThanOrEq: "Conteggio Note inferiore o uguale a"
|
||||||
|
notesMoreThanOrEq: "Conteggio Note maggiore o uguale a"
|
||||||
and: "E"
|
and: "E"
|
||||||
or: "O"
|
or: "O"
|
||||||
not: "NON"
|
not: "NON"
|
||||||
@@ -1311,8 +1320,8 @@ _ad:
|
|||||||
hide: "Nascondi"
|
hide: "Nascondi"
|
||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
|
enterEmail: "Inserisci l'indirizzo di posta elettronica che hai registrato nel tuo profilo. Il collegamento necessario per ripristinare la password verrà inviato a questo indirizzo."
|
||||||
ifNoEmail: "Se nessun indirizzo e-mail è stato registrato, si prega di contattare l'amministratore·trice dell'istanza."
|
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
|
||||||
contactAdmin: "Poiché questa istanza non permette l'utilizzo di una mail, si prega di contattare l'amministratore·trice dell'istanza per poter ripristinare la password."
|
contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
|
||||||
_gallery:
|
_gallery:
|
||||||
my: "Le mie pubblicazioni"
|
my: "Le mie pubblicazioni"
|
||||||
liked: "Pubblicazioni che mi piacciono"
|
liked: "Pubblicazioni che mi piacciono"
|
||||||
@@ -1867,4 +1876,22 @@ _dialog:
|
|||||||
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
|
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
|
||||||
_disabledTimeline:
|
_disabledTimeline:
|
||||||
title: "Timeline disabilitata"
|
title: "Timeline disabilitata"
|
||||||
description: "Il tuo ruolo non ha i permessi per accedere a questa timeline"
|
description: "Il ruolo in cui sei non ti permette di leggere questa timeline"
|
||||||
|
_drivecleaner:
|
||||||
|
orderBySizeDesc: "Dal più grande al più piccolo"
|
||||||
|
orderByCreatedAtAsc: "Dal più vecchio al più recente"
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "Creazione Webhook"
|
||||||
|
name: "Nome"
|
||||||
|
secret: "Segreto"
|
||||||
|
events: "Quando eseguire il Webhook"
|
||||||
|
active: "Attivo"
|
||||||
|
_events:
|
||||||
|
follow: "Quando segui un profilo"
|
||||||
|
followed: "Quando ti segue un profilo"
|
||||||
|
note: "Quando pubblichi una Nota"
|
||||||
|
reply: "Quando rispondono ad una Nota"
|
||||||
|
renote: "Quando la Nota è Rinotata"
|
||||||
|
reaction: "Quando ricevo una reazione"
|
||||||
|
mention: "Quando mi menzionano"
|
||||||
|
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ aboutX: "{x}について"
|
|||||||
emojiStyle: "絵文字のスタイル"
|
emojiStyle: "絵文字のスタイル"
|
||||||
native: "ネイティブ"
|
native: "ネイティブ"
|
||||||
disableDrawer: "メニューをドロワーで表示しない"
|
disableDrawer: "メニューをドロワーで表示しない"
|
||||||
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
|
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
|
||||||
noHistory: "履歴はありません"
|
noHistory: "履歴はありません"
|
||||||
signinHistory: "ログイン履歴"
|
signinHistory: "ログイン履歴"
|
||||||
enableAdvancedMfm: "高度なMFMを有効にする"
|
enableAdvancedMfm: "高度なMFMを有効にする"
|
||||||
@@ -500,12 +500,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
|
|||||||
objectStorageEndpoint: "Endpoint"
|
objectStorageEndpoint: "Endpoint"
|
||||||
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
|
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
|
||||||
objectStorageRegion: "Region"
|
objectStorageRegion: "Region"
|
||||||
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
|
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
|
||||||
objectStorageUseSSL: "SSLを使用する"
|
objectStorageUseSSL: "SSLを使用する"
|
||||||
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
||||||
objectStorageUseProxy: "Proxyを利用する"
|
objectStorageUseProxy: "Proxyを利用する"
|
||||||
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
||||||
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
||||||
|
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
|
||||||
serverLogs: "サーバーログ"
|
serverLogs: "サーバーログ"
|
||||||
deleteAll: "全て削除"
|
deleteAll: "全て削除"
|
||||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
||||||
@@ -594,7 +595,6 @@ tokenRequested: "アカウントへのアクセス許可"
|
|||||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
||||||
notificationType: "通知の種類"
|
notificationType: "通知の種類"
|
||||||
edit: "編集"
|
edit: "編集"
|
||||||
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
|
||||||
emailServer: "メールサーバー"
|
emailServer: "メールサーバー"
|
||||||
enableEmail: "メール配信機能を有効化する"
|
enableEmail: "メール配信機能を有効化する"
|
||||||
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
||||||
@@ -920,6 +920,7 @@ pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知
|
|||||||
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
|
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
|
||||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
|
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
|
||||||
windowMaximize: "最大化"
|
windowMaximize: "最大化"
|
||||||
|
windowMinimize: "最小化"
|
||||||
windowRestore: "元に戻す"
|
windowRestore: "元に戻す"
|
||||||
caption: "キャプション"
|
caption: "キャプション"
|
||||||
loggedInAsBot: "Botアカウントでログイン中"
|
loggedInAsBot: "Botアカウントでログイン中"
|
||||||
@@ -961,7 +962,9 @@ copyErrorInfo: "エラー情報をコピー"
|
|||||||
joinThisServer: "このサーバーに登録する"
|
joinThisServer: "このサーバーに登録する"
|
||||||
exploreOtherServers: "他のサーバーを探す"
|
exploreOtherServers: "他のサーバーを探す"
|
||||||
letsLookAtTimeline: "タイムラインを見てみる"
|
letsLookAtTimeline: "タイムラインを見てみる"
|
||||||
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
|
disableFederationConfirm: "連合なしにしますか?"
|
||||||
|
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
||||||
|
disableFederationOk: "連合なしにする"
|
||||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||||
postToTheChannel: "チャンネルに投稿"
|
postToTheChannel: "チャンネルに投稿"
|
||||||
@@ -981,6 +984,20 @@ drivecleaner: "ドライブクリーナー"
|
|||||||
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
|
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
|
||||||
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
|
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
|
||||||
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
|
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
|
||||||
|
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||||
|
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||||
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
|
largeNoteReactions: "ノートのリアクションを大きく表示"
|
||||||
|
noteIdOrUrl: "ノートIDまたはURL"
|
||||||
|
migration: "アカウントの引っ越し"
|
||||||
|
moveTo: "このアカウントを新しいアカウントに引っ越す"
|
||||||
|
moveToLabel: "引っ越し先のアカウント:"
|
||||||
|
moveAccountDescription: "この操作は取り消せません。まずは引っ越し先のアカウントでこのアカウントに対しエイリアスを作成したことを確認してください。エイリアス作成後、引っ越し先のアカウントをこのように入力してください:@person@instance.com"
|
||||||
|
moveFrom: "別のアカウントからこのアカウントに引っ越す"
|
||||||
|
moveFromLabel: "引っ越し元のアカウント:"
|
||||||
|
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したい場合、ここでエイリアスを作成しておく必要があります。必ず引っ越しを実行する前に作成してください!引っ越し元のアカウントをこのように入力してください:@person@instance.com"
|
||||||
|
migrationConfirm: "本当にこのアカウントを {account} に引っ越しますか?一度引っ越しを行うと取り消せず、二度とこのアカウントを元の状態で使用できなくなります。\nまた、引っ越し先のアカウントでエイリアスを作成したことを確認してください。"
|
||||||
|
accountMoved: "このユーザーは新しいアカウントに引っ越しました:"
|
||||||
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日時"
|
earnedAt: "獲得日時"
|
||||||
@@ -1279,6 +1296,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||||
followingLessThanOrEq: "フォロー数が~以下"
|
followingLessThanOrEq: "フォロー数が~以下"
|
||||||
followingMoreThanOrEq: "フォロー数が~以上"
|
followingMoreThanOrEq: "フォロー数が~以上"
|
||||||
|
notesLessThanOrEq: "投稿数が~以下"
|
||||||
|
notesMoreThanOrEq: "投稿数が~以上"
|
||||||
and: "~かつ~"
|
and: "~かつ~"
|
||||||
or: "~または~"
|
or: "~または~"
|
||||||
not: "~ではない"
|
not: "~ではない"
|
||||||
@@ -1929,4 +1948,19 @@ _disabledTimeline:
|
|||||||
|
|
||||||
_drivecleaner:
|
_drivecleaner:
|
||||||
orderBySizeDesc: "サイズが大きい順"
|
orderBySizeDesc: "サイズが大きい順"
|
||||||
orderByCreatedAtAsc: "追加日が古い順"
|
orderByCreatedAtAsc: "追加日が古い順"
|
||||||
|
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "Webhookを作成"
|
||||||
|
name: "名前"
|
||||||
|
secret: "シークレット"
|
||||||
|
events: "Webhookを実行するタイミング"
|
||||||
|
active: "有効"
|
||||||
|
_events:
|
||||||
|
follow: "フォローしたとき"
|
||||||
|
followed: "フォローされたとき"
|
||||||
|
note: "ノートを投稿したとき"
|
||||||
|
reply: "返信されたとき"
|
||||||
|
renote: "Renoteされたとき"
|
||||||
|
reaction: "リアクションがあったとき"
|
||||||
|
mention: "メンションされたとき"
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ cancel: "やめとく"
|
|||||||
noThankYou: "やめとく"
|
noThankYou: "やめとく"
|
||||||
enterUsername: "ユーザー名を入れてや"
|
enterUsername: "ユーザー名を入れてや"
|
||||||
renotedBy: "{user}がRenoteしたで"
|
renotedBy: "{user}がRenoteしたで"
|
||||||
noNotes: "ノートなんてあらへんで"
|
noNotes: "ノートはあらへん"
|
||||||
noNotifications: "通知なんてあらへんで"
|
noNotifications: "通知はあらへん"
|
||||||
instance: "サーバー"
|
instance: "サーバー"
|
||||||
settings: "設定"
|
settings: "設定"
|
||||||
basicSettings: "基本設定"
|
basicSettings: "基本設定"
|
||||||
@@ -25,13 +25,13 @@ otherSettings: "ほかの設定"
|
|||||||
openInWindow: "ウィンドウで開くで"
|
openInWindow: "ウィンドウで開くで"
|
||||||
profile: "プロフィール"
|
profile: "プロフィール"
|
||||||
timeline: "タイムライン"
|
timeline: "タイムライン"
|
||||||
noAccountDescription: "自己紹介食ってもた"
|
noAccountDescription: "自己紹介はあらへん"
|
||||||
login: "ログイン"
|
login: "ログイン"
|
||||||
loggingIn: "ログインしよるで"
|
loggingIn: "ログインしよるで"
|
||||||
logout: "ログアウト"
|
logout: "ログアウト"
|
||||||
signup: "新規登録"
|
signup: "新規登録"
|
||||||
uploading: "アップロードしとるで"
|
uploading: "アップロードしとるで"
|
||||||
save: "保存"
|
save: "とっとく"
|
||||||
users: "ユーザー"
|
users: "ユーザー"
|
||||||
addUser: "ユーザーを追加や"
|
addUser: "ユーザーを追加や"
|
||||||
favorite: "お気に入り"
|
favorite: "お気に入り"
|
||||||
@@ -81,9 +81,9 @@ followsYou: "フォローされとるで"
|
|||||||
createList: "リスト作る"
|
createList: "リスト作る"
|
||||||
manageLists: "リストの管理"
|
manageLists: "リストの管理"
|
||||||
error: "エラー"
|
error: "エラー"
|
||||||
somethingHappened: "なんかアカンことが起こったで"
|
somethingHappened: "なんかあかんわ"
|
||||||
retry: "もっぺんやる?"
|
retry: "もっぺんやる?"
|
||||||
pageLoadError: "ページの読み込みに失敗してもうたわ…"
|
pageLoadError: "ページが読み込めんかったわ。"
|
||||||
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
|
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
|
||||||
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
|
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
|
||||||
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
|
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
|
||||||
@@ -108,8 +108,8 @@ inChannelQuote: "チャンネル内引用"
|
|||||||
pinnedNote: "ピン留めされとるノート"
|
pinnedNote: "ピン留めされとるノート"
|
||||||
pinned: "ピン留めしとく"
|
pinned: "ピン留めしとく"
|
||||||
you: "あんた"
|
you: "あんた"
|
||||||
clickToShow: "押したら見えるで"
|
clickToShow: "押したら出ら"
|
||||||
sensitive: "ちょっとアカンやつやで"
|
sensitive: "気いつけて見いや"
|
||||||
add: "増やす"
|
add: "増やす"
|
||||||
reaction: "リアクション"
|
reaction: "リアクション"
|
||||||
reactions: "リアクション"
|
reactions: "リアクション"
|
||||||
@@ -122,8 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
|
|||||||
enterFileName: "ファイル名を入れてや"
|
enterFileName: "ファイル名を入れてや"
|
||||||
mute: "ミュート"
|
mute: "ミュート"
|
||||||
unmute: "ミュートやめたる"
|
unmute: "ミュートやめたる"
|
||||||
renoteMute: "リノートは見いひん"
|
renoteMute: "Renoteは見いひん"
|
||||||
renoteUnmute: "リノートもやっぱ見るわ"
|
renoteUnmute: "Renoteもやっぱ見るわ"
|
||||||
block: "ブロック"
|
block: "ブロック"
|
||||||
unblock: "ブロックやめたる"
|
unblock: "ブロックやめたる"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
@@ -141,14 +141,14 @@ editWidgetsExit: "編集終ったで"
|
|||||||
customEmojis: "カスタム絵文字"
|
customEmojis: "カスタム絵文字"
|
||||||
emoji: "絵文字"
|
emoji: "絵文字"
|
||||||
emojis: "絵文字"
|
emojis: "絵文字"
|
||||||
emojiName: "絵文字名"
|
emojiName: "絵文字はんの名前"
|
||||||
emojiUrl: "絵文字画像URL"
|
emojiUrl: "絵文字画像URL"
|
||||||
addEmoji: "絵文字を追加"
|
addEmoji: "絵文字を追加"
|
||||||
settingGuide: "ええ感じの設定"
|
settingGuide: "ええ感じの設定"
|
||||||
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||||
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
|
cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。"
|
||||||
flagAsBot: "Botにするで"
|
flagAsBot: "Botにするで"
|
||||||
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
|
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
|
||||||
flagAsCat: "Catやで"
|
flagAsCat: "Catやで"
|
||||||
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
||||||
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
|
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
|
||||||
@@ -194,10 +194,10 @@ network: "ネットワーク"
|
|||||||
disk: "ディスク"
|
disk: "ディスク"
|
||||||
instanceInfo: "サーバー情報"
|
instanceInfo: "サーバー情報"
|
||||||
statistics: "統計"
|
statistics: "統計"
|
||||||
clearQueue: "キューにさいなら"
|
clearQueue: "キューをほかす"
|
||||||
clearQueueConfirmTitle: "キューをクリアしまっか?"
|
clearQueueConfirmTitle: "キューをほかしとこか?"
|
||||||
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
|
clearQueueConfirmText: "未配達の投稿は配送されんなるで。ふつうこの操作を行う必要は無いんやけどな。"
|
||||||
clearCachedFiles: "キャッシュにさいなら"
|
clearCachedFiles: "キャッシュをほかす"
|
||||||
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
|
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
|
||||||
blockedInstances: "ブロックしたサーバー"
|
blockedInstances: "ブロックしたサーバー"
|
||||||
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
|
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
|
||||||
@@ -206,7 +206,7 @@ mutedUsers: "ミュートしたユーザー"
|
|||||||
blockedUsers: "ブロックしたユーザー"
|
blockedUsers: "ブロックしたユーザー"
|
||||||
noUsers: "ユーザーはおらん"
|
noUsers: "ユーザーはおらん"
|
||||||
editProfile: "プロフィールをいじる"
|
editProfile: "プロフィールをいじる"
|
||||||
noteDeleteConfirm: "このノートを削除しまっか?"
|
noteDeleteConfirm: "このノートをほかしてええか?"
|
||||||
pinLimitExceeded: "これ以上ピン留めできひん"
|
pinLimitExceeded: "これ以上ピン留めできひん"
|
||||||
intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
|
intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
|
||||||
done: "でけた"
|
done: "でけた"
|
||||||
@@ -226,9 +226,9 @@ notResponding: "応答してへんで"
|
|||||||
instanceFollowing: "サーバーのフォロー"
|
instanceFollowing: "サーバーのフォロー"
|
||||||
instanceFollowers: "サーバーのフォロワー\n"
|
instanceFollowers: "サーバーのフォロワー\n"
|
||||||
instanceUsers: "サーバーのユーザー"
|
instanceUsers: "サーバーのユーザー"
|
||||||
changePassword: "パスワード変える"
|
changePassword: "パスワードをいじる"
|
||||||
security: "セキュリティ"
|
security: "セキュリティ"
|
||||||
retypedNotMatch: "入れたやつ同じになってないで。"
|
retypedNotMatch: "入れたやつ合うてへんわ。"
|
||||||
currentPassword: "今のパスワード"
|
currentPassword: "今のパスワード"
|
||||||
newPassword: "次のパスワード"
|
newPassword: "次のパスワード"
|
||||||
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
|
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
|
||||||
@@ -258,7 +258,7 @@ uploadFromUrlRequested: "アップロードしたい言うといたで"
|
|||||||
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
|
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
|
||||||
explore: "みつける"
|
explore: "みつける"
|
||||||
messageRead: "もう読んだ"
|
messageRead: "もう読んだ"
|
||||||
noMoreHistory: "これより過去の履歴はあらへんで"
|
noMoreHistory: "これより昔のんはあらへんで"
|
||||||
startMessaging: "チャットやるで"
|
startMessaging: "チャットやるで"
|
||||||
nUsersRead: "{n}人が読んでもうた"
|
nUsersRead: "{n}人が読んでもうた"
|
||||||
agreeTo: "{0}に同意したで"
|
agreeTo: "{0}に同意したで"
|
||||||
@@ -294,14 +294,14 @@ createFolder: "フォルダー作る"
|
|||||||
renameFolder: "フォルダー名を変える"
|
renameFolder: "フォルダー名を変える"
|
||||||
deleteFolder: "フォルダーをほかす"
|
deleteFolder: "フォルダーをほかす"
|
||||||
addFile: "ファイルを追加"
|
addFile: "ファイルを追加"
|
||||||
emptyDrive: "ドライブにはなんも残っとらん"
|
emptyDrive: "ドライブは空っぽや"
|
||||||
emptyFolder: "このフォルダーは空や"
|
emptyFolder: "このフォルダーは空や"
|
||||||
unableToDelete: "消そうおもってんけどな、あかんかったわ"
|
unableToDelete: "消せんかったわ"
|
||||||
inputNewFileName: "今度のファイル名は何にするん?"
|
inputNewFileName: "今度のファイル名は何にするん?"
|
||||||
inputNewDescription: "新しいキャプションを入れてや"
|
inputNewDescription: "新しいキャプションを入れてや"
|
||||||
inputNewFolderName: "今度のフォルダ名は何にするん?"
|
inputNewFolderName: "今度のフォルダ名は何にするん?"
|
||||||
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
|
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
|
||||||
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
|
hasChildFilesOrFolders: "このフォルダは空っぽちゃうから消されへん"
|
||||||
copyUrl: "URLをコピー"
|
copyUrl: "URLをコピー"
|
||||||
rename: "名前を変えるで"
|
rename: "名前を変えるで"
|
||||||
avatar: "アイコン"
|
avatar: "アイコン"
|
||||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにする
|
|||||||
objectStorageUseProxy: "Proxyを使う"
|
objectStorageUseProxy: "Proxyを使う"
|
||||||
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?"
|
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?"
|
||||||
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
|
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
|
||||||
|
s3ForcePathStyleDesc: "s3ForcePathStyleを使たらバケット名をURLのホスト名やなくてパスの一部として必ず指定させるようになるで。セルフホストされたMinioとかを使うてるんやったら有効にせなあかん場合があるで。"
|
||||||
serverLogs: "サーバーログ"
|
serverLogs: "サーバーログ"
|
||||||
deleteAll: "全部ほかす"
|
deleteAll: "全部ほかす"
|
||||||
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
|
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
|
||||||
@@ -594,7 +595,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
|
|||||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
|
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
|
||||||
notificationType: "通知の種類"
|
notificationType: "通知の種類"
|
||||||
edit: "編集"
|
edit: "編集"
|
||||||
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
|
|
||||||
emailServer: "メールサーバー"
|
emailServer: "メールサーバー"
|
||||||
enableEmail: "メール配信を受け取る"
|
enableEmail: "メール配信を受け取る"
|
||||||
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
||||||
@@ -644,7 +644,7 @@ reporter: "通報者"
|
|||||||
reporteeOrigin: "通報先"
|
reporteeOrigin: "通報先"
|
||||||
reporterOrigin: "通報元"
|
reporterOrigin: "通報元"
|
||||||
forwardReport: "リモートサーバーに通報を転送するで"
|
forwardReport: "リモートサーバーに通報を転送するで"
|
||||||
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
|
forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
|
||||||
send: "送信"
|
send: "送信"
|
||||||
abuseMarkAsResolved: "対応したで"
|
abuseMarkAsResolved: "対応したで"
|
||||||
openInNewTab: "新しいタブで開く"
|
openInNewTab: "新しいタブで開く"
|
||||||
@@ -740,7 +740,7 @@ capacity: "容量"
|
|||||||
inUse: "使用中"
|
inUse: "使用中"
|
||||||
editCode: "コードを編集"
|
editCode: "コードを編集"
|
||||||
apply: "適用"
|
apply: "適用"
|
||||||
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
|
receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
|
||||||
emailNotification: "メール通知"
|
emailNotification: "メール通知"
|
||||||
publish: "公開"
|
publish: "公開"
|
||||||
inChannelSearch: "チャンネル内検索"
|
inChannelSearch: "チャンネル内検索"
|
||||||
@@ -768,7 +768,7 @@ active: "アクティブ"
|
|||||||
offline: "オフライン"
|
offline: "オフライン"
|
||||||
notRecommended: "あんま推奨しやんで"
|
notRecommended: "あんま推奨しやんで"
|
||||||
botProtection: "Botプロテクション"
|
botProtection: "Botプロテクション"
|
||||||
instanceBlocking: "インスタンスブロック"
|
instanceBlocking: "サーバーブロック"
|
||||||
selectAccount: "アカウントを選んでなー"
|
selectAccount: "アカウントを選んでなー"
|
||||||
switchAccount: "アカウントを変えるで"
|
switchAccount: "アカウントを変えるで"
|
||||||
enabled: "有効"
|
enabled: "有効"
|
||||||
@@ -852,8 +852,8 @@ themeColor: "テーマカラー"
|
|||||||
size: "大きさ"
|
size: "大きさ"
|
||||||
numberOfColumn: "列の数"
|
numberOfColumn: "列の数"
|
||||||
searchByGoogle: "探す"
|
searchByGoogle: "探す"
|
||||||
instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ"
|
instanceDefaultLightTheme: "サーバーおすすめの明るいテーマ"
|
||||||
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ"
|
instanceDefaultDarkTheme: "サーバーおすすめのの暗いテーマ"
|
||||||
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
|
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
|
||||||
mutePeriod: "ミュートする期間"
|
mutePeriod: "ミュートする期間"
|
||||||
period: "期限"
|
period: "期限"
|
||||||
@@ -867,7 +867,7 @@ reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
|
|||||||
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
|
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
|
||||||
rateLimitExceeded: "レート制限が超えたみたいやで"
|
rateLimitExceeded: "レート制限が超えたみたいやで"
|
||||||
cropImage: "画像のクロップ"
|
cropImage: "画像のクロップ"
|
||||||
cropImageAsk: "画像をクロップしたってええか?"
|
cropImageAsk: "画像をクロップしてもええか?"
|
||||||
cropYes: "切り抜いたる"
|
cropYes: "切り抜いたる"
|
||||||
cropNo: "切り抜かへん"
|
cropNo: "切り抜かへん"
|
||||||
file: "ファイル"
|
file: "ファイル"
|
||||||
@@ -902,11 +902,11 @@ sensitiveMediaDetection: "センシティブなメディアの検出"
|
|||||||
localOnly: "ローカルのみ"
|
localOnly: "ローカルのみ"
|
||||||
remoteOnly: "リモートのみ"
|
remoteOnly: "リモートのみ"
|
||||||
failedToUpload: "アップロードに失敗してもうたわ…"
|
failedToUpload: "アップロードに失敗してもうたわ…"
|
||||||
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたでアップロードできまへん。"
|
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへんわ。"
|
||||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いでアップロードできまへん。"
|
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへんわ。"
|
||||||
beta: "ベータ"
|
beta: "ベータ"
|
||||||
enableAutoSensitive: "自動NSFW判定"
|
enableAutoSensitive: "自動NSFW判定"
|
||||||
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、インスタンスによっては自動で設定されることがあるで。"
|
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
|
||||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
|
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
|
||||||
navbar: "ナビゲーションバー"
|
navbar: "ナビゲーションバー"
|
||||||
shuffle: "シャッフルするで"
|
shuffle: "シャッフルするで"
|
||||||
@@ -916,7 +916,7 @@ pushNotification: "プッシュ通知"
|
|||||||
subscribePushNotification: "プッシュ通知をオンにするで"
|
subscribePushNotification: "プッシュ通知をオンにするで"
|
||||||
unsubscribePushNotification: "プッシュ通知を止めるで"
|
unsubscribePushNotification: "プッシュ通知を止めるで"
|
||||||
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
|
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
|
||||||
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。"
|
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。"
|
||||||
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
|
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
|
||||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
|
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
|
||||||
windowMaximize: "最大化"
|
windowMaximize: "最大化"
|
||||||
@@ -932,7 +932,7 @@ numberOfLikes: "いいね数"
|
|||||||
show: "表示"
|
show: "表示"
|
||||||
neverShow: "今後表示しない"
|
neverShow: "今後表示しない"
|
||||||
remindMeLater: "また後で"
|
remindMeLater: "また後で"
|
||||||
didYouLikeMisskey: "Misskeyを気に入っとっただけましたん?"
|
didYouLikeMisskey: "Misskey気に入ってくれた?"
|
||||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
|
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
|
||||||
roles: "ロール"
|
roles: "ロール"
|
||||||
role: "ロール"
|
role: "ロール"
|
||||||
@@ -942,7 +942,7 @@ assign: "アサイン"
|
|||||||
unassign: "アサインを解除"
|
unassign: "アサインを解除"
|
||||||
color: "色"
|
color: "色"
|
||||||
manageCustomEmojis: "カスタム絵文字の管理"
|
manageCustomEmojis: "カスタム絵文字の管理"
|
||||||
youCannotCreateAnymore: "これ以上作れなさそうや"
|
youCannotCreateAnymore: "これ以上作れなさそうやわ"
|
||||||
cannotPerformTemporary: "一時的に利用できへんで"
|
cannotPerformTemporary: "一時的に利用できへんで"
|
||||||
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
|
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
|
||||||
preset: "プリセット"
|
preset: "プリセット"
|
||||||
@@ -961,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー"
|
|||||||
joinThisServer: "このサーバーに登録するわ"
|
joinThisServer: "このサーバーに登録するわ"
|
||||||
exploreOtherServers: "他のサーバー見てみる"
|
exploreOtherServers: "他のサーバー見てみる"
|
||||||
letsLookAtTimeline: "タイムライン見てみーや"
|
letsLookAtTimeline: "タイムライン見てみーや"
|
||||||
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。"
|
disableFederationConfirm: "連合なしにしとくか?"
|
||||||
|
disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。"
|
||||||
|
disableFederationOk: "連合なしにしとく"
|
||||||
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
|
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
|
||||||
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
|
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
|
||||||
postToTheChannel: "チャンネルに投稿"
|
postToTheChannel: "チャンネルに投稿"
|
||||||
@@ -977,6 +979,15 @@ notesSearchNotAvailable: "ノート検索は使われへんで。"
|
|||||||
license: "ライセンス"
|
license: "ライセンス"
|
||||||
unfavoriteConfirm: "ほんまに気に入らんの?"
|
unfavoriteConfirm: "ほんまに気に入らんの?"
|
||||||
myClips: "自分のクリップ"
|
myClips: "自分のクリップ"
|
||||||
|
drivecleaner: "ドライブキレイキレイ"
|
||||||
|
retryAllQueuesNow: "キューを全部もっかいやり直す"
|
||||||
|
retryAllQueuesConfirmTitle: "もっかいやってみるか?"
|
||||||
|
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
|
||||||
|
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
|
||||||
|
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
|
||||||
|
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||||
|
largeNoteReactions: "ノートのリアクションを大きする"
|
||||||
|
noteIdOrUrl: "ノートIDかURL"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "貰った日ぃ"
|
earnedAt: "貰った日ぃ"
|
||||||
_types:
|
_types:
|
||||||
@@ -1037,7 +1048,7 @@ _achievements:
|
|||||||
_login7:
|
_login7:
|
||||||
title: "ビギナーⅡ"
|
title: "ビギナーⅡ"
|
||||||
description: "通算7日ログインした"
|
description: "通算7日ログインした"
|
||||||
flavor: "慣れてきたんちゃう?"
|
flavor: "慣れてきたんとちゃう?"
|
||||||
_login15:
|
_login15:
|
||||||
title: "ビギナーⅢ"
|
title: "ビギナーⅢ"
|
||||||
description: "通算15日ログインした"
|
description: "通算15日ログインした"
|
||||||
@@ -1141,7 +1152,7 @@ _achievements:
|
|||||||
_iLoveMisskey:
|
_iLoveMisskey:
|
||||||
title: "Misskey好きやねん"
|
title: "Misskey好きやねん"
|
||||||
description: "\"I ❤ #Misskey\"を投稿した"
|
description: "\"I ❤ #Misskey\"を投稿した"
|
||||||
flavor: "Misskeyを使ってくれてありがとうな~ by 開発チーム"
|
flavor: "Misskeyを使ってくれておおきにな~ by 開発チーム"
|
||||||
_foundTreasure:
|
_foundTreasure:
|
||||||
title: "なんでも鑑定団"
|
title: "なんでも鑑定団"
|
||||||
description: "隠されたお宝を発見した"
|
description: "隠されたお宝を発見した"
|
||||||
@@ -1167,7 +1178,7 @@ _achievements:
|
|||||||
description: "ホームタイムラインの流速が20npmを超す"
|
description: "ホームタイムラインの流速が20npmを超す"
|
||||||
_viewInstanceChart:
|
_viewInstanceChart:
|
||||||
title: "アナリスト"
|
title: "アナリスト"
|
||||||
description: "インスタンスのチャートを表示した"
|
description: "サーバーのチャートを表示した"
|
||||||
_outputHelloWorldOnScratchpad:
|
_outputHelloWorldOnScratchpad:
|
||||||
title: "Hello, world!"
|
title: "Hello, world!"
|
||||||
description: "スクラッチパッドで hello worldを出力した"
|
description: "スクラッチパッドで hello worldを出力した"
|
||||||
@@ -1204,7 +1215,7 @@ _achievements:
|
|||||||
_loggedInOnNewYearsDay:
|
_loggedInOnNewYearsDay:
|
||||||
title: "あけましておめでとうございます!"
|
title: "あけましておめでとうございます!"
|
||||||
description: "元旦にログインした"
|
description: "元旦にログインした"
|
||||||
flavor: "今年も弊インスタンスをよろしくお願いします"
|
flavor: "今年も弊サーバーをよろしゅう頼みますわ"
|
||||||
_cookieClicked:
|
_cookieClicked:
|
||||||
title: "クッキー叩くやつ"
|
title: "クッキー叩くやつ"
|
||||||
description: "クッキー叩いてもうた"
|
description: "クッキー叩いてもうた"
|
||||||
@@ -1219,8 +1230,8 @@ _role:
|
|||||||
name: "ロール名"
|
name: "ロール名"
|
||||||
description: "ロールの説明"
|
description: "ロールの説明"
|
||||||
permission: "ロールの権限"
|
permission: "ロールの権限"
|
||||||
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はインスタンスの全ての設定を変更できるで。"
|
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はサーバーの全ての設定を変更できるで。"
|
||||||
assignTarget: "アサインターゲット"
|
assignTarget: "アサイン"
|
||||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。"
|
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。"
|
||||||
manual: "マニュアル"
|
manual: "マニュアル"
|
||||||
conditional: "コンディショナル"
|
conditional: "コンディショナル"
|
||||||
@@ -1249,7 +1260,7 @@ _role:
|
|||||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
canInvite: "インスタンス招待コードの発行"
|
canInvite: "サーバー招待コードの発行"
|
||||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
pinMax: "ノートのピン留めの最大数"
|
pinMax: "ノートのピン留めの最大数"
|
||||||
@@ -1261,7 +1272,7 @@ _role:
|
|||||||
userListMax: "ユーザーリストの作成可能数"
|
userListMax: "ユーザーリストの作成可能数"
|
||||||
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
|
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
|
||||||
rateLimitFactor: "レートリミット"
|
rateLimitFactor: "レートリミット"
|
||||||
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。"
|
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
|
||||||
canHideAds: "広告を表示させへん"
|
canHideAds: "広告を表示させへん"
|
||||||
canSearchNotes: "ノート検索を使わすかどうか"
|
canSearchNotes: "ノート検索を使わすかどうか"
|
||||||
_condition:
|
_condition:
|
||||||
@@ -1273,6 +1284,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||||
followingLessThanOrEq: "フォロー数が~以下"
|
followingLessThanOrEq: "フォロー数が~以下"
|
||||||
followingMoreThanOrEq: "フォロー数が~以上"
|
followingMoreThanOrEq: "フォロー数が~以上"
|
||||||
|
notesLessThanOrEq: "投稿数が~以下しかない"
|
||||||
|
notesMoreThanOrEq: "投稿を~以上しとる"
|
||||||
and: "~かつ~"
|
and: "~かつ~"
|
||||||
or: "~または~"
|
or: "~または~"
|
||||||
not: "~ではない"
|
not: "~ではない"
|
||||||
@@ -1312,7 +1325,7 @@ _ad:
|
|||||||
_forgotPassword:
|
_forgotPassword:
|
||||||
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
|
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
|
||||||
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
|
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
|
||||||
contactAdmin: "このインスタンスはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
|
contactAdmin: "このサーバーはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
|
||||||
_gallery:
|
_gallery:
|
||||||
my: "あんたの投稿"
|
my: "あんたの投稿"
|
||||||
liked: "いいねした投稿"
|
liked: "いいねした投稿"
|
||||||
@@ -1397,10 +1410,10 @@ _wordMute:
|
|||||||
hard: "ハード"
|
hard: "ハード"
|
||||||
mutedNotes: "ミュートされたノート"
|
mutedNotes: "ミュートされたノート"
|
||||||
_instanceMute:
|
_instanceMute:
|
||||||
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
||||||
instanceMuteDescription2: "改行で区切って設定するんやで"
|
instanceMuteDescription2: "改行で区切って設定するんやで"
|
||||||
title: "設定したインスタンスのノートを隠すで。"
|
title: "設定したサーバーのノートを隠すで。"
|
||||||
heading: "ミュートするインスタンス"
|
heading: "ミュートするサーバー"
|
||||||
_theme:
|
_theme:
|
||||||
explore: "テーマを探す"
|
explore: "テーマを探す"
|
||||||
install: "テーマのインストール"
|
install: "テーマのインストール"
|
||||||
@@ -1622,7 +1635,7 @@ _widgets:
|
|||||||
digitalClock: "デジタル時計"
|
digitalClock: "デジタル時計"
|
||||||
unixClock: "UNIX時計"
|
unixClock: "UNIX時計"
|
||||||
federation: "連合"
|
federation: "連合"
|
||||||
instanceCloud: "インスタンスクラウド"
|
instanceCloud: "サーバークラウド"
|
||||||
postForm: "投稿フォーム"
|
postForm: "投稿フォーム"
|
||||||
slideshow: "スライドショー"
|
slideshow: "スライドショー"
|
||||||
button: "ボタン"
|
button: "ボタン"
|
||||||
@@ -1673,7 +1686,7 @@ _visibility:
|
|||||||
specified: "ダイレクト"
|
specified: "ダイレクト"
|
||||||
specifiedDescription: "選んだユーザーのみに公開するで"
|
specifiedDescription: "選んだユーザーのみに公開するで"
|
||||||
disableFederation: "連合なし"
|
disableFederation: "連合なし"
|
||||||
disableFederationDescription: "他インスタンスへは送らんとくわ"
|
disableFederationDescription: "他サーバーへは送らんとくわ"
|
||||||
_postForm:
|
_postForm:
|
||||||
replyPlaceholder: "このノートに返信..."
|
replyPlaceholder: "このノートに返信..."
|
||||||
quotePlaceholder: "このノートを引用..."
|
quotePlaceholder: "このノートを引用..."
|
||||||
@@ -1868,3 +1881,21 @@ _dialog:
|
|||||||
_disabledTimeline:
|
_disabledTimeline:
|
||||||
title: "使われへんタイムライン"
|
title: "使われへんタイムライン"
|
||||||
description: "あんたの今のロールやったら、このタイムラインは使われへんで。"
|
description: "あんたの今のロールやったら、このタイムラインは使われへんで。"
|
||||||
|
_drivecleaner:
|
||||||
|
orderBySizeDesc: "サイズのでかい順"
|
||||||
|
orderByCreatedAtAsc: "追加日の古い順"
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "Webhookをつくる"
|
||||||
|
name: "名前"
|
||||||
|
secret: "シークレット"
|
||||||
|
events: "Webhookを投げるタイミング"
|
||||||
|
active: "有効"
|
||||||
|
_events:
|
||||||
|
follow: "フォローしたとき~!"
|
||||||
|
followed: "フォローもらったとき~!"
|
||||||
|
note: "ノートを投稿したとき~!"
|
||||||
|
reply: "返信があるとき~!"
|
||||||
|
renote: "Renoteされるとき~!"
|
||||||
|
reaction: "リアクションがあるとき~!"
|
||||||
|
mention: "メンションがあるとき~!"
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -103,3 +103,4 @@ _deck:
|
|||||||
_columns:
|
_columns:
|
||||||
notifications: "Ilɣuyen"
|
notifications: "Ilɣuyen"
|
||||||
list: "Tibdarin"
|
list: "Tibdarin"
|
||||||
|
|
||||||
|
|||||||
@@ -83,3 +83,4 @@ _deck:
|
|||||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||||
tl: "ಸಮಯಸಾಲು"
|
tl: "ಸಮಯಸಾಲು"
|
||||||
mentions: "ಹೆಸರಿಸಿದ"
|
mentions: "ಹೆಸರಿಸಿದ"
|
||||||
|
|
||||||
|
|||||||
@@ -592,7 +592,6 @@ tokenRequested: "계정 접근 허용"
|
|||||||
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
|
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
|
||||||
notificationType: "알림 유형"
|
notificationType: "알림 유형"
|
||||||
edit: "편집"
|
edit: "편집"
|
||||||
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
|
|
||||||
emailServer: "메일 서버"
|
emailServer: "메일 서버"
|
||||||
enableEmail: "이메일 송신 기능 활성화"
|
enableEmail: "이메일 송신 기능 활성화"
|
||||||
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
|
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
|
||||||
@@ -959,7 +958,6 @@ copyErrorInfo: "오류 정보 복사"
|
|||||||
joinThisServer: "이 서버에 가입"
|
joinThisServer: "이 서버에 가입"
|
||||||
exploreOtherServers: "다른 서버 둘러보기"
|
exploreOtherServers: "다른 서버 둘러보기"
|
||||||
letsLookAtTimeline: "타임라인 구경하기"
|
letsLookAtTimeline: "타임라인 구경하기"
|
||||||
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
|
|
||||||
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
|
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
|
||||||
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
|
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
|
||||||
postToTheChannel: "채널에 게시하기"
|
postToTheChannel: "채널에 게시하기"
|
||||||
@@ -1849,3 +1847,7 @@ _deck:
|
|||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
|
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
|
||||||
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
|
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "이름"
|
||||||
|
active: "활성화"
|
||||||
|
|
||||||
|
|||||||
@@ -368,3 +368,4 @@ _deck:
|
|||||||
list: "ລາຍການ"
|
list: "ລາຍການ"
|
||||||
channel: "ຊ່ອງ"
|
channel: "ຊ່ອງ"
|
||||||
mentions: "ກ່າວເຖິງ"
|
mentions: "ກ່າວເຖິງ"
|
||||||
|
|
||||||
|
|||||||
@@ -483,3 +483,6 @@ _deck:
|
|||||||
antenna: "Antennes"
|
antenna: "Antennes"
|
||||||
list: "Lijsten"
|
list: "Lijsten"
|
||||||
mentions: "Vermeldingen"
|
mentions: "Vermeldingen"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Naam"
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
---
|
---
|
||||||
_lang_: "Norsk Bokmål"
|
_lang_: "Norsk Bokmål"
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
|
|||||||
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
|
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
|
||||||
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
|
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
|
||||||
selectList: "Wybierz listę"
|
selectList: "Wybierz listę"
|
||||||
|
selectChannel: "Wybierz kanał"
|
||||||
selectAntenna: "Wybierz Antennę"
|
selectAntenna: "Wybierz Antennę"
|
||||||
selectWidget: "Wybierz widżet"
|
selectWidget: "Wybierz widżet"
|
||||||
editWidgets: "Edytuj widżety"
|
editWidgets: "Edytuj widżety"
|
||||||
@@ -149,6 +150,7 @@ flagAsCatDescription: "Przełącz tę opcję, aby konto było oznaczone jako kot
|
|||||||
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
|
flagShowTimelineReplies: "Pokazuj odpowiedzi na osi czasu"
|
||||||
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
||||||
addAccount: "Dodaj konto"
|
addAccount: "Dodaj konto"
|
||||||
|
reloadAccountsList: "Odśwież listę kont"
|
||||||
loginFailed: "Nie udało się zalogować"
|
loginFailed: "Nie udało się zalogować"
|
||||||
showOnRemote: "Zobacz na zdalnej instancji"
|
showOnRemote: "Zobacz na zdalnej instancji"
|
||||||
general: "Ogólne"
|
general: "Ogólne"
|
||||||
@@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
|
|||||||
youHaveNoLists: "Nie masz żadnej listy"
|
youHaveNoLists: "Nie masz żadnej listy"
|
||||||
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
|
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
|
||||||
proxyAccount: "Konto proxy"
|
proxyAccount: "Konto proxy"
|
||||||
|
proxyAccountDescription: "Opis konta pełnomocniczego"
|
||||||
host: "Host"
|
host: "Host"
|
||||||
selectUser: "Wybierz użytkownika"
|
selectUser: "Wybierz użytkownika"
|
||||||
recipient: "Odbiorca"
|
recipient: "Odbiorca"
|
||||||
@@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
|
|||||||
startMessaging: "Rozpocznij czat"
|
startMessaging: "Rozpocznij czat"
|
||||||
nUsersRead: "przeczytano przez {n}"
|
nUsersRead: "przeczytano przez {n}"
|
||||||
agreeTo: "Wyrażam zgodę na {0}"
|
agreeTo: "Wyrażam zgodę na {0}"
|
||||||
|
agreeBelow: "Zaakceptuj poniżej"
|
||||||
tos: "Regulamin"
|
tos: "Regulamin"
|
||||||
start: "Rozpocznij"
|
start: "Rozpocznij"
|
||||||
home: "Strona główna"
|
home: "Strona główna"
|
||||||
@@ -385,13 +389,19 @@ about: "Informacje"
|
|||||||
aboutMisskey: "O Misskey"
|
aboutMisskey: "O Misskey"
|
||||||
administrator: "Admin"
|
administrator: "Admin"
|
||||||
token: "Token"
|
token: "Token"
|
||||||
|
2fa: "Klucz 2FA "
|
||||||
|
totp: "Klucz aplikacji uwierzytelniającej (totp)"
|
||||||
|
totpDescription: "Opis klucza czasowego"
|
||||||
moderator: "Moderator"
|
moderator: "Moderator"
|
||||||
moderation: "Moderacja"
|
moderation: "Moderacja"
|
||||||
nUsersMentioned: "{n} wspomnianych użytkowników"
|
nUsersMentioned: "{n} wspomnianych użytkowników"
|
||||||
|
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
|
||||||
securityKey: "Klucz bezpieczeństwa"
|
securityKey: "Klucz bezpieczeństwa"
|
||||||
lastUsed: "Ostatnio używane"
|
lastUsed: "Ostatnio używane"
|
||||||
|
lastUsedAt: "Ostatnio używane w"
|
||||||
unregister: "Cofnij rejestrację"
|
unregister: "Cofnij rejestrację"
|
||||||
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
|
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
|
||||||
|
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
|
||||||
resetPassword: "Zresetuj hasło"
|
resetPassword: "Zresetuj hasło"
|
||||||
newPasswordIs: "Nowe hasło to „{password}”"
|
newPasswordIs: "Nowe hasło to „{password}”"
|
||||||
reduceUiAnimation: "Ogranicz animacje w UI"
|
reduceUiAnimation: "Ogranicz animacje w UI"
|
||||||
@@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
|
|||||||
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
|
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
|
||||||
deleteAllFiles: "Usuń wszystkie pliki"
|
deleteAllFiles: "Usuń wszystkie pliki"
|
||||||
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
|
deleteAllFilesConfirm: "Czy na pewno chcesz usunąć wszystkie pliki?"
|
||||||
|
removeAllFollowing: "Przestań obserwować"
|
||||||
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
|
removeAllFollowingDescription: "Przestań obserwować wszystkie konta z {host}. Wykonaj to, jeżeli instancja już nie istnieje."
|
||||||
userSuspended: "To konto zostało zawieszone."
|
userSuspended: "To konto zostało zawieszone."
|
||||||
userSilenced: "Ten użytkownik został wyciszony."
|
userSilenced: "Ten użytkownik został wyciszony."
|
||||||
yourAccountSuspendedTitle: "To konto jest zawieszone"
|
yourAccountSuspendedTitle: "To konto jest zawieszone"
|
||||||
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
|
yourAccountSuspendedDescription: "To konto zostało zawieszone z powodu złamania regulaminu serwera lub innych podobnych. Skontaktuj się z administratorem, jeśli chciałbyś poznać bardziej szczegółowy powód. Proszę nie zakładać nowego konta."
|
||||||
|
tokenRevoked: "Token odrzucony"
|
||||||
|
tokenRevokedDescription: "Opis odrzuconego tokena"
|
||||||
|
accountDeleted: "Konto usunięte"
|
||||||
|
accountDeletedDescription: "Opis konta usuniętego"
|
||||||
menu: "Menu"
|
menu: "Menu"
|
||||||
divider: "Rozdzielacz"
|
divider: "Rozdzielacz"
|
||||||
addItem: "Dodaj element"
|
addItem: "Dodaj element"
|
||||||
@@ -548,7 +563,9 @@ author: "Autor"
|
|||||||
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
||||||
manage: "Zarządzanie"
|
manage: "Zarządzanie"
|
||||||
plugins: "Wtyczki"
|
plugins: "Wtyczki"
|
||||||
|
preferencesBackups: "Kopia zapasowa ustawień"
|
||||||
deck: "Tablica"
|
deck: "Tablica"
|
||||||
|
undeck: "oddkouj"
|
||||||
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
|
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
|
||||||
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
|
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
|
||||||
width: "Szerokość"
|
width: "Szerokość"
|
||||||
@@ -564,7 +581,6 @@ tokenRequested: "Przydziel dostęp do konta"
|
|||||||
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
|
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
|
||||||
notificationType: "Rodzaj powiadomień"
|
notificationType: "Rodzaj powiadomień"
|
||||||
edit: "Edytuj"
|
edit: "Edytuj"
|
||||||
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
|
|
||||||
emailServer: "Serwer poczty e-mail"
|
emailServer: "Serwer poczty e-mail"
|
||||||
enableEmail: "Włącz dostarczanie wiadomości e-mail"
|
enableEmail: "Włącz dostarczanie wiadomości e-mail"
|
||||||
emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
|
emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
|
||||||
@@ -816,6 +832,8 @@ tenMinutes: "10 minut"
|
|||||||
oneHour: "1 godzina"
|
oneHour: "1 godzina"
|
||||||
oneDay: "1 dzień"
|
oneDay: "1 dzień"
|
||||||
oneWeek: "1 tydzień"
|
oneWeek: "1 tydzień"
|
||||||
|
oneMonth: "jeden miesiąc"
|
||||||
|
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
|
||||||
file: "Pliki"
|
file: "Pliki"
|
||||||
recommended: "Zalecane"
|
recommended: "Zalecane"
|
||||||
check: "Zweryfikuj"
|
check: "Zweryfikuj"
|
||||||
@@ -1358,3 +1376,7 @@ _deck:
|
|||||||
channel: "Kanały"
|
channel: "Kanały"
|
||||||
mentions: "Wspomnienia"
|
mentions: "Wspomnienia"
|
||||||
direct: "Bezpośredni"
|
direct: "Bezpośredni"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Nazwa"
|
||||||
|
active: "Właczono"
|
||||||
|
|
||||||
|
|||||||
@@ -555,3 +555,6 @@ _deck:
|
|||||||
list: "Listas"
|
list: "Listas"
|
||||||
mentions: "Menções"
|
mentions: "Menções"
|
||||||
direct: "Notas diretas"
|
direct: "Notas diretas"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Nome"
|
||||||
|
|
||||||
|
|||||||
@@ -561,7 +561,6 @@ tokenRequested: "Acordă acces la cont"
|
|||||||
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
|
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
|
||||||
notificationType: "Tipul notificării"
|
notificationType: "Tipul notificării"
|
||||||
edit: "Editează"
|
edit: "Editează"
|
||||||
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
|
|
||||||
emailServer: "Server email"
|
emailServer: "Server email"
|
||||||
enableEmail: "Activează distribuția de emailuri"
|
enableEmail: "Activează distribuția de emailuri"
|
||||||
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
|
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
|
||||||
@@ -702,3 +701,6 @@ _deck:
|
|||||||
list: "Liste"
|
list: "Liste"
|
||||||
channel: "Canale"
|
channel: "Canale"
|
||||||
mentions: "Mențiuni"
|
mentions: "Mențiuni"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Nume"
|
||||||
|
|
||||||
|
|||||||
@@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи"
|
|||||||
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
|
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
|
||||||
notificationType: "Тип уведомления"
|
notificationType: "Тип уведомления"
|
||||||
edit: "Изменить"
|
edit: "Изменить"
|
||||||
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
|
|
||||||
emailServer: "Сервер электронной почты"
|
emailServer: "Сервер электронной почты"
|
||||||
enableEmail: "Включить обмен электронной почтой"
|
enableEmail: "Включить обмен электронной почтой"
|
||||||
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
|
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
|
||||||
@@ -951,7 +950,6 @@ copyErrorInfo: "Скопировать код ошибки"
|
|||||||
joinThisServer: "Присоединяйтесь к этому серверу"
|
joinThisServer: "Присоединяйтесь к этому серверу"
|
||||||
exploreOtherServers: "Искать другие сервера"
|
exploreOtherServers: "Искать другие сервера"
|
||||||
letsLookAtTimeline: "Давайте посмотрим на ленту"
|
letsLookAtTimeline: "Давайте посмотрим на ленту"
|
||||||
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
|
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "Разблокировано в"
|
earnedAt: "Разблокировано в"
|
||||||
_types:
|
_types:
|
||||||
@@ -1837,3 +1835,7 @@ _deck:
|
|||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
|
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
|
||||||
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
|
charactersBelow: "Это ниже минимального количества символов! У вас {current} / из {min}"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Название"
|
||||||
|
active: "Вкл."
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -586,7 +586,6 @@ tokenRequested: "Povoliť prístup k účtu"
|
|||||||
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
|
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
|
||||||
notificationType: "Typ oznámenia"
|
notificationType: "Typ oznámenia"
|
||||||
edit: "Upraviť"
|
edit: "Upraviť"
|
||||||
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
|
|
||||||
emailServer: "Email server"
|
emailServer: "Email server"
|
||||||
enableEmail: "Zapnúť email"
|
enableEmail: "Zapnúť email"
|
||||||
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
|
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
|
||||||
@@ -1475,3 +1474,7 @@ _deck:
|
|||||||
channel: "Kanály"
|
channel: "Kanály"
|
||||||
mentions: "Zmienky"
|
mentions: "Zmienky"
|
||||||
direct: "Priame poznámky"
|
direct: "Priame poznámky"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Názov"
|
||||||
|
active: "Zapnuté"
|
||||||
|
|
||||||
|
|||||||
@@ -442,3 +442,6 @@ _deck:
|
|||||||
antenna: "Antenner"
|
antenna: "Antenner"
|
||||||
list: "Listor"
|
list: "Listor"
|
||||||
mentions: "Omnämningar"
|
mentions: "Omnämningar"
|
||||||
|
_webhookSettings:
|
||||||
|
active: "Aktiverad"
|
||||||
|
|
||||||
|
|||||||
@@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
|
|||||||
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
|
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
|
||||||
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
|
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
|
||||||
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
|
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
|
||||||
|
tokenRevoked: "โทเค็นไม่ถูกต้อง"
|
||||||
|
accountDeleted: "ลบบัญชีแล้ว"
|
||||||
menu: "เมนู"
|
menu: "เมนู"
|
||||||
divider: "ตัวแบ่ง"
|
divider: "ตัวแบ่ง"
|
||||||
addItem: "เพิ่มรายการ"
|
addItem: "เพิ่มรายการ"
|
||||||
@@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
|
|||||||
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
|
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
|
||||||
notificationType: "ประเภทการแจ้งเตือน"
|
notificationType: "ประเภทการแจ้งเตือน"
|
||||||
edit: "แก้ไข"
|
edit: "แก้ไข"
|
||||||
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
|
|
||||||
emailServer: "อีเมล์เซิร์ฟเวอร์"
|
emailServer: "อีเมล์เซิร์ฟเวอร์"
|
||||||
enableEmail: "เปิดใช้งานการกระจายอีเมล"
|
enableEmail: "เปิดใช้งานการกระจายอีเมล"
|
||||||
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
|
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
|
||||||
@@ -954,11 +955,22 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
|
|||||||
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
|
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
|
||||||
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
|
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
|
||||||
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
|
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
|
||||||
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
|
|
||||||
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
|
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
|
||||||
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
|
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
|
||||||
postToTheChannel: "โพสต์ลงช่อง"
|
postToTheChannel: "โพสต์ลงช่อง"
|
||||||
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
|
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
|
||||||
|
likeOnly: "ที่ชอบเท่านั้น"
|
||||||
|
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
|
||||||
|
sensitiveWords: "คำที่ละเอียดอ่อน"
|
||||||
|
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
|
||||||
|
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
|
||||||
|
license: "ใบอนุญาต"
|
||||||
|
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
|
||||||
|
myClips: "คลิปของฉัน"
|
||||||
|
drivecleaner: "ทำความสะอาดไดรฟ์"
|
||||||
|
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
|
||||||
|
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
|
||||||
|
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "ได้รับเมื่อ"
|
earnedAt: "ได้รับเมื่อ"
|
||||||
_types:
|
_types:
|
||||||
@@ -1218,6 +1230,8 @@ _role:
|
|||||||
iconUrl: "ไอคอน URL"
|
iconUrl: "ไอคอน URL"
|
||||||
asBadge: "แสดงเป็นตรา"
|
asBadge: "แสดงเป็นตรา"
|
||||||
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
|
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
|
||||||
|
displayOrder: "ตำแหน่ง"
|
||||||
|
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
|
||||||
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
||||||
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
||||||
priority: "ลำดับความสำคัญ"
|
priority: "ลำดับความสำคัญ"
|
||||||
@@ -1243,6 +1257,7 @@ _role:
|
|||||||
rateLimitFactor: "ขีดจำกัดอัตรา"
|
rateLimitFactor: "ขีดจำกัดอัตรา"
|
||||||
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
||||||
canHideAds: "ซ่อนโฆษณา"
|
canHideAds: "ซ่อนโฆษณา"
|
||||||
|
canSearchNotes: "การใช้การค้นหาโน้ต"
|
||||||
_condition:
|
_condition:
|
||||||
isLocal: "ผู้ใช้ภายใน"
|
isLocal: "ผู้ใช้ภายใน"
|
||||||
isRemote: "ผู้ใช้ระยะไกล"
|
isRemote: "ผู้ใช้ระยะไกล"
|
||||||
@@ -1844,3 +1859,13 @@ _deck:
|
|||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
|
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
|
||||||
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
|
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
|
||||||
|
_disabledTimeline:
|
||||||
|
title: "ปิดใช้งานไทม์ไลน์"
|
||||||
|
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
|
||||||
|
_drivecleaner:
|
||||||
|
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
|
||||||
|
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "ชื่อ"
|
||||||
|
active: "เปิดใช้งาน"
|
||||||
|
|
||||||
|
|||||||
@@ -60,3 +60,4 @@ _deck:
|
|||||||
_columns:
|
_columns:
|
||||||
notifications: "Bildirim"
|
notifications: "Bildirim"
|
||||||
tl: "Zaman çizelgesi"
|
tl: "Zaman çizelgesi"
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
_lang_: "ياپونچە"
|
_lang_: "ياپونچە"
|
||||||
search: "ئىزدەش"
|
search: "ئىزدەش"
|
||||||
searchByGoogle: "ئىزدەش"
|
searchByGoogle: "ئىزدەش"
|
||||||
|
|
||||||
|
|||||||
@@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту"
|
|||||||
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
|
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
|
||||||
notificationType: "Тип сповіщення"
|
notificationType: "Тип сповіщення"
|
||||||
edit: "Редагувати"
|
edit: "Редагувати"
|
||||||
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
|
|
||||||
emailServer: "Email сервер"
|
emailServer: "Email сервер"
|
||||||
enableEmail: "Увімкнути функцію доставки пошти"
|
enableEmail: "Увімкнути функцію доставки пошти"
|
||||||
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
|
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
|
||||||
@@ -1639,3 +1638,7 @@ _deck:
|
|||||||
channel: "Канали"
|
channel: "Канали"
|
||||||
mentions: "Згадки"
|
mentions: "Згадки"
|
||||||
direct: "Особисте"
|
direct: "Особисте"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Ім'я"
|
||||||
|
active: "Увімкнено"
|
||||||
|
|
||||||
|
|||||||
@@ -585,7 +585,6 @@ tokenRequested: "Cấp quyền truy cập vào tài khoản"
|
|||||||
pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
|
pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
|
||||||
notificationType: "Loại thông báo"
|
notificationType: "Loại thông báo"
|
||||||
edit: "Sửa"
|
edit: "Sửa"
|
||||||
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
|
|
||||||
emailServer: "Email máy chủ"
|
emailServer: "Email máy chủ"
|
||||||
enableEmail: "Bật phân phối email"
|
enableEmail: "Bật phân phối email"
|
||||||
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
|
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
|
||||||
@@ -1705,3 +1704,7 @@ _deck:
|
|||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
|
charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}"
|
||||||
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"
|
charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}"
|
||||||
|
_webhookSettings:
|
||||||
|
name: "Tên"
|
||||||
|
active: "Đã bật"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ cancel: "取消"
|
|||||||
noThankYou: "不用,谢谢"
|
noThankYou: "不用,谢谢"
|
||||||
enterUsername: "输入用户名"
|
enterUsername: "输入用户名"
|
||||||
renotedBy: "由 {user} 转贴"
|
renotedBy: "由 {user} 转贴"
|
||||||
noNotes: "没有帖子"
|
noNotes: "没有帖文"
|
||||||
noNotifications: "无通知"
|
noNotifications: "无通知"
|
||||||
instance: "服务器"
|
instance: "服务器"
|
||||||
settings: "设置"
|
settings: "设置"
|
||||||
@@ -25,7 +25,7 @@ otherSettings: "其他设置"
|
|||||||
openInWindow: "在新窗口中打开"
|
openInWindow: "在新窗口中打开"
|
||||||
profile: "个人资料"
|
profile: "个人资料"
|
||||||
timeline: "时间线"
|
timeline: "时间线"
|
||||||
noAccountDescription: "这个人很懒,没有写自我介绍"
|
noAccountDescription: "此用户尚无自我介绍"
|
||||||
login: "登录"
|
login: "登录"
|
||||||
loggingIn: "正在登录..."
|
loggingIn: "正在登录..."
|
||||||
logout: "登出"
|
logout: "登出"
|
||||||
@@ -85,7 +85,7 @@ somethingHappened: "出现了一些问题!"
|
|||||||
retry: "重试"
|
retry: "重试"
|
||||||
pageLoadError: "页面加载失败。"
|
pageLoadError: "页面加载失败。"
|
||||||
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
||||||
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
|
serverIsDead: "没有服务器响应。 请稍后再试。"
|
||||||
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
||||||
enterListName: "输入列表名称"
|
enterListName: "输入列表名称"
|
||||||
privacy: "隐私"
|
privacy: "隐私"
|
||||||
@@ -95,7 +95,7 @@ follow: "关注"
|
|||||||
followRequest: "关注申请"
|
followRequest: "关注申请"
|
||||||
followRequests: "关注申请"
|
followRequests: "关注申请"
|
||||||
unfollow: "取消关注"
|
unfollow: "取消关注"
|
||||||
followRequestPending: "发送关注请求"
|
followRequestPending: "关注请求批准中"
|
||||||
enterEmoji: "输入表情符号"
|
enterEmoji: "输入表情符号"
|
||||||
renote: "转发"
|
renote: "转发"
|
||||||
unrenote: "取消转发"
|
unrenote: "取消转发"
|
||||||
@@ -119,7 +119,7 @@ rememberNoteVisibility: "保存上次设置的可见性"
|
|||||||
attachCancel: "删除附件"
|
attachCancel: "删除附件"
|
||||||
markAsSensitive: "标记为敏感内容"
|
markAsSensitive: "标记为敏感内容"
|
||||||
unmarkAsSensitive: "取消标记为敏感内容"
|
unmarkAsSensitive: "取消标记为敏感内容"
|
||||||
enterFileName: "请输入文件名"
|
enterFileName: "输入文件名"
|
||||||
mute: "屏蔽"
|
mute: "屏蔽"
|
||||||
unmute: "解除屏蔽"
|
unmute: "解除屏蔽"
|
||||||
renoteMute: "屏蔽转帖"
|
renoteMute: "屏蔽转帖"
|
||||||
@@ -145,7 +145,7 @@ emojiName: "表情符号名称"
|
|||||||
emojiUrl: "表情符号地址"
|
emojiUrl: "表情符号地址"
|
||||||
addEmoji: "添加表情符号"
|
addEmoji: "添加表情符号"
|
||||||
settingGuide: "推荐配置"
|
settingGuide: "推荐配置"
|
||||||
cacheRemoteFiles: "远程文件缓存"
|
cacheRemoteFiles: "缓存远程文件"
|
||||||
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
|
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
|
||||||
flagAsBot: "这是一个机器人账号"
|
flagAsBot: "这是一个机器人账号"
|
||||||
flagAsBotDescription: "如果此帐户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此帐户识别为机器人。"
|
flagAsBotDescription: "如果此帐户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此帐户识别为机器人。"
|
||||||
@@ -153,7 +153,7 @@ flagAsCat: "将这个账户设定为一只猫"
|
|||||||
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。"
|
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。"
|
||||||
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||||
autoAcceptFollowed: "自动允许关注者的关注"
|
autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
|
||||||
addAccount: "添加账户"
|
addAccount: "添加账户"
|
||||||
reloadAccountsList: "更新账户列表"
|
reloadAccountsList: "更新账户列表"
|
||||||
loginFailed: "登录失败"
|
loginFailed: "登录失败"
|
||||||
@@ -203,7 +203,7 @@ blockedInstances: "被阻拦的服务器"
|
|||||||
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
|
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
|
||||||
muteAndBlock: "屏蔽/拉黑"
|
muteAndBlock: "屏蔽/拉黑"
|
||||||
mutedUsers: "已屏蔽用户"
|
mutedUsers: "已屏蔽用户"
|
||||||
blockedUsers: "被拉黑的用户"
|
blockedUsers: "已拉黑的用户"
|
||||||
noUsers: "无用户"
|
noUsers: "无用户"
|
||||||
editProfile: "编辑资料"
|
editProfile: "编辑资料"
|
||||||
noteDeleteConfirm: "要删除该帖子吗?"
|
noteDeleteConfirm: "要删除该帖子吗?"
|
||||||
@@ -336,7 +336,7 @@ enableLocalTimeline: "启用本地时间线功能"
|
|||||||
enableGlobalTimeline: "启用全局时间线"
|
enableGlobalTimeline: "启用全局时间线"
|
||||||
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
|
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
|
||||||
registration: "注册"
|
registration: "注册"
|
||||||
enableRegistration: "允许新用户注册"
|
enableRegistration: "允许任何人注册"
|
||||||
invite: "邀请"
|
invite: "邀请"
|
||||||
driveCapacityPerLocalAccount: "每个用户的网盘空间"
|
driveCapacityPerLocalAccount: "每个用户的网盘空间"
|
||||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||||
@@ -354,7 +354,7 @@ pinnedNotes: "已置顶的帖子"
|
|||||||
hcaptcha: "hCaptcha"
|
hcaptcha: "hCaptcha"
|
||||||
enableHcaptcha: "启用 hCaptcha"
|
enableHcaptcha: "启用 hCaptcha"
|
||||||
hcaptchaSiteKey: "网站密钥"
|
hcaptchaSiteKey: "网站密钥"
|
||||||
hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
|
hcaptchaSecretKey: "密钥"
|
||||||
recaptcha: "reCAPTCHA"
|
recaptcha: "reCAPTCHA"
|
||||||
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
|
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
|
||||||
recaptchaSiteKey: "网站密钥"
|
recaptchaSiteKey: "网站密钥"
|
||||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "如果不使用https进行API连接,请关闭。"
|
|||||||
objectStorageUseProxy: "使用代理"
|
objectStorageUseProxy: "使用代理"
|
||||||
objectStorageUseProxyDesc: "如果您不使用代理进行API连接,请将其关闭。"
|
objectStorageUseProxyDesc: "如果您不使用代理进行API连接,请将其关闭。"
|
||||||
objectStorageSetPublicRead: "上传时设置为public-read"
|
objectStorageSetPublicRead: "上传时设置为public-read"
|
||||||
|
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
|
||||||
serverLogs: "服务器日志"
|
serverLogs: "服务器日志"
|
||||||
deleteAll: "全部删除"
|
deleteAll: "全部删除"
|
||||||
showFixedPostForm: "在时间线顶部显示发帖框"
|
showFixedPostForm: "在时间线顶部显示发帖框"
|
||||||
@@ -594,7 +595,6 @@ tokenRequested: "允许访问账户"
|
|||||||
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
|
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
|
||||||
notificationType: "通知类型"
|
notificationType: "通知类型"
|
||||||
edit: "编辑"
|
edit: "编辑"
|
||||||
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
|
|
||||||
emailServer: "邮件服务器"
|
emailServer: "邮件服务器"
|
||||||
enableEmail: "启用发送邮件功能"
|
enableEmail: "启用发送邮件功能"
|
||||||
emailConfigInfo: "用于确认电子邮件和密码重置"
|
emailConfigInfo: "用于确认电子邮件和密码重置"
|
||||||
@@ -961,7 +961,9 @@ copyErrorInfo: "复制错误信息"
|
|||||||
joinThisServer: "在本服务器上注册"
|
joinThisServer: "在本服务器上注册"
|
||||||
exploreOtherServers: "探索其他服务器"
|
exploreOtherServers: "探索其他服务器"
|
||||||
letsLookAtTimeline: "时间线"
|
letsLookAtTimeline: "时间线"
|
||||||
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。"
|
disableFederationConfirm: "确定要禁用联合?"
|
||||||
|
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
|
||||||
|
disableFederationOk: "联合禁用"
|
||||||
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
|
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
|
||||||
emailNotSupported: "此服务器不支持发送邮件"
|
emailNotSupported: "此服务器不支持发送邮件"
|
||||||
postToTheChannel: "发布到频道"
|
postToTheChannel: "发布到频道"
|
||||||
@@ -977,6 +979,15 @@ notesSearchNotAvailable: "帖子检索不可用"
|
|||||||
license: "许可信息"
|
license: "许可信息"
|
||||||
unfavoriteConfirm: "确定要取消收藏吗?"
|
unfavoriteConfirm: "确定要取消收藏吗?"
|
||||||
myClips: "我的便签"
|
myClips: "我的便签"
|
||||||
|
drivecleaner: "网盘整理"
|
||||||
|
retryAllQueuesNow: "立刻重试所有队列"
|
||||||
|
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
|
||||||
|
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
|
||||||
|
enableChartsForRemoteUser: "生成远程用户的图表"
|
||||||
|
enableChartsForFederatedInstances: "生成远程服务器的图表"
|
||||||
|
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
|
||||||
|
largeNoteReactions: "使用大图标来显示回应"
|
||||||
|
noteIdOrUrl: "帖子ID或URL"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "达成时间"
|
earnedAt: "达成时间"
|
||||||
_types:
|
_types:
|
||||||
@@ -1249,7 +1260,7 @@ _role:
|
|||||||
gtlAvailable: "查看全局时间线"
|
gtlAvailable: "查看全局时间线"
|
||||||
ltlAvailable: "查看本地时间线"
|
ltlAvailable: "查看本地时间线"
|
||||||
canPublicNote: "允许公开发帖"
|
canPublicNote: "允许公开发帖"
|
||||||
canInvite: "发放实例邀请码"
|
canInvite: "发放服务器邀请码"
|
||||||
canManageCustomEmojis: "管理自定义表情符号"
|
canManageCustomEmojis: "管理自定义表情符号"
|
||||||
driveCapacity: "网盘容量"
|
driveCapacity: "网盘容量"
|
||||||
pinMax: "帖子置顶数量限制"
|
pinMax: "帖子置顶数量限制"
|
||||||
@@ -1273,6 +1284,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "关注者不少于"
|
followersMoreThanOrEq: "关注者不少于"
|
||||||
followingLessThanOrEq: "关注中不多于"
|
followingLessThanOrEq: "关注中不多于"
|
||||||
followingMoreThanOrEq: "关注中不少于"
|
followingMoreThanOrEq: "关注中不少于"
|
||||||
|
notesLessThanOrEq: "帖子数在~以下"
|
||||||
|
notesMoreThanOrEq: "帖子数在~以上"
|
||||||
and: "符合以下全部条件"
|
and: "符合以下全部条件"
|
||||||
or: "符合以下任一条件"
|
or: "符合以下任一条件"
|
||||||
not: "不符合以下任何条件"
|
not: "不符合以下任何条件"
|
||||||
@@ -1621,7 +1634,7 @@ _widgets:
|
|||||||
photos: "照片"
|
photos: "照片"
|
||||||
digitalClock: "数字时钟"
|
digitalClock: "数字时钟"
|
||||||
unixClock: "UNIX时钟"
|
unixClock: "UNIX时钟"
|
||||||
federation: "联邦宇宙"
|
federation: "联合"
|
||||||
instanceCloud: "服务器云"
|
instanceCloud: "服务器云"
|
||||||
postForm: "投稿窗口"
|
postForm: "投稿窗口"
|
||||||
slideshow: "幻灯片展示"
|
slideshow: "幻灯片展示"
|
||||||
@@ -1868,3 +1881,21 @@ _dialog:
|
|||||||
_disabledTimeline:
|
_disabledTimeline:
|
||||||
title: "时间线已禁用"
|
title: "时间线已禁用"
|
||||||
description: "您不能在当前角色使用时间线。"
|
description: "您不能在当前角色使用时间线。"
|
||||||
|
_drivecleaner:
|
||||||
|
orderBySizeDesc: "按大小降序排列"
|
||||||
|
orderByCreatedAtAsc: "按添加日期降序排列"
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "创建 Webhook"
|
||||||
|
name: "名称"
|
||||||
|
secret: "密钥"
|
||||||
|
events: "何时运行Webhook"
|
||||||
|
active: "已启用"
|
||||||
|
_events:
|
||||||
|
follow: "关注时"
|
||||||
|
followed: "被关注时"
|
||||||
|
note: "发布贴文时"
|
||||||
|
reply: "收到回复时"
|
||||||
|
renote: "被转发时"
|
||||||
|
reaction: "被回应时"
|
||||||
|
mention: "被提及时"
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ gotIt: "知道了"
|
|||||||
cancel: "取消"
|
cancel: "取消"
|
||||||
noThankYou: "現在不要"
|
noThankYou: "現在不要"
|
||||||
enterUsername: "輸入使用者名稱"
|
enterUsername: "輸入使用者名稱"
|
||||||
renotedBy: "{user} 轉傳了"
|
renotedBy: "{user} 轉發了"
|
||||||
noNotes: "無貼文。"
|
noNotes: "無貼文。"
|
||||||
noNotifications: "沒有通知"
|
noNotifications: "沒有通知"
|
||||||
instance: "實例"
|
instance: "實例"
|
||||||
@@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
|
|||||||
enterEmoji: "輸入表情符號"
|
enterEmoji: "輸入表情符號"
|
||||||
renote: "轉發"
|
renote: "轉發"
|
||||||
unrenote: "取消轉發"
|
unrenote: "取消轉發"
|
||||||
renoted: "轉傳成功"
|
renoted: "轉發成功"
|
||||||
cantRenote: "無法轉發此貼文。"
|
cantRenote: "無法轉發此貼文。"
|
||||||
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
|
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
||||||
quote: "引用"
|
quote: "引用"
|
||||||
inChannelRenote: "在頻道內轉發"
|
inChannelRenote: "在頻道內轉發"
|
||||||
inChannelQuote: "在頻道內引用"
|
inChannelQuote: "在頻道內引用"
|
||||||
@@ -531,8 +531,8 @@ installedDate: "安裝時間"
|
|||||||
lastUsedDate: "最後上線日期"
|
lastUsedDate: "最後上線日期"
|
||||||
state: "狀態"
|
state: "狀態"
|
||||||
sort: "排序"
|
sort: "排序"
|
||||||
ascendingOrder: "遞增"
|
ascendingOrder: "昇冪"
|
||||||
descendingOrder: "遞減"
|
descendingOrder: "降冪"
|
||||||
scratchpad: "暫存記憶體"
|
scratchpad: "暫存記憶體"
|
||||||
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
|
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Misskey互動的结果。"
|
||||||
output: "輸出"
|
output: "輸出"
|
||||||
@@ -594,7 +594,6 @@ tokenRequested: "允許存取帳戶"
|
|||||||
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
|
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
|
||||||
notificationType: "通知形式"
|
notificationType: "通知形式"
|
||||||
edit: "編輯"
|
edit: "編輯"
|
||||||
useStarForReactionFallback: "以★代替未知的表情符號"
|
|
||||||
emailServer: "電郵伺服器"
|
emailServer: "電郵伺服器"
|
||||||
enableEmail: "啟用發送電郵功能"
|
enableEmail: "啟用發送電郵功能"
|
||||||
emailConfigInfo: "用於確認電郵地址及密碼重置"
|
emailConfigInfo: "用於確認電郵地址及密碼重置"
|
||||||
@@ -678,8 +677,8 @@ sentReactionsCount: "反應發送次數"
|
|||||||
receivedReactionsCount: "收到反應次數"
|
receivedReactionsCount: "收到反應次數"
|
||||||
pollVotesCount: "已統計的投票數"
|
pollVotesCount: "已統計的投票數"
|
||||||
pollVotedCount: "已投票數"
|
pollVotedCount: "已投票數"
|
||||||
yes: "確定"
|
yes: "是"
|
||||||
no: "取消"
|
no: "否"
|
||||||
driveFilesCount: "雲端硬碟檔案數量"
|
driveFilesCount: "雲端硬碟檔案數量"
|
||||||
driveUsage: "雲端硬碟使用量"
|
driveUsage: "雲端硬碟使用量"
|
||||||
noCrawle: "拒絕搜尋引擎索引"
|
noCrawle: "拒絕搜尋引擎索引"
|
||||||
@@ -961,7 +960,6 @@ copyErrorInfo: "複製錯誤資訊"
|
|||||||
joinThisServer: "在此伺服器上註冊"
|
joinThisServer: "在此伺服器上註冊"
|
||||||
exploreOtherServers: "探索其他伺服器"
|
exploreOtherServers: "探索其他伺服器"
|
||||||
letsLookAtTimeline: "看看時間軸"
|
letsLookAtTimeline: "看看時間軸"
|
||||||
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
|
|
||||||
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
|
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
|
||||||
emailNotSupported: "這個伺服器不支援寄送郵件"
|
emailNotSupported: "這個伺服器不支援寄送郵件"
|
||||||
postToTheChannel: "發布到頻道"
|
postToTheChannel: "發布到頻道"
|
||||||
@@ -973,6 +971,19 @@ rolesAssignedToMe: "指派給自己的角色"
|
|||||||
resetPasswordConfirm: "重設密碼?"
|
resetPasswordConfirm: "重設密碼?"
|
||||||
sensitiveWords: "敏感詞"
|
sensitiveWords: "敏感詞"
|
||||||
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
|
sensitiveWordsDescription: "將含有設定詞彙的貼文可見性設為發送至首頁。可以用換行來進行複數的設定。"
|
||||||
|
notesSearchNotAvailable: "無法使用搜尋貼文功能。"
|
||||||
|
license: "授權"
|
||||||
|
unfavoriteConfirm: "要取消收錄我的最愛嗎?"
|
||||||
|
myClips: "我的摘錄"
|
||||||
|
drivecleaner: "雲端硬碟清掃器"
|
||||||
|
retryAllQueuesNow: "立刻重試所有佇列"
|
||||||
|
retryAllQueuesConfirmTitle: "要現在重試嗎?"
|
||||||
|
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
|
||||||
|
enableChartsForRemoteUser: "生成遠端用戶的圖表"
|
||||||
|
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
|
||||||
|
showClipButtonInNoteFooter: "將摘錄添加至貼文"
|
||||||
|
largeNoteReactions: "將貼文的反應放大顯示"
|
||||||
|
noteIdOrUrl: "貼文ID或URL"
|
||||||
_achievements:
|
_achievements:
|
||||||
earnedAt: "獲得日期"
|
earnedAt: "獲得日期"
|
||||||
_types:
|
_types:
|
||||||
@@ -1089,7 +1100,7 @@ _achievements:
|
|||||||
title: "有備而來"
|
title: "有備而來"
|
||||||
description: "設定了個人檔案"
|
description: "設定了個人檔案"
|
||||||
_markedAsCat:
|
_markedAsCat:
|
||||||
title: "我是貓"
|
title: "吾輩乃貓是也"
|
||||||
description: "已將帳戶設定為貓"
|
description: "已將帳戶設定為貓"
|
||||||
flavor: "還沒有名字。"
|
flavor: "還沒有名字。"
|
||||||
_following1:
|
_following1:
|
||||||
@@ -1269,6 +1280,8 @@ _role:
|
|||||||
followersMoreThanOrEq: "追隨者人數在~以上"
|
followersMoreThanOrEq: "追隨者人數在~以上"
|
||||||
followingLessThanOrEq: "追隨人數在~以下"
|
followingLessThanOrEq: "追隨人數在~以下"
|
||||||
followingMoreThanOrEq: "追隨人數在~以上"
|
followingMoreThanOrEq: "追隨人數在~以上"
|
||||||
|
notesLessThanOrEq: "發布數在~以下"
|
||||||
|
notesMoreThanOrEq: "發布數在~以上"
|
||||||
and: "~和~"
|
and: "~和~"
|
||||||
or: "~或~"
|
or: "~或~"
|
||||||
not: "~否"
|
not: "~否"
|
||||||
@@ -1498,7 +1511,7 @@ _time:
|
|||||||
_tutorial:
|
_tutorial:
|
||||||
title: "Misskey使用方法"
|
title: "Misskey使用方法"
|
||||||
step1_1: "歡迎!"
|
step1_1: "歡迎!"
|
||||||
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」"
|
step1_2: "此為「時間軸」頁面,它會按照時間順序顯示你「追隨」的人發出的「貼文」。"
|
||||||
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
step1_3: "由於你沒有發佈任何貼文,也沒有追隨任何人,所以你的時間軸目前是空的。"
|
||||||
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
|
step2_1: "在發文或追隨其他人之前先讓我們設定一下個人資料吧。"
|
||||||
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
|
step2_2: "提供一些關於自己的資訊來讓其他人更有追隨你的意願。"
|
||||||
@@ -1864,3 +1877,21 @@ _dialog:
|
|||||||
_disabledTimeline:
|
_disabledTimeline:
|
||||||
title: "停用的時間軸"
|
title: "停用的時間軸"
|
||||||
description: "目前的角色無法使用這個時間軸。"
|
description: "目前的角色無法使用這個時間軸。"
|
||||||
|
_drivecleaner:
|
||||||
|
orderBySizeDesc: "檔案由大到小"
|
||||||
|
orderByCreatedAtAsc: "依照加入的日期順序"
|
||||||
|
_webhookSettings:
|
||||||
|
createWebhook: "建立 Webhook"
|
||||||
|
name: "名稱"
|
||||||
|
secret: "秘密"
|
||||||
|
events: "什麼時候運行Webhook"
|
||||||
|
active: "已啟用"
|
||||||
|
_events:
|
||||||
|
follow: "當你追隨時"
|
||||||
|
followed: "當被追隨時"
|
||||||
|
note: "當發布貼文時"
|
||||||
|
reply: "當收到回覆時"
|
||||||
|
renote: "當被轉發時"
|
||||||
|
reaction: "當獲得反應時"
|
||||||
|
mention: "當被提到時"
|
||||||
|
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.10.0",
|
"version": "13.11.0-beta.7",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/misskey-dev/misskey.git"
|
"url": "https://github.com/misskey-dev/misskey.git"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.29.3",
|
"packageManager": "pnpm@8.1.1",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build-pre": "node ./scripts/build-pre.js",
|
"build-pre": "node ./scripts/build-pre.js",
|
||||||
"build": "pnpm build-pre && pnpm -r build && pnpm gulp",
|
"build": "pnpm build-pre && pnpm -r build && pnpm gulp",
|
||||||
|
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
|
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
|
||||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
|
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
|
||||||
"init": "pnpm migrate",
|
"init": "pnpm migrate",
|
||||||
@@ -50,16 +51,16 @@
|
|||||||
"gulp-replace": "1.1.4",
|
"gulp-replace": "1.1.4",
|
||||||
"gulp-terser": "2.1.0",
|
"gulp-terser": "2.1.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"typescript": "4.9.5"
|
"typescript": "5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gulp": "4.0.10",
|
"@types/gulp": "4.0.10",
|
||||||
"@types/gulp-rename": "2.0.1",
|
"@types/gulp-rename": "2.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||||
"@typescript-eslint/parser": "5.54.1",
|
"@typescript-eslint/parser": "5.57.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.7.0",
|
"cypress": "12.9.0",
|
||||||
"eslint": "8.35.0",
|
"eslint": "8.37.0",
|
||||||
"start-server-and-test": "2.0.0"
|
"start-server-and-test": "2.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
import Redis from 'ioredis';
|
||||||
import { loadConfig } from './built/config.js';
|
import { loadConfig } from './built/config.js';
|
||||||
import { createRedisConnection } from './built/redis.js';
|
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const redis = createRedisConnection(config);
|
const redis = new Redis({
|
||||||
|
port: config.redis.port,
|
||||||
|
host: config.redis.host,
|
||||||
|
family: config.redis.family == null ? 0 : config.redis.family,
|
||||||
|
password: config.redis.pass,
|
||||||
|
keyPrefix: `${config.redis.prefix}:`,
|
||||||
|
db: config.redis.db ?? 0,
|
||||||
|
});
|
||||||
|
|
||||||
redis.on('connect', () => redis.disconnect());
|
redis.on('connect', () => redis.disconnect());
|
||||||
redis.on('error', (e) => {
|
redis.on('error', (e) => {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export class enableChartsForRemoteUser1679639483253 {
|
||||||
|
name = 'enableChartsForRemoteUser1679639483253'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForRemoteUser" boolean NOT NULL DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForRemoteUser"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/backend/migration/1679651580149-cleanup.js
Normal file
11
packages/backend/migration/1679651580149-cleanup.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export class cleanup1679651580149 {
|
||||||
|
name = 'cleanup1679651580149'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "useStarForReactionFallback"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "useStarForReactionFallback" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export class enableChartsForFederatedInstances1679652081809 {
|
||||||
|
name = 'enableChartsForFederatedInstances1679652081809'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableChartsForFederatedInstances" boolean NOT NULL DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableChartsForFederatedInstances"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/backend/migration/1680228513388-channelFavorite.js
Normal file
21
packages/backend/migration/1680228513388-channelFavorite.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export class channelFavorite1680228513388 {
|
||||||
|
name = 'channelFavorite1680228513388'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`CREATE TABLE "channel_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "channelId" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, CONSTRAINT "PK_59bddfd54d48689a298d41af00c" PRIMARY KEY ("id")); COMMENT ON COLUMN "channel_favorite"."createdAt" IS 'The created date of the ChannelFavorite.'`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_735a5544f9249d412255f47f95" ON "channel_favorite" ("createdAt") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_d3ca0db011b75ac2a940a2337d" ON "channel_favorite" ("channelId") `);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_8302bd27226605ece14842fb25" ON "channel_favorite" ("userId") `);
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2" FOREIGN KEY ("channelId") REFERENCES "channel"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" ADD CONSTRAINT "FK_8302bd27226605ece14842fb25a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_8302bd27226605ece14842fb25a"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel_favorite" DROP CONSTRAINT "FK_d3ca0db011b75ac2a940a2337d2"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_8302bd27226605ece14842fb25"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_d3ca0db011b75ac2a940a2337d"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_735a5544f9249d412255f47f95"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "channel_favorite"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export class channelNotePining1680238118084 {
|
||||||
|
name = 'channelNotePining1680238118084'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel" ADD "pinnedNoteIds" character varying(128) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "channel" DROP COLUMN "pinnedNoteIds"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/backend/migration/1680491187535-cleanup.js
Normal file
10
packages/backend/migration/1680491187535-cleanup.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export class cleanup1680491187535 {
|
||||||
|
name = 'cleanup1680491187535'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP TABLE "antenna_note" `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/backend/migration/1680582195041-cleanup.js
Normal file
11
packages/backend/migration/1680582195041-cleanup.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export class cleanup1680582195041 {
|
||||||
|
name = 'cleanup1680582195041'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`DROP TABLE "notification" `);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export class AvatarUrlAndBannerUrl1680775031481 {
|
||||||
|
name = 'AvatarUrlAndBannerUrl1680775031481'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "avatarUrl" character varying(512)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "bannerUrl" character varying(512)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "avatarBlurhash" character varying(128)`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "bannerBlurhash" character varying(128)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerBlurhash"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarBlurhash"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "bannerUrl"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "avatarUrl"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/backend/migration/1680931179228-account-move.js
Normal file
17
packages/backend/migration/1680931179228-account-move.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export class AccountMove1680931179228 {
|
||||||
|
name = 'AccountMove1680931179228'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "movedToUri" character varying(512)`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "alsoKnownAs" text`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."alsoKnownAs" IS 'URIs the user is known as too'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "alsoKnownAs"`);
|
||||||
|
await queryRunner.query(`COMMENT ON COLUMN "user"."movedToUri" IS 'The URI of the new account of the User'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "movedToUri"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,44 +22,46 @@
|
|||||||
"test-and-coverage": "pnpm jest-and-coverage"
|
"test-and-coverage": "pnpm jest-and-coverage"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "^1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
"@swc/core-darwin-arm64": "^1.3.38",
|
"@swc/core-darwin-arm64": "1.3.46",
|
||||||
"@swc/core-darwin-x64": "^1.3.38",
|
"@swc/core-darwin-x64": "1.3.46",
|
||||||
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
|
"@swc/core-linux-arm-gnueabihf": "1.3.46",
|
||||||
"@swc/core-linux-arm64-gnu": "^1.3.38",
|
"@swc/core-linux-arm64-gnu": "1.3.46",
|
||||||
"@swc/core-linux-arm64-musl": "^1.3.38",
|
"@swc/core-linux-arm64-musl": "1.3.46",
|
||||||
"@swc/core-linux-x64-gnu": "^1.3.38",
|
"@swc/core-linux-x64-gnu": "1.3.46",
|
||||||
"@swc/core-linux-x64-musl": "^1.3.38",
|
"@swc/core-linux-x64-musl": "1.3.46",
|
||||||
"@swc/core-win32-arm64-msvc": "^1.3.38",
|
"@swc/core-win32-arm64-msvc": "1.3.46",
|
||||||
"@swc/core-win32-ia32-msvc": "^1.3.38",
|
"@swc/core-win32-ia32-msvc": "1.3.46",
|
||||||
"@swc/core-win32-x64-msvc": "^1.3.38",
|
"@swc/core-win32-x64-msvc": "1.3.46",
|
||||||
"@tensorflow/tfjs": "4.2.0",
|
"@tensorflow/tfjs": "4.2.0",
|
||||||
"@tensorflow/tfjs-node": "4.2.0"
|
"@tensorflow/tfjs-node": "4.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.306.0",
|
||||||
|
"@aws-sdk/lib-storage": "3.306.0",
|
||||||
|
"@aws-sdk/node-http-handler": "3.306.0",
|
||||||
"@bull-board/api": "5.0.0",
|
"@bull-board/api": "5.0.0",
|
||||||
"@bull-board/fastify": "5.0.0",
|
"@bull-board/fastify": "5.0.0",
|
||||||
"@bull-board/ui": "5.0.0",
|
"@bull-board/ui": "5.0.0",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.1.2",
|
||||||
"@fastify/accepts": "4.1.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "8.3.0",
|
"@fastify/cookie": "8.3.0",
|
||||||
"@fastify/cors": "8.2.0",
|
"@fastify/cors": "8.2.1",
|
||||||
"@fastify/http-proxy": "8.4.0",
|
"@fastify/http-proxy": "9.0.0",
|
||||||
"@fastify/multipart": "7.4.2",
|
"@fastify/multipart": "7.5.0",
|
||||||
"@fastify/static": "6.9.0",
|
"@fastify/static": "6.10.0",
|
||||||
"@fastify/view": "7.4.1",
|
"@fastify/view": "7.4.1",
|
||||||
"@nestjs/common": "9.3.9",
|
"@nestjs/common": "9.4.0",
|
||||||
"@nestjs/core": "9.3.9",
|
"@nestjs/core": "9.4.0",
|
||||||
"@nestjs/testing": "9.3.9",
|
"@nestjs/testing": "9.4.0",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.0.2",
|
"@sinonjs/fake-timers": "10.0.2",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.38",
|
"@swc/core": "1.3.46",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1318.0",
|
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bull": "4.10.4",
|
"bull": "4.10.4",
|
||||||
@@ -74,7 +76,7 @@
|
|||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"fastify": "4.14.1",
|
"fastify": "4.15.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "18.2.1",
|
"file-type": "18.2.1",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
@@ -86,21 +88,21 @@
|
|||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "3.1.0",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "21.1.0",
|
"jsdom": "21.1.1",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.1.1",
|
"jsonld": "8.1.1",
|
||||||
"jsrsasign": "10.6.1",
|
"jsrsasign": "10.7.0",
|
||||||
"mfm-js": "0.23.3",
|
"mfm-js": "0.23.3",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "0.0.15",
|
"misskey-js": "workspace:*",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.0",
|
"node-fetch": "3.3.1",
|
||||||
"nodemailer": "6.9.1",
|
"nodemailer": "6.9.1",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "0.10.0",
|
"oauth": "0.10.0",
|
||||||
"os-utils": "0.0.14",
|
"os-utils": "0.0.14",
|
||||||
"otpauth": "^9.0.2",
|
"otpauth": "9.1.1",
|
||||||
"parse5": "7.1.2",
|
"parse5": "7.1.2",
|
||||||
"pg": "8.10.0",
|
"pg": "8.10.0",
|
||||||
"private-ip": "3.0.0",
|
"private-ip": "3.0.0",
|
||||||
@@ -123,7 +125,7 @@
|
|||||||
"sanitize-html": "2.10.0",
|
"sanitize-html": "2.10.0",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"semver": "7.3.8",
|
"semver": "7.3.8",
|
||||||
"sharp": "0.31.3",
|
"sharp": "0.32.0",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
@@ -131,25 +133,25 @@
|
|||||||
"systeminformation": "5.17.12",
|
"systeminformation": "5.17.12",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.3",
|
"tsc-alias": "1.8.5",
|
||||||
"tsconfig-paths": "4.1.2",
|
"tsconfig-paths": "4.2.0",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.11",
|
"typeorm": "0.3.13",
|
||||||
"typescript": "4.9.5",
|
"typescript": "5.0.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.11",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.5.0",
|
"web-push": "3.5.0",
|
||||||
"websocket": "1.0.34",
|
"websocket": "1.0.34",
|
||||||
"ws": "8.12.1",
|
"ws": "8.13.0",
|
||||||
"xev": "3.0.2"
|
"xev": "3.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jest/globals": "29.5.0",
|
"@jest/globals": "29.5.0",
|
||||||
"@swc/jest": "0.2.24",
|
"@swc/jest": "0.2.24",
|
||||||
"@types/accepts": "1.3.5",
|
"@types/accepts": "1.3.5",
|
||||||
"@types/archiver": "5.3.1",
|
"@types/archiver": "5.3.2",
|
||||||
"@types/bcryptjs": "2.4.2",
|
"@types/bcryptjs": "2.4.2",
|
||||||
"@types/bull": "4.10.0",
|
"@types/bull": "4.10.0",
|
||||||
"@types/cbor": "6.0.0",
|
"@types/cbor": "6.0.0",
|
||||||
@@ -158,13 +160,13 @@
|
|||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.21",
|
"@types/fluent-ffmpeg": "2.1.21",
|
||||||
"@types/ioredis": "4.28.10",
|
"@types/ioredis": "4.28.10",
|
||||||
"@types/jest": "29.4.0",
|
"@types/jest": "29.5.0",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "21.1.0",
|
"@types/jsdom": "21.1.1",
|
||||||
"@types/jsonld": "1.5.8",
|
"@types/jsonld": "1.5.8",
|
||||||
"@types/jsrsasign": "10.5.5",
|
"@types/jsrsasign": "10.5.8",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/node": "18.15.0",
|
"@types/node": "18.15.11",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.7",
|
"@types/nodemailer": "6.4.7",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
@@ -176,7 +178,7 @@
|
|||||||
"@types/ratelimiter": "3.4.4",
|
"@types/ratelimiter": "3.4.4",
|
||||||
"@types/redis": "4.0.11",
|
"@types/redis": "4.0.11",
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.4",
|
||||||
"@types/sanitize-html": "2.8.1",
|
"@types/sanitize-html": "2.9.0",
|
||||||
"@types/semver": "7.3.13",
|
"@types/semver": "7.3.13",
|
||||||
"@types/sharp": "0.31.1",
|
"@types/sharp": "0.31.1",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.2",
|
||||||
@@ -188,10 +190,11 @@
|
|||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.4",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||||
"@typescript-eslint/parser": "5.54.1",
|
"@typescript-eslint/parser": "5.57.1",
|
||||||
|
"aws-sdk-client-mock": "^2.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.35.0",
|
"eslint": "8.37.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"jest": "29.5.0",
|
"jest": "29.5.0",
|
||||||
|
|||||||
@@ -2,18 +2,15 @@ import { setTimeout } from 'node:timers/promises';
|
|||||||
import { Global, Inject, Module } from '@nestjs/common';
|
import { Global, Inject, Module } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { createRedisConnection } from '@/redis.js';
|
|
||||||
import { DI } from './di-symbols.js';
|
import { DI } from './di-symbols.js';
|
||||||
import { loadConfig } from './config.js';
|
import { loadConfig } from './config.js';
|
||||||
import { createPostgresDataSource } from './postgres.js';
|
import { createPostgresDataSource } from './postgres.js';
|
||||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
const $config: Provider = {
|
const $config: Provider = {
|
||||||
provide: DI.config,
|
provide: DI.config,
|
||||||
useValue: config,
|
useValue: loadConfig(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const $db: Provider = {
|
const $db: Provider = {
|
||||||
@@ -28,18 +25,31 @@ const $db: Provider = {
|
|||||||
const $redis: Provider = {
|
const $redis: Provider = {
|
||||||
provide: DI.redis,
|
provide: DI.redis,
|
||||||
useFactory: (config) => {
|
useFactory: (config) => {
|
||||||
const redisClient = createRedisConnection(config);
|
return new Redis({
|
||||||
return redisClient;
|
port: config.redis.port,
|
||||||
|
host: config.redis.host,
|
||||||
|
family: config.redis.family == null ? 0 : config.redis.family,
|
||||||
|
password: config.redis.pass,
|
||||||
|
keyPrefix: `${config.redis.prefix}:`,
|
||||||
|
db: config.redis.db ?? 0,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
|
|
||||||
const $redisSubscriber: Provider = {
|
const $redisForPubsub: Provider = {
|
||||||
provide: DI.redisSubscriber,
|
provide: DI.redisForPubsub,
|
||||||
useFactory: (config) => {
|
useFactory: (config) => {
|
||||||
const redisSubscriber = createRedisConnection(config);
|
const redis = new Redis({
|
||||||
redisSubscriber.subscribe(config.host);
|
port: config.redisForPubsub.port,
|
||||||
return redisSubscriber;
|
host: config.redisForPubsub.host,
|
||||||
|
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||||
|
password: config.redisForPubsub.pass,
|
||||||
|
keyPrefix: `${config.redisForPubsub.prefix}:`,
|
||||||
|
db: config.redisForPubsub.db ?? 0,
|
||||||
|
});
|
||||||
|
redis.subscribe(config.host);
|
||||||
|
return redis;
|
||||||
},
|
},
|
||||||
inject: [DI.config],
|
inject: [DI.config],
|
||||||
};
|
};
|
||||||
@@ -47,14 +57,14 @@ const $redisSubscriber: Provider = {
|
|||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RepositoryModule],
|
imports: [RepositoryModule],
|
||||||
providers: [$config, $db, $redis, $redisSubscriber],
|
providers: [$config, $db, $redis, $redisForPubsub],
|
||||||
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule],
|
exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule],
|
||||||
})
|
})
|
||||||
export class GlobalModule implements OnApplicationShutdown {
|
export class GlobalModule implements OnApplicationShutdown {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db) private db: DataSource,
|
@Inject(DI.db) private db: DataSource,
|
||||||
@Inject(DI.redis) private redisClient: Redis.Redis,
|
@Inject(DI.redis) private redisClient: Redis.Redis,
|
||||||
@Inject(DI.redisSubscriber) private redisSubscriber: Redis.Redis,
|
@Inject(DI.redisForPubsub) private redisForPubsub: Redis.Redis,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationShutdown(signal: string): Promise<void> {
|
async onApplicationShutdown(signal: string): Promise<void> {
|
||||||
@@ -69,7 +79,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.db.destroy(),
|
this.db.destroy(),
|
||||||
this.redisClient.disconnect(),
|
this.redisClient.disconnect(),
|
||||||
this.redisSubscriber.disconnect(),
|
this.redisForPubsub.disconnect(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,22 @@ export type Source = {
|
|||||||
db?: number;
|
db?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
};
|
};
|
||||||
|
redisForPubsub?: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
family?: number;
|
||||||
|
pass: string;
|
||||||
|
db?: number;
|
||||||
|
prefix?: string;
|
||||||
|
};
|
||||||
|
redisForJobQueue?: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
family?: number;
|
||||||
|
pass: string;
|
||||||
|
db?: number;
|
||||||
|
prefix?: string;
|
||||||
|
};
|
||||||
elasticsearch: {
|
elasticsearch: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -91,6 +107,8 @@ export type Mixin = {
|
|||||||
mediaProxy: string;
|
mediaProxy: string;
|
||||||
externalMediaProxyEnabled: boolean;
|
externalMediaProxyEnabled: boolean;
|
||||||
videoThumbnailGenerator: string | null;
|
videoThumbnailGenerator: string | null;
|
||||||
|
redisForPubsub: NonNullable<Source['redisForPubsub']>;
|
||||||
|
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = Source & Mixin;
|
export type Config = Source & Mixin;
|
||||||
@@ -151,6 +169,8 @@ export function loadConfig() {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||||
|
if (config.redisForPubsub == null) config.redisForPubsub = config.redis;
|
||||||
|
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis;
|
||||||
|
|
||||||
return Object.assign(config, mixin);
|
return Object.assign(config, mixin);
|
||||||
}
|
}
|
||||||
|
|||||||
114
packages/backend/src/core/AccountMoveService.ts
Normal file
114
packages/backend/src/core/AccountMoveService.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { IsNull } from 'typeorm';
|
||||||
|
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
|
import { User } from '@/models/entities/User.js';
|
||||||
|
import type { FollowingsRepository, UsersRepository } from '@/models/index.js';
|
||||||
|
|
||||||
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||||
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
|
||||||
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountMoveService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
private apRendererService: ApRendererService,
|
||||||
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
|
private globalEventService: GlobalEventService,
|
||||||
|
private userFollowingService: UserFollowingService,
|
||||||
|
private accountUpdateService: AccountUpdateService,
|
||||||
|
private relayService: RelayService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a local account to a remote account.
|
||||||
|
*
|
||||||
|
* After delivering Move activity, its local followers unfollow the old account and then follow the new one.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async moveToRemote(src: LocalUser, dst: User): Promise<unknown> {
|
||||||
|
// Make sure that the destination is a remote account.
|
||||||
|
if (this.userEntityService.isLocalUser(dst)) throw new Error('move destiantion is not remote');
|
||||||
|
if (!dst.uri) throw new Error('destination uri is empty');
|
||||||
|
|
||||||
|
// add movedToUri to indicate that the user has moved
|
||||||
|
const update = {} as Partial<User>;
|
||||||
|
update.alsoKnownAs = src.alsoKnownAs?.concat([dst.uri]) ?? [dst.uri];
|
||||||
|
update.movedToUri = dst.uri;
|
||||||
|
await this.usersRepository.update(src.id, update);
|
||||||
|
|
||||||
|
const srcPerson = await this.apRendererService.renderPerson(src);
|
||||||
|
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
||||||
|
await this.apDeliverManagerService.deliverToFollowers(src, updateAct);
|
||||||
|
this.relayService.deliverToRelays(src, updateAct);
|
||||||
|
|
||||||
|
// Deliver Move activity to the followers of the old account
|
||||||
|
const moveAct = this.apRendererService.addContext(this.apRendererService.renderMove(src, dst));
|
||||||
|
await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
|
||||||
|
|
||||||
|
// Publish meUpdated event
|
||||||
|
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
|
||||||
|
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
|
||||||
|
|
||||||
|
// follow the new account and unfollow the old one
|
||||||
|
const followings = await this.followingsRepository.find({
|
||||||
|
relations: {
|
||||||
|
follower: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
followeeId: src.id,
|
||||||
|
followerHost: IsNull(), // follower is local
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const following of followings) {
|
||||||
|
if (!following.follower) continue;
|
||||||
|
try {
|
||||||
|
await this.userFollowingService.follow(following.follower, dst);
|
||||||
|
await this.userFollowingService.unfollow(following.follower, src);
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return iObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an alias of an old remote account.
|
||||||
|
*
|
||||||
|
* The user's new profile will be published to the followers.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async createAlias(me: LocalUser, updates: Partial<User>): Promise<unknown> {
|
||||||
|
await this.usersRepository.update(me.id, updates);
|
||||||
|
|
||||||
|
// Publish meUpdated event
|
||||||
|
const iObj = await this.userEntityService.pack<true, true>(me.id, me, {
|
||||||
|
detail: true,
|
||||||
|
includeSecrets: true,
|
||||||
|
});
|
||||||
|
this.globalEventService.publishMainStream(me.id, 'meUpdated', iObj);
|
||||||
|
|
||||||
|
if (me.isLocked === false) {
|
||||||
|
await this.userFollowingService.acceptAllFollowRequests(me);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accountUpdateService.publishToFollowers(me.id);
|
||||||
|
|
||||||
|
return iObj;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ export class AccountUpdateService {
|
|||||||
public async publishToFollowers(userId: User['id']) {
|
public async publishToFollowers(userId: User['id']) {
|
||||||
const user = await this.usersRepository.findOneBy({ id: userId });
|
const user = await this.usersRepository.findOneBy({ id: userId });
|
||||||
if (user == null) throw new Error('user not found');
|
if (user == null) throw new Error('user not found');
|
||||||
|
|
||||||
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
|
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
|
const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
|
|||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
|
import type { MutingsRepository, NotesRepository, AntennasRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
@@ -24,8 +24,11 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
private antennas: Antenna[];
|
private antennas: Antenna[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redis)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForPubsub)
|
||||||
|
private redisForPubsub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
@@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
@Inject(DI.antennasRepository)
|
@Inject(DI.antennasRepository)
|
||||||
private antennasRepository: AntennasRepository,
|
private antennasRepository: AntennasRepository,
|
||||||
|
|
||||||
@@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
this.antennasFetched = false;
|
this.antennasFetched = false;
|
||||||
this.antennas = [];
|
this.antennas = [];
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onRedisMessage);
|
this.redisForPubsub.on('message', this.onRedisMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
this.redisSubscriber.off('message', this.onRedisMessage);
|
this.redisForPubsub.off('message', this.onRedisMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
||||||
// 通知しない設定になっているか、自分自身の投稿なら既読にする
|
this.redisClient.xadd(
|
||||||
const read = !antenna.notify || (antenna.userId === noteUser.id);
|
`antennaTimeline:${antenna.id}`,
|
||||||
|
'MAXLEN', '~', '200',
|
||||||
this.antennaNotesRepository.insert({
|
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||||
id: this.idService.genId(),
|
'note', note.id);
|
||||||
antennaId: antenna.id,
|
|
||||||
noteId: note.id,
|
|
||||||
read: read,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||||
|
|
||||||
if (!read) {
|
|
||||||
const mutings = await this.mutingsRepository.find({
|
|
||||||
where: {
|
|
||||||
muterId: antenna.userId,
|
|
||||||
},
|
|
||||||
select: ['muteeId'],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Copy
|
|
||||||
const _note: Note = {
|
|
||||||
...note,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (note.replyId != null) {
|
|
||||||
_note.reply = await this.notesRepository.findOneByOrFail({ id: note.replyId });
|
|
||||||
}
|
|
||||||
if (note.renoteId != null) {
|
|
||||||
_note.renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2秒経っても既読にならなかったら通知
|
|
||||||
setTimeout(async () => {
|
|
||||||
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
|
|
||||||
if (unread) {
|
|
||||||
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
|
||||||
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
|
|
||||||
antenna: { id: antenna.id, name: antenna.name },
|
|
||||||
note: await this.noteEntityService.pack(note),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||||
|
|||||||
172
packages/backend/src/core/CacheService.ts
Normal file
172
packages/backend/src/core/CacheService.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import type { BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheService implements OnApplicationShutdown {
|
||||||
|
public userByIdCache: MemoryKVCache<User>;
|
||||||
|
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
|
||||||
|
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
||||||
|
public uriPersonCache: MemoryKVCache<User | null>;
|
||||||
|
public userProfileCache: RedisKVCache<UserProfile>;
|
||||||
|
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||||
|
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||||
|
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||||
|
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||||
|
public userFollowingsCache: RedisKVCache<Set<string>>;
|
||||||
|
public userFollowingChannelsCache: RedisKVCache<Set<string>>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.redisForPubsub)
|
||||||
|
private redisForPubsub: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
|
@Inject(DI.mutingsRepository)
|
||||||
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.blockingsRepository)
|
||||||
|
private blockingsRepository: BlockingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.renoteMutingsRepository)
|
||||||
|
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.channelFollowingsRepository)
|
||||||
|
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||||
|
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
|
) {
|
||||||
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
|
this.userByIdCache = new MemoryKVCache<User>(Infinity);
|
||||||
|
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
||||||
|
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
|
||||||
|
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
|
||||||
|
|
||||||
|
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.userProfilesRepository.findOneByOrFail({ userId: key }),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.mutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.renoteMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'renoteMutings', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.renoteMutingsRepository.find({ where: { muterId: key }, select: ['muteeId'] }).then(xs => new Set(xs.map(x => x.muteeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userFollowingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowings', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.followingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userFollowingChannelsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userFollowingChannels', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60, // 1m
|
||||||
|
fetcher: (key) => this.channelFollowingsRepository.find({ where: { followerId: key }, select: ['followeeId'] }).then(xs => new Set(xs.map(x => x.followeeId))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||||
|
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.redisForPubsub.on('message', this.onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async onMessage(_: string, data: string): Promise<void> {
|
||||||
|
const obj = JSON.parse(data);
|
||||||
|
|
||||||
|
if (obj.channel === 'internal') {
|
||||||
|
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||||
|
switch (type) {
|
||||||
|
case 'userChangeSuspendedState':
|
||||||
|
case 'remoteUserUpdated': {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
|
||||||
|
this.userByIdCache.set(user.id, user);
|
||||||
|
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||||
|
if (v.value?.id === user.id) {
|
||||||
|
this.uriPersonCache.set(k, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.userEntityService.isLocalUser(user)) {
|
||||||
|
this.localUserByNativeTokenCache.set(user.token!, user);
|
||||||
|
this.localUserByIdCache.set(user.id, user);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'userTokenRegenerated': {
|
||||||
|
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
|
||||||
|
this.localUserByNativeTokenCache.delete(body.oldToken);
|
||||||
|
this.localUserByNativeTokenCache.set(body.newToken, user);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'follow': {
|
||||||
|
const follower = this.userByIdCache.get(body.followerId);
|
||||||
|
if (follower) follower.followingCount++;
|
||||||
|
const followee = this.userByIdCache.get(body.followeeId);
|
||||||
|
if (followee) followee.followersCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public findUserById(userId: User['id']) {
|
||||||
|
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
|
this.redisForPubsub.off('message', this.onMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AccountMoveService } from './AccountMoveService.js';
|
||||||
import { AccountUpdateService } from './AccountUpdateService.js';
|
import { AccountUpdateService } from './AccountUpdateService.js';
|
||||||
import { AiService } from './AiService.js';
|
import { AiService } from './AiService.js';
|
||||||
import { AntennaService } from './AntennaService.js';
|
import { AntennaService } from './AntennaService.js';
|
||||||
@@ -38,9 +39,9 @@ import { S3Service } from './S3Service.js';
|
|||||||
import { SignupService } from './SignupService.js';
|
import { SignupService } from './SignupService.js';
|
||||||
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
|
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
|
||||||
import { UserBlockingService } from './UserBlockingService.js';
|
import { UserBlockingService } from './UserBlockingService.js';
|
||||||
import { UserCacheService } from './UserCacheService.js';
|
import { CacheService } from './CacheService.js';
|
||||||
import { UserFollowingService } from './UserFollowingService.js';
|
import { UserFollowingService } from './UserFollowingService.js';
|
||||||
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
|
import { UserKeypairService } from './UserKeypairService.js';
|
||||||
import { UserListService } from './UserListService.js';
|
import { UserListService } from './UserListService.js';
|
||||||
import { UserMutingService } from './UserMutingService.js';
|
import { UserMutingService } from './UserMutingService.js';
|
||||||
import { UserSuspendService } from './UserSuspendService.js';
|
import { UserSuspendService } from './UserSuspendService.js';
|
||||||
@@ -119,6 +120,7 @@ import type { Provider } from '@nestjs/common';
|
|||||||
|
|
||||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||||
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
const $LoggerService: Provider = { provide: 'LoggerService', useExisting: LoggerService };
|
||||||
|
const $AccountMoveService: Provider = { provide: 'AccountMoveService', useExisting: AccountMoveService };
|
||||||
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
const $AccountUpdateService: Provider = { provide: 'AccountUpdateService', useExisting: AccountUpdateService };
|
||||||
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
const $AiService: Provider = { provide: 'AiService', useExisting: AiService };
|
||||||
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
const $AntennaService: Provider = { provide: 'AntennaService', useExisting: AntennaService };
|
||||||
@@ -159,9 +161,9 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
|
|||||||
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
|
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
|
||||||
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
|
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
|
||||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
|
||||||
const $UserCacheService: Provider = { provide: 'UserCacheService', useExisting: UserCacheService };
|
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
|
||||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
|
||||||
const $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
|
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||||
@@ -242,6 +244,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LoggerService,
|
LoggerService,
|
||||||
|
AccountMoveService,
|
||||||
AccountUpdateService,
|
AccountUpdateService,
|
||||||
AiService,
|
AiService,
|
||||||
AntennaService,
|
AntennaService,
|
||||||
@@ -282,9 +285,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
SignupService,
|
SignupService,
|
||||||
TwoFactorAuthenticationService,
|
TwoFactorAuthenticationService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
UserCacheService,
|
CacheService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairStoreService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
@@ -359,6 +362,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
|
|
||||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||||
$LoggerService,
|
$LoggerService,
|
||||||
|
$AccountMoveService,
|
||||||
$AccountUpdateService,
|
$AccountUpdateService,
|
||||||
$AiService,
|
$AiService,
|
||||||
$AntennaService,
|
$AntennaService,
|
||||||
@@ -399,9 +403,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$SignupService,
|
$SignupService,
|
||||||
$TwoFactorAuthenticationService,
|
$TwoFactorAuthenticationService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$UserCacheService,
|
$CacheService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairStoreService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
@@ -477,6 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
exports: [
|
exports: [
|
||||||
QueueModule,
|
QueueModule,
|
||||||
LoggerService,
|
LoggerService,
|
||||||
|
AccountMoveService,
|
||||||
AccountUpdateService,
|
AccountUpdateService,
|
||||||
AiService,
|
AiService,
|
||||||
AntennaService,
|
AntennaService,
|
||||||
@@ -517,9 +522,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
SignupService,
|
SignupService,
|
||||||
TwoFactorAuthenticationService,
|
TwoFactorAuthenticationService,
|
||||||
UserBlockingService,
|
UserBlockingService,
|
||||||
UserCacheService,
|
CacheService,
|
||||||
UserFollowingService,
|
UserFollowingService,
|
||||||
UserKeypairStoreService,
|
UserKeypairService,
|
||||||
UserListService,
|
UserListService,
|
||||||
UserMutingService,
|
UserMutingService,
|
||||||
UserSuspendService,
|
UserSuspendService,
|
||||||
@@ -593,6 +598,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
|
|
||||||
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
//#region 文字列ベースでのinjection用(循環参照対応のため)
|
||||||
$LoggerService,
|
$LoggerService,
|
||||||
|
$AccountMoveService,
|
||||||
$AccountUpdateService,
|
$AccountUpdateService,
|
||||||
$AiService,
|
$AiService,
|
||||||
$AntennaService,
|
$AntennaService,
|
||||||
@@ -633,9 +639,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||||||
$SignupService,
|
$SignupService,
|
||||||
$TwoFactorAuthenticationService,
|
$TwoFactorAuthenticationService,
|
||||||
$UserBlockingService,
|
$UserBlockingService,
|
||||||
$UserCacheService,
|
$CacheService,
|
||||||
$UserFollowingService,
|
$UserFollowingService,
|
||||||
$UserKeypairStoreService,
|
$UserKeypairService,
|
||||||
$UserListService,
|
$UserListService,
|
||||||
$UserMutingService,
|
$UserMutingService,
|
||||||
$UserSuspendService,
|
$UserSuspendService,
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DataSource, In, IsNull } from 'typeorm';
|
import { DataSource, In, IsNull } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { EmojisRepository, Note } from '@/models/index.js';
|
import type { EmojisRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { ReactionService } from '@/core/ReactionService.js';
|
|
||||||
import { query } from '@/misc/prelude/url.js';
|
import { query } from '@/misc/prelude/url.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomEmojiService {
|
export class CustomEmojiService {
|
||||||
private cache: Cache<Emoji | null>;
|
private cache: MemoryKVCache<Emoji | null>;
|
||||||
|
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
@@ -32,9 +36,16 @@ export class CustomEmojiService {
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private emojiEntityService: EmojiEntityService,
|
private emojiEntityService: EmojiEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private reactionService: ReactionService,
|
|
||||||
) {
|
) {
|
||||||
this.cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
|
this.cache = new MemoryKVCache<Emoji | null>(1000 * 60 * 60 * 12);
|
||||||
|
|
||||||
|
this.localEmojisCache = new RedisSingleCache<Map<string, Emoji>>(this.redisClient, 'localEmojis', {
|
||||||
|
lifetime: 1000 * 60 * 30, // 30m
|
||||||
|
memoryCacheLifetime: 1000 * 60 * 3, // 3m
|
||||||
|
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value.values()),
|
||||||
|
fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -60,7 +71,7 @@ export class CustomEmojiService {
|
|||||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
if (data.host == null) {
|
if (data.host == null) {
|
||||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||||
@@ -70,6 +81,146 @@ export class CustomEmojiService {
|
|||||||
return emoji;
|
return emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async update(id: Emoji['id'], data: {
|
||||||
|
name?: string;
|
||||||
|
category?: string | null;
|
||||||
|
aliases?: string[];
|
||||||
|
license?: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
|
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||||
|
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||||
|
|
||||||
|
await this.emojisRepository.update(emoji.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
name: data.name,
|
||||||
|
category: data.category,
|
||||||
|
aliases: data.aliases,
|
||||||
|
license: data.license,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
const updated = await this.emojiEntityService.packDetailed(emoji.id);
|
||||||
|
|
||||||
|
if (emoji.name === data.name) {
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: [updated],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||||
|
emoji: updated,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async addAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
|
||||||
|
const emojis = await this.emojisRepository.findBy({
|
||||||
|
id: In(ids),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
await this.emojisRepository.update(emoji.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
aliases: [...new Set(emoji.aliases.concat(aliases))],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async setAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
|
||||||
|
await this.emojisRepository.update({
|
||||||
|
id: In(ids),
|
||||||
|
}, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
aliases: aliases,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async removeAliasesBulk(ids: Emoji['id'][], aliases: string[]) {
|
||||||
|
const emojis = await this.emojisRepository.findBy({
|
||||||
|
id: In(ids),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
await this.emojisRepository.update(emoji.id, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
aliases: emoji.aliases.filter(x => !aliases.includes(x)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async setCategoryBulk(ids: Emoji['id'][], category: string | null) {
|
||||||
|
await this.emojisRepository.update({
|
||||||
|
id: In(ids),
|
||||||
|
}, {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
category: category,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||||
|
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async delete(id: Emoji['id']) {
|
||||||
|
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||||
|
|
||||||
|
await this.emojisRepository.delete(emoji.id);
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: [await this.emojiEntityService.packDetailed(emoji)],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async deleteBulk(ids: Emoji['id'][]) {
|
||||||
|
const emojis = await this.emojisRepository.findBy({
|
||||||
|
id: In(ids),
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
await this.emojisRepository.delete(emoji.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.localEmojisCache.refresh();
|
||||||
|
|
||||||
|
this.globalEventService.publishBroadcastStream('emojiDeleted', {
|
||||||
|
emojis: await this.emojiEntityService.packDetailedMany(emojis),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
||||||
// クエリに使うホスト
|
// クエリに使うホスト
|
||||||
@@ -84,7 +235,7 @@ export class CustomEmojiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||||
if (!match) return { name: null, host: null };
|
if (!match) return { name: null, host: null };
|
||||||
|
|
||||||
@@ -143,30 +294,6 @@ export class CustomEmojiService {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public aggregateNoteEmojis(notes: Note[]) {
|
|
||||||
let emojis: { name: string | null; host: string | null; }[] = [];
|
|
||||||
for (const note of notes) {
|
|
||||||
emojis = emojis.concat(note.emojis
|
|
||||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
|
||||||
if (note.renote) {
|
|
||||||
emojis = emojis.concat(note.renote.emojis
|
|
||||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
|
||||||
if (note.renote.user) {
|
|
||||||
emojis = emojis.concat(note.renote.user.emojis
|
|
||||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
|
|
||||||
emojis = emojis.concat(customReactions);
|
|
||||||
if (note.user) {
|
|
||||||
emojis = emojis.concat(note.user.emojis
|
|
||||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
* 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -36,8 +36,5 @@ export class DeleteAccountService {
|
|||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Terminate streaming
|
|
||||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { sharpBmp } from 'sharp-read-bmp';
|
import { sharpBmp } from 'sharp-read-bmp';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
@@ -36,7 +37,6 @@ import { bindThis } from '@/decorators.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { correctFilename } from '@/misc/correct-filename.js';
|
import { correctFilename } from '@/misc/correct-filename.js';
|
||||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import type S3 from 'aws-sdk/clients/s3.js';
|
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
@@ -81,6 +81,7 @@ type UploadFromUrlArgs = {
|
|||||||
export class DriveService {
|
export class DriveService {
|
||||||
private registerLogger: Logger;
|
private registerLogger: Logger;
|
||||||
private downloaderLogger: Logger;
|
private downloaderLogger: Logger;
|
||||||
|
private deleteLogger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@@ -118,6 +119,7 @@ export class DriveService {
|
|||||||
const logger = new Logger('drive', 'blue');
|
const logger = new Logger('drive', 'blue');
|
||||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||||
this.downloaderLogger = logger.createSubLogger('downloader');
|
this.downloaderLogger = logger.createSubLogger('downloader');
|
||||||
|
this.deleteLogger = logger.createSubLogger('delete');
|
||||||
}
|
}
|
||||||
|
|
||||||
/***
|
/***
|
||||||
@@ -368,7 +370,7 @@ export class DriveService {
|
|||||||
Body: stream,
|
Body: stream,
|
||||||
ContentType: type,
|
ContentType: type,
|
||||||
CacheControl: 'max-age=31536000, immutable',
|
CacheControl: 'max-age=31536000, immutable',
|
||||||
} as S3.PutObjectRequest;
|
} as PutObjectCommandInput;
|
||||||
|
|
||||||
if (filename) params.ContentDisposition = contentDisposition(
|
if (filename) params.ContentDisposition = contentDisposition(
|
||||||
'inline',
|
'inline',
|
||||||
@@ -378,21 +380,16 @@ export class DriveService {
|
|||||||
);
|
);
|
||||||
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||||
|
|
||||||
const s3 = this.s3Service.getS3(meta);
|
await this.s3Service.upload(meta, params)
|
||||||
|
|
||||||
const upload = s3.upload(params, {
|
|
||||||
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
|
|
||||||
await upload.promise()
|
|
||||||
.then(
|
.then(
|
||||||
result => {
|
result => {
|
||||||
if (result) {
|
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
||||||
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
|
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
|
||||||
} else {
|
} else { // AbortMultipartUploadCommandOutput
|
||||||
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`);
|
this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
|
.catch(
|
||||||
err => {
|
err => {
|
||||||
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
|
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
|
||||||
},
|
},
|
||||||
@@ -528,10 +525,10 @@ export class DriveService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const properties: {
|
const properties: {
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
orientation?: number;
|
orientation?: number;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (info.width) {
|
if (info.width) {
|
||||||
properties['width'] = info.width;
|
properties['width'] = info.width;
|
||||||
@@ -616,17 +613,20 @@ export class DriveService {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
|
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
|
||||||
// Publish driveFileCreated event
|
// Publish driveFileCreated event
|
||||||
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
|
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
|
||||||
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
|
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 統計を更新
|
|
||||||
this.driveChart.update(file, true);
|
this.driveChart.update(file, true);
|
||||||
this.perUserDriveChart.update(file, true);
|
if (file.userHost == null) {
|
||||||
if (file.userHost !== null) {
|
// ローカルユーザーのみ
|
||||||
this.instanceChart.updateDrive(file, true);
|
this.perUserDriveChart.update(file, true);
|
||||||
|
} else {
|
||||||
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateDrive(file, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
@@ -692,7 +692,7 @@ export class DriveService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async deletePostProcess(file: DriveFile, isExpired = false) {
|
private async deletePostProcess(file: DriveFile, isExpired = false) {
|
||||||
// リモートファイル期限切れ削除後は直リンクにする
|
// リモートファイル期限切れ削除後は直リンクにする
|
||||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||||
this.driveFilesRepository.update(file.id, {
|
this.driveFilesRepository.update(file.id, {
|
||||||
isLink: true,
|
isLink: true,
|
||||||
@@ -709,33 +709,36 @@ export class DriveService {
|
|||||||
this.driveFilesRepository.delete(file.id);
|
this.driveFilesRepository.delete(file.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 統計を更新
|
|
||||||
this.driveChart.update(file, false);
|
this.driveChart.update(file, false);
|
||||||
this.perUserDriveChart.update(file, false);
|
if (file.userHost == null) {
|
||||||
if (file.userHost !== null) {
|
// ローカルユーザーのみ
|
||||||
this.instanceChart.updateDrive(file, false);
|
this.perUserDriveChart.update(file, false);
|
||||||
|
} else {
|
||||||
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateDrive(file, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async deleteObjectStorageFile(key: string) {
|
public async deleteObjectStorageFile(key: string) {
|
||||||
const meta = await this.metaService.fetch();
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
const s3 = this.s3Service.getS3(meta);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await s3.deleteObject({
|
const param = {
|
||||||
Bucket: meta.objectStorageBucket!,
|
Bucket: meta.objectStorageBucket,
|
||||||
Key: key,
|
Key: key,
|
||||||
}).promise();
|
} as DeleteObjectCommandInput;
|
||||||
|
|
||||||
|
await this.s3Service.delete(meta, param);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.code === 'NoSuchKey') {
|
if (err.name === 'NoSuchKey') {
|
||||||
console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err);
|
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
|
||||||
|
cause: err,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
throw new Error(`Failed to delete the file from the object storage with the given key: ${key}`, {
|
|
||||||
cause: err,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import type { InstancesRepository } from '@/models/index.js';
|
import type { InstancesRepository } from '@/models/index.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
@@ -9,23 +10,40 @@ import { bindThis } from '@/decorators.js';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FederatedInstanceService {
|
export class FederatedInstanceService {
|
||||||
private cache: Cache<Instance>;
|
public federatedInstanceCache: RedisKVCache<Instance | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
this.cache = new Cache<Instance>(1000 * 60 * 60);
|
this.federatedInstanceCache = new RedisKVCache<Instance | null>(this.redisClient, 'federatedInstance', {
|
||||||
|
lifetime: 1000 * 60 * 60 * 24, // 24h
|
||||||
|
memoryCacheLifetime: 1000 * 60 * 30, // 30m
|
||||||
|
fetcher: (key) => this.instancesRepository.findOneBy({ host: key }),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
firstRetrievedAt: new Date(parsed.firstRetrievedAt),
|
||||||
|
latestRequestReceivedAt: parsed.latestRequestReceivedAt ? new Date(parsed.latestRequestReceivedAt) : null,
|
||||||
|
infoUpdatedAt: parsed.infoUpdatedAt ? new Date(parsed.infoUpdatedAt) : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async fetch(host: string): Promise<Instance> {
|
public async fetch(host: string): Promise<Instance> {
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
const cached = this.cache.get(host);
|
const cached = await this.federatedInstanceCache.get(host);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const index = await this.instancesRepository.findOneBy({ host });
|
const index = await this.instancesRepository.findOneBy({ host });
|
||||||
@@ -37,10 +55,10 @@ export class FederatedInstanceService {
|
|||||||
firstRetrievedAt: new Date(),
|
firstRetrievedAt: new Date(),
|
||||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
this.cache.set(host, i);
|
this.federatedInstanceCache.set(host, i);
|
||||||
return i;
|
return i;
|
||||||
} else {
|
} else {
|
||||||
this.cache.set(host, index);
|
this.federatedInstanceCache.set(host, index);
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,10 +67,10 @@ export class FederatedInstanceService {
|
|||||||
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
|
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
const cached = this.cache.get(host);
|
const cached = await this.federatedInstanceCache.get(host);
|
||||||
if (cached == null) return;
|
if (cached == null) return;
|
||||||
|
|
||||||
this.cache.set(host, {
|
this.federatedInstanceCache.set(host, {
|
||||||
...cached,
|
...cached,
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import type {
|
|||||||
MainStreamTypes,
|
MainStreamTypes,
|
||||||
NoteStreamTypes,
|
NoteStreamTypes,
|
||||||
UserListStreamTypes,
|
UserListStreamTypes,
|
||||||
UserStreamTypes,
|
|
||||||
} from '@/server/api/stream/types.js';
|
} from '@/server/api/stream/types.js';
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
@@ -49,11 +48,6 @@ export class GlobalEventService {
|
|||||||
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
this.publish('internal', type, typeof value === 'undefined' ? null : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public publishUserEvent<K extends keyof UserStreamTypes>(userId: User['id'], type: K, value?: UserStreamTypes[K]): void {
|
|
||||||
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
||||||
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { ulid } from 'ulid';
|
import { ulid } from 'ulid';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { genAid } from '@/misc/id/aid.js';
|
import { genAid, parseAid } from '@/misc/id/aid.js';
|
||||||
import { genMeid } from '@/misc/id/meid.js';
|
import { genMeid } from '@/misc/id/meid.js';
|
||||||
import { genMeidg } from '@/misc/id/meidg.js';
|
import { genMeidg } from '@/misc/id/meidg.js';
|
||||||
import { genObjectId } from '@/misc/id/object-id.js';
|
import { genObjectId } from '@/misc/id/object-id.js';
|
||||||
@@ -32,4 +32,17 @@ export class IdService {
|
|||||||
default: throw new Error('unrecognized id generation method');
|
default: throw new Error('unrecognized id generation method');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public parse(id: string): { date: Date; } {
|
||||||
|
switch (this.method) {
|
||||||
|
case 'aid': return parseAid(id);
|
||||||
|
// TODO
|
||||||
|
//case 'meid':
|
||||||
|
//case 'meidg':
|
||||||
|
//case 'ulid':
|
||||||
|
//case 'objectid':
|
||||||
|
default: throw new Error('unrecognized id generation method');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import type { LocalUser } from '@/models/entities/User.js';
|
import type { LocalUser } from '@/models/entities/User.js';
|
||||||
import type { UsersRepository } from '@/models/index.js';
|
import type { UsersRepository } from '@/models/index.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemorySingleCache } from '@/misc/cache.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
@@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InstanceActorService {
|
export class InstanceActorService {
|
||||||
private cache: Cache<LocalUser>;
|
private cache: MemorySingleCache<LocalUser>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
@@ -19,12 +19,12 @@ export class InstanceActorService {
|
|||||||
|
|
||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
) {
|
) {
|
||||||
this.cache = new Cache<LocalUser>(Infinity);
|
this.cache = new MemorySingleCache<LocalUser>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getInstanceActor(): Promise<LocalUser> {
|
public async getInstanceActor(): Promise<LocalUser> {
|
||||||
const cached = this.cache.get(null);
|
const cached = this.cache.get();
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({
|
const user = await this.usersRepository.findOneBy({
|
||||||
@@ -33,11 +33,11 @@ export class InstanceActorService {
|
|||||||
}) as LocalUser | undefined;
|
}) as LocalUser | undefined;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.cache.set(null, user);
|
this.cache.set(user);
|
||||||
return user;
|
return user;
|
||||||
} else {
|
} else {
|
||||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
||||||
this.cache.set(null, created);
|
this.cache.set(created);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
private intervalId: NodeJS.Timer;
|
private intervalId: NodeJS.Timer;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisForPubsub)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisForPubsub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
@@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
}, 1000 * 60 * 5);
|
}, 1000 * 60 * 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
this.redisForPubsub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
|
|||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
this.redisForPubsub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { setImmediate } from 'node:timers/promises';
|
import { setImmediate } from 'node:timers/promises';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { In, DataSource } from 'typeorm';
|
import { In, DataSource } from 'typeorm';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { extractMentions } from '@/misc/extract-mentions.js';
|
import { extractMentions } from '@/misc/extract-mentions.js';
|
||||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||||
@@ -19,7 +20,7 @@ import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js
|
|||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import type { Channel } from '@/models/entities/Channel.js';
|
import type { Channel } from '@/models/entities/Channel.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemorySingleCache } from '@/misc/cache.js';
|
||||||
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
import type { UserProfile } from '@/models/entities/UserProfile.js';
|
||||||
import { RelayService } from '@/core/RelayService.js';
|
import { RelayService } from '@/core/RelayService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
@@ -46,7 +47,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
|||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
const mutedWordsCache = new MemorySingleCache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
@@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@@ -321,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
const note = await this.insertNote(user, data, tags, emojis, mentionedUsers);
|
||||||
|
|
||||||
|
if (data.channel) {
|
||||||
|
this.redisClient.xadd(
|
||||||
|
`channelTimeline:${data.channel.id}`,
|
||||||
|
'MAXLEN', '~', '1000',
|
||||||
|
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||||
|
'note', note.id);
|
||||||
|
}
|
||||||
|
|
||||||
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
setImmediate('post created', { signal: this.#shutdownController.signal }).then(
|
||||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
||||||
() => { /* aborted, ignore this */ },
|
() => { /* aborted, ignore this */ },
|
||||||
@@ -435,15 +447,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
createdAt: User['createdAt'];
|
createdAt: User['createdAt'];
|
||||||
isBot: User['isBot'];
|
isBot: User['isBot'];
|
||||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||||
// 統計を更新
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
this.notesChart.update(note, true);
|
this.notesChart.update(note, true);
|
||||||
this.perUserNotesChart.update(user, note, true);
|
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||||
|
this.perUserNotesChart.update(user, note, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
if (this.userEntityService.isRemoteUser(user)) {
|
if (this.userEntityService.isRemoteUser(user)) {
|
||||||
this.federatedInstanceService.fetch(user.host).then(i => {
|
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
||||||
this.instanceChart.updateNote(i.host, note, true);
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateNote(i.host, note, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +473,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
this.incNotesCountOfUser(user);
|
this.incNotesCountOfUser(user);
|
||||||
|
|
||||||
// Word mute
|
// Word mute
|
||||||
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({
|
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
|
||||||
where: {
|
where: {
|
||||||
enableWordMute: true,
|
enableWordMute: true,
|
||||||
},
|
},
|
||||||
@@ -485,18 +502,6 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel
|
|
||||||
if (note.channelId) {
|
|
||||||
this.channelFollowingsRepository.findBy({ followeeId: note.channelId }).then(followings => {
|
|
||||||
for (const following of followings) {
|
|
||||||
this.noteReadService.insertNoteUnread(following.followerId, note, {
|
|
||||||
isSpecified: false,
|
|
||||||
isMentioned: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
this.saveReply(data.reply, note);
|
this.saveReply(data.reply, note);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
|
|||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteDeleteService {
|
export class NoteDeleteService {
|
||||||
@@ -39,6 +40,7 @@ export class NoteDeleteService {
|
|||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDeliverManagerService: ApDeliverManagerService,
|
private apDeliverManagerService: ApDeliverManagerService,
|
||||||
|
private metaService: MetaService,
|
||||||
private notesChart: NotesChart,
|
private notesChart: NotesChart,
|
||||||
private perUserNotesChart: PerUserNotesChart,
|
private perUserNotesChart: PerUserNotesChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
@@ -95,14 +97,19 @@ export class NoteDeleteService {
|
|||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// 統計を更新
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
this.notesChart.update(note, false);
|
this.notesChart.update(note, false);
|
||||||
this.perUserNotesChart.update(user, note, false);
|
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||||
|
this.perUserNotesChart.update(user, note, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(user)) {
|
if (this.userEntityService.isRemoteUser(user)) {
|
||||||
this.federatedInstanceService.fetch(user.host).then(i => {
|
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||||
this.instanceChart.updateNote(i.host, note, false);
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateNote(i.host, note, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In, IsNull, Not } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Channel } from '@/models/entities/Channel.js';
|
|
||||||
import type { Packed } from '@/misc/json-schema.js';
|
import type { Packed } from '@/misc/json-schema.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
|
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { NotificationService } from './NotificationService.js';
|
|
||||||
import { AntennaService } from './AntennaService.js';
|
|
||||||
import { PushNotificationService } from './PushNotificationService.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteReadService implements OnApplicationShutdown {
|
export class NoteReadService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.noteUnreadsRepository)
|
@Inject(DI.noteUnreadsRepository)
|
||||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||||
|
|
||||||
@@ -32,21 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.noteThreadMutingsRepository)
|
@Inject(DI.noteThreadMutingsRepository)
|
||||||
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.channelFollowingsRepository)
|
|
||||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.antennaNotesRepository)
|
|
||||||
private antennaNotesRepository: AntennaNotesRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private notificationService: NotificationService,
|
|
||||||
private antennaService: AntennaService,
|
|
||||||
private pushNotificationService: PushNotificationService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
isMentioned: boolean;
|
isMentioned: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
//#region ミュートしているなら無視
|
//#region ミュートしているなら無視
|
||||||
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
|
|
||||||
const mute = await this.mutingsRepository.findBy({
|
const mute = await this.mutingsRepository.findBy({
|
||||||
muterId: userId,
|
muterId: userId,
|
||||||
});
|
});
|
||||||
@@ -77,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
isSpecified: params.isSpecified,
|
isSpecified: params.isSpecified,
|
||||||
isMentioned: params.isMentioned,
|
isMentioned: params.isMentioned,
|
||||||
noteChannelId: note.channelId,
|
|
||||||
noteUserId: note.userId,
|
noteUserId: note.userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
if (params.isSpecified) {
|
if (params.isSpecified) {
|
||||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||||
}
|
}
|
||||||
if (note.channelId) {
|
|
||||||
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
|
||||||
}
|
|
||||||
}, () => { /* aborted, ignore it */ });
|
}, () => { /* aborted, ignore it */ });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,23 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
public async read(
|
public async read(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
notes: (Note | Packed<'Note'>)[],
|
notes: (Note | Packed<'Note'>)[],
|
||||||
info?: {
|
|
||||||
following: Set<User['id']>;
|
|
||||||
followingChannels: Set<Channel['id']>;
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const followingChannels = info?.followingChannels ? info.followingChannels : new Set<string>((await this.channelFollowingsRepository.find({
|
|
||||||
where: {
|
|
||||||
followerId: userId,
|
|
||||||
},
|
|
||||||
select: ['followeeId'],
|
|
||||||
})).map(x => x.followeeId));
|
|
||||||
|
|
||||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
|
||||||
const readMentions: (Note | Packed<'Note'>)[] = [];
|
const readMentions: (Note | Packed<'Note'>)[] = [];
|
||||||
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
const readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
||||||
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
|
||||||
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
|
|
||||||
|
|
||||||
for (const note of notes) {
|
for (const note of notes) {
|
||||||
if (note.mentions && note.mentions.includes(userId)) {
|
if (note.mentions && note.mentions.includes(userId)) {
|
||||||
@@ -129,25 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||||
readSpecifiedNotes.push(note);
|
readSpecifiedNotes.push(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.channelId && followingChannels.has(note.channelId)) {
|
|
||||||
readChannelNotes.push(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
|
|
||||||
for (const antenna of myAntennas) {
|
|
||||||
if (await this.antennaService.checkHitAntenna(antenna, note, note.user)) {
|
|
||||||
readAntennaNotes.push(note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
|
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
|
||||||
// Remove the record
|
// Remove the record
|
||||||
await this.noteUnreadsRepository.delete({
|
await this.noteUnreadsRepository.delete({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
|
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: ↓まとめてクエリしたい
|
// TODO: ↓まとめてクエリしたい
|
||||||
@@ -171,49 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.noteUnreadsRepository.countBy({
|
|
||||||
userId: userId,
|
|
||||||
noteChannelId: Not(IsNull()),
|
|
||||||
}).then(channelNoteCount => {
|
|
||||||
if (channelNoteCount === 0) {
|
|
||||||
// 全て既読になったイベントを発行
|
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllChannels');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.notificationService.readNotificationByQuery(userId, {
|
|
||||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readAntennaNotes.length > 0) {
|
|
||||||
await this.antennaNotesRepository.update({
|
|
||||||
antennaId: In(myAntennas.map(a => a.id)),
|
|
||||||
noteId: In(readAntennaNotes.map(n => n.id)),
|
|
||||||
}, {
|
|
||||||
read: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: まとめてクエリしたい
|
|
||||||
for (const antenna of myAntennas) {
|
|
||||||
const count = await this.antennaNotesRepository.countBy({
|
|
||||||
antennaId: antenna.id,
|
|
||||||
read: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
|
|
||||||
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
|
|
||||||
if (!unread) {
|
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
|
|
||||||
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
|
import Redis from 'ioredis';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { MutingsRepository, NotificationsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
import type { MutingsRepository, UserProfile, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Notification } from '@/models/entities/Notification.js';
|
import type { Notification } from '@/models/entities/Notification.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
@@ -11,21 +12,22 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|||||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||||
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NotificationService implements OnApplicationShutdown {
|
export class NotificationService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.userProfilesRepository)
|
@Inject(DI.userProfilesRepository)
|
||||||
private userProfilesRepository: UserProfilesRepository,
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
@Inject(DI.notificationsRepository)
|
|
||||||
private notificationsRepository: NotificationsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
@@ -34,54 +36,36 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private pushNotificationService: PushNotificationService,
|
private pushNotificationService: PushNotificationService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async readNotification(
|
public async readAllNotification(
|
||||||
userId: User['id'],
|
userId: User['id'],
|
||||||
notificationIds: Notification['id'][],
|
force = false,
|
||||||
) {
|
) {
|
||||||
if (notificationIds.length === 0) return;
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${userId}`);
|
||||||
|
|
||||||
|
const latestNotificationIdsRes = await this.redisClient.xrevrange(
|
||||||
|
`notificationTimeline:${userId}`,
|
||||||
|
'+',
|
||||||
|
'-',
|
||||||
|
'COUNT', 1);
|
||||||
|
const latestNotificationId = latestNotificationIdsRes[0]?.[0];
|
||||||
|
|
||||||
// Update documents
|
if (latestNotificationId == null) return;
|
||||||
const result = await this.notificationsRepository.update({
|
|
||||||
notifieeId: userId,
|
|
||||||
id: In(notificationIds),
|
|
||||||
isRead: false,
|
|
||||||
}, {
|
|
||||||
isRead: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.affected === 0) return;
|
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
|
||||||
|
|
||||||
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
|
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
||||||
else return this.postReadNotifications(userId, notificationIds);
|
return this.postReadAllNotifications(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async readNotificationByQuery(
|
|
||||||
userId: User['id'],
|
|
||||||
query: Record<string, any>,
|
|
||||||
) {
|
|
||||||
const notificationIds = await this.notificationsRepository.findBy({
|
|
||||||
...query,
|
|
||||||
notifieeId: userId,
|
|
||||||
isRead: false,
|
|
||||||
}).then(notifications => notifications.map(notification => notification.id));
|
|
||||||
|
|
||||||
return this.readNotification(userId, notificationIds);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private postReadAllNotifications(userId: User['id']) {
|
private postReadAllNotifications(userId: User['id']) {
|
||||||
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
this.globalEventService.publishMainStream(userId, 'readAllNotifications');
|
||||||
return this.pushNotificationService.pushNotification(userId, 'readAllNotifications', undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private postReadNotifications(userId: User['id'], notificationIds: Notification['id'][]) {
|
|
||||||
return this.pushNotificationService.pushNotification(userId, 'readNotifications', { notificationIds });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -90,45 +74,43 @@ export class NotificationService implements OnApplicationShutdown {
|
|||||||
type: Notification['type'],
|
type: Notification['type'],
|
||||||
data: Partial<Notification>,
|
data: Partial<Notification>,
|
||||||
): Promise<Notification | null> {
|
): Promise<Notification | null> {
|
||||||
if (data.notifierId && (notifieeId === data.notifierId)) {
|
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||||
return null;
|
const isMuted = profile.mutingNotificationTypes.includes(type);
|
||||||
|
if (isMuted) return null;
|
||||||
|
|
||||||
|
if (data.notifierId) {
|
||||||
|
if (notifieeId === data.notifierId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
|
||||||
|
if (mutings.has(data.notifierId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await this.userProfilesRepository.findOneBy({ userId: notifieeId });
|
const notification = {
|
||||||
|
|
||||||
const isMuted = profile?.mutingNotificationTypes.includes(type);
|
|
||||||
|
|
||||||
// Create notification
|
|
||||||
const notification = await this.notificationsRepository.insert({
|
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
notifieeId: notifieeId,
|
|
||||||
type: type,
|
type: type,
|
||||||
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
|
|
||||||
isRead: isMuted,
|
|
||||||
...data,
|
...data,
|
||||||
} as Partial<Notification>)
|
} as Notification;
|
||||||
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
|
|
||||||
|
|
||||||
const packed = await this.notificationEntityService.pack(notification, {});
|
const redisIdPromise = this.redisClient.xadd(
|
||||||
|
`notificationTimeline:${notifieeId}`,
|
||||||
|
'MAXLEN', '~', '300',
|
||||||
|
`${this.idService.parse(notification.id).date.getTime()}-*`,
|
||||||
|
'data', JSON.stringify(notification));
|
||||||
|
|
||||||
|
const packed = await this.notificationEntityService.pack(notification, notifieeId, {});
|
||||||
|
|
||||||
// Publish notification event
|
// Publish notification event
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||||
|
|
||||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||||
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
|
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||||
if (fresh == null) return; // 既に削除されているかもしれない
|
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
|
||||||
if (fresh.isRead) return;
|
|
||||||
|
|
||||||
//#region ただしミュートしているユーザーからの通知なら無視
|
|
||||||
const mutings = await this.mutingsRepository.findBy({
|
|
||||||
muterId: notifieeId,
|
|
||||||
});
|
|
||||||
if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ type PushNotificationsTypes = {
|
|||||||
antenna: { id: string, name: string };
|
antenna: { id: string, name: string };
|
||||||
note: Packed<'Note'>;
|
note: Packed<'Note'>;
|
||||||
};
|
};
|
||||||
'readNotifications': { notificationIds: string[] };
|
|
||||||
'readAllNotifications': undefined;
|
|
||||||
'readAntenna': { antennaId: string };
|
|
||||||
'readAllAntennas': undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reduce length because push message servers have character limits
|
// Reduce length because push message servers have character limits
|
||||||
@@ -72,14 +68,6 @@ export class PushNotificationService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const subscription of subscriptions) {
|
for (const subscription of subscriptions) {
|
||||||
// Continue if sendReadMessage is false
|
|
||||||
if ([
|
|
||||||
'readNotifications',
|
|
||||||
'readAllNotifications',
|
|
||||||
'readAntenna',
|
|
||||||
'readAllAntennas',
|
|
||||||
].includes(type) && !subscription.sendReadMessage) continue;
|
|
||||||
|
|
||||||
const pushSubscription = {
|
const pushSubscription = {
|
||||||
endpoint: subscription.endpoint,
|
endpoint: subscription.endpoint,
|
||||||
keys: {
|
keys: {
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, End
|
|||||||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
||||||
return new Bull<T>(name, {
|
return new Bull<T>(name, {
|
||||||
redis: {
|
redis: {
|
||||||
port: config.redis.port,
|
port: config.redisForJobQueue.port,
|
||||||
host: config.redis.host,
|
host: config.redisForJobQueue.host,
|
||||||
family: config.redis.family == null ? 0 : config.redis.family,
|
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||||
password: config.redis.pass,
|
password: config.redisForJobQueue.pass,
|
||||||
db: config.redis.db ?? 0,
|
db: config.redisForJobQueue.db ?? 0,
|
||||||
},
|
},
|
||||||
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
|
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue` : 'queue',
|
||||||
limiter: limitPerSec > 0 ? {
|
limiter: limitPerSec > 0 ? {
|
||||||
max: limitPerSec,
|
max: limitPerSec,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull } from 'typeorm';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { EmojisRepository, BlockingsRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/index.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import type { RemoteUser, User } from '@/models/entities/User.js';
|
import type { RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
@@ -20,6 +19,9 @@ import { MetaService } from '@/core/MetaService.js';
|
|||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
|
|
||||||
|
const FALLBACK = '❤';
|
||||||
|
|
||||||
const legacies: Record<string, string> = {
|
const legacies: Record<string, string> = {
|
||||||
'like': '👍',
|
'like': '👍',
|
||||||
@@ -58,9 +60,6 @@ export class ReactionService {
|
|||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.blockingsRepository)
|
|
||||||
private blockingsRepository: BlockingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
@@ -72,6 +71,7 @@ export class ReactionService {
|
|||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private customEmojiService: CustomEmojiService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
@@ -102,7 +102,6 @@ export class ReactionService {
|
|||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '❤️';
|
reaction = '❤️';
|
||||||
} else {
|
} else {
|
||||||
// TODO: cache
|
|
||||||
reaction = await this.toDbReaction(reaction, user.host);
|
reaction = await this.toDbReaction(reaction, user.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,25 +146,31 @@ export class ReactionService {
|
|||||||
.where('id = :id', { id: note.id })
|
.where('id = :id', { id: note.id })
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
this.perUserReactionsChart.update(user, note);
|
const meta = await this.metaService.fetch();
|
||||||
|
|
||||||
|
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||||
|
this.perUserReactionsChart.update(user, note);
|
||||||
|
}
|
||||||
|
|
||||||
// カスタム絵文字リアクションだったら絵文字情報も送る
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
||||||
const decodedReaction = this.decodeReaction(reaction);
|
const decodedReaction = this.decodeReaction(reaction);
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOne({
|
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
|
||||||
where: {
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
|
||||||
name: decodedReaction.name,
|
: await this.emojisRepository.findOne(
|
||||||
host: decodedReaction.host ?? IsNull(),
|
{
|
||||||
},
|
where: {
|
||||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
name: decodedReaction.name,
|
||||||
});
|
host: decodedReaction.host,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||||
reaction: decodedReaction.reaction,
|
reaction: decodedReaction.reaction,
|
||||||
emoji: emoji != null ? {
|
emoji: customEmoji != null ? {
|
||||||
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
|
||||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||||
url: emoji.publicUrl || emoji.originalUrl,
|
url: customEmoji.publicUrl || customEmoji.originalUrl,
|
||||||
} : null,
|
} : null,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
@@ -251,12 +256,6 @@ export class ReactionService {
|
|||||||
//#endregion
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getFallbackReaction(): Promise<string> {
|
|
||||||
const meta = await this.metaService.fetch();
|
|
||||||
return meta.useStarForReactionFallback ? '⭐' : '👍';
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public convertLegacyReactions(reactions: Record<string, number>) {
|
public convertLegacyReactions(reactions: Record<string, number>) {
|
||||||
const _reactions = {} as Record<string, number>;
|
const _reactions = {} as Record<string, number>;
|
||||||
@@ -290,7 +289,7 @@ export class ReactionService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
public async toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> {
|
||||||
if (reaction == null) return await this.getFallbackReaction();
|
if (reaction == null) return FALLBACK;
|
||||||
|
|
||||||
reacterHost = this.utilityService.toPunyNullable(reacterHost);
|
reacterHost = this.utilityService.toPunyNullable(reacterHost);
|
||||||
|
|
||||||
@@ -310,15 +309,17 @@ export class ReactionService {
|
|||||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const name = custom[1];
|
const name = custom[1];
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
const emoji = reacterHost == null
|
||||||
host: reacterHost ?? IsNull(),
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||||
name,
|
: await this.emojisRepository.findOneBy({
|
||||||
});
|
host: reacterHost,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.getFallbackReaction();
|
return FALLBACK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
|||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
import type { RelaysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemorySingleCache } from '@/misc/cache.js';
|
||||||
import type { Relay } from '@/models/entities/Relay.js';
|
import type { Relay } from '@/models/entities/Relay.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||||
@@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RelayService {
|
export class RelayService {
|
||||||
private relaysCache: Cache<Relay[]>;
|
private relaysCache: MemorySingleCache<Relay[]>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
@@ -30,7 +30,7 @@ export class RelayService {
|
|||||||
private createSystemUserService: CreateSystemUserService,
|
private createSystemUserService: CreateSystemUserService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
) {
|
) {
|
||||||
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
|
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -109,7 +109,7 @@ export class RelayService {
|
|||||||
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
||||||
if (activity == null) return;
|
if (activity == null) return;
|
||||||
|
|
||||||
const relays = await this.relaysCache.fetch(null, () => this.relaysRepository.findBy({
|
const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
}));
|
}));
|
||||||
if (relays.length === 0) return;
|
if (relays.length === 0) return;
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import Redis from 'ioredis';
|
import Redis from 'ioredis';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { UserCacheService } from '@/core/UserCacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
import type { RoleCondFormulaValue } from '@/models/entities/Role.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||||
@@ -57,15 +57,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleService implements OnApplicationShutdown {
|
export class RoleService implements OnApplicationShutdown {
|
||||||
private rolesCache: Cache<Role[]>;
|
private rolesCache: MemorySingleCache<Role[]>;
|
||||||
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
||||||
|
|
||||||
public static AlreadyAssignedError = class extends Error {};
|
public static AlreadyAssignedError = class extends Error {};
|
||||||
public static NotAssignedError = class extends Error {};
|
public static NotAssignedError = class extends Error {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisForPubsub)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisForPubsub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
@@ -77,17 +77,17 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||||
|
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private userCacheService: UserCacheService,
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
|
|
||||||
this.rolesCache = new Cache<Role[]>(Infinity);
|
this.rolesCache = new MemorySingleCache<Role[]>(1000 * 60 * 60 * 1);
|
||||||
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
|
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(1000 * 60 * 60 * 1);
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
this.redisForPubsub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'roleCreated': {
|
case 'roleCreated': {
|
||||||
const cached = this.rolesCache.get(null);
|
const cached = this.rolesCache.get();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
cached.push({
|
cached.push({
|
||||||
...body,
|
...body,
|
||||||
@@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'roleUpdated': {
|
case 'roleUpdated': {
|
||||||
const cached = this.rolesCache.get(null);
|
const cached = this.rolesCache.get();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
const i = cached.findIndex(x => x.id === body.id);
|
const i = cached.findIndex(x => x.id === body.id);
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
@@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'roleDeleted': {
|
case 'roleDeleted': {
|
||||||
const cached = this.rolesCache.get(null);
|
const cached = this.rolesCache.get();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
|
this.rolesCache.set(cached.filter(x => x.id !== body.id));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -192,6 +192,12 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
case 'followingMoreThanOrEq': {
|
case 'followingMoreThanOrEq': {
|
||||||
return user.followingCount >= value.value;
|
return user.followingCount >= value.value;
|
||||||
}
|
}
|
||||||
|
case 'notesLessThanOrEq': {
|
||||||
|
return user.notesCount <= value.value;
|
||||||
|
}
|
||||||
|
case 'notesMoreThanOrEq': {
|
||||||
|
return user.notesCount >= value.value;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -208,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
// 期限切れのロールを除外
|
// 期限切れのロールを除外
|
||||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
||||||
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
|
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
||||||
return [...assignedRoles, ...matchedCondRoles];
|
return [...assignedRoles, ...matchedCondRoles];
|
||||||
}
|
}
|
||||||
@@ -225,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
// 期限切れのロールを除外
|
// 期限切れのロールを除外
|
||||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
|
||||||
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
const badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||||
if (badgeCondRoles.length > 0) {
|
if (badgeCondRoles.length > 0) {
|
||||||
const user = roles.some(r => r.target === 'conditional') ? await this.userCacheService.findById(userId) : null;
|
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||||
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
const matchedBadgeCondRoles = badgeCondRoles.filter(r => this.evalCond(user!, r.condFormula));
|
||||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||||
} else {
|
} else {
|
||||||
@@ -295,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
|
||||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
|
||||||
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||||
roleId: In(moderatorRoles.map(r => r.id)),
|
roleId: In(moderatorRoles.map(r => r.id)),
|
||||||
@@ -315,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getAdministratorIds(): Promise<User['id'][]> {
|
public async getAdministratorIds(): Promise<User['id'][]> {
|
||||||
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
|
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||||
const administratorRoles = roles.filter(r => r.isAdministrator);
|
const administratorRoles = roles.filter(r => r.isAdministrator);
|
||||||
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||||
roleId: In(administratorRoles.map(r => r.id)),
|
roleId: In(administratorRoles.map(r => r.id)),
|
||||||
@@ -394,6 +400,6 @@ export class RoleService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
this.redisForPubsub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
import * as https from 'node:https';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import S3 from 'aws-sdk/clients/s3.js';
|
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||||
|
import { Upload } from '@aws-sdk/lib-storage';
|
||||||
|
import { NodeHttpHandler, NodeHttpHandlerOptions } from '@aws-sdk/node-http-handler';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { Meta } from '@/models/entities/Meta.js';
|
import type { Meta } from '@/models/entities/Meta.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class S3Service {
|
export class S3Service {
|
||||||
@@ -18,25 +23,47 @@ export class S3Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getS3(meta: Meta) {
|
public getS3Client(meta: Meta): S3Client {
|
||||||
const u = meta.objectStorageEndpoint
|
const u = meta.objectStorageEndpoint
|
||||||
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
|
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
|
||||||
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
|
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
|
||||||
|
|
||||||
return new S3({
|
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
|
||||||
endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0
|
const handlerOption: NodeHttpHandlerOptions = {};
|
||||||
? meta.objectStorageEndpoint
|
if (meta.objectStorageUseSSL) {
|
||||||
: undefined,
|
handlerOption.httpsAgent = agent as https.Agent;
|
||||||
accessKeyId: meta.objectStorageAccessKey!,
|
} else {
|
||||||
secretAccessKey: meta.objectStorageSecretKey!,
|
handlerOption.httpAgent = agent as http.Agent;
|
||||||
region: meta.objectStorageRegion ?? undefined,
|
}
|
||||||
sslEnabled: meta.objectStorageUseSSL,
|
|
||||||
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted
|
return new S3Client({
|
||||||
? false
|
endpoint: meta.objectStorageEndpoint ? u : undefined,
|
||||||
: meta.objectStorageS3ForcePathStyle,
|
credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? {
|
||||||
httpOptions: {
|
accessKeyId: meta.objectStorageAccessKey,
|
||||||
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
secretAccessKey: meta.objectStorageSecretKey,
|
||||||
},
|
} : undefined,
|
||||||
|
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined, // 空文字列もundefinedにするため ?? は使わない
|
||||||
|
tls: meta.objectStorageUseSSL,
|
||||||
|
forcePathStyle: meta.objectStorageEndpoint ? meta.objectStorageS3ForcePathStyle : false, // AWS with endPoint omitted
|
||||||
|
requestHandler: new NodeHttpHandler(handlerOption),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async upload(meta: Meta, input: PutObjectCommandInput) {
|
||||||
|
const client = this.getS3Client(meta);
|
||||||
|
return new Upload({
|
||||||
|
client,
|
||||||
|
params: input,
|
||||||
|
partSize: (client.config.endpoint && (await client.config.endpoint()).hostname === 'storage.googleapis.com')
|
||||||
|
? 500 * 1024 * 1024
|
||||||
|
: 8 * 1024 * 1024,
|
||||||
|
}).done();
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public delete(meta: Meta, input: DeleteObjectCommandInput) {
|
||||||
|
const client = this.getS3Client(meta);
|
||||||
|
return client.send(new DeleteObjectCommand(input));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,30 @@
|
|||||||
|
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import Redis from 'ioredis';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { WebhookService } from '@/core/WebhookService.js';
|
import { WebhookService } from '@/core/WebhookService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserBlockingService implements OnApplicationShutdown {
|
export class UserBlockingService implements OnModuleInit {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private userFollowingService: UserFollowingService;
|
||||||
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
|
||||||
private blockingsByUserIdCache: Cache<User['id'][]>;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
private moduleRef: ModuleRef,
|
||||||
private redisSubscriber: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followingsRepository)
|
|
||||||
private followingsRepository: FollowingsRepository,
|
|
||||||
|
|
||||||
@Inject(DI.followRequestsRepository)
|
@Inject(DI.followRequestsRepository)
|
||||||
private followRequestsRepository: FollowRequestsRepository,
|
private followRequestsRepository: FollowRequestsRepository,
|
||||||
|
|
||||||
@@ -47,47 +37,20 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
@Inject(DI.userListJoiningsRepository)
|
@Inject(DI.userListJoiningsRepository)
|
||||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||||
|
|
||||||
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('user-block');
|
this.logger = this.loggerService.getLogger('user-block');
|
||||||
|
|
||||||
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
|
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
onModuleInit() {
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
this.userFollowingService = this.moduleRef.get('UserFollowingService');
|
||||||
const obj = JSON.parse(data);
|
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
|
||||||
switch (type) {
|
|
||||||
case 'blockingCreated': {
|
|
||||||
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
|
||||||
if (cached) {
|
|
||||||
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'blockingDeleted': {
|
|
||||||
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
|
||||||
if (cached) {
|
|
||||||
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -95,8 +58,8 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.cancelRequest(blocker, blockee),
|
this.cancelRequest(blocker, blockee),
|
||||||
this.cancelRequest(blockee, blocker),
|
this.cancelRequest(blockee, blocker),
|
||||||
this.unFollow(blocker, blockee),
|
this.userFollowingService.unfollow(blocker, blockee),
|
||||||
this.unFollow(blockee, blocker),
|
this.userFollowingService.unfollow(blockee, blocker),
|
||||||
this.removeFromList(blockee, blocker),
|
this.removeFromList(blockee, blocker),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -111,6 +74,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
await this.blockingsRepository.insert(blocking);
|
await this.blockingsRepository.insert(blocking);
|
||||||
|
|
||||||
|
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||||
|
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('blockingCreated', {
|
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
@@ -148,7 +114,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
this.userEntityService.pack(followee, follower, {
|
this.userEntityService.pack(followee, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
@@ -173,54 +138,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async unFollow(follower: User, followee: User) {
|
|
||||||
const following = await this.followingsRepository.findOneBy({
|
|
||||||
followerId: follower.id,
|
|
||||||
followeeId: followee.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (following == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
this.followingsRepository.delete(following.id),
|
|
||||||
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),
|
|
||||||
this.usersRepository.decrement({ id: followee.id }, 'followersCount', 1),
|
|
||||||
this.perUserFollowingChart.update(follower, followee, false),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Publish unfollow event
|
|
||||||
if (this.userEntityService.isLocalUser(follower)) {
|
|
||||||
this.userEntityService.pack(followee, follower, {
|
|
||||||
detail: true,
|
|
||||||
}).then(async packed => {
|
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
|
||||||
for (const webhook of webhooks) {
|
|
||||||
this.queueService.webhookDeliver(webhook, 'unfollow', {
|
|
||||||
user: packed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// リモートにフォローをしていたらUndoFollow送信
|
|
||||||
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderFollow(follower, followee), follower));
|
|
||||||
this.queueService.deliver(follower, content, followee.inbox, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// リモートからフォローをされていたらRejectFollow送信
|
|
||||||
if (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower)) {
|
|
||||||
const content = this.apRendererService.addContext(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee), followee));
|
|
||||||
this.queueService.deliver(followee, content, follower.inbox, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async removeFromList(listOwner: User, user: User) {
|
private async removeFromList(listOwner: User, user: User) {
|
||||||
const userLists = await this.userListsRepository.findBy({
|
const userLists = await this.userListsRepository.findBy({
|
||||||
@@ -254,6 +171,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
await this.blockingsRepository.delete(blocking.id);
|
await this.blockingsRepository.delete(blocking.id);
|
||||||
|
|
||||||
|
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||||
|
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||||
blockerId: blocker.id,
|
blockerId: blocker.id,
|
||||||
blockeeId: blockee.id,
|
blockeeId: blockee.id,
|
||||||
@@ -268,17 +188,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||||
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
|
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||||
where: {
|
|
||||||
blockerId,
|
|
||||||
},
|
|
||||||
select: ['blockeeId'],
|
|
||||||
}).then(records => records.map(record => record.blockeeId)));
|
|
||||||
return blockedUserIds.includes(blockeeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import type { UsersRepository } from '@/models/index.js';
|
|
||||||
import { Cache } from '@/misc/cache.js';
|
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
|
||||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserCacheService implements OnApplicationShutdown {
|
|
||||||
public userByIdCache: Cache<User>;
|
|
||||||
public localUserByNativeTokenCache: Cache<LocalUser | null>;
|
|
||||||
public localUserByIdCache: Cache<LocalUser>;
|
|
||||||
public uriPersonCache: Cache<User | null>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.redisSubscriber)
|
|
||||||
private redisSubscriber: Redis.Redis,
|
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
) {
|
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
|
||||||
|
|
||||||
this.userByIdCache = new Cache<User>(Infinity);
|
|
||||||
this.localUserByNativeTokenCache = new Cache<LocalUser | null>(Infinity);
|
|
||||||
this.localUserByIdCache = new Cache<LocalUser>(Infinity);
|
|
||||||
this.uriPersonCache = new Cache<User | null>(Infinity);
|
|
||||||
|
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private async onMessage(_: string, data: string): Promise<void> {
|
|
||||||
const obj = JSON.parse(data);
|
|
||||||
|
|
||||||
if (obj.channel === 'internal') {
|
|
||||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
|
||||||
switch (type) {
|
|
||||||
case 'userChangeSuspendedState':
|
|
||||||
case 'remoteUserUpdated': {
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
|
|
||||||
this.userByIdCache.set(user.id, user);
|
|
||||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
|
||||||
if (v.value?.id === user.id) {
|
|
||||||
this.uriPersonCache.set(k, user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.userEntityService.isLocalUser(user)) {
|
|
||||||
this.localUserByNativeTokenCache.set(user.token, user);
|
|
||||||
this.localUserByIdCache.set(user.id, user);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'userTokenRegenerated': {
|
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: body.id }) as LocalUser;
|
|
||||||
this.localUserByNativeTokenCache.delete(body.oldToken);
|
|
||||||
this.localUserByNativeTokenCache.set(body.newToken, user);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'follow': {
|
|
||||||
const follower = this.userByIdCache.get(body.followerId);
|
|
||||||
if (follower) follower.followingCount++;
|
|
||||||
const followee = this.userByIdCache.get(body.followeeId);
|
|
||||||
if (followee) followee.followersCount++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public findById(userId: User['id']) {
|
|
||||||
return this.userByIdCache.fetch(userId, () => this.usersRepository.findOneByOrFail({ id: userId }));
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
|
||||||
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, RemoteUser, User } from '@/models/entities/User.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
@@ -17,6 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import Logger from '../logger.js';
|
import Logger from '../logger.js';
|
||||||
|
|
||||||
const logger = new Logger('following/create');
|
const logger = new Logger('following/create');
|
||||||
@@ -35,8 +38,12 @@ type Remote = RemoteUser | {
|
|||||||
type Both = Local | Remote;
|
type Both = Local | Remote;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserFollowingService {
|
export class UserFollowingService implements OnModuleInit {
|
||||||
|
private userBlockingService: UserBlockingService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private moduleRef: ModuleRef,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@@ -52,11 +59,12 @@ export class UserFollowingService {
|
|||||||
@Inject(DI.instancesRepository)
|
@Inject(DI.instancesRepository)
|
||||||
private instancesRepository: InstancesRepository,
|
private instancesRepository: InstancesRepository,
|
||||||
|
|
||||||
|
private cacheService: CacheService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private userBlockingService: UserBlockingService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private queueService: QueueService,
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
|
private metaService: MetaService,
|
||||||
private notificationService: NotificationService,
|
private notificationService: NotificationService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
private webhookService: WebhookService,
|
private webhookService: WebhookService,
|
||||||
@@ -66,6 +74,10 @@ export class UserFollowingService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
this.userBlockingService = this.moduleRef.get('UserBlockingService');
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
@@ -170,6 +182,8 @@ export class UserFollowingService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
const req = await this.followRequestsRepository.findOneBy({
|
const req = await this.followRequestsRepository.findOneBy({
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
@@ -200,14 +214,18 @@ export class UserFollowingService {
|
|||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(i => {
|
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||||
this.instanceChart.updateFollowing(i.host, true);
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateFollowing(i.host, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(followee.host).then(i => {
|
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||||
this.instanceChart.updateFollowers(i.host, true);
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateFollowers(i.host, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -219,7 +237,6 @@ export class UserFollowingService {
|
|||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||||
@@ -273,6 +290,8 @@ export class UserFollowingService {
|
|||||||
|
|
||||||
await this.followingsRepository.delete(following.id);
|
await this.followingsRepository.delete(following.id);
|
||||||
|
|
||||||
|
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||||
|
|
||||||
this.decrementFollowing(follower, followee);
|
this.decrementFollowing(follower, followee);
|
||||||
|
|
||||||
// Publish unfollow event
|
// Publish unfollow event
|
||||||
@@ -280,7 +299,6 @@ export class UserFollowingService {
|
|||||||
this.userEntityService.pack(followee.id, follower, {
|
this.userEntityService.pack(followee.id, follower, {
|
||||||
detail: true,
|
detail: true,
|
||||||
}).then(async packed => {
|
}).then(async packed => {
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
@@ -320,14 +338,18 @@ export class UserFollowingService {
|
|||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(follower.host).then(i => {
|
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||||
this.instanceChart.updateFollowing(i.host, false);
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateFollowing(i.host, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.fetch(followee.host).then(i => {
|
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||||
this.instanceChart.updateFollowers(i.host, false);
|
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||||
|
this.instanceChart.updateFollowers(i.host, false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -569,7 +591,6 @@ export class UserFollowingService {
|
|||||||
detail: true,
|
detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
|
||||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
||||||
|
|
||||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||||
|
|||||||
34
packages/backend/src/core/UserKeypairService.ts
Normal file
34
packages/backend/src/core/UserKeypairService.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import type { User } from '@/models/entities/User.js';
|
||||||
|
import type { UserKeypairsRepository } from '@/models/index.js';
|
||||||
|
import { RedisKVCache } from '@/misc/cache.js';
|
||||||
|
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserKeypairService {
|
||||||
|
private cache: RedisKVCache<UserKeypair>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
|
@Inject(DI.userKeypairsRepository)
|
||||||
|
private userKeypairsRepository: UserKeypairsRepository,
|
||||||
|
) {
|
||||||
|
this.cache = new RedisKVCache<UserKeypair>(this.redisClient, 'userKeypair', {
|
||||||
|
lifetime: 1000 * 60 * 60 * 24, // 24h
|
||||||
|
memoryCacheLifetime: Infinity,
|
||||||
|
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
|
||||||
|
toRedisConverter: (value) => JSON.stringify(value),
|
||||||
|
fromRedisConverter: (value) => JSON.parse(value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
||||||
|
return await this.cache.fetch(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import type { User } from '@/models/entities/User.js';
|
|
||||||
import type { UserKeypairsRepository } from '@/models/index.js';
|
|
||||||
import { Cache } from '@/misc/cache.js';
|
|
||||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UserKeypairStoreService {
|
|
||||||
private cache: Cache<UserKeypair>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(DI.userKeypairsRepository)
|
|
||||||
private userKeypairsRepository: UserKeypairsRepository,
|
|
||||||
) {
|
|
||||||
this.cache = new Cache<UserKeypair>(Infinity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bindThis
|
|
||||||
public async getUserKeypair(userId: User['id']): Promise<UserKeypair> {
|
|
||||||
return await this.cache.fetch(userId, () => this.userKeypairsRepository.findOneByOrFail({ userId: userId }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +1,47 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { UsersRepository, MutingsRepository } from '@/models/index.js';
|
import { In } from 'typeorm';
|
||||||
|
import type { MutingsRepository, Muting } from '@/models/index.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { QueueService } from '@/core/QueueService.js';
|
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserMutingService {
|
export class UserMutingService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.usersRepository)
|
|
||||||
private usersRepository: UsersRepository,
|
|
||||||
|
|
||||||
@Inject(DI.mutingsRepository)
|
@Inject(DI.mutingsRepository)
|
||||||
private mutingsRepository: MutingsRepository,
|
private mutingsRepository: MutingsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private queueService: QueueService,
|
private cacheService: CacheService,
|
||||||
private globalEventService: GlobalEventService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async mute(user: User, target: User): Promise<void> {
|
public async mute(user: User, target: User, expiresAt: Date | null = null): Promise<void> {
|
||||||
await this.mutingsRepository.insert({
|
await this.mutingsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
expiresAt: expiresAt ?? null,
|
||||||
muterId: user.id,
|
muterId: user.id,
|
||||||
muteeId: target.id,
|
muteeId: target.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.cacheService.userMutingsCache.refresh(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async unmute(mutings: Muting[]): Promise<void> {
|
||||||
|
if (mutings.length === 0) return;
|
||||||
|
|
||||||
|
await this.mutingsRepository.delete({
|
||||||
|
id: In(mutings.map(m => m.id)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const muterIds = [...new Set(mutings.map(m => m.muterId))];
|
||||||
|
for (const muterId of muterIds) {
|
||||||
|
this.cacheService.userMutingsCache.refresh(muterId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class VideoProcessingService {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 280);
|
return await this.imageProcessingService.convertToWebp(`${dir}/out.png`, 498, 422);
|
||||||
} finally {
|
} finally {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
|
|||||||
private webhooks: Webhook[] = [];
|
private webhooks: Webhook[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.redisSubscriber)
|
@Inject(DI.redisForPubsub)
|
||||||
private redisSubscriber: Redis.Redis,
|
private redisForPubsub: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.webhooksRepository)
|
@Inject(DI.webhooksRepository)
|
||||||
private webhooksRepository: WebhooksRepository,
|
private webhooksRepository: WebhooksRepository,
|
||||||
) {
|
) {
|
||||||
//this.onMessage = this.onMessage.bind(this);
|
//this.onMessage = this.onMessage.bind(this);
|
||||||
this.redisSubscriber.on('message', this.onMessage);
|
this.redisForPubsub.on('message', this.onMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public onApplicationShutdown(signal?: string | undefined) {
|
public onApplicationShutdown(signal?: string | undefined) {
|
||||||
this.redisSubscriber.off('message', this.onMessage);
|
this.redisForPubsub.off('message', this.onMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import escapeRegexp from 'escape-regexp';
|
|||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { Cache } from '@/misc/cache.js';
|
import { MemoryKVCache } from '@/misc/cache.js';
|
||||||
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
import type { UserPublickey } from '@/models/entities/UserPublickey.js';
|
||||||
import { UserCacheService } from '@/core/UserCacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { Note } from '@/models/entities/Note.js';
|
import type { Note } from '@/models/entities/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
import { RemoteUser, User } from '@/models/entities/User.js';
|
||||||
@@ -31,8 +31,8 @@ export type UriParseResult = {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApDbResolverService {
|
export class ApDbResolverService {
|
||||||
private publicKeyCache: Cache<UserPublickey | null>;
|
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
||||||
private publicKeyByUserIdCache: Cache<UserPublickey | null>;
|
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
@@ -47,11 +47,11 @@ export class ApDbResolverService {
|
|||||||
@Inject(DI.userPublickeysRepository)
|
@Inject(DI.userPublickeysRepository)
|
||||||
private userPublickeysRepository: UserPublickeysRepository,
|
private userPublickeysRepository: UserPublickeysRepository,
|
||||||
|
|
||||||
private userCacheService: UserCacheService,
|
private cacheService: CacheService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
) {
|
) {
|
||||||
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
||||||
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
@@ -107,11 +107,11 @@ export class ApDbResolverService {
|
|||||||
if (parsed.local) {
|
if (parsed.local) {
|
||||||
if (parsed.type !== 'users') return null;
|
if (parsed.type !== 'users') return null;
|
||||||
|
|
||||||
return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
||||||
id: parsed.id,
|
id: parsed.id,
|
||||||
}).then(x => x ?? undefined)) ?? null;
|
}).then(x => x ?? undefined)) ?? null;
|
||||||
} else {
|
} else {
|
||||||
return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
||||||
uri: parsed.uri,
|
uri: parsed.uri,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ export class ApDbResolverService {
|
|||||||
if (key == null) return null;
|
if (key == null) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: await this.userCacheService.findById(key.userId) as RemoteUser,
|
user: await this.cacheService.findUserById(key.userId) as RemoteUser,
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In, IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||||
@@ -22,7 +22,7 @@ import { QueueService } from '@/core/QueueService.js';
|
|||||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||||
import { ApNoteService } from './models/ApNoteService.js';
|
import { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
@@ -31,7 +31,7 @@ import { ApAudienceService } from './ApAudienceService.js';
|
|||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||||
import type { Resolver } from './ApResolverService.js';
|
import type { Resolver } from './ApResolverService.js';
|
||||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApInboxService {
|
export class ApInboxService {
|
||||||
@@ -80,7 +80,7 @@ export class ApInboxService {
|
|||||||
) {
|
) {
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performActivity(actor: RemoteUser, activity: IObject) {
|
public async performActivity(actor: RemoteUser, activity: IObject) {
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
@@ -139,6 +139,8 @@ export class ApInboxService {
|
|||||||
await this.block(actor, activity);
|
await this.block(actor, activity);
|
||||||
} else if (isFlag(activity)) {
|
} else if (isFlag(activity)) {
|
||||||
await this.flag(actor, activity);
|
await this.flag(actor, activity);
|
||||||
|
} else if (isMove(activity)) {
|
||||||
|
await this.move(actor, activity);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
this.logger.warn(`unrecognized activity type: ${activity.type}`);
|
||||||
}
|
}
|
||||||
@@ -147,15 +149,15 @@ export class ApInboxService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
private async follow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
private async follow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||||
const followee = await this.apDbResolverService.getUserFromApId(activity.object);
|
const followee = await this.apDbResolverService.getUserFromApId(activity.object);
|
||||||
|
|
||||||
if (followee == null) {
|
if (followee == null) {
|
||||||
return 'skip: followee not found';
|
return 'skip: followee not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (followee.host != null) {
|
if (followee.host != null) {
|
||||||
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
|
return 'skip: フォローしようとしているユーザーはローカルユーザーではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userFollowingService.follow(actor, followee, activity.id);
|
await this.userFollowingService.follow(actor, followee, activity.id);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
@@ -183,16 +185,16 @@ export class ApInboxService {
|
|||||||
const uri = activity.id ?? activity;
|
const uri = activity.id ?? activity;
|
||||||
|
|
||||||
this.logger.info(`Accept: ${uri}`);
|
this.logger.info(`Accept: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(err => {
|
const object = await resolver.resolve(activity.object).catch(err => {
|
||||||
this.logger.error(`Resolution failed: ${err}`);
|
this.logger.error(`Resolution failed: ${err}`);
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFollow(object)) return await this.acceptFollow(actor, object);
|
if (isFollow(object)) return await this.acceptFollow(actor, object);
|
||||||
|
|
||||||
return `skip: Unknown Accept type: ${getApType(object)}`;
|
return `skip: Unknown Accept type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,18 +227,18 @@ export class ApInboxService {
|
|||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.target == null) {
|
if (activity.target == null) {
|
||||||
throw new Error('target is null');
|
throw new Error('target is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.target === actor.featured) {
|
if (activity.target === actor.featured) {
|
||||||
const note = await this.apNoteService.resolveNote(activity.object);
|
const note = await this.apNoteService.resolveNote(activity.object);
|
||||||
if (note == null) throw new Error('note not found');
|
if (note == null) throw new Error('note not found');
|
||||||
await this.notePiningService.addPinned(actor, note.id);
|
await this.notePiningService.addPinned(actor, note.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`unknown target: ${activity.target}`);
|
throw new Error(`unknown target: ${activity.target}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,10 +407,10 @@ export class ApInboxService {
|
|||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 削除対象objectのtype
|
// 削除対象objectのtype
|
||||||
let formerType: string | undefined;
|
let formerType: string | undefined;
|
||||||
|
|
||||||
if (typeof activity.object === 'string') {
|
if (typeof activity.object === 'string') {
|
||||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||||
formerType = undefined;
|
formerType = undefined;
|
||||||
@@ -420,19 +422,19 @@ export class ApInboxService {
|
|||||||
formerType = toSingle(object.type);
|
formerType = toSingle(object.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = getApId(activity.object);
|
const uri = getApId(activity.object);
|
||||||
|
|
||||||
// type不明でもactorとobjectが同じならばそれはPersonに違いない
|
// type不明でもactorとobjectが同じならばそれはPersonに違いない
|
||||||
if (!formerType && actor.uri === uri) {
|
if (!formerType && actor.uri === uri) {
|
||||||
formerType = 'Person';
|
formerType = 'Person';
|
||||||
}
|
}
|
||||||
|
|
||||||
// それでもなかったらおそらくNote
|
// それでもなかったらおそらくNote
|
||||||
if (!formerType) {
|
if (!formerType) {
|
||||||
formerType = 'Note';
|
formerType = 'Note';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validPost.includes(formerType)) {
|
if (validPost.includes(formerType)) {
|
||||||
return await this.deleteNote(actor, uri);
|
return await this.deleteNote(actor, uri);
|
||||||
} else if (validActor.includes(formerType)) {
|
} else if (validActor.includes(formerType)) {
|
||||||
@@ -445,44 +447,44 @@ export class ApInboxService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
private async deleteActor(actor: RemoteUser, uri: string): Promise<string> {
|
private async deleteActor(actor: RemoteUser, uri: string): Promise<string> {
|
||||||
this.logger.info(`Deleting the Actor: ${uri}`);
|
this.logger.info(`Deleting the Actor: ${uri}`);
|
||||||
|
|
||||||
if (actor.uri !== uri) {
|
if (actor.uri !== uri) {
|
||||||
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
return `skip: delete actor ${actor.uri} !== ${uri}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
const user = await this.usersRepository.findOneBy({ id: actor.id });
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return 'skip: actor not found';
|
return 'skip: actor not found';
|
||||||
} else if (user.isDeleted) {
|
} else if (user.isDeleted) {
|
||||||
return 'skip: already deleted';
|
return 'skip: already deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
const job = await this.queueService.createDeleteAccountJob(actor);
|
const job = await this.queueService.createDeleteAccountJob(actor);
|
||||||
|
|
||||||
await this.usersRepository.update(actor.id, {
|
await this.usersRepository.update(actor.id, {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return `ok: queued ${job.name} ${job.id}`;
|
return `ok: queued ${job.name} ${job.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async deleteNote(actor: RemoteUser, uri: string): Promise<string> {
|
private async deleteNote(actor: RemoteUser, uri: string): Promise<string> {
|
||||||
this.logger.info(`Deleting the Note: ${uri}`);
|
this.logger.info(`Deleting the Note: ${uri}`);
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return 'message not found';
|
return 'message not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.userId !== actor.id) {
|
if (note.userId !== actor.id) {
|
||||||
return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
|
return '投稿を削除しようとしているユーザーは投稿の作成者ではありません';
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.noteDeleteService.delete(actor, note);
|
await this.noteDeleteService.delete(actor, note);
|
||||||
return 'ok: note deleted';
|
return 'ok: note deleted';
|
||||||
} finally {
|
} finally {
|
||||||
@@ -536,23 +538,23 @@ export class ApInboxService {
|
|||||||
@bindThis
|
@bindThis
|
||||||
private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
private async rejectFollow(actor: RemoteUser, activity: IFollow): Promise<string> {
|
||||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||||
|
|
||||||
const follower = await this.apDbResolverService.getUserFromApId(activity.actor);
|
const follower = await this.apDbResolverService.getUserFromApId(activity.actor);
|
||||||
|
|
||||||
if (follower == null) {
|
if (follower == null) {
|
||||||
return 'skip: follower not found';
|
return 'skip: follower not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.userEntityService.isLocalUser(follower)) {
|
if (!this.userEntityService.isLocalUser(follower)) {
|
||||||
return 'skip: follower is not a local user';
|
return 'skip: follower is not a local user';
|
||||||
}
|
}
|
||||||
|
|
||||||
// relay
|
// relay
|
||||||
const match = activity.id?.match(/follow-relay\/(\w+)/);
|
const match = activity.id?.match(/follow-relay\/(\w+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
return await this.relayService.relayRejected(match[1]);
|
return await this.relayService.relayRejected(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.userFollowingService.remoteReject(actor, follower);
|
await this.userFollowingService.remoteReject(actor, follower);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
}
|
}
|
||||||
@@ -562,18 +564,18 @@ export class ApInboxService {
|
|||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.target == null) {
|
if (activity.target == null) {
|
||||||
throw new Error('target is null');
|
throw new Error('target is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.target === actor.featured) {
|
if (activity.target === actor.featured) {
|
||||||
const note = await this.apNoteService.resolveNote(activity.object);
|
const note = await this.apNoteService.resolveNote(activity.object);
|
||||||
if (note == null) throw new Error('note not found');
|
if (note == null) throw new Error('note not found');
|
||||||
await this.notePiningService.removePinned(actor, note.id);
|
await this.notePiningService.removePinned(actor, note.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`unknown target: ${activity.target}`);
|
throw new Error(`unknown target: ${activity.target}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,24 +584,24 @@ export class ApInboxService {
|
|||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
throw new Error('invalid actor');
|
throw new Error('invalid actor');
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = activity.id ?? activity;
|
const uri = activity.id ?? activity;
|
||||||
|
|
||||||
this.logger.info(`Undo: ${uri}`);
|
this.logger.info(`Undo: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFollow(object)) return await this.undoFollow(actor, object);
|
if (isFollow(object)) return await this.undoFollow(actor, object);
|
||||||
if (isBlock(object)) return await this.undoBlock(actor, object);
|
if (isBlock(object)) return await this.undoBlock(actor, object);
|
||||||
if (isLike(object)) return await this.undoLike(actor, object);
|
if (isLike(object)) return await this.undoLike(actor, object);
|
||||||
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
||||||
if (isAccept(object)) return await this.undoAccept(actor, object);
|
if (isAccept(object)) return await this.undoAccept(actor, object);
|
||||||
|
|
||||||
return `skip: unknown object type ${getApType(object)}`;
|
return `skip: unknown object type ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,17 +611,17 @@ export class ApInboxService {
|
|||||||
if (follower == null) {
|
if (follower == null) {
|
||||||
return 'skip: follower not found';
|
return 'skip: follower not found';
|
||||||
}
|
}
|
||||||
|
|
||||||
const following = await this.followingsRepository.findOneBy({
|
const following = await this.followingsRepository.findOneBy({
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
followeeId: actor.id,
|
followeeId: actor.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (following) {
|
if (following) {
|
||||||
await this.userFollowingService.unfollow(follower, actor);
|
await this.userFollowingService.unfollow(follower, actor);
|
||||||
return 'ok: unfollowed';
|
return 'ok: unfollowed';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'skip: フォローされていない';
|
return 'skip: フォローされていない';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,16 +710,16 @@ export class ApInboxService {
|
|||||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Update');
|
this.logger.debug('Update');
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isActor(object)) {
|
if (isActor(object)) {
|
||||||
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
@@ -728,4 +730,55 @@ export class ApInboxService {
|
|||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unknown type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async move(actor: RemoteUser, activity: IMove): Promise<string> {
|
||||||
|
// fetch the new and old accounts
|
||||||
|
const targetUri = getApHrefNullable(activity.target);
|
||||||
|
if (!targetUri) return 'skip: invalid activity target';
|
||||||
|
const new_acc = await this.apPersonService.resolvePerson(targetUri);
|
||||||
|
const old_acc = await this.apPersonService.resolvePerson(actor.uri);
|
||||||
|
|
||||||
|
// update them if they're remote
|
||||||
|
if (new_acc.uri) await this.apPersonService.updatePerson(new_acc.uri);
|
||||||
|
if (old_acc.uri) await this.apPersonService.updatePerson(old_acc.uri);
|
||||||
|
|
||||||
|
// check if alsoKnownAs of the new account is valid
|
||||||
|
let isValidMove = true;
|
||||||
|
if (old_acc.uri) {
|
||||||
|
if (!new_acc.alsoKnownAs?.includes(old_acc.uri)) {
|
||||||
|
isValidMove = false;
|
||||||
|
}
|
||||||
|
} else if (!new_acc.alsoKnownAs?.includes(old_acc.id)) {
|
||||||
|
isValidMove = false;
|
||||||
|
}
|
||||||
|
if (!isValidMove) {
|
||||||
|
return 'skip: accounts invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
// add target uri to movedToUri in order to indicate that the user has moved
|
||||||
|
await this.usersRepository.update(old_acc.id, { movedToUri: targetUri });
|
||||||
|
|
||||||
|
// follow the new account and unfollow the old one
|
||||||
|
const followings = await this.followingsRepository.find({
|
||||||
|
relations: {
|
||||||
|
follower: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
followeeId: old_acc.id,
|
||||||
|
followerHost: IsNull(), // follower is local
|
||||||
|
}
|
||||||
|
});
|
||||||
|
followings.forEach(async (following) => {
|
||||||
|
if (!following.follower) return;
|
||||||
|
try {
|
||||||
|
await this.userFollowingService.follow(following.follower, new_acc);
|
||||||
|
await this.userFollowingService.unfollow(following.follower, old_acc);
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,18 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
|||||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||||
import type { Poll } from '@/models/entities/Poll.js';
|
import type { Poll } from '@/models/entities/Poll.js';
|
||||||
import type { PollVote } from '@/models/entities/PollVote.js';
|
import type { PollVote } from '@/models/entities/PollVote.js';
|
||||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
import { MfmService } from '@/core/MfmService.js';
|
import { MfmService } from '@/core/MfmService.js';
|
||||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
|
import { isNotNull } from '@/misc/is-not-null.js';
|
||||||
import { LdSignatureService } from './LdSignatureService.js';
|
import { LdSignatureService } from './LdSignatureService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
import type { IIdentifier } from './models/identifier.js';
|
import type { IIdentifier } from './models/identifier.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -50,10 +52,11 @@ export class ApRendererService {
|
|||||||
@Inject(DI.pollsRepository)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
private customEmojiService: CustomEmojiService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private driveFileEntityService: DriveFileEntityService,
|
private driveFileEntityService: DriveFileEntityService,
|
||||||
private ldSignatureService: LdSignatureService,
|
private ldSignatureService: LdSignatureService,
|
||||||
private userKeypairStoreService: UserKeypairStoreService,
|
private userKeypairService: UserKeypairService,
|
||||||
private apMfmService: ApMfmService,
|
private apMfmService: ApMfmService,
|
||||||
private mfmService: MfmService,
|
private mfmService: MfmService,
|
||||||
) {
|
) {
|
||||||
@@ -272,11 +275,7 @@ export class ApRendererService {
|
|||||||
|
|
||||||
if (reaction.startsWith(':')) {
|
if (reaction.startsWith(':')) {
|
||||||
const name = reaction.replaceAll(':', '');
|
const name = reaction.replaceAll(':', '');
|
||||||
// TODO: cache
|
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||||
const emoji = await this.emojisRepository.findOneBy({
|
|
||||||
name,
|
|
||||||
host: IsNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
||||||
}
|
}
|
||||||
@@ -293,6 +292,22 @@ export class ApRendererService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public renderMove(
|
||||||
|
src: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||||
|
dst: { id: User['id']; host: User['host']; uri: User['host'] },
|
||||||
|
): IMove {
|
||||||
|
const actor = this.userEntityService.isLocalUser(src) ? `${this.config.url}/users/${src.id}` : src.uri!;
|
||||||
|
const target = this.userEntityService.isLocalUser(dst) ? `${this.config.url}/users/${dst.id}` : dst.uri!;
|
||||||
|
return {
|
||||||
|
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||||
|
actor,
|
||||||
|
type: 'Move',
|
||||||
|
object: actor,
|
||||||
|
target,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
||||||
const getPromisedFiles = async (ids: string[]) => {
|
const getPromisedFiles = async (ids: string[]) => {
|
||||||
@@ -473,7 +488,7 @@ export class ApRendererService {
|
|||||||
...hashtagTags,
|
...hashtagTags,
|
||||||
];
|
];
|
||||||
|
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const person = {
|
const person = {
|
||||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||||
@@ -499,6 +514,14 @@ export class ApRendererService {
|
|||||||
attachment: attachment.length ? attachment : undefined,
|
attachment: attachment.length ? attachment : undefined,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
|
if (user.movedToUri) {
|
||||||
|
person.movedTo = user.movedToUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.alsoKnownAs) {
|
||||||
|
person.alsoKnownAs = user.alsoKnownAs;
|
||||||
|
}
|
||||||
|
|
||||||
if (profile.birthday) {
|
if (profile.birthday) {
|
||||||
person['vcard:bday'] = profile.birthday;
|
person['vcard:bday'] = profile.birthday;
|
||||||
}
|
}
|
||||||
@@ -640,7 +663,7 @@ export class ApRendererService {
|
|||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const ldSignature = this.ldSignatureService.use();
|
||||||
ldSignature.debug = false;
|
ldSignature.debug = false;
|
||||||
@@ -701,13 +724,9 @@ export class ApRendererService {
|
|||||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||||
if (names == null || names.length === 0) return [];
|
if (names == null || names.length === 0) return [];
|
||||||
|
|
||||||
const emojis = await Promise.all(
|
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||||
names.map(name => this.emojisRepository.findOneBy({
|
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||||
name,
|
|
||||||
host: IsNull(),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
return emojis.filter(emoji => emoji != null) as Emoji[];
|
return emojis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user