Compare commits
407 Commits
13.10.2
...
13.11.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
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
|
||||
#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 └─────────────────────────────
|
||||
|
||||
|
@@ -62,6 +62,22 @@ redis:
|
||||
#prefix: example-prefix
|
||||
#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 └─────────────────────────────
|
||||
|
||||
|
@@ -62,6 +62,22 @@ redis:
|
||||
#prefix: example-prefix
|
||||
#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 └─────────────────────────────
|
||||
|
||||
|
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
|
||||
- frontend
|
||||
- sw
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
@@ -61,6 +62,7 @@ jobs:
|
||||
matrix:
|
||||
workspace:
|
||||
- backend
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
with:
|
||||
|
58
.github/workflows/storybook.yml
vendored
Normal file
58
.github/workflows/storybook.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: Storybook
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- l10n_develop
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
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
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=7168"
|
||||
- name: Publish to Chromatic
|
||||
id: chromatic
|
||||
uses: chromaui/action@v1
|
||||
with:
|
||||
exitOnceUploaded: true
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
storybookBuildDir: storybook-static
|
||||
workingDir: packages/frontend
|
||||
- name: Compare on Chromatic
|
||||
if: github.event_name == 'pull_request_target'
|
||||
run: pnpm --filter frontend chromatic -d storybook-static --exit-once-uploaded --patch-build ${{ github.head_ref }}...${{ github.base_ref }}
|
||||
env:
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
- 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_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:6
|
||||
image: redis:7
|
||||
ports:
|
||||
- 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_HOST_AUTH_METHOD: trust
|
||||
redis:
|
||||
image: redis:6
|
||||
image: redis:7
|
||||
ports:
|
||||
- 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
|
||||
/files
|
||||
ormconfig.json
|
||||
temp
|
||||
/packages/frontend/src/**/*.stories.ts
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
|
56
CHANGELOG.md
56
CHANGELOG.md
@@ -5,13 +5,67 @@
|
||||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
-
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
-->
|
||||
|
||||
## 13.x.x (unreleased)
|
||||
|
||||
### NOTE
|
||||
- Redis 7.xが必要です
|
||||
|
||||
### General
|
||||
- チャンネルをお気に入りに登録できるように
|
||||
- チャンネルにノートをピン留めできるように
|
||||
|
||||
### Client
|
||||
- 検索ページでURLを入力した際に照会したときと同等の挙動をするように
|
||||
- ノートのリアクションを大きく表示するオプションを追加
|
||||
- ギャラリー一覧にメディア表示と同じように NSFW 設定を反映するように(ホバーで表示)
|
||||
- オブジェクトストレージの設定画面を分かりやすく
|
||||
- 「にゃああああああああああああああ!!!!!!!!!!!!」 (`isCat`) 有効時にアバターに表示される猫耳について挙動を変更
|
||||
- 「UIにぼかし効果を使用」 (`useBlurEffect`) で次の挙動が有効になります
|
||||
- 猫耳のアバター内部部分をぼかしでマスク表示してより猫耳っぽく見えるように
|
||||
- 猫耳の色がアバター上部のピクセルから決定されます(無効化時はアバター全体の平均色)
|
||||
- 左耳は上からおよそ 10%, 左からおよそ 20% の位置で決定します
|
||||
- 右耳は上からおよそ 10%, 左からおよそ 80% の位置で決定します
|
||||
- 「UIのアニメーションを減らす」 (`reduceAnimation`) で猫耳を撫でられなくなります
|
||||
|
||||
### 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
|
||||
|
110
CONTRIBUTING.md
110
CONTRIBUTING.md
@@ -203,6 +203,116 @@ niraxは、Misskeyで使用しているオリジナルのフロントエンド
|
||||
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
|
||||
### 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/frontend/package.json", "./packages/frontend/"]
|
||||
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 \
|
||||
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.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<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>
|
||||
</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:
|
||||
host: localhost
|
||||
port: 6379
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#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 └─────────────────────────────
|
||||
|
||||
|
@@ -545,7 +545,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
|
||||
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
|
||||
notificationType: "أنواع الإشعارات"
|
||||
edit: "التعديل"
|
||||
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
|
||||
emailServer: "خادم البريد الإلكتروني"
|
||||
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
|
||||
email: "البريد الإلكتروني "
|
||||
@@ -1275,3 +1274,7 @@ _deck:
|
||||
channel: "القنوات"
|
||||
mentions: "الإشارات"
|
||||
direct: "مباشرة"
|
||||
_webhookSettings:
|
||||
name: "الإسم"
|
||||
active: "مفعّل"
|
||||
|
||||
|
@@ -562,7 +562,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
|
||||
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
|
||||
notificationType: "বিজ্ঞপ্তির ধরন"
|
||||
edit: "সম্পাদনা"
|
||||
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
|
||||
emailServer: "ইমেইল সার্ভার"
|
||||
enableEmail: "ইমেইল বিতরণ চালু করুন"
|
||||
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
|
||||
@@ -1354,3 +1353,7 @@ _deck:
|
||||
channel: "চ্যানেলগুলি"
|
||||
mentions: "উল্লেখসমূহ"
|
||||
direct: "ডাইরেক্ট নোটগুলি"
|
||||
_webhookSettings:
|
||||
name: "নাম"
|
||||
active: "চালু"
|
||||
|
||||
|
@@ -460,3 +460,4 @@ _deck:
|
||||
list: "Llistes"
|
||||
mentions: "Mencions"
|
||||
direct: "Publicacions directes"
|
||||
|
||||
|
@@ -776,3 +776,7 @@ _deck:
|
||||
list: "Seznamy"
|
||||
channel: "Kanály"
|
||||
mentions: "Zmínění"
|
||||
_webhookSettings:
|
||||
name: "Jméno"
|
||||
active: "Zapnuto"
|
||||
|
||||
|
@@ -1,2 +1,3 @@
|
||||
---
|
||||
_lang_: "Dansk"
|
||||
|
||||
|
@@ -460,7 +460,7 @@ aboutX: "Über {x}"
|
||||
emojiStyle: "Emoji-Stil"
|
||||
native: "Nativ"
|
||||
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"
|
||||
signinHistory: "Anmeldungsverlauf"
|
||||
enableAdvancedMfm: "Erweitertes MFM aktivieren"
|
||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Deaktiviere dies, falls du für API-Verbindungen kein
|
||||
objectStorageUseProxy: "Über Proxy verbinden"
|
||||
objectStorageUseProxyDesc: "Deaktiviere dies, falls du für Verbindungen zur API keinen Proxy verwenden wirst"
|
||||
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"
|
||||
deleteAll: "Alle löschen"
|
||||
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."
|
||||
notificationType: "Art der Benachrichtigung"
|
||||
edit: "Bearbeiten"
|
||||
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
|
||||
emailServer: "Email-Server"
|
||||
enableEmail: "Email-Versand aktivieren"
|
||||
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"
|
||||
exploreOtherServers: "Eine andere Instanz finden"
|
||||
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."
|
||||
emailNotSupported: "Diese Instanz unterstützt das Versenden von Emails nicht"
|
||||
postToTheChannel: "In Kanal senden"
|
||||
@@ -981,6 +983,11 @@ 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:
|
||||
earnedAt: "Freigeschaltet am"
|
||||
_types:
|
||||
@@ -1277,6 +1284,8 @@ _role:
|
||||
followersMoreThanOrEq: "Hat X oder mehr Follower"
|
||||
followingLessThanOrEq: "Folgt X oder weniger Benutzern"
|
||||
followingMoreThanOrEq: "Folgt X oder mehr Benutzern"
|
||||
notesLessThanOrEq: "Beitragszahl ist kleiner-gleich"
|
||||
notesMoreThanOrEq: "Beitragszahl ist größer-gleich"
|
||||
and: "UND-Bedingung"
|
||||
or: "ODER-Bedingung"
|
||||
not: "NICHT-Bedingung"
|
||||
@@ -1875,3 +1884,18 @@ _disabledTimeline:
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "Absteigende Dateigrößen"
|
||||
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: "Αντένες"
|
||||
list: "Λίστα"
|
||||
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"
|
||||
objectStorageUseProxyDesc: "Turn this off if you are not going to use a Proxy for API connections"
|
||||
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"
|
||||
deleteAll: "Delete all"
|
||||
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."
|
||||
notificationType: "Notification type"
|
||||
edit: "Edit"
|
||||
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
|
||||
emailServer: "Email server"
|
||||
enableEmail: "Enable email distribution"
|
||||
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"
|
||||
exploreOtherServers: "Look for another instance"
|
||||
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."
|
||||
emailNotSupported: "This instance does not support sending emails"
|
||||
postToTheChannel: "Post to channel"
|
||||
@@ -981,6 +983,11 @@ 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"
|
||||
_achievements:
|
||||
earnedAt: "Unlocked at"
|
||||
_types:
|
||||
@@ -1277,6 +1284,8 @@ _role:
|
||||
followersMoreThanOrEq: "Has X or more followers"
|
||||
followingLessThanOrEq: "Follows X or fewer 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"
|
||||
or: "OR-Condition"
|
||||
not: "NOT-Condition"
|
||||
@@ -1875,3 +1884,18 @@ _disabledTimeline:
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "Descending Filesizes"
|
||||
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í"
|
||||
notificationType: "Tipo de notificación"
|
||||
edit: "Editar"
|
||||
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
|
||||
emailServer: "Servidor de correo"
|
||||
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"
|
||||
@@ -961,7 +960,6 @@ copyErrorInfo: "Copiar detalles del error"
|
||||
joinThisServer: "Registrarse en esta instancia"
|
||||
exploreOtherServers: "Buscar otra instancia"
|
||||
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."
|
||||
emailNotSupported: "Esta instancia no soporta el envío de correo electrónico"
|
||||
postToTheChannel: "Publicar en el canal"
|
||||
@@ -1875,3 +1873,7 @@ _disabledTimeline:
|
||||
_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."
|
||||
notificationType: "Type de notifications"
|
||||
edit: "Editer"
|
||||
useStarForReactionFallback: "Utiliser ★ comme alternative si l’émoji de réaction est inconnu"
|
||||
emailServer: "Serveur mail"
|
||||
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."
|
||||
@@ -1468,3 +1467,7 @@ _deck:
|
||||
channel: "Canaux"
|
||||
mentions: "Mentions"
|
||||
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."
|
||||
notificationType: "Jenis pemberitahuan"
|
||||
edit: "Sunting"
|
||||
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
|
||||
emailServer: "Peladen surel"
|
||||
enableEmail: "Nyalakan distribusi surel"
|
||||
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
|
||||
@@ -1804,3 +1803,7 @@ _deck:
|
||||
channel: "Kanal"
|
||||
mentions: "Sebutan"
|
||||
direct: "Langsung"
|
||||
_webhookSettings:
|
||||
name: "Nama"
|
||||
active: "Aktif"
|
||||
|
||||
|
@@ -170,7 +170,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
|
||||
host: "Server remoto"
|
||||
selectUser: "Seleziona profilo"
|
||||
recipient: "Destinatario"
|
||||
annotation: "Descrizione"
|
||||
annotation: "Annotazione"
|
||||
federation: "Federazione"
|
||||
instances: "Istanza"
|
||||
registeredAt: "Registrato presso"
|
||||
@@ -212,7 +212,7 @@ intro: "L'installazione di Misskey è terminata! Si prega di creare il profilo a
|
||||
done: "Fine"
|
||||
processing: "In elaborazione"
|
||||
preview: "Anteprima"
|
||||
default: "Medio"
|
||||
default: "Predefinito"
|
||||
defaultValueIs: "Predefinito: {value}"
|
||||
noCustomEmojis: "Nessun emoji"
|
||||
noJobs: "Nessun lavoro"
|
||||
@@ -237,14 +237,14 @@ more: "Di più!"
|
||||
featured: "Tendenze"
|
||||
usernameOrUserId: "Nome utente o ID utente"
|
||||
noSuchUser: "Nessun utente trovato"
|
||||
lookup: "Cerca"
|
||||
lookup: "Ricerca remota"
|
||||
announcements: "Annunci"
|
||||
imageUrl: "URL dell'immagine"
|
||||
remove: "Elimina"
|
||||
removed: "Eliminato con successo"
|
||||
removeAreYouSure: "Vuoi davvero eliminare \"{x}\"?"
|
||||
deleteAreYouSure: "Eliminare \"{x}\"?"
|
||||
resetAreYouSure: "Reimposta"
|
||||
resetAreYouSure: "Ripristinare?"
|
||||
saved: "Salvato"
|
||||
messaging: "Messaggi"
|
||||
upload: "Carica"
|
||||
@@ -409,7 +409,7 @@ lastUsedAt: "Uso più recente: {t}"
|
||||
unregister: "Annulla l'iscrizione"
|
||||
passwordLessLogin: "Accedi senza password"
|
||||
passwordLessLoginDescription: "Accedi senza password, usando la chiave di sicurezza"
|
||||
resetPassword: "Reimposta password"
|
||||
resetPassword: "Ripristina la password"
|
||||
newPasswordIs: "La tua nuova password è「{password}」"
|
||||
reduceUiAnimation: "Ridurre le animazioni dell'interfaccia"
|
||||
share: "Condividi"
|
||||
@@ -565,8 +565,8 @@ enableInfiniteScroll: "Abilita scorrimento infinito"
|
||||
visibility: "Visibilità"
|
||||
poll: "Sondaggio"
|
||||
useCw: "Nascondere media"
|
||||
enablePlayer: "Apri in lettore video"
|
||||
disablePlayer: "Chiudi il lettore"
|
||||
enablePlayer: "Visualizza"
|
||||
disablePlayer: "Chiudi"
|
||||
expandTweet: "Espandi tweet"
|
||||
themeEditor: "Editor di temi"
|
||||
description: "Descrizione"
|
||||
@@ -594,10 +594,9 @@ tokenRequested: "Autorizza accesso al profilo"
|
||||
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
|
||||
notificationType: "Tipo di notifiche"
|
||||
edit: "Modifica"
|
||||
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
|
||||
emailServer: "Server 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"
|
||||
emailAddress: "Indirizzo di posta elettronica"
|
||||
smtpConfig: "Impostazioni del server SMTP"
|
||||
@@ -961,16 +960,15 @@ copyErrorInfo: "Copia le informazioni sull'errore"
|
||||
joinThisServer: "Registrati su questa istanza"
|
||||
exploreOtherServers: "Trova altre istanze"
|
||||
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 questo nodo è solo ad invito. Devi inserire un codice d'invito valido. Puoi richiedere un codice all'amministratore."
|
||||
invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione."
|
||||
emailNotSupported: "L'istanza non supporta l'invio di email"
|
||||
postToTheChannel: "Pubblica sul canale"
|
||||
postToTheChannel: "Pubblica nel canale"
|
||||
cannotBeChangedLater: "Non sarà più modificabile"
|
||||
reactionAcceptance: "Accettazione reazioni"
|
||||
likeOnly: "Solo i Like"
|
||||
likeOnlyForRemote: "Solo Like remoti"
|
||||
rolesAssignedToMe: "I miei ruoli"
|
||||
resetPasswordConfirm: "Vuoi reimpostare la password?"
|
||||
resetPasswordConfirm: "Vuoi davvero ripristinare la password?"
|
||||
sensitiveWords: "Parole sensibili"
|
||||
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."
|
||||
@@ -978,6 +976,14 @@ license: "Licenza"
|
||||
unfavoriteConfirm: "Vuoi davvero rimuovere la preferenza?"
|
||||
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:
|
||||
earnedAt: "Data di conseguimento"
|
||||
_types:
|
||||
@@ -1274,6 +1280,8 @@ _role:
|
||||
followersMoreThanOrEq: "Ha più di N follower"
|
||||
followingLessThanOrEq: "Segue N profili o meno"
|
||||
followingMoreThanOrEq: "Segue N profili o più"
|
||||
notesLessThanOrEq: "Conteggio Note inferiore o uguale a"
|
||||
notesMoreThanOrEq: "Conteggio Note maggiore o uguale a"
|
||||
and: "E"
|
||||
or: "O"
|
||||
not: "NON"
|
||||
@@ -1312,8 +1320,8 @@ _ad:
|
||||
hide: "Nascondi"
|
||||
_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."
|
||||
ifNoEmail: "Se nessun indirizzo e-mail è stato registrato, si prega di contattare l'amministratore·trice 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."
|
||||
ifNoEmail: "Se il tuo indirizzo email non risulta registrato, contatta l'amministrazione dell'istanza."
|
||||
contactAdmin: "Poiché questa istanza non permette di impostare l'indirizzo mail, contatta l'amministrazione per ripristinare la password.\n"
|
||||
_gallery:
|
||||
my: "Le mie pubblicazioni"
|
||||
liked: "Pubblicazioni che mi piacciono"
|
||||
@@ -1868,7 +1876,22 @@ _dialog:
|
||||
charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({corrente})"
|
||||
_disabledTimeline:
|
||||
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: "絵文字のスタイル"
|
||||
native: "ネイティブ"
|
||||
disableDrawer: "メニューをドロワーで表示しない"
|
||||
showNoteActionsOnlyHover: "ノートの操作部をホバー時のみ表示する"
|
||||
showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
|
||||
noHistory: "履歴はありません"
|
||||
signinHistory: "ログイン履歴"
|
||||
enableAdvancedMfm: "高度なMFMを有効にする"
|
||||
@@ -500,12 +500,13 @@ objectStoragePrefixDesc: "このprefixのディレクトリ下に格納されま
|
||||
objectStorageEndpoint: "Endpoint"
|
||||
objectStorageEndpointDesc: "S3の場合は空、それ以外の場合は各サービスのendpointを指定してください。'<host>'または'<host>:<port>'のように指定します。"
|
||||
objectStorageRegion: "Region"
|
||||
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は、空または'us-east-1'にしてください。"
|
||||
objectStorageRegionDesc: "'xx-east-1'のようなregionを指定してください。使用サービスにregionの概念がない場合は'us-east-1'にしてください。AWS設定ファイルまたは環境変数を参照する場合は空にしてください。"
|
||||
objectStorageUseSSL: "SSLを使用する"
|
||||
objectStorageUseSSLDesc: "API接続にhttpsを使用しない場合はオフにしてください"
|
||||
objectStorageUseProxy: "Proxyを利用する"
|
||||
objectStorageUseProxyDesc: "API接続にproxyを利用しない場合はオフにしてください"
|
||||
objectStorageSetPublicRead: "アップロード時に'public-read'を設定する"
|
||||
s3ForcePathStyleDesc: "s3ForcePathStyleを有効にすると、バケット名をURLのホスト名ではなくパスの一部として指定することを強制します。セルフホストされたMinioなどの使用時に有効にする必要がある場合があります。"
|
||||
serverLogs: "サーバーログ"
|
||||
deleteAll: "全て削除"
|
||||
showFixedPostForm: "タイムライン上部に投稿フォームを表示する"
|
||||
@@ -594,7 +595,6 @@ tokenRequested: "アカウントへのアクセス許可"
|
||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
||||
emailServer: "メールサーバー"
|
||||
enableEmail: "メール配信機能を有効化する"
|
||||
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
||||
@@ -961,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー"
|
||||
joinThisServer: "このサーバーに登録する"
|
||||
exploreOtherServers: "他のサーバーを探す"
|
||||
letsLookAtTimeline: "タイムラインを見てみる"
|
||||
disableFederationWarn: "連合が無効になっています。無効にしても投稿が非公開にはなりません。ほとんどの場合、このオプションを有効にする必要はありません。"
|
||||
disableFederationConfirm: "連合なしにしますか?"
|
||||
disableFederationConfirmWarn: "連合なしにしても投稿は非公開になりません。ほとんどの場合、連合なしにする必要はありません。"
|
||||
disableFederationOk: "連合なしにする"
|
||||
invitationRequiredToRegister: "現在このサーバーは招待制です。招待コードをお持ちの方のみ登録できます。"
|
||||
emailNotSupported: "このサーバーではメール配信はサポートされていません"
|
||||
postToTheChannel: "チャンネルに投稿"
|
||||
@@ -981,6 +983,11 @@ drivecleaner: "ドライブクリーナー"
|
||||
retryAllQueuesNow: "すべてのキューを今すぐ再試行"
|
||||
retryAllQueuesConfirmTitle: "今すぐ再試行しますか?"
|
||||
retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。"
|
||||
enableChartsForRemoteUser: "リモートユーザーのチャートを生成"
|
||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを生成"
|
||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||
largeNoteReactions: "ノートのリアクションを大きく表示"
|
||||
noteIdOrUrl: "ノートIDまたはURL"
|
||||
|
||||
_achievements:
|
||||
earnedAt: "獲得日時"
|
||||
@@ -1279,6 +1286,8 @@ _role:
|
||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||
followingLessThanOrEq: "フォロー数が~以下"
|
||||
followingMoreThanOrEq: "フォロー数が~以上"
|
||||
notesLessThanOrEq: "投稿数が~以下"
|
||||
notesMoreThanOrEq: "投稿数が~以上"
|
||||
and: "~かつ~"
|
||||
or: "~または~"
|
||||
not: "~ではない"
|
||||
@@ -1929,4 +1938,20 @@ _disabledTimeline:
|
||||
|
||||
_drivecleaner:
|
||||
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: "やめとく"
|
||||
enterUsername: "ユーザー名を入れてや"
|
||||
renotedBy: "{user}がRenoteしたで"
|
||||
noNotes: "ノートなんてあらへんで"
|
||||
noNotifications: "通知なんてあらへんで"
|
||||
noNotes: "ノートはあらへん"
|
||||
noNotifications: "通知はあらへん"
|
||||
instance: "サーバー"
|
||||
settings: "設定"
|
||||
basicSettings: "基本設定"
|
||||
@@ -25,13 +25,13 @@ otherSettings: "ほかの設定"
|
||||
openInWindow: "ウィンドウで開くで"
|
||||
profile: "プロフィール"
|
||||
timeline: "タイムライン"
|
||||
noAccountDescription: "自己紹介食ってもた"
|
||||
noAccountDescription: "自己紹介はあらへん"
|
||||
login: "ログイン"
|
||||
loggingIn: "ログインしよるで"
|
||||
logout: "ログアウト"
|
||||
signup: "新規登録"
|
||||
uploading: "アップロードしとるで"
|
||||
save: "保存"
|
||||
save: "とっとく"
|
||||
users: "ユーザー"
|
||||
addUser: "ユーザーを追加や"
|
||||
favorite: "お気に入り"
|
||||
@@ -81,9 +81,9 @@ followsYou: "フォローされとるで"
|
||||
createList: "リスト作る"
|
||||
manageLists: "リストの管理"
|
||||
error: "エラー"
|
||||
somethingHappened: "なんかアカンことが起こったで"
|
||||
somethingHappened: "なんかあかんわ"
|
||||
retry: "もっぺんやる?"
|
||||
pageLoadError: "ページの読み込みに失敗してもうたわ…"
|
||||
pageLoadError: "ページが読み込めんかったわ。"
|
||||
pageLoadErrorDescription: "これは普通ならネットワークかブラウザキャッシュが悪さしてるんよ。キャッシュをほかすか、もうちょっとだけ待ってくれへん?"
|
||||
serverIsDead: "サーバーからの応答がないで。もうちょい待ってから試してみてな。"
|
||||
youShouldUpgradeClient: "このページを表示するには、リロードして新しいバージョンのクライアントを使ってなー。"
|
||||
@@ -108,8 +108,8 @@ inChannelQuote: "チャンネル内引用"
|
||||
pinnedNote: "ピン留めされとるノート"
|
||||
pinned: "ピン留めしとく"
|
||||
you: "あんた"
|
||||
clickToShow: "押したら見えるで"
|
||||
sensitive: "ちょっとアカンやつやで"
|
||||
clickToShow: "押したら出ら"
|
||||
sensitive: "気いつけて見いや"
|
||||
add: "増やす"
|
||||
reaction: "リアクション"
|
||||
reactions: "リアクション"
|
||||
@@ -122,8 +122,8 @@ unmarkAsSensitive: "そこまでアカンことないやろ"
|
||||
enterFileName: "ファイル名を入れてや"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュートやめたる"
|
||||
renoteMute: "リノートは見いひん"
|
||||
renoteUnmute: "リノートもやっぱ見るわ"
|
||||
renoteMute: "Renoteは見いひん"
|
||||
renoteUnmute: "Renoteもやっぱ見るわ"
|
||||
block: "ブロック"
|
||||
unblock: "ブロックやめたる"
|
||||
suspend: "凍結"
|
||||
@@ -141,14 +141,14 @@ editWidgetsExit: "編集終ったで"
|
||||
customEmojis: "カスタム絵文字"
|
||||
emoji: "絵文字"
|
||||
emojis: "絵文字"
|
||||
emojiName: "絵文字名"
|
||||
emojiName: "絵文字はんの名前"
|
||||
emojiUrl: "絵文字画像URL"
|
||||
addEmoji: "絵文字を追加"
|
||||
settingGuide: "ええ感じの設定"
|
||||
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||
cacheRemoteFilesDescription: "この設定を切っとくと、リモートファイルをキャッシュせず直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルが作られんくなるから通信量が増えるで。"
|
||||
cacheRemoteFilesDescription: "この設定を切っとったら、リモートファイルをキャッシュせんと直リンクするようになるで。サーバーの容量は節約できるけど、サムネイルを作らんなるから通信量が増えるで。"
|
||||
flagAsBot: "Botにするで"
|
||||
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖するのを避けるために開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
|
||||
flagAsBotDescription: "もしこのアカウントをプログラム使うて運用するんやったら、このフラグをオンにしてや。オンにすれば、反応がバーッて連鎖せんように開発者が使うたり、Misskeyのシステム上での扱いがBotに合ったもんになるからな。"
|
||||
flagAsCat: "Catやで"
|
||||
flagAsCatDescription: "ワレ、猫ちゃんならこのフラグをつけてみ?"
|
||||
flagShowTimelineReplies: "タイムラインにノートへの返信を表示するで"
|
||||
@@ -194,10 +194,10 @@ network: "ネットワーク"
|
||||
disk: "ディスク"
|
||||
instanceInfo: "サーバー情報"
|
||||
statistics: "統計"
|
||||
clearQueue: "キューにさいなら"
|
||||
clearQueueConfirmTitle: "キューをクリアしまっか?"
|
||||
clearQueueConfirmText: "未配達の投稿は配送されなくなるで。ふつうこの操作を行う必要は無いんやけどな。"
|
||||
clearCachedFiles: "キャッシュにさいなら"
|
||||
clearQueue: "キューをほかす"
|
||||
clearQueueConfirmTitle: "キューをほかしとこか?"
|
||||
clearQueueConfirmText: "未配達の投稿は配送されんなるで。ふつうこの操作を行う必要は無いんやけどな。"
|
||||
clearCachedFiles: "キャッシュをほかす"
|
||||
clearCachedFilesConfirm: "キャッシュされとるリモートファイルをみんなほかしてええか?"
|
||||
blockedInstances: "ブロックしたサーバー"
|
||||
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定してな。ブロックされてもうたサーバーとはもう金輪際やり取りできひんくなるで。ついでにそのサブドメインもブロックするで。"
|
||||
@@ -206,7 +206,7 @@ mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
noUsers: "ユーザーはおらん"
|
||||
editProfile: "プロフィールをいじる"
|
||||
noteDeleteConfirm: "このノートを削除しまっか?"
|
||||
noteDeleteConfirm: "このノートをほかしてええか?"
|
||||
pinLimitExceeded: "これ以上ピン留めできひん"
|
||||
intro: "Misskeyのインストールが完了したで!管理者アカウントを作ってや。"
|
||||
done: "でけた"
|
||||
@@ -226,9 +226,9 @@ notResponding: "応答してへんで"
|
||||
instanceFollowing: "サーバーのフォロー"
|
||||
instanceFollowers: "サーバーのフォロワー\n"
|
||||
instanceUsers: "サーバーのユーザー"
|
||||
changePassword: "パスワード変える"
|
||||
changePassword: "パスワードをいじる"
|
||||
security: "セキュリティ"
|
||||
retypedNotMatch: "入れたやつ同じになってないで。"
|
||||
retypedNotMatch: "入れたやつ合うてへんわ。"
|
||||
currentPassword: "今のパスワード"
|
||||
newPassword: "次のパスワード"
|
||||
newPasswordRetype: "今度のパスワード(もっぺん入れて)"
|
||||
@@ -258,7 +258,7 @@ uploadFromUrlRequested: "アップロードしたい言うといたで"
|
||||
uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間かかるかもしれへんわ。"
|
||||
explore: "みつける"
|
||||
messageRead: "もう読んだ"
|
||||
noMoreHistory: "これより過去の履歴はあらへんで"
|
||||
noMoreHistory: "これより昔のんはあらへんで"
|
||||
startMessaging: "チャットやるで"
|
||||
nUsersRead: "{n}人が読んでもうた"
|
||||
agreeTo: "{0}に同意したで"
|
||||
@@ -294,14 +294,14 @@ createFolder: "フォルダー作る"
|
||||
renameFolder: "フォルダー名を変える"
|
||||
deleteFolder: "フォルダーをほかす"
|
||||
addFile: "ファイルを追加"
|
||||
emptyDrive: "ドライブにはなんも残っとらん"
|
||||
emptyDrive: "ドライブは空っぽや"
|
||||
emptyFolder: "このフォルダーは空や"
|
||||
unableToDelete: "消そうおもってんけどな、あかんかったわ"
|
||||
unableToDelete: "消せんかったわ"
|
||||
inputNewFileName: "今度のファイル名は何にするん?"
|
||||
inputNewDescription: "新しいキャプションを入れてや"
|
||||
inputNewFolderName: "今度のフォルダ名は何にするん?"
|
||||
circularReferenceFolder: "移動先のフォルダーは、移動するフォルダーのサブフォルダーや。"
|
||||
hasChildFilesOrFolders: "このフォルダ、まだなんか入っとるから消されへん"
|
||||
hasChildFilesOrFolders: "このフォルダは空っぽちゃうから消されへん"
|
||||
copyUrl: "URLをコピー"
|
||||
rename: "名前を変えるで"
|
||||
avatar: "アイコン"
|
||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "API接続にhttpsを使わん場合はオフにする
|
||||
objectStorageUseProxy: "Proxyを使う"
|
||||
objectStorageUseProxyDesc: "API接続にproxy使わんのやったら切ってくれへん?"
|
||||
objectStorageSetPublicRead: "アップロードした時に'public-read'を設定してや"
|
||||
s3ForcePathStyleDesc: "s3ForcePathStyleを使たらバケット名をURLのホスト名やなくてパスの一部として必ず指定させるようになるで。セルフホストされたMinioとかを使うてるんやったら有効にせなあかん場合があるで。"
|
||||
serverLogs: "サーバーログ"
|
||||
deleteAll: "全部ほかす"
|
||||
showFixedPostForm: "タイムラインの上の方で投稿できるようにやってくれへん?"
|
||||
@@ -594,7 +595,6 @@ tokenRequested: "アカウントへのアクセス許してやったらどうや
|
||||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
|
||||
emailServer: "メールサーバー"
|
||||
enableEmail: "メール配信を受け取る"
|
||||
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
||||
@@ -644,7 +644,7 @@ reporter: "通報者"
|
||||
reporteeOrigin: "通報先"
|
||||
reporterOrigin: "通報元"
|
||||
forwardReport: "リモートサーバーに通報を転送するで"
|
||||
forwardReportIsAnonymous: "リモートインスタンスからはあんたの情報は見れへんくって、匿名のシステムアカウントとして表示されるで。"
|
||||
forwardReportIsAnonymous: "リモートサーバーからはあんたの情報は見えんなって、匿名のシステムアカウントとして表示されるで。"
|
||||
send: "送信"
|
||||
abuseMarkAsResolved: "対応したで"
|
||||
openInNewTab: "新しいタブで開く"
|
||||
@@ -740,7 +740,7 @@ capacity: "容量"
|
||||
inUse: "使用中"
|
||||
editCode: "コードを編集"
|
||||
apply: "適用"
|
||||
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
|
||||
receiveAnnouncementFromInstance: "サーバーからのお知らせを受け取る"
|
||||
emailNotification: "メール通知"
|
||||
publish: "公開"
|
||||
inChannelSearch: "チャンネル内検索"
|
||||
@@ -768,7 +768,7 @@ active: "アクティブ"
|
||||
offline: "オフライン"
|
||||
notRecommended: "あんま推奨しやんで"
|
||||
botProtection: "Botプロテクション"
|
||||
instanceBlocking: "インスタンスブロック"
|
||||
instanceBlocking: "サーバーブロック"
|
||||
selectAccount: "アカウントを選んでなー"
|
||||
switchAccount: "アカウントを変えるで"
|
||||
enabled: "有効"
|
||||
@@ -852,8 +852,8 @@ themeColor: "テーマカラー"
|
||||
size: "大きさ"
|
||||
numberOfColumn: "列の数"
|
||||
searchByGoogle: "探す"
|
||||
instanceDefaultLightTheme: "インスタンスの最初の明るいテーマ"
|
||||
instanceDefaultDarkTheme: "インスタンスの最初の暗いテーマ"
|
||||
instanceDefaultLightTheme: "サーバーおすすめの明るいテーマ"
|
||||
instanceDefaultDarkTheme: "サーバーおすすめのの暗いテーマ"
|
||||
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入するで。"
|
||||
mutePeriod: "ミュートする期間"
|
||||
period: "期限"
|
||||
@@ -867,7 +867,7 @@ reflectMayTakeTime: "反映されるまで時間がかかることがあるで"
|
||||
failedToFetchAccountInformation: "アカウントの取得に失敗したみたいや…"
|
||||
rateLimitExceeded: "レート制限が超えたみたいやで"
|
||||
cropImage: "画像のクロップ"
|
||||
cropImageAsk: "画像をクロップしたってええか?"
|
||||
cropImageAsk: "画像をクロップしてもええか?"
|
||||
cropYes: "切り抜いたる"
|
||||
cropNo: "切り抜かへん"
|
||||
file: "ファイル"
|
||||
@@ -902,11 +902,11 @@ sensitiveMediaDetection: "センシティブなメディアの検出"
|
||||
localOnly: "ローカルのみ"
|
||||
remoteOnly: "リモートのみ"
|
||||
failedToUpload: "アップロードに失敗してもうたわ…"
|
||||
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたでアップロードできまへん。"
|
||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いでアップロードできまへん。"
|
||||
cannotUploadBecauseInappropriate: "不適切な内容を含むかもしれへんって判定されたからアップロードできへんわ。"
|
||||
cannotUploadBecauseNoFreeSpace: "ドライブの空き容量が無いからアップロードできへんわ。"
|
||||
beta: "ベータ"
|
||||
enableAutoSensitive: "自動NSFW判定"
|
||||
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、インスタンスによっては自動で設定されることがあるで。"
|
||||
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
|
||||
activeEmailValidationDescription: "ユーザーのメールアドレスのバリデーションを、捨てアドかどうかや実際に通信可能かどうかとかを判定して積極的に行うで。オフにすると単に文字列として正しいかどうかだけチェックするで。"
|
||||
navbar: "ナビゲーションバー"
|
||||
shuffle: "シャッフルするで"
|
||||
@@ -916,7 +916,7 @@ pushNotification: "プッシュ通知"
|
||||
subscribePushNotification: "プッシュ通知をオンにするで"
|
||||
unsubscribePushNotification: "プッシュ通知を止めるで"
|
||||
pushNotificationAlreadySubscribed: "プッシュ通知はオンになってるで"
|
||||
pushNotificationNotSupported: "ブラウザかインスタンスがプッシュ通知に対応してないみたいやで。"
|
||||
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に対応してないみたいやで。"
|
||||
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を消すで"
|
||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」っていう表示が一瞬表示されるようになるで。端末の電池使用量が増える可能性があるで。"
|
||||
windowMaximize: "最大化"
|
||||
@@ -932,7 +932,7 @@ numberOfLikes: "いいね数"
|
||||
show: "表示"
|
||||
neverShow: "今後表示しない"
|
||||
remindMeLater: "また後で"
|
||||
didYouLikeMisskey: "Misskeyを気に入っとっただけましたん?"
|
||||
didYouLikeMisskey: "Misskey気に入ってくれた?"
|
||||
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアやで。これからも開発を続けれるように、寄付したってな~。"
|
||||
roles: "ロール"
|
||||
role: "ロール"
|
||||
@@ -942,7 +942,7 @@ assign: "アサイン"
|
||||
unassign: "アサインを解除"
|
||||
color: "色"
|
||||
manageCustomEmojis: "カスタム絵文字の管理"
|
||||
youCannotCreateAnymore: "これ以上作れなさそうや"
|
||||
youCannotCreateAnymore: "これ以上作れなさそうやわ"
|
||||
cannotPerformTemporary: "一時的に利用できへんで"
|
||||
cannotPerformTemporaryDescription: "操作回数が制限を超えたから一時的に利用できへんくなったで。ちょっと時間置いてからもう一回やってやー。"
|
||||
preset: "プリセット"
|
||||
@@ -961,7 +961,9 @@ copyErrorInfo: "エラー情報をコピー"
|
||||
joinThisServer: "このサーバーに登録するわ"
|
||||
exploreOtherServers: "他のサーバー見てみる"
|
||||
letsLookAtTimeline: "タイムライン見てみーや"
|
||||
disableFederationWarn: "連合が無効になっとるで。無効にしても投稿は非公開ってわけちゃうねん。大体の場合はこのオプションを有効にする必要は別にないで。"
|
||||
disableFederationConfirm: "連合なしにしとくか?"
|
||||
disableFederationConfirmWarn: "連合なしにしても投稿は非公開にはならへんで。大体の場合は連合なしにする必要はないで。"
|
||||
disableFederationOk: "連合なしにしとく"
|
||||
invitationRequiredToRegister: "今このサーバー招待制になってもうてんねん。招待コードを持っとるんやったら登録できるで。"
|
||||
emailNotSupported: "このサーバーはメール配信がサポートされてへんみたいやわ"
|
||||
postToTheChannel: "チャンネルに投稿"
|
||||
@@ -981,6 +983,11 @@ drivecleaner: "ドライブキレイキレイ"
|
||||
retryAllQueuesNow: "キューを全部もっかいやり直す"
|
||||
retryAllQueuesConfirmTitle: "もっかいやってみるか?"
|
||||
retryAllQueuesConfirmText: "一時的にサーバー重なるかもしれへんで。"
|
||||
enableChartsForRemoteUser: "リモートユーザーのチャートを作る"
|
||||
enableChartsForFederatedInstances: "リモートサーバーのチャートを作る"
|
||||
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
|
||||
largeNoteReactions: "ノートのリアクションを大きする"
|
||||
noteIdOrUrl: "ノートIDかURL"
|
||||
_achievements:
|
||||
earnedAt: "貰った日ぃ"
|
||||
_types:
|
||||
@@ -1041,7 +1048,7 @@ _achievements:
|
||||
_login7:
|
||||
title: "ビギナーⅡ"
|
||||
description: "通算7日ログインした"
|
||||
flavor: "慣れてきたんちゃう?"
|
||||
flavor: "慣れてきたんとちゃう?"
|
||||
_login15:
|
||||
title: "ビギナーⅢ"
|
||||
description: "通算15日ログインした"
|
||||
@@ -1145,7 +1152,7 @@ _achievements:
|
||||
_iLoveMisskey:
|
||||
title: "Misskey好きやねん"
|
||||
description: "\"I ❤ #Misskey\"を投稿した"
|
||||
flavor: "Misskeyを使ってくれてありがとうな~ by 開発チーム"
|
||||
flavor: "Misskeyを使ってくれておおきにな~ by 開発チーム"
|
||||
_foundTreasure:
|
||||
title: "なんでも鑑定団"
|
||||
description: "隠されたお宝を発見した"
|
||||
@@ -1171,7 +1178,7 @@ _achievements:
|
||||
description: "ホームタイムラインの流速が20npmを超す"
|
||||
_viewInstanceChart:
|
||||
title: "アナリスト"
|
||||
description: "インスタンスのチャートを表示した"
|
||||
description: "サーバーのチャートを表示した"
|
||||
_outputHelloWorldOnScratchpad:
|
||||
title: "Hello, world!"
|
||||
description: "スクラッチパッドで hello worldを出力した"
|
||||
@@ -1208,7 +1215,7 @@ _achievements:
|
||||
_loggedInOnNewYearsDay:
|
||||
title: "あけましておめでとうございます!"
|
||||
description: "元旦にログインした"
|
||||
flavor: "今年も弊インスタンスをよろしくお願いします"
|
||||
flavor: "今年も弊サーバーをよろしゅう頼みますわ"
|
||||
_cookieClicked:
|
||||
title: "クッキー叩くやつ"
|
||||
description: "クッキー叩いてもうた"
|
||||
@@ -1223,8 +1230,8 @@ _role:
|
||||
name: "ロール名"
|
||||
description: "ロールの説明"
|
||||
permission: "ロールの権限"
|
||||
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はインスタンスの全ての設定を変更できるで。"
|
||||
assignTarget: "アサインターゲット"
|
||||
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関わる操作を行えるで。\n<b>管理者</b>はサーバーの全ての設定を変更できるで。"
|
||||
assignTarget: "アサイン"
|
||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれてるかを手動で管理するで。\n<b>コンディショナル</b>は条件を設定して、それに合うユーザーが自動で含まれるようになるで。"
|
||||
manual: "マニュアル"
|
||||
conditional: "コンディショナル"
|
||||
@@ -1253,7 +1260,7 @@ _role:
|
||||
gtlAvailable: "グローバルタイムラインの閲覧"
|
||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||
canPublicNote: "パブリック投稿の許可"
|
||||
canInvite: "インスタンス招待コードの発行"
|
||||
canInvite: "サーバー招待コードの発行"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
pinMax: "ノートのピン留めの最大数"
|
||||
@@ -1265,7 +1272,7 @@ _role:
|
||||
userListMax: "ユーザーリストの作成可能数"
|
||||
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
|
||||
rateLimitFactor: "レートリミット"
|
||||
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩くなって、大きいほど制限されるで。"
|
||||
descriptionOfRateLimitFactor: "ちっちゃいほど制限が緩なって、大きいほど制限されるで。"
|
||||
canHideAds: "広告を表示させへん"
|
||||
canSearchNotes: "ノート検索を使わすかどうか"
|
||||
_condition:
|
||||
@@ -1277,6 +1284,8 @@ _role:
|
||||
followersMoreThanOrEq: "フォロワー数が~以上"
|
||||
followingLessThanOrEq: "フォロー数が~以下"
|
||||
followingMoreThanOrEq: "フォロー数が~以上"
|
||||
notesLessThanOrEq: "投稿数が~以下しかない"
|
||||
notesMoreThanOrEq: "投稿を~以上しとる"
|
||||
and: "~かつ~"
|
||||
or: "~または~"
|
||||
not: "~ではない"
|
||||
@@ -1316,7 +1325,7 @@ _ad:
|
||||
_forgotPassword:
|
||||
enterEmail: "アカウントに登録したメールアドレスをここに入力してや。そのアドレス宛に、パスワードリセット用のリンクが送られるから待っててな~。"
|
||||
ifNoEmail: "メールアドレスを登録してへんのやったら、管理者まで教えてな~。"
|
||||
contactAdmin: "このインスタンスはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
|
||||
contactAdmin: "このサーバーはメールに対応してへんから、パスワードリセットをしたいときは管理者まで教えてな~。"
|
||||
_gallery:
|
||||
my: "あんたの投稿"
|
||||
liked: "いいねした投稿"
|
||||
@@ -1401,10 +1410,10 @@ _wordMute:
|
||||
hard: "ハード"
|
||||
mutedNotes: "ミュートされたノート"
|
||||
_instanceMute:
|
||||
instanceMuteDescription: "ミュートしたインスタンスのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
||||
instanceMuteDescription: "ミュートしたサーバーのユーザーへの返信を含めて、設定したインスタンスの全てのノートとRenoteをミュートにするで。"
|
||||
instanceMuteDescription2: "改行で区切って設定するんやで"
|
||||
title: "設定したインスタンスのノートを隠すで。"
|
||||
heading: "ミュートするインスタンス"
|
||||
title: "設定したサーバーのノートを隠すで。"
|
||||
heading: "ミュートするサーバー"
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
install: "テーマのインストール"
|
||||
@@ -1626,7 +1635,7 @@ _widgets:
|
||||
digitalClock: "デジタル時計"
|
||||
unixClock: "UNIX時計"
|
||||
federation: "連合"
|
||||
instanceCloud: "インスタンスクラウド"
|
||||
instanceCloud: "サーバークラウド"
|
||||
postForm: "投稿フォーム"
|
||||
slideshow: "スライドショー"
|
||||
button: "ボタン"
|
||||
@@ -1677,7 +1686,7 @@ _visibility:
|
||||
specified: "ダイレクト"
|
||||
specifiedDescription: "選んだユーザーのみに公開するで"
|
||||
disableFederation: "連合なし"
|
||||
disableFederationDescription: "他インスタンスへは送らんとくわ"
|
||||
disableFederationDescription: "他サーバーへは送らんとくわ"
|
||||
_postForm:
|
||||
replyPlaceholder: "このノートに返信..."
|
||||
quotePlaceholder: "このノートを引用..."
|
||||
@@ -1875,3 +1884,18 @@ _disabledTimeline:
|
||||
_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:
|
||||
notifications: "Ilɣuyen"
|
||||
list: "Tibdarin"
|
||||
|
||||
|
@@ -83,3 +83,4 @@ _deck:
|
||||
notifications: "ಅಧಿಸೂಚನೆಗಳು"
|
||||
tl: "ಸಮಯಸಾಲು"
|
||||
mentions: "ಹೆಸರಿಸಿದ"
|
||||
|
||||
|
@@ -592,7 +592,6 @@ tokenRequested: "계정 접근 허용"
|
||||
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
|
||||
notificationType: "알림 유형"
|
||||
edit: "편집"
|
||||
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
|
||||
emailServer: "메일 서버"
|
||||
enableEmail: "이메일 송신 기능 활성화"
|
||||
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
|
||||
@@ -959,7 +958,6 @@ copyErrorInfo: "오류 정보 복사"
|
||||
joinThisServer: "이 서버에 가입"
|
||||
exploreOtherServers: "다른 서버 둘러보기"
|
||||
letsLookAtTimeline: "타임라인 구경하기"
|
||||
disableFederationWarn: "연합이 비활성화됩니다. 비활성화해도 게시물이 비공개가 되지는 않습니다. 대부분의 경우 이 옵션을 활성화할 필요가 없습니다."
|
||||
invitationRequiredToRegister: "현재 이 서버는 비공개입니다. 회원가입을 하시려면 초대 코드가 필요합니다."
|
||||
emailNotSupported: "이 서버에서는 메일 전송을 지원하지 않습니다"
|
||||
postToTheChannel: "채널에 게시하기"
|
||||
@@ -1849,3 +1847,7 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {min}"
|
||||
charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}"
|
||||
_webhookSettings:
|
||||
name: "이름"
|
||||
active: "활성화"
|
||||
|
||||
|
@@ -368,3 +368,4 @@ _deck:
|
||||
list: "ລາຍການ"
|
||||
channel: "ຊ່ອງ"
|
||||
mentions: "ກ່າວເຖິງ"
|
||||
|
||||
|
@@ -483,3 +483,6 @@ _deck:
|
||||
antenna: "Antennes"
|
||||
list: "Lijsten"
|
||||
mentions: "Vermeldingen"
|
||||
_webhookSettings:
|
||||
name: "Naam"
|
||||
|
||||
|
@@ -1,2 +1,3 @@
|
||||
---
|
||||
_lang_: "Norsk Bokmål"
|
||||
|
||||
|
@@ -129,6 +129,7 @@ unblockConfirm: "Czy na pewno chcesz odblokować to konto?"
|
||||
suspendConfirm: "Czy na pewno chcesz zawiesić to konto?"
|
||||
unsuspendConfirm: "Czy na pewno chcesz cofnąć zawieszenie tego konta?"
|
||||
selectList: "Wybierz listę"
|
||||
selectChannel: "Wybierz kanał"
|
||||
selectAntenna: "Wybierz Antennę"
|
||||
selectWidget: "Wybierz widżet"
|
||||
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"
|
||||
autoAcceptFollowed: "Automatycznie przyjmuj prośby o możliwość obserwacji od użytkowników, których obserwujesz"
|
||||
addAccount: "Dodaj konto"
|
||||
reloadAccountsList: "Odśwież listę kont"
|
||||
loginFailed: "Nie udało się zalogować"
|
||||
showOnRemote: "Zobacz na zdalnej instancji"
|
||||
general: "Ogólne"
|
||||
@@ -159,6 +161,7 @@ searchWith: "Szukaj: {q}"
|
||||
youHaveNoLists: "Nie masz żadnej listy"
|
||||
followConfirm: "Czy na pewno chcesz zaobserwować {name}?"
|
||||
proxyAccount: "Konto proxy"
|
||||
proxyAccountDescription: "Opis konta pełnomocniczego"
|
||||
host: "Host"
|
||||
selectUser: "Wybierz użytkownika"
|
||||
recipient: "Odbiorca"
|
||||
@@ -253,6 +256,7 @@ noMoreHistory: "Nie ma dalszej historii"
|
||||
startMessaging: "Rozpocznij czat"
|
||||
nUsersRead: "przeczytano przez {n}"
|
||||
agreeTo: "Wyrażam zgodę na {0}"
|
||||
agreeBelow: "Zaakceptuj poniżej"
|
||||
tos: "Regulamin"
|
||||
start: "Rozpocznij"
|
||||
home: "Strona główna"
|
||||
@@ -385,13 +389,19 @@ about: "Informacje"
|
||||
aboutMisskey: "O Misskey"
|
||||
administrator: "Admin"
|
||||
token: "Token"
|
||||
2fa: "Klucz 2FA "
|
||||
totp: "Klucz aplikacji uwierzytelniającej (totp)"
|
||||
totpDescription: "Opis klucza czasowego"
|
||||
moderator: "Moderator"
|
||||
moderation: "Moderacja"
|
||||
nUsersMentioned: "{n} wspomnianych użytkowników"
|
||||
securityKeyAndPasskey: "Klucz bezpieczeństwa i klucze Passkey"
|
||||
securityKey: "Klucz bezpieczeństwa"
|
||||
lastUsed: "Ostatnio używane"
|
||||
lastUsedAt: "Ostatnio używane w"
|
||||
unregister: "Cofnij rejestrację"
|
||||
passwordLessLogin: "Skonfiguruj logowanie bez użycia hasła"
|
||||
passwordLessLoginDescription: "Opis logowania bez użycia hasła"
|
||||
resetPassword: "Zresetuj hasło"
|
||||
newPasswordIs: "Nowe hasło to „{password}”"
|
||||
reduceUiAnimation: "Ogranicz animacje w UI"
|
||||
@@ -518,11 +528,16 @@ disablePagesScript: "Wyłącz AiScript na Stronach"
|
||||
updateRemoteUser: "Aktualizuj zdalne dane o użytkowniku"
|
||||
deleteAllFiles: "Usuń 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."
|
||||
userSuspended: "To konto zostało zawieszone."
|
||||
userSilenced: "Ten użytkownik został wyciszony."
|
||||
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."
|
||||
tokenRevoked: "Token odrzucony"
|
||||
tokenRevokedDescription: "Opis odrzuconego tokena"
|
||||
accountDeleted: "Konto usunięte"
|
||||
accountDeletedDescription: "Opis konta usuniętego"
|
||||
menu: "Menu"
|
||||
divider: "Rozdzielacz"
|
||||
addItem: "Dodaj element"
|
||||
@@ -548,7 +563,9 @@ author: "Autor"
|
||||
leaveConfirm: "Są niezapisane zmiany. Czy chcesz je odrzucić?"
|
||||
manage: "Zarządzanie"
|
||||
plugins: "Wtyczki"
|
||||
preferencesBackups: "Kopia zapasowa ustawień"
|
||||
deck: "Tablica"
|
||||
undeck: "oddkouj"
|
||||
useBlurEffectForModal: "Używaj efektu rozmycia w modalach"
|
||||
useFullReactionPicker: "Używaj pełnowymiarowego wybornika reakcji"
|
||||
width: "Szerokość"
|
||||
@@ -564,7 +581,6 @@ tokenRequested: "Przydziel dostęp do konta"
|
||||
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
|
||||
notificationType: "Rodzaj powiadomień"
|
||||
edit: "Edytuj"
|
||||
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
|
||||
emailServer: "Serwer poczty 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"
|
||||
@@ -816,6 +832,8 @@ tenMinutes: "10 minut"
|
||||
oneHour: "1 godzina"
|
||||
oneDay: "1 dzień"
|
||||
oneWeek: "1 tydzień"
|
||||
oneMonth: "jeden miesiąc"
|
||||
failedToFetchAccountInformation: "Nie udało się uzyskać informacji o koncie"
|
||||
file: "Pliki"
|
||||
recommended: "Zalecane"
|
||||
check: "Zweryfikuj"
|
||||
@@ -1358,3 +1376,7 @@ _deck:
|
||||
channel: "Kanały"
|
||||
mentions: "Wspomnienia"
|
||||
direct: "Bezpośredni"
|
||||
_webhookSettings:
|
||||
name: "Nazwa"
|
||||
active: "Właczono"
|
||||
|
||||
|
@@ -555,3 +555,6 @@ _deck:
|
||||
list: "Listas"
|
||||
mentions: "Menções"
|
||||
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."
|
||||
notificationType: "Tipul notificării"
|
||||
edit: "Editează"
|
||||
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
|
||||
emailServer: "Server email"
|
||||
enableEmail: "Activează distribuția de emailuri"
|
||||
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
|
||||
@@ -702,3 +701,6 @@ _deck:
|
||||
list: "Liste"
|
||||
channel: "Canale"
|
||||
mentions: "Mențiuni"
|
||||
_webhookSettings:
|
||||
name: "Nume"
|
||||
|
||||
|
@@ -585,7 +585,6 @@ tokenRequested: "Открыть доступ к учётной записи"
|
||||
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
|
||||
notificationType: "Тип уведомления"
|
||||
edit: "Изменить"
|
||||
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
|
||||
emailServer: "Сервер электронной почты"
|
||||
enableEmail: "Включить обмен электронной почтой"
|
||||
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
|
||||
@@ -951,7 +950,6 @@ copyErrorInfo: "Скопировать код ошибки"
|
||||
joinThisServer: "Присоединяйтесь к этому серверу"
|
||||
exploreOtherServers: "Искать другие сервера"
|
||||
letsLookAtTimeline: "Давайте посмотрим на ленту"
|
||||
disableFederationWarn: "Объединение отключено. Если вы отключите это, сообщение не будет приватным. В большинстве случаев вам не нужно включать эту опцию."
|
||||
_achievements:
|
||||
earnedAt: "Разблокировано в"
|
||||
_types:
|
||||
@@ -1837,3 +1835,7 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "Превышено максимальное количество символов! У вас {current} / из {max}"
|
||||
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."
|
||||
notificationType: "Typ oznámenia"
|
||||
edit: "Upraviť"
|
||||
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
|
||||
emailServer: "Email server"
|
||||
enableEmail: "Zapnúť email"
|
||||
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
|
||||
@@ -1475,3 +1474,7 @@ _deck:
|
||||
channel: "Kanály"
|
||||
mentions: "Zmienky"
|
||||
direct: "Priame poznámky"
|
||||
_webhookSettings:
|
||||
name: "Názov"
|
||||
active: "Zapnuté"
|
||||
|
||||
|
@@ -442,3 +442,6 @@ _deck:
|
||||
antenna: "Antenner"
|
||||
list: "Listor"
|
||||
mentions: "Omnämningar"
|
||||
_webhookSettings:
|
||||
active: "Aktiverad"
|
||||
|
||||
|
@@ -544,6 +544,8 @@ userSuspended: "ผู้ใช้รายนี้ถูกระงับก
|
||||
userSilenced: "ผู้ใช้รายนี้กำลังถูกปิดกั้น"
|
||||
yourAccountSuspendedTitle: "บัญชีนี้นั้นถูกระงับ"
|
||||
yourAccountSuspendedDescription: "บัญชีนี้ถูกระงับ เนื่องจากละเมิดข้อกำหนดในการให้บริการของเซิร์ฟเวอร์หรืออาจจะละเมิดหลักเกณฑ์ชุมชน หรือ อาจจะโดนร้องเรียนเรื่องการละเมิดลิขสิทธิ์และอื่นๆอย่างต่อเนื่องซ้ำๆ หากคุณคิดว่าไม่ได้ทำผิดจริงๆหรือตัดสินผิดพลาด ได้โปรดกรุณาติดต่อผู้ดูแลระบบหากคุณต้องการทราบเหตุผลโดยละเอียดเพิ่มเติม และขอความกรุณาอย่าสร้างบัญชีใหม่"
|
||||
tokenRevoked: "โทเค็นไม่ถูกต้อง"
|
||||
accountDeleted: "ลบบัญชีแล้ว"
|
||||
menu: "เมนู"
|
||||
divider: "ตัวแบ่ง"
|
||||
addItem: "เพิ่มรายการ"
|
||||
@@ -587,7 +589,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
|
||||
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
|
||||
notificationType: "ประเภทการแจ้งเตือน"
|
||||
edit: "แก้ไข"
|
||||
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
|
||||
emailServer: "อีเมล์เซิร์ฟเวอร์"
|
||||
enableEmail: "เปิดใช้งานการกระจายอีเมล"
|
||||
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
|
||||
@@ -954,11 +955,22 @@ copyErrorInfo: "คัดลอกรายละเอียดข้อผิ
|
||||
joinThisServer: "ลงชื่อสมัครใช้ในอินสแตนซ์นี้"
|
||||
exploreOtherServers: "มองหาอินสแตนซ์อื่น"
|
||||
letsLookAtTimeline: "ลองดูที่ไทม์ไลน์"
|
||||
disableFederationWarn: "การดำเนินการนี้ถ้าหากจะปิดใช้งานการรวมศูนย์ แต่โพสต์ดังกล่าวนั้นจะยังคงเป็นสาธารณะต่อไป ยกเว้นแต่ว่าจะตั้งค่าเป็นอย่างอื่น โดยปกติคุณไม่จำเป็นต้องใช้การตั้งค่านี้นะ"
|
||||
invitationRequiredToRegister: "อินสแตนซ์นี้เป็นแบบรับเชิญเท่านั้น คุณต้องป้อนรหัสเชิญที่ถูกต้องถึงจะลงทะเบียนได้นะค่ะ"
|
||||
emailNotSupported: "อินสแตนซ์นี้ไม่รองรับการส่งอีเมลนะค่ะ"
|
||||
postToTheChannel: "โพสต์ลงช่อง"
|
||||
cannotBeChangedLater: "สิ่งนี้ไม่สามารถเปลี่ยนแปลงได้ในภายหลังนะ"
|
||||
likeOnly: "ที่ชอบเท่านั้น"
|
||||
resetPasswordConfirm: "รีเซ็ตรหัสผ่านของคุณจริงๆหรอ?"
|
||||
sensitiveWords: "คำที่ละเอียดอ่อน"
|
||||
sensitiveWordsDescription: "การเปิดเผยโน้ตทั้งหมดที่มีคำที่กำหนดค่าไว้จะถูกตั้งค่าเป็น \"หน้าแรก\" โดยอัตโนมัติ คุณยังสามารถแสดงหลายรายการได้โดยแยกรายการโดยใช้ตัวแบ่งบรรทัดได้นะ"
|
||||
notesSearchNotAvailable: "การค้นหาโน้ตไม่พร้อมใช้งานนะค่ะ"
|
||||
license: "ใบอนุญาต"
|
||||
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
|
||||
myClips: "คลิปของฉัน"
|
||||
drivecleaner: "ทำความสะอาดไดรฟ์"
|
||||
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
|
||||
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
|
||||
_achievements:
|
||||
earnedAt: "ได้รับเมื่อ"
|
||||
_types:
|
||||
@@ -1218,6 +1230,8 @@ _role:
|
||||
iconUrl: "ไอคอน URL"
|
||||
asBadge: "แสดงเป็นตรา"
|
||||
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
|
||||
displayOrder: "ตำแหน่ง"
|
||||
descriptionOfDisplayOrder: "ยิ่งตัวเลขสูง ตำแหน่ง UI ก็ยิ่งสูงขึ้นนะ"
|
||||
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
|
||||
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
|
||||
priority: "ลำดับความสำคัญ"
|
||||
@@ -1243,6 +1257,7 @@ _role:
|
||||
rateLimitFactor: "ขีดจำกัดอัตรา"
|
||||
descriptionOfRateLimitFactor: "ขีดจํากัดอัตราที่ต่ำกว่ามีข้อจํากัดน้อยกว่าข้อจํากัดที่สูงกว่า"
|
||||
canHideAds: "ซ่อนโฆษณา"
|
||||
canSearchNotes: "การใช้การค้นหาโน้ต"
|
||||
_condition:
|
||||
isLocal: "ผู้ใช้ภายใน"
|
||||
isRemote: "ผู้ใช้ระยะไกล"
|
||||
@@ -1844,3 +1859,13 @@ _deck:
|
||||
_dialog:
|
||||
charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}"
|
||||
charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}"
|
||||
_disabledTimeline:
|
||||
title: "ปิดใช้งานไทม์ไลน์"
|
||||
description: "คุณไม่สามารถใช้ไทม์ไลน์นี้ภายใต้บทบาทปัจจุบันของคุณได้"
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "ขนาดไฟล์จากมากไปหาน้อย"
|
||||
orderByCreatedAtAsc: "วันที่จากน้อยไปหามาก"
|
||||
_webhookSettings:
|
||||
name: "ชื่อ"
|
||||
active: "เปิดใช้งาน"
|
||||
|
||||
|
@@ -60,3 +60,4 @@ _deck:
|
||||
_columns:
|
||||
notifications: "Bildirim"
|
||||
tl: "Zaman çizelgesi"
|
||||
|
||||
|
@@ -2,3 +2,4 @@
|
||||
_lang_: "ياپونچە"
|
||||
search: "ئىزدەش"
|
||||
searchByGoogle: "ئىزدەش"
|
||||
|
||||
|
@@ -576,7 +576,6 @@ tokenRequested: "Надати доступ до акаунту"
|
||||
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
|
||||
notificationType: "Тип сповіщення"
|
||||
edit: "Редагувати"
|
||||
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
|
||||
emailServer: "Email сервер"
|
||||
enableEmail: "Увімкнути функцію доставки пошти"
|
||||
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
|
||||
@@ -1639,3 +1638,7 @@ _deck:
|
||||
channel: "Канали"
|
||||
mentions: "Згадки"
|
||||
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."
|
||||
notificationType: "Loại thông báo"
|
||||
edit: "Sửa"
|
||||
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
|
||||
emailServer: "Email máy chủ"
|
||||
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"
|
||||
@@ -1705,3 +1704,7 @@ _deck:
|
||||
_dialog:
|
||||
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}"
|
||||
_webhookSettings:
|
||||
name: "Tên"
|
||||
active: "Đã bật"
|
||||
|
||||
|
@@ -16,7 +16,7 @@ cancel: "取消"
|
||||
noThankYou: "不用,谢谢"
|
||||
enterUsername: "输入用户名"
|
||||
renotedBy: "由 {user} 转贴"
|
||||
noNotes: "没有帖子"
|
||||
noNotes: "没有帖文"
|
||||
noNotifications: "无通知"
|
||||
instance: "服务器"
|
||||
settings: "设置"
|
||||
@@ -25,7 +25,7 @@ otherSettings: "其他设置"
|
||||
openInWindow: "在新窗口中打开"
|
||||
profile: "个人资料"
|
||||
timeline: "时间线"
|
||||
noAccountDescription: "这个人很懒,没有写自我介绍"
|
||||
noAccountDescription: "此用户尚无自我介绍"
|
||||
login: "登录"
|
||||
loggingIn: "正在登录..."
|
||||
logout: "登出"
|
||||
@@ -85,7 +85,7 @@ somethingHappened: "出现了一些问题!"
|
||||
retry: "重试"
|
||||
pageLoadError: "页面加载失败。"
|
||||
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
|
||||
serverIsDead: "服务器没有响应。 请稍等片刻,然后重试。"
|
||||
serverIsDead: "没有服务器响应。 请稍后再试。"
|
||||
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
|
||||
enterListName: "输入列表名称"
|
||||
privacy: "隐私"
|
||||
@@ -95,7 +95,7 @@ follow: "关注"
|
||||
followRequest: "关注申请"
|
||||
followRequests: "关注申请"
|
||||
unfollow: "取消关注"
|
||||
followRequestPending: "发送关注请求"
|
||||
followRequestPending: "关注请求批准中"
|
||||
enterEmoji: "输入表情符号"
|
||||
renote: "转发"
|
||||
unrenote: "取消转发"
|
||||
@@ -119,7 +119,7 @@ rememberNoteVisibility: "保存上次设置的可见性"
|
||||
attachCancel: "删除附件"
|
||||
markAsSensitive: "标记为敏感内容"
|
||||
unmarkAsSensitive: "取消标记为敏感内容"
|
||||
enterFileName: "请输入文件名"
|
||||
enterFileName: "输入文件名"
|
||||
mute: "屏蔽"
|
||||
unmute: "解除屏蔽"
|
||||
renoteMute: "屏蔽转帖"
|
||||
@@ -145,7 +145,7 @@ emojiName: "表情符号名称"
|
||||
emojiUrl: "表情符号地址"
|
||||
addEmoji: "添加表情符号"
|
||||
settingGuide: "推荐配置"
|
||||
cacheRemoteFiles: "远程文件缓存"
|
||||
cacheRemoteFiles: "缓存远程文件"
|
||||
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
|
||||
flagAsBot: "这是一个机器人账号"
|
||||
flagAsBotDescription: "如果此帐户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此帐户识别为机器人。"
|
||||
@@ -153,7 +153,7 @@ flagAsCat: "将这个账户设定为一只猫"
|
||||
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。"
|
||||
flagShowTimelineReplies: "在时间线上显示帖子的回复"
|
||||
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
|
||||
autoAcceptFollowed: "自动允许关注者的关注"
|
||||
autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求"
|
||||
addAccount: "添加账户"
|
||||
reloadAccountsList: "更新账户列表"
|
||||
loginFailed: "登录失败"
|
||||
@@ -203,7 +203,7 @@ blockedInstances: "被阻拦的服务器"
|
||||
blockedInstancesDescription: "设定要阻拦的服务器,以换行来进行分割。被阻拦的服务器将无法与本服务器进行交换通讯。"
|
||||
muteAndBlock: "屏蔽/拉黑"
|
||||
mutedUsers: "已屏蔽用户"
|
||||
blockedUsers: "被拉黑的用户"
|
||||
blockedUsers: "已拉黑的用户"
|
||||
noUsers: "无用户"
|
||||
editProfile: "编辑资料"
|
||||
noteDeleteConfirm: "要删除该帖子吗?"
|
||||
@@ -336,7 +336,7 @@ enableLocalTimeline: "启用本地时间线功能"
|
||||
enableGlobalTimeline: "启用全局时间线"
|
||||
disablingTimelinesInfo: "即使时间线功能被禁用,出于方便,管理员和数据图表也可以继续使用。"
|
||||
registration: "注册"
|
||||
enableRegistration: "允许新用户注册"
|
||||
enableRegistration: "允许任何人注册"
|
||||
invite: "邀请"
|
||||
driveCapacityPerLocalAccount: "每个用户的网盘空间"
|
||||
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
|
||||
@@ -354,7 +354,7 @@ pinnedNotes: "已置顶的帖子"
|
||||
hcaptcha: "hCaptcha"
|
||||
enableHcaptcha: "启用 hCaptcha"
|
||||
hcaptchaSiteKey: "网站密钥"
|
||||
hcaptchaSecretKey: "hCaptcha 密钥(SecretKey)"
|
||||
hcaptchaSecretKey: "密钥"
|
||||
recaptcha: "reCAPTCHA"
|
||||
enableRecaptcha: "启用 reCAPTCHA\n(请注意, 此功能在中国大陆不可用. 如果启用, 可能导致无法正常使用登录或注册等功能)"
|
||||
recaptchaSiteKey: "网站密钥"
|
||||
@@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "如果不使用https进行API连接,请关闭。"
|
||||
objectStorageUseProxy: "使用代理"
|
||||
objectStorageUseProxyDesc: "如果您不使用代理进行API连接,请将其关闭。"
|
||||
objectStorageSetPublicRead: "上传时设置为public-read"
|
||||
s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定为 URL 中路径的一部分,而不是主机名。使用自托管 Minio 等时可能需要启用。"
|
||||
serverLogs: "服务器日志"
|
||||
deleteAll: "全部删除"
|
||||
showFixedPostForm: "在时间线顶部显示发帖框"
|
||||
@@ -594,7 +595,6 @@ tokenRequested: "允许访问账户"
|
||||
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
|
||||
notificationType: "通知类型"
|
||||
edit: "编辑"
|
||||
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
|
||||
emailServer: "邮件服务器"
|
||||
enableEmail: "启用发送邮件功能"
|
||||
emailConfigInfo: "用于确认电子邮件和密码重置"
|
||||
@@ -961,7 +961,9 @@ copyErrorInfo: "复制错误信息"
|
||||
joinThisServer: "在本服务器上注册"
|
||||
exploreOtherServers: "探索其他服务器"
|
||||
letsLookAtTimeline: "时间线"
|
||||
disableFederationWarn: "联合被禁用。 禁用它并不能使帖子变成私人的。 在大多数情况下,这个选项不需要被启用。"
|
||||
disableFederationConfirm: "确定要禁用联合?"
|
||||
disableFederationConfirmWarn: "禁用联合不会将帖子设为私有。在大多数情况下,不需要禁用联合。"
|
||||
disableFederationOk: "联合禁用"
|
||||
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
|
||||
emailNotSupported: "此服务器不支持发送邮件"
|
||||
postToTheChannel: "发布到频道"
|
||||
@@ -981,6 +983,11 @@ drivecleaner: "网盘整理"
|
||||
retryAllQueuesNow: "立刻重试所有队列"
|
||||
retryAllQueuesConfirmTitle: "要再尝试一次吗?"
|
||||
retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加"
|
||||
enableChartsForRemoteUser: "生成远程用户的图表"
|
||||
enableChartsForFederatedInstances: "生成远程服务器的图表"
|
||||
showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
|
||||
largeNoteReactions: "使用大图标来显示回应"
|
||||
noteIdOrUrl: "帖子ID或URL"
|
||||
_achievements:
|
||||
earnedAt: "达成时间"
|
||||
_types:
|
||||
@@ -1253,7 +1260,7 @@ _role:
|
||||
gtlAvailable: "查看全局时间线"
|
||||
ltlAvailable: "查看本地时间线"
|
||||
canPublicNote: "允许公开发帖"
|
||||
canInvite: "发放实例邀请码"
|
||||
canInvite: "发放服务器邀请码"
|
||||
canManageCustomEmojis: "管理自定义表情符号"
|
||||
driveCapacity: "网盘容量"
|
||||
pinMax: "帖子置顶数量限制"
|
||||
@@ -1277,6 +1284,8 @@ _role:
|
||||
followersMoreThanOrEq: "关注者不少于"
|
||||
followingLessThanOrEq: "关注中不多于"
|
||||
followingMoreThanOrEq: "关注中不少于"
|
||||
notesLessThanOrEq: "帖子数在~以下"
|
||||
notesMoreThanOrEq: "帖子数在~以上"
|
||||
and: "符合以下全部条件"
|
||||
or: "符合以下任一条件"
|
||||
not: "不符合以下任何条件"
|
||||
@@ -1625,7 +1634,7 @@ _widgets:
|
||||
photos: "照片"
|
||||
digitalClock: "数字时钟"
|
||||
unixClock: "UNIX时钟"
|
||||
federation: "联邦宇宙"
|
||||
federation: "联合"
|
||||
instanceCloud: "服务器云"
|
||||
postForm: "投稿窗口"
|
||||
slideshow: "幻灯片展示"
|
||||
@@ -1875,3 +1884,18 @@ _disabledTimeline:
|
||||
_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: "取消"
|
||||
noThankYou: "現在不要"
|
||||
enterUsername: "輸入使用者名稱"
|
||||
renotedBy: "{user} 轉傳了"
|
||||
renotedBy: "{user} 轉發了"
|
||||
noNotes: "無貼文。"
|
||||
noNotifications: "沒有通知"
|
||||
instance: "實例"
|
||||
@@ -99,9 +99,9 @@ followRequestPending: "追隨許可批准中"
|
||||
enterEmoji: "輸入表情符號"
|
||||
renote: "轉發"
|
||||
unrenote: "取消轉發"
|
||||
renoted: "轉傳成功"
|
||||
renoted: "轉發成功"
|
||||
cantRenote: "無法轉發此貼文。"
|
||||
cantReRenote: "無法轉傳之前已經轉傳過的內容。"
|
||||
cantReRenote: "無法轉發之前已經轉發過的內容。"
|
||||
quote: "引用"
|
||||
inChannelRenote: "在頻道內轉發"
|
||||
inChannelQuote: "在頻道內引用"
|
||||
@@ -594,7 +594,6 @@ tokenRequested: "允許存取帳戶"
|
||||
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
|
||||
notificationType: "通知形式"
|
||||
edit: "編輯"
|
||||
useStarForReactionFallback: "以★代替未知的表情符號"
|
||||
emailServer: "電郵伺服器"
|
||||
enableEmail: "啟用發送電郵功能"
|
||||
emailConfigInfo: "用於確認電郵地址及密碼重置"
|
||||
@@ -678,8 +677,8 @@ sentReactionsCount: "反應發送次數"
|
||||
receivedReactionsCount: "收到反應次數"
|
||||
pollVotesCount: "已統計的投票數"
|
||||
pollVotedCount: "已投票數"
|
||||
yes: "確定"
|
||||
no: "取消"
|
||||
yes: "是"
|
||||
no: "否"
|
||||
driveFilesCount: "雲端硬碟檔案數量"
|
||||
driveUsage: "雲端硬碟使用量"
|
||||
noCrawle: "拒絕搜尋引擎索引"
|
||||
@@ -961,7 +960,6 @@ copyErrorInfo: "複製錯誤資訊"
|
||||
joinThisServer: "在此伺服器上註冊"
|
||||
exploreOtherServers: "探索其他伺服器"
|
||||
letsLookAtTimeline: "看看時間軸"
|
||||
disableFederationWarn: "聯邦被停用了。即使停用也不會讓您的貼文不公開,在大多數情況下,不需要啟用這個選項。"
|
||||
invitationRequiredToRegister: "目前這個伺服器為邀請制,必須擁有邀請碼才能註冊。"
|
||||
emailNotSupported: "這個伺服器不支援寄送郵件"
|
||||
postToTheChannel: "發布到頻道"
|
||||
@@ -981,6 +979,11 @@ drivecleaner: "雲端硬碟清掃器"
|
||||
retryAllQueuesNow: "立刻重試所有佇列"
|
||||
retryAllQueuesConfirmTitle: "要現在重試嗎?"
|
||||
retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。"
|
||||
enableChartsForRemoteUser: "生成遠端用戶的圖表"
|
||||
enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
|
||||
showClipButtonInNoteFooter: "將摘錄添加至貼文"
|
||||
largeNoteReactions: "將貼文的反應放大顯示"
|
||||
noteIdOrUrl: "貼文ID或URL"
|
||||
_achievements:
|
||||
earnedAt: "獲得日期"
|
||||
_types:
|
||||
@@ -1097,7 +1100,7 @@ _achievements:
|
||||
title: "有備而來"
|
||||
description: "設定了個人檔案"
|
||||
_markedAsCat:
|
||||
title: "我是貓"
|
||||
title: "吾輩乃貓是也"
|
||||
description: "已將帳戶設定為貓"
|
||||
flavor: "還沒有名字。"
|
||||
_following1:
|
||||
@@ -1277,6 +1280,8 @@ _role:
|
||||
followersMoreThanOrEq: "追隨者人數在~以上"
|
||||
followingLessThanOrEq: "追隨人數在~以下"
|
||||
followingMoreThanOrEq: "追隨人數在~以上"
|
||||
notesLessThanOrEq: "發布數在~以下"
|
||||
notesMoreThanOrEq: "發布數在~以上"
|
||||
and: "~和~"
|
||||
or: "~或~"
|
||||
not: "~否"
|
||||
@@ -1875,3 +1880,18 @@ _disabledTimeline:
|
||||
_drivecleaner:
|
||||
orderBySizeDesc: "檔案由大到小"
|
||||
orderByCreatedAtAsc: "依照加入的日期順序"
|
||||
_webhookSettings:
|
||||
createWebhook: "建立 Webhook"
|
||||
name: "名稱"
|
||||
secret: "秘密"
|
||||
events: "什麼時候運行Webhook"
|
||||
active: "已啟用"
|
||||
_events:
|
||||
follow: "當你追隨時"
|
||||
followed: "當被追隨時"
|
||||
note: "當發布貼文時"
|
||||
reply: "當收到回覆時"
|
||||
renote: "當被轉發時"
|
||||
reaction: "當獲得反應時"
|
||||
mention: "當被提到時"
|
||||
|
||||
|
14
package.json
14
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.10.2",
|
||||
"version": "13.11.0-beta.5",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@7.29.3",
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
@@ -50,16 +50,16 @@
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"typescript": "4.9.5"
|
||||
"typescript": "5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
"@typescript-eslint/parser": "5.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.7.0",
|
||||
"eslint": "8.35.0",
|
||||
"cypress": "12.9.0",
|
||||
"eslint": "8.37.0",
|
||||
"start-server-and-test": "2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@@ -1,8 +1,15 @@
|
||||
import Redis from 'ioredis';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { createRedisConnection } from './built/redis.js';
|
||||
|
||||
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('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"`);
|
||||
}
|
||||
}
|
@@ -22,44 +22,46 @@
|
||||
"test-and-coverage": "pnpm jest-and-coverage"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "^1.3.11",
|
||||
"@swc/core-darwin-arm64": "^1.3.38",
|
||||
"@swc/core-darwin-x64": "^1.3.38",
|
||||
"@swc/core-linux-arm-gnueabihf": "^1.3.38",
|
||||
"@swc/core-linux-arm64-gnu": "^1.3.38",
|
||||
"@swc/core-linux-arm64-musl": "^1.3.38",
|
||||
"@swc/core-linux-x64-gnu": "^1.3.38",
|
||||
"@swc/core-linux-x64-musl": "^1.3.38",
|
||||
"@swc/core-win32-arm64-msvc": "^1.3.38",
|
||||
"@swc/core-win32-ia32-msvc": "^1.3.38",
|
||||
"@swc/core-win32-x64-msvc": "^1.3.38",
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.3.46",
|
||||
"@swc/core-darwin-x64": "1.3.46",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.46",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.46",
|
||||
"@swc/core-linux-arm64-musl": "1.3.46",
|
||||
"@swc/core-linux-x64-gnu": "1.3.46",
|
||||
"@swc/core-linux-x64-musl": "1.3.46",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.46",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.46",
|
||||
"@swc/core-win32-x64-msvc": "1.3.46",
|
||||
"@tensorflow/tfjs": "4.2.0",
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
},
|
||||
"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/fastify": "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/cookie": "8.3.0",
|
||||
"@fastify/cors": "8.2.0",
|
||||
"@fastify/http-proxy": "8.4.0",
|
||||
"@fastify/multipart": "7.4.2",
|
||||
"@fastify/static": "6.9.0",
|
||||
"@fastify/cors": "8.2.1",
|
||||
"@fastify/http-proxy": "9.0.0",
|
||||
"@fastify/multipart": "7.5.0",
|
||||
"@fastify/static": "6.10.0",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.3.9",
|
||||
"@nestjs/core": "9.3.9",
|
||||
"@nestjs/testing": "9.3.9",
|
||||
"@nestjs/common": "9.4.0",
|
||||
"@nestjs/core": "9.4.0",
|
||||
"@nestjs/testing": "9.4.0",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.38",
|
||||
"@swc/core": "1.3.46",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"autwh": "0.1.0",
|
||||
"aws-sdk": "2.1318.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bull": "4.10.4",
|
||||
@@ -74,7 +76,7 @@
|
||||
"date-fns": "2.29.3",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.14.1",
|
||||
"fastify": "4.15.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.2.1",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
@@ -86,21 +88,21 @@
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "21.1.0",
|
||||
"jsdom": "21.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.1.1",
|
||||
"jsrsasign": "10.6.1",
|
||||
"jsrsasign": "10.7.0",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "0.0.15",
|
||||
"misskey-js": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.0",
|
||||
"node-fetch": "3.3.1",
|
||||
"nodemailer": "6.9.1",
|
||||
"nsfwjs": "2.4.2",
|
||||
"oauth": "0.10.0",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "^9.0.2",
|
||||
"otpauth": "9.1.1",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.10.0",
|
||||
"private-ip": "3.0.0",
|
||||
@@ -123,7 +125,7 @@
|
||||
"sanitize-html": "2.10.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"semver": "7.3.8",
|
||||
"sharp": "0.31.3",
|
||||
"sharp": "0.32.0",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
@@ -131,25 +133,25 @@
|
||||
"systeminformation": "5.17.12",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.3",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
"typescript": "4.9.5",
|
||||
"typeorm": "0.3.13",
|
||||
"typescript": "5.0.3",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.5.0",
|
||||
"websocket": "1.0.34",
|
||||
"ws": "8.12.1",
|
||||
"ws": "8.13.0",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.5.0",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.1",
|
||||
"@types/archiver": "5.3.2",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "4.10.0",
|
||||
"@types/cbor": "6.0.0",
|
||||
@@ -158,13 +160,13 @@
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/ioredis": "4.28.10",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "21.1.0",
|
||||
"@types/jsdom": "21.1.1",
|
||||
"@types/jsonld": "1.5.8",
|
||||
"@types/jsrsasign": "10.5.5",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.7",
|
||||
"@types/oauth": "0.9.1",
|
||||
@@ -176,7 +178,7 @@
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
"@types/redis": "4.0.11",
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.8.1",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/sharp": "0.31.1",
|
||||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
@@ -188,10 +190,11 @@
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.54.1",
|
||||
"@typescript-eslint/parser": "5.54.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"aws-sdk-client-mock": "^2.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.35.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.5.0",
|
||||
|
@@ -2,18 +2,15 @@ import { setTimeout } from 'node:timers/promises';
|
||||
import { Global, Inject, Module } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { createRedisConnection } from '@/redis.js';
|
||||
import { DI } from './di-symbols.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { createPostgresDataSource } from './postgres.js';
|
||||
import { RepositoryModule } from './models/RepositoryModule.js';
|
||||
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
const $config: Provider = {
|
||||
provide: DI.config,
|
||||
useValue: config,
|
||||
useValue: loadConfig(),
|
||||
};
|
||||
|
||||
const $db: Provider = {
|
||||
@@ -28,18 +25,31 @@ const $db: Provider = {
|
||||
const $redis: Provider = {
|
||||
provide: DI.redis,
|
||||
useFactory: (config) => {
|
||||
const redisClient = createRedisConnection(config);
|
||||
return redisClient;
|
||||
return 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,
|
||||
});
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $redisSubscriber: Provider = {
|
||||
provide: DI.redisSubscriber,
|
||||
const $redisForPubsub: Provider = {
|
||||
provide: DI.redisForPubsub,
|
||||
useFactory: (config) => {
|
||||
const redisSubscriber = createRedisConnection(config);
|
||||
redisSubscriber.subscribe(config.host);
|
||||
return redisSubscriber;
|
||||
const redis = new Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
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],
|
||||
};
|
||||
@@ -47,14 +57,14 @@ const $redisSubscriber: Provider = {
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisSubscriber],
|
||||
exports: [$config, $db, $redis, $redisSubscriber, RepositoryModule],
|
||||
providers: [$config, $db, $redis, $redisForPubsub],
|
||||
exports: [$config, $db, $redis, $redisForPubsub, RepositoryModule],
|
||||
})
|
||||
export class GlobalModule implements OnApplicationShutdown {
|
||||
constructor(
|
||||
@Inject(DI.db) private db: DataSource,
|
||||
@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> {
|
||||
@@ -69,7 +79,7 @@ export class GlobalModule implements OnApplicationShutdown {
|
||||
await Promise.all([
|
||||
this.db.destroy(),
|
||||
this.redisClient.disconnect(),
|
||||
this.redisSubscriber.disconnect(),
|
||||
this.redisForPubsub.disconnect(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@@ -33,6 +33,22 @@ export type Source = {
|
||||
db?: number;
|
||||
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: {
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -91,6 +107,8 @@ export type Mixin = {
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
videoThumbnailGenerator: string | null;
|
||||
redisForPubsub: NonNullable<Source['redisForPubsub']>;
|
||||
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
|
||||
};
|
||||
|
||||
export type Config = Source & Mixin;
|
||||
@@ -151,6 +169,8 @@ export function loadConfig() {
|
||||
: null;
|
||||
|
||||
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);
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import type { Packed } from '@/misc/json-schema.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 { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
@@ -24,8 +24,11 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
private antennas: Antenna[];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
@@ -33,9 +36,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.antennaNotesRepository)
|
||||
private antennaNotesRepository: AntennaNotesRepository,
|
||||
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@@ -52,12 +52,12 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
this.antennasFetched = false;
|
||||
this.antennas = [];
|
||||
|
||||
this.redisSubscriber.on('message', this.onRedisMessage);
|
||||
this.redisForPubsub.on('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisSubscriber.off('message', this.onRedisMessage);
|
||||
this.redisForPubsub.off('message', this.onRedisMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -92,54 +92,13 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public async addNoteToAntenna(antenna: Antenna, note: Note, noteUser: { id: User['id']; }): Promise<void> {
|
||||
// 通知しない設定になっているか、自分自身の投稿なら既読にする
|
||||
const read = !antenna.notify || (antenna.userId === noteUser.id);
|
||||
|
||||
this.antennaNotesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
antennaId: antenna.id,
|
||||
noteId: note.id,
|
||||
read: read,
|
||||
});
|
||||
|
||||
this.redisClient.xadd(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
'MAXLEN', '~', '200',
|
||||
`${this.idService.parse(note.id).date.getTime()}-*`,
|
||||
'note', note.id);
|
||||
|
||||
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: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
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);
|
||||
}
|
||||
}
|
@@ -38,9 +38,9 @@ import { S3Service } from './S3Service.js';
|
||||
import { SignupService } from './SignupService.js';
|
||||
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
|
||||
import { UserBlockingService } from './UserBlockingService.js';
|
||||
import { UserCacheService } from './UserCacheService.js';
|
||||
import { CacheService } from './CacheService.js';
|
||||
import { UserFollowingService } from './UserFollowingService.js';
|
||||
import { UserKeypairStoreService } from './UserKeypairStoreService.js';
|
||||
import { UserKeypairService } from './UserKeypairService.js';
|
||||
import { UserListService } from './UserListService.js';
|
||||
import { UserMutingService } from './UserMutingService.js';
|
||||
import { UserSuspendService } from './UserSuspendService.js';
|
||||
@@ -159,9 +159,9 @@ const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
|
||||
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
|
||||
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
|
||||
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 $UserKeypairStoreService: Provider = { provide: 'UserKeypairStoreService', useExisting: UserKeypairStoreService };
|
||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
|
||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
|
||||
const $UserMutingService: Provider = { provide: 'UserMutingService', useExisting: UserMutingService };
|
||||
const $UserSuspendService: Provider = { provide: 'UserSuspendService', useExisting: UserSuspendService };
|
||||
@@ -282,9 +282,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
SignupService,
|
||||
TwoFactorAuthenticationService,
|
||||
UserBlockingService,
|
||||
UserCacheService,
|
||||
CacheService,
|
||||
UserFollowingService,
|
||||
UserKeypairStoreService,
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserSuspendService,
|
||||
@@ -399,9 +399,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$SignupService,
|
||||
$TwoFactorAuthenticationService,
|
||||
$UserBlockingService,
|
||||
$UserCacheService,
|
||||
$CacheService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairStoreService,
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserSuspendService,
|
||||
@@ -517,9 +517,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
SignupService,
|
||||
TwoFactorAuthenticationService,
|
||||
UserBlockingService,
|
||||
UserCacheService,
|
||||
CacheService,
|
||||
UserFollowingService,
|
||||
UserKeypairStoreService,
|
||||
UserKeypairService,
|
||||
UserListService,
|
||||
UserMutingService,
|
||||
UserSuspendService,
|
||||
@@ -633,9 +633,9 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$SignupService,
|
||||
$TwoFactorAuthenticationService,
|
||||
$UserBlockingService,
|
||||
$UserCacheService,
|
||||
$CacheService,
|
||||
$UserFollowingService,
|
||||
$UserKeypairStoreService,
|
||||
$UserKeypairService,
|
||||
$UserListService,
|
||||
$UserMutingService,
|
||||
$UserSuspendService,
|
||||
|
@@ -1,24 +1,28 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DataSource, In, IsNull } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.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 { Cache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { ReactionService } from '@/core/ReactionService.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
|
||||
@Injectable()
|
||||
export class CustomEmojiService {
|
||||
private cache: Cache<Emoji | null>;
|
||||
private cache: MemoryKVCache<Emoji | null>;
|
||||
public localEmojisCache: RedisSingleCache<Map<string, Emoji>>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@@ -32,9 +36,16 @@ export class CustomEmojiService {
|
||||
private idService: IdService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
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
|
||||
@@ -60,7 +71,7 @@ export class CustomEmojiService {
|
||||
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (data.host == null) {
|
||||
await this.db.queryResultCache?.remove(['meta_emojis']);
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiAdded', {
|
||||
emoji: await this.emojiEntityService.packDetailed(emoji.id),
|
||||
@@ -70,6 +81,146 @@ export class CustomEmojiService {
|
||||
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
|
||||
private normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
|
||||
// クエリに使うホスト
|
||||
@@ -84,7 +235,7 @@ export class CustomEmojiService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
public parseEmojiStr(emojiName: string, noteUserHost: string | null) {
|
||||
const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
|
||||
if (!match) return { name: null, host: null };
|
||||
|
||||
@@ -143,30 +294,6 @@ export class CustomEmojiService {
|
||||
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, {
|
||||
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 { sharpBmp } from 'sharp-read-bmp';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -36,7 +37,6 @@ import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { correctFilename } from '@/misc/correct-filename.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import type S3 from 'aws-sdk/clients/s3.js';
|
||||
|
||||
type AddFileArgs = {
|
||||
/** User who wish to add file */
|
||||
@@ -81,6 +81,7 @@ type UploadFromUrlArgs = {
|
||||
export class DriveService {
|
||||
private registerLogger: Logger;
|
||||
private downloaderLogger: Logger;
|
||||
private deleteLogger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@@ -118,6 +119,7 @@ export class DriveService {
|
||||
const logger = new Logger('drive', 'blue');
|
||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||
this.downloaderLogger = logger.createSubLogger('downloader');
|
||||
this.deleteLogger = logger.createSubLogger('delete');
|
||||
}
|
||||
|
||||
/***
|
||||
@@ -368,7 +370,7 @@ export class DriveService {
|
||||
Body: stream,
|
||||
ContentType: type,
|
||||
CacheControl: 'max-age=31536000, immutable',
|
||||
} as S3.PutObjectRequest;
|
||||
} as PutObjectCommandInput;
|
||||
|
||||
if (filename) params.ContentDisposition = contentDisposition(
|
||||
'inline',
|
||||
@@ -378,21 +380,16 @@ export class DriveService {
|
||||
);
|
||||
if (meta.objectStorageSetPublicRead) params.ACL = 'public-read';
|
||||
|
||||
const s3 = this.s3Service.getS3(meta);
|
||||
|
||||
const upload = s3.upload(params, {
|
||||
partSize: s3.endpoint.hostname === 'storage.googleapis.com' ? 500 * 1024 * 1024 : 8 * 1024 * 1024,
|
||||
});
|
||||
|
||||
await upload.promise()
|
||||
await this.s3Service.upload(meta, params)
|
||||
.then(
|
||||
result => {
|
||||
if (result) {
|
||||
if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput
|
||||
this.registerLogger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`);
|
||||
} else {
|
||||
this.registerLogger.error(`Upload Result Empty: key = ${key}, filename = ${filename}`);
|
||||
} else { // AbortMultipartUploadCommandOutput
|
||||
this.registerLogger.error(`Upload Result Aborted: key = ${key}, filename = ${filename}`);
|
||||
}
|
||||
},
|
||||
})
|
||||
.catch(
|
||||
err => {
|
||||
this.registerLogger.error(`Upload Failed: key = ${key}, filename = ${filename}`, err);
|
||||
},
|
||||
@@ -528,10 +525,10 @@ export class DriveService {
|
||||
};
|
||||
|
||||
const properties: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
orientation?: number;
|
||||
} = {};
|
||||
width?: number;
|
||||
height?: number;
|
||||
orientation?: number;
|
||||
} = {};
|
||||
|
||||
if (info.width) {
|
||||
properties['width'] = info.width;
|
||||
@@ -616,17 +613,20 @@ export class DriveService {
|
||||
|
||||
if (user) {
|
||||
this.driveFileEntityService.pack(file, { self: true }).then(packedFile => {
|
||||
// Publish driveFileCreated event
|
||||
// Publish driveFileCreated event
|
||||
this.globalEventService.publishMainStream(user.id, 'driveFileCreated', packedFile);
|
||||
this.globalEventService.publishDriveStream(user.id, 'fileCreated', packedFile);
|
||||
});
|
||||
}
|
||||
|
||||
// 統計を更新
|
||||
this.driveChart.update(file, true);
|
||||
this.perUserDriveChart.update(file, true);
|
||||
if (file.userHost !== null) {
|
||||
this.instanceChart.updateDrive(file, true);
|
||||
if (file.userHost == null) {
|
||||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, true);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, true);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
@@ -692,7 +692,7 @@ export class DriveService {
|
||||
|
||||
@bindThis
|
||||
private async deletePostProcess(file: DriveFile, isExpired = false) {
|
||||
// リモートファイル期限切れ削除後は直リンクにする
|
||||
// リモートファイル期限切れ削除後は直リンクにする
|
||||
if (isExpired && file.userHost !== null && file.uri != null) {
|
||||
this.driveFilesRepository.update(file.id, {
|
||||
isLink: true,
|
||||
@@ -709,33 +709,36 @@ export class DriveService {
|
||||
this.driveFilesRepository.delete(file.id);
|
||||
}
|
||||
|
||||
// 統計を更新
|
||||
this.driveChart.update(file, false);
|
||||
this.perUserDriveChart.update(file, false);
|
||||
if (file.userHost !== null) {
|
||||
this.instanceChart.updateDrive(file, false);
|
||||
if (file.userHost == null) {
|
||||
// ローカルユーザーのみ
|
||||
this.perUserDriveChart.update(file, false);
|
||||
} else {
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateDrive(file, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteObjectStorageFile(key: string) {
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
const s3 = this.s3Service.getS3(meta);
|
||||
|
||||
try {
|
||||
await s3.deleteObject({
|
||||
Bucket: meta.objectStorageBucket!,
|
||||
const param = {
|
||||
Bucket: meta.objectStorageBucket,
|
||||
Key: key,
|
||||
}).promise();
|
||||
} as DeleteObjectCommandInput;
|
||||
|
||||
await this.s3Service.delete(meta, param);
|
||||
} catch (err: any) {
|
||||
if (err.code === 'NoSuchKey') {
|
||||
console.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err);
|
||||
if (err.name === 'NoSuchKey') {
|
||||
this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error);
|
||||
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,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { InstancesRepository } from '@/models/index.js';
|
||||
import type { Instance } from '@/models/entities/Instance.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@@ -9,7 +9,7 @@ import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class FederatedInstanceService {
|
||||
private cache: Cache<Instance>;
|
||||
private cache: MemoryKVCache<Instance>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.instancesRepository)
|
||||
@@ -18,7 +18,7 @@ export class FederatedInstanceService {
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
this.cache = new Cache<Instance>(1000 * 60 * 60);
|
||||
this.cache = new MemoryKVCache<Instance>(1000 * 60 * 60);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -14,7 +14,6 @@ import type {
|
||||
MainStreamTypes,
|
||||
NoteStreamTypes,
|
||||
UserListStreamTypes,
|
||||
UserStreamTypes,
|
||||
} from '@/server/api/stream/types.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -49,11 +48,6 @@ export class GlobalEventService {
|
||||
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
|
||||
public publishBroadcastStream<K extends keyof BroadcastTypes>(type: K, value?: BroadcastTypes[K]): void {
|
||||
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ulid } from 'ulid';
|
||||
import { DI } from '@/di-symbols.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 { genMeidg } from '@/misc/id/meidg.js';
|
||||
import { genObjectId } from '@/misc/id/object-id.js';
|
||||
@@ -32,4 +32,17 @@ export class IdService {
|
||||
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 type { LocalUser } from '@/models/entities/User.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 { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -11,7 +11,7 @@ const ACTOR_USERNAME = 'instance.actor' as const;
|
||||
|
||||
@Injectable()
|
||||
export class InstanceActorService {
|
||||
private cache: Cache<LocalUser>;
|
||||
private cache: MemorySingleCache<LocalUser>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@@ -19,12 +19,12 @@ export class InstanceActorService {
|
||||
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
) {
|
||||
this.cache = new Cache<LocalUser>(Infinity);
|
||||
this.cache = new MemorySingleCache<LocalUser>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getInstanceActor(): Promise<LocalUser> {
|
||||
const cached = this.cache.get(null);
|
||||
const cached = this.cache.get();
|
||||
if (cached) return cached;
|
||||
|
||||
const user = await this.usersRepository.findOneBy({
|
||||
@@ -33,11 +33,11 @@ export class InstanceActorService {
|
||||
}) as LocalUser | undefined;
|
||||
|
||||
if (user) {
|
||||
this.cache.set(null, user);
|
||||
this.cache.set(user);
|
||||
return user;
|
||||
} else {
|
||||
const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as LocalUser;
|
||||
this.cache.set(null, created);
|
||||
this.cache.set(created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
@@ -14,8 +14,8 @@ export class MetaService implements OnApplicationShutdown {
|
||||
private intervalId: NodeJS.Timer;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
@@ -33,7 +33,7 @@ export class MetaService implements OnApplicationShutdown {
|
||||
}, 1000 * 60 * 5);
|
||||
}
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -122,6 +122,6 @@ export class MetaService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
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 * as mfm from 'mfm-js';
|
||||
import { In, DataSource } from 'typeorm';
|
||||
import Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { extractMentions } from '@/misc/extract-mentions.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 type { Channel } from '@/models/entities/Channel.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 { RelayService } from '@/core/RelayService.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 { 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';
|
||||
|
||||
@@ -150,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -321,6 +325,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
|
||||
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(
|
||||
() => this.postNoteCreated(note, user, data, silent, tags!, mentionedUsers!),
|
||||
() => { /* aborted, ignore this */ },
|
||||
@@ -435,15 +447,20 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
createdAt: User['createdAt'];
|
||||
isBot: User['isBot'];
|
||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||
// 統計を更新
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
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
|
||||
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.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);
|
||||
|
||||
// Word mute
|
||||
mutedWordsCache.fetch(null, () => this.userProfilesRepository.find({
|
||||
mutedWordsCache.fetch(() => this.userProfilesRepository.find({
|
||||
where: {
|
||||
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) {
|
||||
this.saveReply(data.reply, note);
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
@@ -39,6 +40,7 @@ export class NoteDeleteService {
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private metaService: MetaService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private instanceChart: InstanceChart,
|
||||
@@ -95,14 +97,19 @@ export class NoteDeleteService {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 統計を更新
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
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)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(i => {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
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 { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { In, IsNull, Not } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.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 { Note } from '@/models/entities/Note.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { UsersRepository, NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository, FollowingsRepository, ChannelFollowingsRepository, AntennaNotesRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { NoteUnreadsRepository, MutingsRepository, NoteThreadMutingsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NotificationService } from './NotificationService.js';
|
||||
import { AntennaService } from './AntennaService.js';
|
||||
import { PushNotificationService } from './PushNotificationService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteReadService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.noteUnreadsRepository)
|
||||
private noteUnreadsRepository: NoteUnreadsRepository,
|
||||
|
||||
@@ -32,21 +24,8 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
@Inject(DI.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 globalEventService: GlobalEventService,
|
||||
private notificationService: NotificationService,
|
||||
private antennaService: AntennaService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -57,7 +36,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
isMentioned: boolean;
|
||||
}): Promise<void> {
|
||||
//#region ミュートしているなら無視
|
||||
// TODO: 現在の仕様ではChannelにミュートは適用されないのでよしなにケアする
|
||||
const mute = await this.mutingsRepository.findBy({
|
||||
muterId: userId,
|
||||
});
|
||||
@@ -77,7 +55,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
userId: userId,
|
||||
isSpecified: params.isSpecified,
|
||||
isMentioned: params.isMentioned,
|
||||
noteChannelId: note.channelId,
|
||||
noteUserId: note.userId,
|
||||
};
|
||||
|
||||
@@ -95,9 +72,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
if (params.isSpecified) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||
}
|
||||
if (note.channelId) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
||||
}
|
||||
}, () => { /* aborted, ignore it */ });
|
||||
}
|
||||
|
||||
@@ -105,23 +79,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
public async read(
|
||||
userId: User['id'],
|
||||
notes: (Note | Packed<'Note'>)[],
|
||||
info?: {
|
||||
following: Set<User['id']>;
|
||||
followingChannels: Set<Channel['id']>;
|
||||
},
|
||||
): 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 readSpecifiedNotes: (Note | Packed<'Note'>)[] = [];
|
||||
const readChannelNotes: (Note | Packed<'Note'>)[] = [];
|
||||
const readAntennaNotes: (Note | Packed<'Note'>)[] = [];
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.mentions && note.mentions.includes(userId)) {
|
||||
@@ -129,25 +89,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||
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
|
||||
await this.noteUnreadsRepository.delete({
|
||||
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: ↓まとめてクエリしたい
|
||||
@@ -171,49 +119,6 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
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 Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
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 { Notification } from '@/models/entities/Notification.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 { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.notificationsRepository)
|
||||
private notificationsRepository: NotificationsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@@ -34,54 +36,36 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async readNotification(
|
||||
public async readAllNotification(
|
||||
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
|
||||
const result = await this.notificationsRepository.update({
|
||||
notifieeId: userId,
|
||||
id: In(notificationIds),
|
||||
isRead: false,
|
||||
}, {
|
||||
isRead: true,
|
||||
});
|
||||
if (latestNotificationId == null) return;
|
||||
|
||||
if (result.affected === 0) return;
|
||||
this.redisClient.set(`latestReadNotification:${userId}`, latestNotificationId);
|
||||
|
||||
if (!await this.userEntityService.getHasUnreadNotification(userId)) return this.postReadAllNotifications(userId);
|
||||
else return this.postReadNotifications(userId, notificationIds);
|
||||
}
|
||||
|
||||
@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);
|
||||
if (force || latestReadNotificationId == null || (latestReadNotificationId < latestNotificationId)) {
|
||||
return this.postReadAllNotifications(userId);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private postReadAllNotifications(userId: User['id']) {
|
||||
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
|
||||
@@ -90,45 +74,43 @@ export class NotificationService implements OnApplicationShutdown {
|
||||
type: Notification['type'],
|
||||
data: Partial<Notification>,
|
||||
): Promise<Notification | null> {
|
||||
if (data.notifierId && (notifieeId === data.notifierId)) {
|
||||
return null;
|
||||
const profile = await this.cacheService.userProfileCache.fetch(notifieeId);
|
||||
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 isMuted = profile?.mutingNotificationTypes.includes(type);
|
||||
|
||||
// Create notification
|
||||
const notification = await this.notificationsRepository.insert({
|
||||
const notification = {
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
notifieeId: notifieeId,
|
||||
type: type,
|
||||
// 相手がこの通知をミュートしているようなら、既読を予めつけておく
|
||||
isRead: isMuted,
|
||||
...data,
|
||||
} as Partial<Notification>)
|
||||
.then(x => this.notificationsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
} as Notification;
|
||||
|
||||
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
|
||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||
|
||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const fresh = await this.notificationsRepository.findOneBy({ id: notification.id });
|
||||
if (fresh == null) 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
|
||||
setTimeout(2000, 'unread notification', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const latestReadNotificationId = await this.redisClient.get(`latestReadNotification:${notifieeId}`);
|
||||
if (latestReadNotificationId && (latestReadNotificationId >= await redisIdPromise)) return;
|
||||
|
||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||
|
@@ -15,10 +15,6 @@ type PushNotificationsTypes = {
|
||||
antenna: { id: string, name: string };
|
||||
note: Packed<'Note'>;
|
||||
};
|
||||
'readNotifications': { notificationIds: string[] };
|
||||
'readAllNotifications': undefined;
|
||||
'readAntenna': { antennaId: string };
|
||||
'readAllAntennas': undefined;
|
||||
};
|
||||
|
||||
// Reduce length because push message servers have character limits
|
||||
@@ -72,14 +68,6 @@ export class PushNotificationService {
|
||||
});
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
// Continue if sendReadMessage is false
|
||||
if ([
|
||||
'readNotifications',
|
||||
'readAllNotifications',
|
||||
'readAntenna',
|
||||
'readAllAntennas',
|
||||
].includes(type) && !subscription.sendReadMessage) continue;
|
||||
|
||||
const pushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
|
@@ -8,13 +8,13 @@ import type { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, End
|
||||
function q<T>(config: Config, name: string, limitPerSec = -1) {
|
||||
return new Bull<T>(name, {
|
||||
redis: {
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
db: config.redis.db ?? 0,
|
||||
port: config.redisForJobQueue.port,
|
||||
host: config.redisForJobQueue.host,
|
||||
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||
password: config.redisForJobQueue.pass,
|
||||
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 ? {
|
||||
max: limitPerSec,
|
||||
duration: 1000,
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull } from 'typeorm';
|
||||
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 type { RemoteUser, User } from '@/models/entities/User.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 { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'like': '👍',
|
||||
@@ -58,9 +60,6 @@ export class ReactionService {
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@@ -72,6 +71,7 @@ export class ReactionService {
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private metaService: MetaService,
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
@@ -102,7 +102,6 @@ export class ReactionService {
|
||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote') && (user.host != null))) {
|
||||
reaction = '❤️';
|
||||
} else {
|
||||
// TODO: cache
|
||||
reaction = await this.toDbReaction(reaction, user.host);
|
||||
}
|
||||
|
||||
@@ -147,25 +146,31 @@ export class ReactionService {
|
||||
.where('id = :id', { id: note.id })
|
||||
.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 emoji = await this.emojisRepository.findOne({
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
host: decodedReaction.host ?? IsNull(),
|
||||
},
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
});
|
||||
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
|
||||
: await this.emojisRepository.findOne(
|
||||
{
|
||||
where: {
|
||||
name: decodedReaction.name,
|
||||
host: decodedReaction.host,
|
||||
},
|
||||
});
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||
reaction: decodedReaction.reaction,
|
||||
emoji: emoji != null ? {
|
||||
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
||||
emoji: customEmoji != null ? {
|
||||
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl || emoji.originalUrl,
|
||||
url: customEmoji.publicUrl || customEmoji.originalUrl,
|
||||
} : null,
|
||||
userId: user.id,
|
||||
});
|
||||
@@ -251,12 +256,6 @@ export class ReactionService {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getFallbackReaction(): Promise<string> {
|
||||
const meta = await this.metaService.fetch();
|
||||
return meta.useStarForReactionFallback ? '⭐' : '👍';
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public convertLegacyReactions(reactions: Record<string, number>) {
|
||||
const _reactions = {} as Record<string, number>;
|
||||
@@ -290,7 +289,7 @@ export class ReactionService {
|
||||
|
||||
@bindThis
|
||||
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);
|
||||
|
||||
@@ -310,15 +309,17 @@ export class ReactionService {
|
||||
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
host: reacterHost ?? IsNull(),
|
||||
name,
|
||||
});
|
||||
const emoji = reacterHost == null
|
||||
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
||||
: await this.emojisRepository.findOneBy({
|
||||
host: reacterHost,
|
||||
name,
|
||||
});
|
||||
|
||||
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
||||
}
|
||||
|
||||
return await this.getFallbackReaction();
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -3,7 +3,7 @@ import { IsNull } from 'typeorm';
|
||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||
import type { RelaysRepository, UsersRepository } from '@/models/index.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 { QueueService } from '@/core/QueueService.js';
|
||||
import { CreateSystemUserService } from '@/core/CreateSystemUserService.js';
|
||||
@@ -16,7 +16,7 @@ const ACTOR_USERNAME = 'relay.actor' as const;
|
||||
|
||||
@Injectable()
|
||||
export class RelayService {
|
||||
private relaysCache: Cache<Relay[]>;
|
||||
private relaysCache: MemorySingleCache<Relay[]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
@@ -30,7 +30,7 @@ export class RelayService {
|
||||
private createSystemUserService: CreateSystemUserService,
|
||||
private apRendererService: ApRendererService,
|
||||
) {
|
||||
this.relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
|
||||
this.relaysCache = new MemorySingleCache<Relay[]>(1000 * 60 * 10);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -109,7 +109,7 @@ export class RelayService {
|
||||
public async deliverToRelays(user: { id: User['id']; host: null; }, activity: any): Promise<void> {
|
||||
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',
|
||||
}));
|
||||
if (relays.length === 0) return;
|
||||
|
@@ -2,12 +2,12 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { In } from 'typeorm';
|
||||
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 { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
@@ -57,15 +57,15 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown {
|
||||
private rolesCache: Cache<Role[]>;
|
||||
private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
|
||||
private rolesCache: MemorySingleCache<Role[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<RoleAssignment[]>;
|
||||
|
||||
public static AlreadyAssignedError = class extends Error {};
|
||||
public static NotAssignedError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
@@ -77,17 +77,17 @@ export class RoleService implements OnApplicationShutdown {
|
||||
private roleAssignmentsRepository: RoleAssignmentsRepository,
|
||||
|
||||
private metaService: MetaService,
|
||||
private userCacheService: UserCacheService,
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.rolesCache = new Cache<Role[]>(Infinity);
|
||||
this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
|
||||
this.rolesCache = new MemorySingleCache<Role[]>(Infinity);
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<RoleAssignment[]>(Infinity);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -98,7 +98,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'roleCreated': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
const cached = this.rolesCache.get();
|
||||
if (cached) {
|
||||
cached.push({
|
||||
...body,
|
||||
@@ -110,7 +110,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
break;
|
||||
}
|
||||
case 'roleUpdated': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
const cached = this.rolesCache.get();
|
||||
if (cached) {
|
||||
const i = cached.findIndex(x => x.id === body.id);
|
||||
if (i > -1) {
|
||||
@@ -125,9 +125,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||
break;
|
||||
}
|
||||
case 'roleDeleted': {
|
||||
const cached = this.rolesCache.get(null);
|
||||
const cached = this.rolesCache.get();
|
||||
if (cached) {
|
||||
this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
|
||||
this.rolesCache.set(cached.filter(x => x.id !== body.id));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -192,6 +192,12 @@ export class RoleService implements OnApplicationShutdown {
|
||||
case 'followingMoreThanOrEq': {
|
||||
return user.followingCount >= value.value;
|
||||
}
|
||||
case 'notesLessThanOrEq': {
|
||||
return user.notesCount <= value.value;
|
||||
}
|
||||
case 'notesMoreThanOrEq': {
|
||||
return user.notesCount >= value.value;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -208,9 +214,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
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 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));
|
||||
return [...assignedRoles, ...matchedCondRoles];
|
||||
}
|
||||
@@ -225,11 +231,11 @@ export class RoleService implements OnApplicationShutdown {
|
||||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
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 badgeCondRoles = roles.filter(r => r.asBadge && (r.target === 'conditional'));
|
||||
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));
|
||||
return [...assignedBadgeRoles, ...matchedBadgeCondRoles];
|
||||
} else {
|
||||
@@ -295,7 +301,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
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 assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||
roleId: In(moderatorRoles.map(r => r.id)),
|
||||
@@ -315,7 +321,7 @@ export class RoleService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
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 assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
|
||||
roleId: In(administratorRoles.map(r => r.id)),
|
||||
@@ -394,6 +400,6 @@ export class RoleService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
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 * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
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 type { Config } from '@/config.js';
|
||||
import type { Meta } from '@/models/entities/Meta.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { DeleteObjectCommandInput, PutObjectCommandInput } from '@aws-sdk/client-s3';
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
@@ -18,25 +23,47 @@ export class S3Service {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getS3(meta: Meta) {
|
||||
public getS3Client(meta: Meta): S3Client {
|
||||
const u = meta.objectStorageEndpoint
|
||||
? `${meta.objectStorageUseSSL ? 'https://' : 'http://'}${meta.objectStorageEndpoint}`
|
||||
: `${meta.objectStorageUseSSL ? 'https://' : 'http://'}example.net`;
|
||||
? `${meta.objectStorageUseSSL ? 'https' : 'http'}://${meta.objectStorageEndpoint}`
|
||||
: `${meta.objectStorageUseSSL ? 'https' : 'http'}://example.net`; // dummy url to select http(s) agent
|
||||
|
||||
return new S3({
|
||||
endpoint: meta.objectStorageEndpoint && meta.objectStorageEndpoint.length > 0
|
||||
? meta.objectStorageEndpoint
|
||||
: undefined,
|
||||
accessKeyId: meta.objectStorageAccessKey!,
|
||||
secretAccessKey: meta.objectStorageSecretKey!,
|
||||
region: meta.objectStorageRegion ?? undefined,
|
||||
sslEnabled: meta.objectStorageUseSSL,
|
||||
s3ForcePathStyle: !meta.objectStorageEndpoint // AWS with endPoint omitted
|
||||
? false
|
||||
: meta.objectStorageS3ForcePathStyle,
|
||||
httpOptions: {
|
||||
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||
},
|
||||
const agent = this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy);
|
||||
const handlerOption: NodeHttpHandlerOptions = {};
|
||||
if (meta.objectStorageUseSSL) {
|
||||
handlerOption.httpsAgent = agent as https.Agent;
|
||||
} else {
|
||||
handlerOption.httpAgent = agent as http.Agent;
|
||||
}
|
||||
|
||||
return new S3Client({
|
||||
endpoint: meta.objectStorageEndpoint ? u : undefined,
|
||||
credentials: (meta.objectStorageAccessKey !== null && meta.objectStorageSecretKey !== null) ? {
|
||||
accessKeyId: meta.objectStorageAccessKey,
|
||||
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 Redis from 'ioredis';
|
||||
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserFollowingService } from '@/core/UserFollowingService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBlockingService implements OnApplicationShutdown {
|
||||
export class UserBlockingService implements OnModuleInit {
|
||||
private logger: Logger;
|
||||
|
||||
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
||||
private blockingsByUserIdCache: Cache<User['id'][]>;
|
||||
private userFollowingService: UserFollowingService;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@@ -47,47 +37,20 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('user-block');
|
||||
|
||||
this.blockingsByUserIdCache = new Cache<User['id'][]>(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 '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;
|
||||
}
|
||||
}
|
||||
onModuleInit() {
|
||||
this.userFollowingService = this.moduleRef.get('UserFollowingService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -95,8 +58,8 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
await Promise.all([
|
||||
this.cancelRequest(blocker, blockee),
|
||||
this.cancelRequest(blockee, blocker),
|
||||
this.unFollow(blocker, blockee),
|
||||
this.unFollow(blockee, blocker),
|
||||
this.userFollowingService.unfollow(blocker, blockee),
|
||||
this.userFollowingService.unfollow(blockee, blocker),
|
||||
this.removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
@@ -111,6 +74,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
@@ -148,7 +114,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
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'));
|
||||
@@ -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
|
||||
private async removeFromList(listOwner: User, user: User) {
|
||||
const userLists = await this.userListsRepository.findBy({
|
||||
@@ -254,6 +171,9 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
@@ -268,17 +188,6 @@ export class UserBlockingService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
|
||||
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);
|
||||
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||
}
|
||||
}
|
||||
|
@@ -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 { IdentifiableError } from '@/misc/identifiable-error.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 { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
@@ -35,8 +38,12 @@ type Remote = RemoteUser | {
|
||||
type Both = Local | Remote;
|
||||
|
||||
@Injectable()
|
||||
export class UserFollowingService {
|
||||
export class UserFollowingService implements OnModuleInit {
|
||||
private userBlockingService: UserBlockingService;
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -52,11 +59,12 @@ export class UserFollowingService {
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private userEntityService: UserEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private metaService: MetaService,
|
||||
private notificationService: NotificationService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
@@ -66,6 +74,10 @@ export class UserFollowingService {
|
||||
) {
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.userBlockingService = this.moduleRef.get('UserBlockingService');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(_follower: { id: User['id'] }, _followee: { id: User['id'] }, requestId?: string): Promise<void> {
|
||||
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({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
@@ -200,14 +214,18 @@ export class UserFollowingService {
|
||||
|
||||
//#region Update instance stats
|
||||
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.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)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(i => {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
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
|
||||
@@ -219,7 +237,6 @@ export class UserFollowingService {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
this.globalEventService.publishUserEvent(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'));
|
||||
@@ -273,6 +290,8 @@ export class UserFollowingService {
|
||||
|
||||
await this.followingsRepository.delete(following.id);
|
||||
|
||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||
|
||||
this.decrementFollowing(follower, followee);
|
||||
|
||||
// Publish unfollow event
|
||||
@@ -280,7 +299,6 @@ export class UserFollowingService {
|
||||
this.userEntityService.pack(followee.id, 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'));
|
||||
@@ -320,14 +338,18 @@ export class UserFollowingService {
|
||||
|
||||
//#region Update instance stats
|
||||
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.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)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(i => {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
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
|
||||
@@ -569,7 +591,6 @@ export class UserFollowingService {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishUserEvent(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'));
|
||||
|
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 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 { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserMutingService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
}
|
||||
|
||||
@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({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: expiresAt ?? null,
|
||||
muterId: user.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 {
|
||||
cleanup();
|
||||
}
|
||||
|
@@ -13,14 +13,14 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
private webhooks: Webhook[] = [];
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
@Inject(DI.redisForPubsub)
|
||||
private redisForPubsub: Redis.Redis,
|
||||
|
||||
@Inject(DI.webhooksRepository)
|
||||
private webhooksRepository: WebhooksRepository,
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
this.redisForPubsub.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -82,6 +82,6 @@ export class WebhookService implements OnApplicationShutdown {
|
||||
|
||||
@bindThis
|
||||
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 type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.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 { UserCacheService } from '@/core/UserCacheService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RemoteUser, User } from '@/models/entities/User.js';
|
||||
@@ -31,8 +31,8 @@ export type UriParseResult = {
|
||||
|
||||
@Injectable()
|
||||
export class ApDbResolverService {
|
||||
private publicKeyCache: Cache<UserPublickey | null>;
|
||||
private publicKeyByUserIdCache: Cache<UserPublickey | null>;
|
||||
private publicKeyCache: MemoryKVCache<UserPublickey | null>;
|
||||
private publicKeyByUserIdCache: MemoryKVCache<UserPublickey | null>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@@ -47,11 +47,11 @@ export class ApDbResolverService {
|
||||
@Inject(DI.userPublickeysRepository)
|
||||
private userPublickeysRepository: UserPublickeysRepository,
|
||||
|
||||
private userCacheService: UserCacheService,
|
||||
private cacheService: CacheService,
|
||||
private apPersonService: ApPersonService,
|
||||
) {
|
||||
this.publicKeyCache = new Cache<UserPublickey | null>(Infinity);
|
||||
this.publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
|
||||
this.publicKeyCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
||||
this.publicKeyByUserIdCache = new MemoryKVCache<UserPublickey | null>(Infinity);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -107,11 +107,11 @@ export class ApDbResolverService {
|
||||
if (parsed.local) {
|
||||
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,
|
||||
}).then(x => x ?? undefined)) ?? null;
|
||||
} 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,
|
||||
}));
|
||||
}
|
||||
@@ -138,7 +138,7 @@ export class ApDbResolverService {
|
||||
if (key == null) return null;
|
||||
|
||||
return {
|
||||
user: await this.userCacheService.findById(key.userId) as RemoteUser,
|
||||
user: await this.cacheService.findUserById(key.userId) as RemoteUser,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
@@ -14,13 +14,15 @@ import type { NoteReaction } from '@/models/entities/NoteReaction.js';
|
||||
import type { Emoji } from '@/models/entities/Emoji.js';
|
||||
import type { Poll } from '@/models/entities/Poll.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 { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { UserKeypair } from '@/models/entities/UserKeypair.js';
|
||||
import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, EmojisRepository, PollsRepository } from '@/models/index.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 { 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';
|
||||
@@ -50,10 +52,11 @@ export class ApRendererService {
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
private userEntityService: UserEntityService,
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
private ldSignatureService: LdSignatureService,
|
||||
private userKeypairStoreService: UserKeypairStoreService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private apMfmService: ApMfmService,
|
||||
private mfmService: MfmService,
|
||||
) {
|
||||
@@ -272,11 +275,7 @@ export class ApRendererService {
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replaceAll(':', '');
|
||||
// TODO: cache
|
||||
const emoji = await this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
});
|
||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||
|
||||
if (emoji) object.tag = [this.renderEmoji(emoji)];
|
||||
}
|
||||
@@ -473,7 +472,7 @@ export class ApRendererService {
|
||||
...hashtagTags,
|
||||
];
|
||||
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const person = {
|
||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||
@@ -640,7 +639,7 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
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();
|
||||
ldSignature.debug = false;
|
||||
@@ -701,13 +700,9 @@ export class ApRendererService {
|
||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||
if (names == null || names.length === 0) return [];
|
||||
|
||||
const emojis = await Promise.all(
|
||||
names.map(name => this.emojisRepository.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
})),
|
||||
);
|
||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||
|
||||
return emojis.filter(emoji => emoji != null) as Emoji[];
|
||||
return emojis;
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -131,7 +131,7 @@ export class ApRequestService {
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private userKeypairStoreService: UserKeypairStoreService,
|
||||
private userKeypairService: UserKeypairService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
@@ -143,7 +143,7 @@ export class ApRequestService {
|
||||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
||||
const body = JSON.stringify(object);
|
||||
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const req = ApRequestCreator.createSignedPost({
|
||||
key: {
|
||||
@@ -170,7 +170,7 @@ export class ApRequestService {
|
||||
*/
|
||||
@bindThis
|
||||
public async signedGet(url: string, user: { id: User['id'] }) {
|
||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const req = ApRequestCreator.createSignedGet({
|
||||
key: {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
@@ -341,15 +342,17 @@ export class ApNoteService {
|
||||
if (!tags) return [];
|
||||
|
||||
const eomjiTags = toArray(tags).filter(isEmoji);
|
||||
|
||||
const existingEmojis = await this.emojisRepository.findBy({
|
||||
host,
|
||||
name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))),
|
||||
});
|
||||
|
||||
return await Promise.all(eomjiTags.map(async tag => {
|
||||
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
|
||||
const name = tag.name!.replaceAll(':', '');
|
||||
tag.icon = toSingle(tag.icon);
|
||||
|
||||
const exists = await this.emojisRepository.findOneBy({
|
||||
host,
|
||||
name,
|
||||
});
|
||||
const exists = existingEmojis.find(x => x.name === name);
|
||||
|
||||
if (exists) {
|
||||
if ((tag.updated != null && exists.updatedAt == null)
|
||||
|
@@ -8,7 +8,7 @@ import type { Config } from '@/config.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { User } from '@/models/entities/User.js';
|
||||
import { truncate } from '@/misc/truncate.js';
|
||||
import type { UserCacheService } from '@/core/UserCacheService.js';
|
||||
import type { CacheService } from '@/core/CacheService.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import type Logger from '@/logger.js';
|
||||
@@ -30,6 +30,8 @@ import { StatusError } from '@/misc/status-error.js';
|
||||
import type { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
@@ -48,11 +50,13 @@ const summaryLength = 2048;
|
||||
export class ApPersonService implements OnModuleInit {
|
||||
private utilityService: UtilityService;
|
||||
private userEntityService: UserEntityService;
|
||||
private driveFileEntityService: DriveFileEntityService;
|
||||
private idService: IdService;
|
||||
private globalEventService: GlobalEventService;
|
||||
private metaService: MetaService;
|
||||
private federatedInstanceService: FederatedInstanceService;
|
||||
private fetchInstanceMetadataService: FetchInstanceMetadataService;
|
||||
private userCacheService: UserCacheService;
|
||||
private cacheService: CacheService;
|
||||
private apResolverService: ApResolverService;
|
||||
private apNoteService: ApNoteService;
|
||||
private apImageService: ApImageService;
|
||||
@@ -92,9 +96,10 @@ export class ApPersonService implements OnModuleInit {
|
||||
//private userEntityService: UserEntityService,
|
||||
//private idService: IdService,
|
||||
//private globalEventService: GlobalEventService,
|
||||
//private metaService: MetaService,
|
||||
//private federatedInstanceService: FederatedInstanceService,
|
||||
//private fetchInstanceMetadataService: FetchInstanceMetadataService,
|
||||
//private userCacheService: UserCacheService,
|
||||
//private cacheService: CacheService,
|
||||
//private apResolverService: ApResolverService,
|
||||
//private apNoteService: ApNoteService,
|
||||
//private apImageService: ApImageService,
|
||||
@@ -110,11 +115,13 @@ export class ApPersonService implements OnModuleInit {
|
||||
onModuleInit() {
|
||||
this.utilityService = this.moduleRef.get('UtilityService');
|
||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||
this.idService = this.moduleRef.get('IdService');
|
||||
this.globalEventService = this.moduleRef.get('GlobalEventService');
|
||||
this.metaService = this.moduleRef.get('MetaService');
|
||||
this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService');
|
||||
this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService');
|
||||
this.userCacheService = this.moduleRef.get('UserCacheService');
|
||||
this.cacheService = this.moduleRef.get('CacheService');
|
||||
this.apResolverService = this.moduleRef.get('ApResolverService');
|
||||
this.apNoteService = this.moduleRef.get('ApNoteService');
|
||||
this.apImageService = this.moduleRef.get('ApImageService');
|
||||
@@ -203,14 +210,14 @@ export class ApPersonService implements OnModuleInit {
|
||||
public async fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
const cached = this.userCacheService.uriPersonCache.get(uri);
|
||||
const cached = this.cacheService.uriPersonCache.get(uri);
|
||||
if (cached) return cached;
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
if (uri.startsWith(this.config.url + '/')) {
|
||||
const id = uri.split('/').pop();
|
||||
const u = await this.usersRepository.findOneBy({ id });
|
||||
if (u) this.userCacheService.uriPersonCache.set(uri, u);
|
||||
if (u) this.cacheService.uriPersonCache.set(uri, u);
|
||||
return u;
|
||||
}
|
||||
|
||||
@@ -218,7 +225,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
const exist = await this.usersRepository.findOneBy({ uri });
|
||||
|
||||
if (exist) {
|
||||
this.userCacheService.uriPersonCache.set(uri, exist);
|
||||
this.cacheService.uriPersonCache.set(uri, exist);
|
||||
return exist;
|
||||
}
|
||||
//#endregion
|
||||
@@ -327,10 +334,12 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.federatedInstanceService.fetch(host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
this.instanceChart.newUser(i.host);
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
if ((await this.metaService.fetch()).enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
});
|
||||
|
||||
this.usersChart.update(user!, true);
|
||||
@@ -350,32 +359,44 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
const avatarId = avatar ? avatar.id : null;
|
||||
const bannerId = banner ? banner.id : null;
|
||||
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
||||
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
||||
const avatarBlurhash = avatar ? avatar.blurhash : null;
|
||||
const bannerBlurhash = banner ? banner.blurhash : null;
|
||||
|
||||
await this.usersRepository.update(user!.id, {
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarUrl,
|
||||
bannerUrl,
|
||||
avatarBlurhash,
|
||||
bannerBlurhash,
|
||||
});
|
||||
|
||||
user!.avatarId = avatarId;
|
||||
user!.bannerId = bannerId;
|
||||
//#endregion
|
||||
user!.avatarId = avatarId;
|
||||
user!.bannerId = bannerId;
|
||||
user!.avatarUrl = avatarUrl;
|
||||
user!.bannerUrl = bannerUrl;
|
||||
user!.avatarBlurhash = avatarBlurhash;
|
||||
user!.bannerBlurhash = bannerBlurhash;
|
||||
//#endregion
|
||||
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||
this.logger.info(`extractEmojis: ${err}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||
this.logger.info(`extractEmojis: ${err}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
|
||||
await this.usersRepository.update(user!.id, {
|
||||
emojis: emojiNames,
|
||||
});
|
||||
//#endregion
|
||||
await this.usersRepository.update(user!.id, {
|
||||
emojis: emojiNames,
|
||||
});
|
||||
//#endregion
|
||||
|
||||
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
|
||||
await this.updateFeatured(user!.id, resolver).catch(err => this.logger.error(err));
|
||||
|
||||
return user!;
|
||||
return user!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -457,10 +478,14 @@ export class ApPersonService implements OnModuleInit {
|
||||
|
||||
if (avatar) {
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
}
|
||||
|
||||
if (banner) {
|
||||
updates.bannerId = banner.id;
|
||||
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
||||
updates.bannerBlurhash = banner.blurhash;
|
||||
}
|
||||
|
||||
// Update user
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AntennaNotesRepository, AntennasRepository } from '@/models/index.js';
|
||||
import type { AntennasRepository } from '@/models/index.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { Antenna } from '@/models/entities/Antenna.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -10,9 +10,6 @@ export class AntennaEntityService {
|
||||
constructor(
|
||||
@Inject(DI.antennasRepository)
|
||||
private antennasRepository: AntennasRepository,
|
||||
|
||||
@Inject(DI.antennaNotesRepository)
|
||||
private antennaNotesRepository: AntennaNotesRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -22,8 +19,6 @@ export class AntennaEntityService {
|
||||
): Promise<Packed<'Antenna'>> {
|
||||
const antenna = typeof src === 'object' ? src : await this.antennasRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const hasUnreadNote = (await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false })) != null;
|
||||
|
||||
return {
|
||||
id: antenna.id,
|
||||
createdAt: antenna.createdAt.toISOString(),
|
||||
@@ -38,7 +33,7 @@ export class AntennaEntityService {
|
||||
withReplies: antenna.withReplies,
|
||||
withFile: antenna.withFile,
|
||||
isActive: antenna.isActive,
|
||||
hasUnreadNote,
|
||||
hasUnreadNote: false, // TODO
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user