Compare commits
212 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
667b324572 | ||
|
|
ab9969283b | ||
|
|
7dc90e078e | ||
|
|
69ffa0e520 | ||
|
|
b48ae3e9fa | ||
|
|
b4b72ab2df | ||
|
|
3d3cf5bd7a | ||
|
|
e6e48fb6bc | ||
|
|
495a19540b | ||
|
|
51afbbaf72 | ||
|
|
f0139ae1e0 | ||
|
|
97d17c537b | ||
|
|
82376f312d | ||
|
|
fd2af6dfe6 | ||
|
|
387dc4bb4b | ||
|
|
536060e63c | ||
|
|
0efd5eff2b | ||
|
|
51bc393d58 | ||
|
|
385846d43d | ||
|
|
aa220a0411 | ||
|
|
52976588a7 | ||
|
|
22bb09c6ed | ||
|
|
150d949a3e | ||
|
|
9f640beecc | ||
|
|
7aba846446 | ||
|
|
9309872cff | ||
|
|
3ea85b14a3 | ||
|
|
3164e7b4fc | ||
|
|
3a020d53d1 | ||
|
|
b6db4ef88c | ||
|
|
92db359654 | ||
|
|
531a003a2a | ||
|
|
436e93540a | ||
|
|
cd10e98937 | ||
|
|
ebb6ac195f | ||
|
|
b7e6e13b8d | ||
|
|
16909ed6bd | ||
|
|
1f53eb2ed1 | ||
|
|
1626e50fbf | ||
|
|
fc277839b6 | ||
|
|
57b31366e5 | ||
|
|
2e3eaaddcc | ||
|
|
1ec5e846c5 | ||
|
|
f1168f0165 | ||
|
|
4f1694cd99 | ||
|
|
1e99782666 | ||
|
|
face6527f2 | ||
|
|
43d87270d9 | ||
|
|
3e72d99cf9 | ||
|
|
6f8736c1af | ||
|
|
baf19420dd | ||
|
|
b951b31ef5 | ||
|
|
4ec6bffca7 | ||
|
|
c5f572dcfd | ||
|
|
f4ec837d6e | ||
|
|
f115116454 | ||
|
|
9eb98ae8a5 | ||
|
|
3f5ea11a1f | ||
|
|
4708c0abef | ||
|
|
b9fd7e1b77 | ||
|
|
2afbd251e1 | ||
|
|
ab97b91606 | ||
|
|
e38e408b97 | ||
|
|
0de009f946 | ||
|
|
a47590e64c | ||
|
|
1fb1875ac3 | ||
|
|
b477de1d98 | ||
|
|
b0420c948c | ||
|
|
b4a278ae54 | ||
|
|
a51fef29c0 | ||
|
|
8e07eb7f44 | ||
|
|
caaa78d98d | ||
|
|
0ea0466313 | ||
|
|
3ae9f4e8e6 | ||
|
|
59afb56b5b | ||
|
|
a596718bbf | ||
|
|
ab992422a8 | ||
|
|
3faad0a5e5 | ||
|
|
ebdfb2feb7 | ||
|
|
dbab122a99 | ||
|
|
e3b826db5a | ||
|
|
7e3f519a5b | ||
|
|
6b54405003 | ||
|
|
e32fb4e86d | ||
|
|
2b9c3f0d5c | ||
|
|
ae7b90de6c | ||
|
|
d74cf9e4ff | ||
|
|
9d5bc6cb28 | ||
|
|
9d3321fca4 | ||
|
|
2bbccde2ce | ||
|
|
47eb0daebb | ||
|
|
fd47bf3483 | ||
|
|
9bf18546fc | ||
|
|
fadcabeaa6 | ||
|
|
2ac36e4a5c | ||
|
|
4b5a400264 | ||
|
|
5b72c08a68 | ||
|
|
9f3b97effb | ||
|
|
34a5dbe21b | ||
|
|
241b186a8a | ||
|
|
a150bc53ab | ||
|
|
4b503f88e1 | ||
|
|
faf1b3559a | ||
|
|
2fb2e52312 | ||
|
|
2a4c432f41 | ||
|
|
4c6cec552e | ||
|
|
c48faca707 | ||
|
|
c9afaba0d4 | ||
|
|
5b48032681 | ||
|
|
83472dbd82 | ||
|
|
2b0a622875 | ||
|
|
4a43e1a9e9 | ||
|
|
1ca350e45d | ||
|
|
38e30c0d54 | ||
|
|
38787712d9 | ||
|
|
0515fed92d | ||
|
|
1d16656b39 | ||
|
|
ed6c781426 | ||
|
|
8cbc0761db | ||
|
|
455ccc660e | ||
|
|
a40b77a66b | ||
|
|
194bc20af1 | ||
|
|
ca94959fff | ||
|
|
bcc20d6dc4 | ||
|
|
0de7a084a9 | ||
|
|
cfc3ab4b04 | ||
|
|
8f42e8434e | ||
|
|
dff465000c | ||
|
|
0f6d26e065 | ||
|
|
cc394d9a4b | ||
|
|
c9934c379f | ||
|
|
eb1e326813 | ||
|
|
a62e4f1cf2 | ||
|
|
dcd5b6d972 | ||
|
|
fedf0d7e20 | ||
|
|
984cfe358d | ||
|
|
aabb1945e8 | ||
|
|
4e0f7ced84 | ||
|
|
41536480ce | ||
|
|
59e160147f | ||
|
|
a38d8a91a1 | ||
|
|
6027b516e1 | ||
|
|
757d9aa5ee | ||
|
|
36af07abe2 | ||
|
|
23c4aa2571 | ||
|
|
1758f29364 | ||
|
|
fa3cf6c299 | ||
|
|
4b556efdaa | ||
|
|
b0834ebf55 | ||
|
|
2234fbcb11 | ||
|
|
8e90484b3e | ||
|
|
0fcb23c4c1 | ||
|
|
776f6fd1f5 | ||
|
|
7b3e3f8e25 | ||
|
|
360d71278a | ||
|
|
663c06be00 | ||
|
|
7ccccf5545 | ||
|
|
f36f4b5398 | ||
|
|
cc4e99fdde | ||
|
|
5764fa55cb | ||
|
|
74565f67f7 | ||
|
|
408e782507 | ||
|
|
cbf8cc376e | ||
|
|
c04f344049 | ||
|
|
b9080da75d | ||
|
|
4d925fc086 | ||
|
|
b74e2e9167 | ||
|
|
ebea1a2962 | ||
|
|
4c432c07cb | ||
|
|
322b3b677f | ||
|
|
1c7e05ce9e | ||
|
|
9ab25ede28 | ||
|
|
174dfb83d0 | ||
|
|
ad8e8793c7 | ||
|
|
1e14612f0e | ||
|
|
9090b745e6 | ||
|
|
d883934826 | ||
|
|
e0bb796aff | ||
|
|
fb54546573 | ||
|
|
9e0b759197 | ||
|
|
41c500851b | ||
|
|
482538c7f8 | ||
|
|
d579687156 | ||
|
|
de970ff54e | ||
|
|
1bfb0dc395 | ||
|
|
da2dfee0a8 | ||
|
|
eaad96aae3 | ||
|
|
0a05841f33 | ||
|
|
68e5b5a84a | ||
|
|
6d6b03dfe2 | ||
|
|
19be113cb4 | ||
|
|
101ca9e0f7 | ||
|
|
906c2863db | ||
|
|
917e67d356 | ||
|
|
cd2e597223 | ||
|
|
03559156b9 | ||
|
|
aebdbf07b4 | ||
|
|
00ab7f5bd1 | ||
|
|
83f780978c | ||
|
|
7f9a151055 | ||
|
|
e0a2e7aedc | ||
|
|
9fe5dc679a | ||
|
|
6ed38f53f5 | ||
|
|
0f07f27642 | ||
|
|
c2c2120b76 | ||
|
|
8477909af2 | ||
|
|
a15e5c52f4 | ||
|
|
1c181df086 | ||
|
|
f5652605ec | ||
|
|
9d3aa6bb41 | ||
|
|
5b64b9001d | ||
|
|
3b89b73d27 |
327 changed files with 5372 additions and 773 deletions
|
|
@ -229,3 +229,8 @@ checkActivityPubGetSignature: false
|
||||||
|
|
||||||
# Upload or download file size limits (bytes)
|
# Upload or download file size limits (bytes)
|
||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
|
# CHMod-style permission bits to apply to uploaded files.
|
||||||
|
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
|
||||||
|
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
|
||||||
|
#filePermissionBits: '644'
|
||||||
|
|
|
||||||
|
|
@ -222,3 +222,8 @@ allowedPrivateNetworks: [
|
||||||
|
|
||||||
# Upload or download file size limits (bytes)
|
# Upload or download file size limits (bytes)
|
||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
|
# CHMod-style permission bits to apply to uploaded files.
|
||||||
|
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
|
||||||
|
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
|
||||||
|
#filePermissionBits: '644'
|
||||||
|
|
|
||||||
|
|
@ -312,3 +312,8 @@ checkActivityPubGetSignature: false
|
||||||
|
|
||||||
# Upload or download file size limits (bytes)
|
# Upload or download file size limits (bytes)
|
||||||
#maxFileSize: 262144000
|
#maxFileSize: 262144000
|
||||||
|
|
||||||
|
# CHMod-style permission bits to apply to uploaded files.
|
||||||
|
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
|
||||||
|
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
|
||||||
|
#filePermissionBits: '644'
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,10 @@ db:
|
||||||
port: 5432
|
port: 5432
|
||||||
|
|
||||||
# Database name
|
# Database name
|
||||||
db: misskey
|
db: sharkey
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
user: example-misskey-user
|
user: sharkey
|
||||||
pass: example-misskey-pass
|
pass: example-misskey-pass
|
||||||
|
|
||||||
# Whether disable Caching queries
|
# Whether disable Caching queries
|
||||||
|
|
@ -334,3 +334,8 @@ checkActivityPubGetSignature: false
|
||||||
|
|
||||||
# PID File of master process
|
# PID File of master process
|
||||||
#pidFile: /tmp/misskey.pid
|
#pidFile: /tmp/misskey.pid
|
||||||
|
|
||||||
|
# CHMod-style permission bits to apply to uploaded files.
|
||||||
|
# Permission bits are specified as a base-8 string representing User/Group/Other permissions.
|
||||||
|
# This setting is only useful for custom deployments, such as using a reverse proxy to serve media.
|
||||||
|
#filePermissionBits: '644'
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,33 @@
|
||||||
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md)
|
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md)
|
||||||
🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -->
|
🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -->
|
||||||
|
|
||||||
**What happened?** _(Please give us a brief description of what happened.)_
|
# **What happened?**
|
||||||
|
<!-- Please give us a brief description of what happened. -->
|
||||||
|
|
||||||
**What did you expect to happen?** _(Please give us a brief description of what you expected to happen.)_
|
# **What did you expect to happen?**
|
||||||
|
<!-- Please give us a brief description of what you expected to happen. -->
|
||||||
|
|
||||||
**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_
|
# **Version**
|
||||||
|
<!-- What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information. -->
|
||||||
|
|
||||||
**Instance** _(What instance of Sharkey are you using?)_
|
# **Instance**
|
||||||
|
<!-- What instance of Sharkey are you using? -->
|
||||||
|
|
||||||
**What type of issue is this?** _(If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side.)_
|
# **What type of issue is this?**
|
||||||
|
<!-- If this happens on your device and has to do with the user interface, it's client-side. If this happens on either with the API or the backend, or you got a server-side error in the client, it's server-side. -->
|
||||||
|
|
||||||
**What browser are you using? (Client-side issues only)**
|
# **What browser are you using? (Client-side issues only)**
|
||||||
|
|
||||||
**What operating system are you using? (Client-side issues only)**
|
# **What operating system are you using? (Client-side issues only)**
|
||||||
|
|
||||||
**How do you deploy Sharkey on your server? (Server-side issues only)**
|
# **How do you deploy Sharkey on your server? (Server-side issues only)**
|
||||||
|
|
||||||
**What operating system are you using? (Server-side issues only)**
|
# **What operating system are you using? (Server-side issues only)**
|
||||||
|
|
||||||
**Relevant log output** _(Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.)_
|
# **Relevant log output**
|
||||||
|
<!-- Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks. -->
|
||||||
|
|
||||||
**Contribution Guidelines**
|
# **Contribution Guidelines**
|
||||||
By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
||||||
- [ ] I agree to follow this project's Contribution Guidelines
|
- [ ] I agree to follow this project's Contribution Guidelines
|
||||||
- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
|
- [ ] I have searched the issue tracker for similar issues, and this is not a duplicate.
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,19 @@
|
||||||
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md)
|
🔒 Found a security vulnerability? [Please disclose it responsibly.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/SECURITY.md)
|
||||||
🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -->
|
🤝 By submitting this feature request, you agree to follow our [Contribution Guidelines.](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md) -->
|
||||||
|
|
||||||
**What feature would you like implemented?** _(Please give us a brief description of what you'd like.)_
|
# **What feature would you like implemented?**
|
||||||
|
<!-- Please give us a brief description of what you'd like. -->
|
||||||
|
|
||||||
**Why should we add this feature?** _(Please give us a brief description of why your feature is important.)_
|
# **Why should we add this feature?**
|
||||||
|
<!-- Please give us a brief description of why your feature is important. -->
|
||||||
|
|
||||||
**Version** _(What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information.)_
|
# **Version**
|
||||||
|
<!-- What version of Sharkey is your instance running? You can find this by clicking your instance's logo at the top left and then clicking instance information. -->
|
||||||
|
|
||||||
**Instance** _(What instance of Sharkey are you using?)_
|
# **Instance**
|
||||||
|
<!-- What instance of Sharkey are you using? -->
|
||||||
|
|
||||||
**Contribution Guidelines**
|
# **Contribution Guidelines**
|
||||||
By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
By submitting this issue, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
||||||
- [ ] I agree to follow this project's Contribution Guidelines
|
- [ ] I agree to follow this project's Contribution Guidelines
|
||||||
- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
|
- [ ] I have searched the issue tracker for similar requests, and this is not a duplicate.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<!-- Thanks for taking the time to make Sharkey better! -->
|
<!-- Thanks for taking the time to make Sharkey better! -->
|
||||||
|
|
||||||
**What does this PR do?** _(Please give us a brief description of what this PR does.)_
|
# **What does this MR do?**
|
||||||
|
<!-- Please give us a brief description of what this PR does. -->
|
||||||
|
|
||||||
**Contribution Guidelines**
|
# **Contribution Guidelines**
|
||||||
By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
By submitting this merge request, you agree to follow our [Contribution Guidelines](https://activitypub.software/TransFem-org/Sharkey/-/blob/develop/CONTRIBUTING.md)
|
||||||
- [ ] I agree to follow this project's Contribution Guidelines
|
- [ ] I agree to follow this project's Contribution Guidelines
|
||||||
- [ ] I have made sure to test this pull request
|
- [ ] I have made sure to test this merge request
|
||||||
|
|
||||||
<!-- Uncomment if your merge request has multiple authors -->
|
<!-- Uncomment if your merge request has multiple authors -->
|
||||||
<!-- Co-authored-by: Name <email@email.com> -->
|
<!-- Co-authored-by: Name <email@email.com> -->
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ RUN apk add ffmpeg tini jemalloc \
|
||||||
&& corepack enable \
|
&& corepack enable \
|
||||||
&& addgroup -g "${GID}" sharkey \
|
&& addgroup -g "${GID}" sharkey \
|
||||||
&& adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \
|
&& adduser -D -u "${UID}" -G sharkey -h /sharkey sharkey \
|
||||||
|
&& mkdir /sharkey/files \
|
||||||
|
&& chown sharkey:sharkey /sharkey/files \
|
||||||
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \
|
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -exec chmod u-s {} \; \
|
||||||
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \;
|
&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /g+s -exec chmod g-s {} \;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,38 @@
|
||||||
# Upgrade Notes
|
# Upgrade Notes
|
||||||
|
|
||||||
|
## 2024.10.0
|
||||||
|
|
||||||
|
### Hellspawns
|
||||||
|
|
||||||
|
Sharkey versions before 2024.10 suffered from a bug in the "Mark instance as NSFW" feature.
|
||||||
|
When a user from such an instance boosted a note, the boost would be converted to a hellspawn (pure renote with Content Warning).
|
||||||
|
Hellspawns are buggy and do not properly federate, so it may be desirable to correct any that already exist in the database.
|
||||||
|
The following script will correct any local or remote hellspawns in the database.
|
||||||
|
|
||||||
|
```postgresql
|
||||||
|
/* Remove "instance is marked as NSFW" hellspawns */
|
||||||
|
UPDATE "note"
|
||||||
|
SET "cw" = null
|
||||||
|
WHERE
|
||||||
|
"renoteId" IS NOT NULL
|
||||||
|
AND "text" IS NULL
|
||||||
|
AND "cw" = 'Instance is marked as NSFW'
|
||||||
|
AND "replyId" IS NULL
|
||||||
|
AND "hasPoll" = false
|
||||||
|
AND "fileIds" = '{}';
|
||||||
|
|
||||||
|
/* Fix legacy / user-created hellspawns */
|
||||||
|
UPDATE "note"
|
||||||
|
SET "text" = '.'
|
||||||
|
WHERE
|
||||||
|
"renoteId" IS NOT NULL
|
||||||
|
AND "text" IS NULL
|
||||||
|
AND "cw" IS NOT NULL
|
||||||
|
AND "replyId" IS NULL
|
||||||
|
AND "hasPoll" = false
|
||||||
|
AND "fileIds" = '{}';
|
||||||
|
```
|
||||||
|
|
||||||
## 2024.9.0
|
## 2024.9.0
|
||||||
|
|
||||||
### Following Feed
|
### Following Feed
|
||||||
|
|
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1733212471,
|
||||||
|
"narHash": "sha256-M1+uCoV5igihRfcUKrr1riygbe73/dzNnzPsmaLCmpo=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "55d15ad12a74eb7d4646254e13638ad0c4128776",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
38
flake.nix
Normal file
38
flake.nix
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs }: {
|
||||||
|
|
||||||
|
nixosConfigurations.container = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules =
|
||||||
|
[
|
||||||
|
( import ./sharkey-service.nix )
|
||||||
|
({ pkgs, ... }: {
|
||||||
|
boot.isContainer = true;
|
||||||
|
|
||||||
|
# Let 'nixos-version --json' know about the Git revision
|
||||||
|
# of this flake.
|
||||||
|
system.configurationRevision = nixpkgs.lib.mkIf (self ? rev) self.rev;
|
||||||
|
|
||||||
|
# Network configuration.
|
||||||
|
networking.useDHCP = false;
|
||||||
|
networking.firewall.allowedTCPPorts = [ 3000 ];
|
||||||
|
|
||||||
|
system.stateVersion = "24.04";
|
||||||
|
|
||||||
|
services.sharkey = {
|
||||||
|
enable = true;
|
||||||
|
package = (pkgs.callPackage ./sharkey.nix {});
|
||||||
|
settings = {
|
||||||
|
url = "https://sharkey.localhost";
|
||||||
|
};
|
||||||
|
redis.createLocally = true;
|
||||||
|
database.createLocally = true;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
8
locales/index.d.ts
vendored
8
locales/index.d.ts
vendored
|
|
@ -9661,6 +9661,10 @@ export interface Locale extends ILocale {
|
||||||
* ロールタイムライン
|
* ロールタイムライン
|
||||||
*/
|
*/
|
||||||
"roleTimeline": string;
|
"roleTimeline": string;
|
||||||
|
/**
|
||||||
|
* Following
|
||||||
|
*/
|
||||||
|
"following": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_dialog": {
|
"_dialog": {
|
||||||
|
|
@ -11374,6 +11378,10 @@ export interface Locale extends ILocale {
|
||||||
* Remote followers may have incomplete or outdated activity
|
* Remote followers may have incomplete or outdated activity
|
||||||
*/
|
*/
|
||||||
"remoteFollowersWarning": string;
|
"remoteFollowersWarning": string;
|
||||||
|
/**
|
||||||
|
* Select a follow relationship...
|
||||||
|
*/
|
||||||
|
"selectFollowRelationship": string;
|
||||||
}
|
}
|
||||||
declare const locales: {
|
declare const locales: {
|
||||||
[lang: string]: Locale;
|
[lang: string]: Locale;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "sharkey",
|
"name": "sharkey",
|
||||||
"version": "2024.9.2",
|
"version": "2024.10.0-dev",
|
||||||
"codename": "shonk",
|
"codename": "shonk",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -66,13 +66,15 @@
|
||||||
"esbuild": "0.23.1",
|
"esbuild": "0.23.1",
|
||||||
"glob": "11.0.0"
|
"glob": "11.0.0"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"cypress": "13.14.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "2.0.3",
|
"@misskey-dev/eslint-plugin": "2.0.3",
|
||||||
"@types/node": "20.14.12",
|
"@types/node": "20.14.12",
|
||||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||||
"@typescript-eslint/parser": "7.17.0",
|
"@typescript-eslint/parser": "7.17.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.14.2",
|
|
||||||
"eslint": "9.8.0",
|
"eslint": "9.8.0",
|
||||||
"globals": "15.9.0",
|
"globals": "15.9.0",
|
||||||
"ncp": "2.0.0",
|
"ncp": "2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@
|
||||||
"@swc/core": "1.6.6",
|
"@swc/core": "1.6.6",
|
||||||
"@transfem-org/sfm-js": "0.24.5",
|
"@transfem-org/sfm-js": "0.24.5",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
|
"@types/psl": "^1.1.3",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
|
|
@ -135,9 +136,9 @@
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"jsonld": "8.3.2",
|
"jsonld": "8.3.2",
|
||||||
"jsrsasign": "11.1.0",
|
"jsrsasign": "11.1.0",
|
||||||
|
"juice": "11.0.0",
|
||||||
"megalodon": "workspace:*",
|
"megalodon": "workspace:*",
|
||||||
"meilisearch": "0.42.0",
|
"meilisearch": "0.42.0",
|
||||||
"juice": "11.0.0",
|
|
||||||
"microformats-parser": "2.0.2",
|
"microformats-parser": "2.0.2",
|
||||||
"mime-types": "2.1.35",
|
"mime-types": "2.1.35",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
|
|
@ -158,6 +159,7 @@
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"proxy-addr": "^2.0.7",
|
"proxy-addr": "^2.0.7",
|
||||||
|
"psl": "^1.13.0",
|
||||||
"pug": "3.0.3",
|
"pug": "3.0.3",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,4 @@ const promises = Array
|
||||||
connectToPostgres()
|
connectToPostgres()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.all(promises);
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@ type Source = {
|
||||||
};
|
};
|
||||||
|
|
||||||
pidFile: string;
|
pidFile: string;
|
||||||
|
filePermissionBits?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Config = {
|
export type Config = {
|
||||||
|
|
@ -212,6 +213,7 @@ export type Config = {
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
pidFile: string;
|
pidFile: string;
|
||||||
|
filePermissionBits?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
|
|
@ -347,6 +349,7 @@ export function loadConfig(): Config {
|
||||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||||
import: config.import,
|
import: config.import,
|
||||||
pidFile: config.pidFile,
|
pidFile: config.pidFile,
|
||||||
|
filePermissionBits: config.filePermissionBits,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,7 +455,10 @@ function applyEnvOverrides(config: Source) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const alwaysStrings = { 'chmodSocket': true } as { [key: string]: boolean };
|
const alwaysStrings: { [key in string]?: boolean } = {
|
||||||
|
'chmodSocket': true,
|
||||||
|
'filePermissionBits': true,
|
||||||
|
};
|
||||||
|
|
||||||
function _assign(path: (string | number)[], lastStep: string | number, value: string) {
|
function _assign(path: (string | number)[], lastStep: string | number, value: string) {
|
||||||
let thisConfig = config as any;
|
let thisConfig = config as any;
|
||||||
|
|
@ -490,7 +496,7 @@ function applyEnvOverrides(config: Source) {
|
||||||
_apply_top(['sentryForBackend', 'enableNodeProfiling']);
|
_apply_top(['sentryForBackend', 'enableNodeProfiling']);
|
||||||
_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
|
_apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]);
|
||||||
_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]);
|
_apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]);
|
||||||
_apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]);
|
_apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile', 'filePermissionBits']]);
|
||||||
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
|
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
|
||||||
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]);
|
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as stream from 'node:stream/promises';
|
import * as stream from 'node:stream/promises';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ipaddr from 'ipaddr.js';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
import { parse } from 'content-disposition';
|
import { parse } from 'content-disposition';
|
||||||
|
|
@ -70,13 +69,6 @@ export class DownloadService {
|
||||||
},
|
},
|
||||||
enableUnixSockets: false,
|
enableUnixSockets: false,
|
||||||
}).on('response', (res: Got.Response) => {
|
}).on('response', (res: Got.Response) => {
|
||||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
|
|
||||||
if (this.isPrivateIp(res.ip)) {
|
|
||||||
this.logger.warn(`Blocked address: ${res.ip}`);
|
|
||||||
req.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = res.headers['content-length'];
|
const contentLength = res.headers['content-length'];
|
||||||
if (contentLength != null) {
|
if (contentLength != null) {
|
||||||
const size = Number(contentLength);
|
const size = Number(contentLength);
|
||||||
|
|
@ -139,18 +131,4 @@ export class DownloadService {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private isPrivateIp(ip: string): boolean {
|
|
||||||
const parsedIp = ipaddr.parse(ip);
|
|
||||||
|
|
||||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
|
||||||
const cidr = ipaddr.parseCIDR(net);
|
|
||||||
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedIp.range() !== 'unicast';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,7 @@ export class EmailService {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
Authorization: truemailAuthKey,
|
Authorization: truemailAuthKey,
|
||||||
},
|
},
|
||||||
|
isLocalAddressAllowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = (await res.json()) as {
|
const json = (await res.json()) as {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import * as net from 'node:net';
|
import * as net from 'node:net';
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
|
@ -25,8 +26,102 @@ export type HttpRequestSendOptions = {
|
||||||
validators?: ((res: Response) => void)[];
|
validators?: ((res: Response) => void)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare module 'node:http' {
|
||||||
|
interface Agent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpRequestServiceAgent extends http.Agent {
|
||||||
|
constructor(
|
||||||
|
private config: Config,
|
||||||
|
options?: http.AgentOptions,
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback)
|
||||||
|
.on('connect', () => {
|
||||||
|
const address = socket.remoteAddress;
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
if (address && ipaddr.isValid(address)) {
|
||||||
|
if (this.isPrivateIp(address)) {
|
||||||
|
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private isPrivateIp(ip: string): boolean {
|
||||||
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
|
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||||
|
const cidr = ipaddr.parseCIDR(net);
|
||||||
|
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedIp.range() !== 'unicast';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpsRequestServiceAgent extends https.Agent {
|
||||||
|
constructor(
|
||||||
|
private config: Config,
|
||||||
|
options?: https.AgentOptions,
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback)
|
||||||
|
.on('connect', () => {
|
||||||
|
const address = socket.remoteAddress;
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
if (address && ipaddr.isValid(address)) {
|
||||||
|
if (this.isPrivateIp(address)) {
|
||||||
|
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private isPrivateIp(ip: string): boolean {
|
||||||
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
|
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||||
|
const cidr = ipaddr.parseCIDR(net);
|
||||||
|
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedIp.range() !== 'unicast';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpRequestService {
|
export class HttpRequestService {
|
||||||
|
/**
|
||||||
|
* Get http non-proxy agent (without local address filtering)
|
||||||
|
*/
|
||||||
|
private httpNative: http.Agent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get https non-proxy agent (without local address filtering)
|
||||||
|
*/
|
||||||
|
private httpsNative: https.Agent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get http non-proxy agent
|
* Get http non-proxy agent
|
||||||
*/
|
*/
|
||||||
|
|
@ -57,19 +152,20 @@ export class HttpRequestService {
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
this.http = new http.Agent({
|
const agentOption = {
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||||
localAddress: config.outgoingAddress,
|
localAddress: config.outgoingAddress,
|
||||||
});
|
};
|
||||||
|
|
||||||
this.https = new https.Agent({
|
this.httpNative = new http.Agent(agentOption);
|
||||||
keepAlive: true,
|
|
||||||
keepAliveMsecs: 30 * 1000,
|
this.httpsNative = new https.Agent(agentOption);
|
||||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
|
||||||
localAddress: config.outgoingAddress,
|
this.http = new HttpRequestServiceAgent(config, agentOption);
|
||||||
});
|
|
||||||
|
this.https = new HttpsRequestServiceAgent(config, agentOption);
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
|
|
@ -104,16 +200,22 @@ export class HttpRequestService {
|
||||||
* @param bypassProxy Allways bypass proxy
|
* @param bypassProxy Allways bypass proxy
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent {
|
||||||
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
||||||
|
if (isLocalAddressAllowed) {
|
||||||
|
return url.protocol === 'http:' ? this.httpNative : this.httpsNative;
|
||||||
|
}
|
||||||
return url.protocol === 'http:' ? this.http : this.https;
|
return url.protocol === 'http:' ? this.http : this.https;
|
||||||
} else {
|
} else {
|
||||||
|
if (isLocalAddressAllowed && (!this.config.proxy)) {
|
||||||
|
return url.protocol === 'http:' ? this.httpNative : this.httpsNative;
|
||||||
|
}
|
||||||
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;
|
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getActivityJson(url: string): Promise<IObject> {
|
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -121,6 +223,7 @@ export class HttpRequestService {
|
||||||
},
|
},
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
size: 1024 * 256,
|
size: 1024 * 256,
|
||||||
|
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||||
}, {
|
}, {
|
||||||
throwErrorWhenResponseNotOk: true,
|
throwErrorWhenResponseNotOk: true,
|
||||||
validators: [validateContentTypeSetAsActivityPub],
|
validators: [validateContentTypeSetAsActivityPub],
|
||||||
|
|
@ -129,13 +232,13 @@ export class HttpRequestService {
|
||||||
const finalUrl = res.url; // redirects may have been involved
|
const finalUrl = res.url; // redirects may have been involved
|
||||||
const activity = await res.json() as IObject;
|
const activity = await res.json() as IObject;
|
||||||
|
|
||||||
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
assertActivityMatchesUrls(activity, [finalUrl]);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>, isLocalAddressAllowed = false): Promise<T> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
|
|
@ -143,19 +246,21 @@ export class HttpRequestService {
|
||||||
}, headers ?? {}),
|
}, headers ?? {}),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
size: 1024 * 256,
|
size: 1024 * 256,
|
||||||
|
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await res.json() as T;
|
return await res.json() as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>, isLocalAddressAllowed = false): Promise<string> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
Accept: accept,
|
Accept: accept,
|
||||||
}, headers ?? {}),
|
}, headers ?? {}),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
|
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await res.text();
|
return await res.text();
|
||||||
|
|
@ -170,6 +275,7 @@ export class HttpRequestService {
|
||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
size?: number,
|
size?: number,
|
||||||
|
isLocalAddressAllowed?: boolean,
|
||||||
} = {},
|
} = {},
|
||||||
extra: HttpRequestSendOptions = {
|
extra: HttpRequestSendOptions = {
|
||||||
throwErrorWhenResponseNotOk: true,
|
throwErrorWhenResponseNotOk: true,
|
||||||
|
|
@ -183,6 +289,8 @@ export class HttpRequestService {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false;
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: args.method ?? 'GET',
|
method: args.method ?? 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -191,7 +299,7 @@ export class HttpRequestService {
|
||||||
},
|
},
|
||||||
body: args.body,
|
body: args.body,
|
||||||
size: args.size ?? 10 * 1024 * 1024,
|
size: args.size ?? 10 * 1024 * 1024,
|
||||||
agent: (url) => this.getAgentByUrl(url),
|
agent: (url) => this.getAgentByUrl(url, false, isLocalAddressAllowed),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { copyFile, mkdir, unlink, writeFile } from 'node:fs/promises';
|
import { copyFile, unlink, writeFile, chmod } from 'node:fs/promises';
|
||||||
import * as Path from 'node:path';
|
import * as Path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
|
|
@ -41,12 +41,20 @@ export class InternalStorageService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async saveFromPath(key: string, srcPath: string): Promise<string> {
|
public async saveFromPath(key: string, srcPath: string): Promise<string> {
|
||||||
await copyFile(srcPath, this.resolvePath(key));
|
await copyFile(srcPath, this.resolvePath(key));
|
||||||
return `${this.config.url}/files/${key}`;
|
return await this.finalizeSavedFile(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async saveFromBuffer(key: string, data: Buffer): Promise<string> {
|
public async saveFromBuffer(key: string, data: Buffer): Promise<string> {
|
||||||
await writeFile(this.resolvePath(key), data);
|
await writeFile(this.resolvePath(key), data);
|
||||||
|
return await this.finalizeSavedFile(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async finalizeSavedFile(key: string): Promise<string> {
|
||||||
|
if (this.config.filePermissionBits) {
|
||||||
|
const path = this.resolvePath(key);
|
||||||
|
await chmod(path, this.config.filePermissionBits);
|
||||||
|
}
|
||||||
return `${this.config.url}/files/${key}`;
|
return `${this.config.url}/files/${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,8 @@ type Option = {
|
||||||
app?: MiApp | null;
|
app?: MiApp | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PureRenoteOption = Option & { renote: MiNote } & ({ text?: null } | { cw?: null } | { reply?: null } | { poll?: null } | { files?: null | [] });
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NoteCreateService implements OnApplicationShutdown {
|
export class NoteCreateService implements OnApplicationShutdown {
|
||||||
#shutdownController = new AbortController();
|
#shutdownController = new AbortController();
|
||||||
|
|
@ -412,7 +414,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
if (user.host && !data.cw) {
|
if (user.host && !data.cw) {
|
||||||
await this.federatedInstanceService.fetch(user.host).then(async i => {
|
await this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||||
if (i.isNSFW) {
|
if (i.isNSFW && !this.isPureRenote(data)) {
|
||||||
data.cw = 'Instance is marked as NSFW';
|
data.cw = 'Instance is marked as NSFW';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -627,6 +629,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.queueService.endedPollNotificationQueue.add(note.id, {
|
this.queueService.endedPollNotificationQueue.add(note.id, {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, {
|
}, {
|
||||||
|
jobId: `pollEnd:${note.id}`,
|
||||||
delay,
|
delay,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
@ -821,6 +824,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
if (!user.noindex) this.index(note);
|
if (!user.noindex) this.index(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isPureRenote(note: Option): note is PureRenoteOption {
|
||||||
|
return this.isRenote(note) && !this.isQuote(note);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private isRenote(note: Option): note is Option & { renote: MiNote } {
|
private isRenote(note: Option): note is Option & { renote: MiNote } {
|
||||||
return note.renote != null;
|
return note.renote != null;
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
if (user.host && !data.cw) {
|
if (user.host && !data.cw) {
|
||||||
await this.federatedInstanceService.fetch(user.host).then(async i => {
|
await this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||||
if (i.isNSFW) {
|
if (i.isNSFW && !this.noteCreateService.isPureRenote(data)) {
|
||||||
data.cw = 'Instance is marked as NSFW';
|
data.cw = 'Instance is marked as NSFW';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -471,8 +471,9 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
|
const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id });
|
||||||
|
|
||||||
const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null;
|
const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null;
|
||||||
|
const pollChanged = data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll);
|
||||||
|
|
||||||
if (Object.keys(update).length > 0 || filesChanged) {
|
if (Object.keys(update).length > 0 || filesChanged || pollChanged) {
|
||||||
const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id });
|
const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id });
|
||||||
|
|
||||||
await this.noteEditRepository.insert({
|
await this.noteEditRepository.insert({
|
||||||
|
|
@ -544,7 +545,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) {
|
if (pollChanged) {
|
||||||
// Start transaction
|
// Start transaction
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
await this.db.transaction(async transactionalEntityManager => {
|
||||||
await transactionalEntityManager.update(MiNote, oldnote.id, note);
|
await transactionalEntityManager.update(MiNote, oldnote.id, note);
|
||||||
|
|
@ -605,10 +606,11 @@ export class NoteEditService implements OnApplicationShutdown {
|
||||||
|
|
||||||
if (data.poll && data.poll.expiresAt) {
|
if (data.poll && data.poll.expiresAt) {
|
||||||
const delay = data.poll.expiresAt.getTime() - Date.now();
|
const delay = data.poll.expiresAt.getTime() - Date.now();
|
||||||
this.queueService.endedPollNotificationQueue.remove(note.id);
|
this.queueService.endedPollNotificationQueue.remove(`pollEnd:${note.id}`);
|
||||||
this.queueService.endedPollNotificationQueue.add(note.id, {
|
this.queueService.endedPollNotificationQueue.add(note.id, {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, {
|
}, {
|
||||||
|
jobId: `pollEnd:${note.id}`,
|
||||||
delay,
|
delay,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export class RemoteUserResolveService {
|
||||||
|
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
if (this.config.host === host) {
|
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||||
this.logger.info(`return local user: ${usernameLower}`);
|
this.logger.info(`return local user: ${usernameLower}`);
|
||||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
||||||
if (u == null) {
|
if (u == null) {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { toASCII } from 'punycode';
|
import punycode from 'punycode/punycode.js';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import RE2 from 're2';
|
import RE2 from 're2';
|
||||||
|
import psl from 'psl';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
@ -34,6 +35,11 @@ export class UtilityService {
|
||||||
return this.toPuny(this.config.host) === this.toPuny(host);
|
return this.toPuny(this.config.host) === this.toPuny(host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isUriLocal(uri: string): boolean {
|
||||||
|
return this.punyHost(uri) === this.toPuny(this.config.host);
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
|
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
|
||||||
if (host == null) return false;
|
if (host == null) return false;
|
||||||
|
|
@ -101,13 +107,13 @@ export class UtilityService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public toPuny(host: string): string {
|
public toPuny(host: string): string {
|
||||||
return toASCII(host.toLowerCase());
|
return punycode.toASCII(host.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public toPunyNullable(host: string | null | undefined): string | null {
|
public toPunyNullable(host: string | null | undefined): string | null {
|
||||||
if (host == null) return null;
|
if (host == null) return null;
|
||||||
return toASCII(host.toLowerCase());
|
return punycode.toASCII(host.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
@ -117,6 +123,26 @@ export class UtilityService {
|
||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private specialSuffix(hostname: string): string | null {
|
||||||
|
// masto.host provides domain names for its clients, we have to
|
||||||
|
// treat it as if it were a public suffix
|
||||||
|
const mastoHost = hostname.match(/\.?([a-zA-Z0-9-]+\.masto\.host)$/i);
|
||||||
|
if (mastoHost) {
|
||||||
|
return mastoHost[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public punyHostPSLDomain(url: string): string {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const hostname = urlObj.hostname;
|
||||||
|
const domain = this.specialSuffix(hostname) ?? psl.get(hostname) ?? hostname;
|
||||||
|
const host = `${this.toPuny(domain)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
public isFederationAllowedHost(host: string): boolean {
|
public isFederationAllowedHost(host: string): boolean {
|
||||||
if (this.meta.federation === 'none') return false;
|
if (this.meta.federation === 'none') return false;
|
||||||
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
||||||
import { MemoryKVCache } from '@/misc/cache.js';
|
import { MemoryKVCache } from '@/misc/cache.js';
|
||||||
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
|
|
@ -55,6 +56,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private apPersonService: ApPersonService,
|
private apPersonService: ApPersonService,
|
||||||
private apLoggerService: ApLoggerService,
|
private apLoggerService: ApLoggerService,
|
||||||
|
private utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||||
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||||
|
|
@ -64,8 +66,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
public parseUri(value: string | IObject | [string | IObject]): UriParseResult {
|
public parseUri(value: string | IObject | [string | IObject]): UriParseResult {
|
||||||
const separator = '/';
|
const separator = '/';
|
||||||
|
|
||||||
const uri = new URL(getApId(value));
|
const apId = getApId(value);
|
||||||
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
const uri = new URL(apId);
|
||||||
|
if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) {
|
||||||
|
return { local: false, uri: apId };
|
||||||
|
}
|
||||||
|
|
||||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository } from '@/models/_.js';
|
import type { FollowingsRepository } from '@/models/_.js';
|
||||||
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
|
|
@ -128,7 +129,7 @@ class DeliverManager {
|
||||||
|
|
||||||
for (const following of followers) {
|
for (const following of followers) {
|
||||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
||||||
if (inbox === null) throw new Error('inbox is null');
|
if (inbox === null) throw new UnrecoverableError(`inbox is null: following ${following.id}`);
|
||||||
inboxes.set(inbox, following.followerSharedInbox != null);
|
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,9 @@ import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
import { getApHrefNullable, getApId, getApIds, getApType, getNullableApId, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isDislike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||||
import { ApNoteService } from './models/ApNoteService.js';
|
import { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
|
|
@ -39,8 +41,7 @@ import { ApAudienceService } from './ApAudienceService.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import { ApQuestionService } from './models/ApQuestionService.js';
|
import { ApQuestionService } from './models/ApQuestionService.js';
|
||||||
import type { Resolver } from './ApResolverService.js';
|
import type { Resolver } from './ApResolverService.js';
|
||||||
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
|
import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IDislike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js';
|
||||||
import { fromTuple } from '@/misc/from-tuple.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApInboxService {
|
export class ApInboxService {
|
||||||
|
|
@ -93,15 +94,26 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
public async performActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise<string | void> {
|
||||||
let result = undefined as string | void;
|
let result = undefined as string | void;
|
||||||
if (isCollectionOrOrderedCollection(activity)) {
|
if (isCollectionOrOrderedCollection(activity)) {
|
||||||
const results = [] as [string, string | void][];
|
const results = [] as [string, string | void][];
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
|
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
|
||||||
|
if (items.length >= resolver.getRecursionLimit()) {
|
||||||
|
throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractDbHost(actor.uri)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
const act = await resolver.resolve(item);
|
const act = await resolver.resolve(item);
|
||||||
|
if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) {
|
||||||
|
this.logger.debug('skipping activity: activity id is null or mismatching');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
results.push([getApId(item), await this.performOneActivity(actor, act)]);
|
results.push([getApId(item), await this.performOneActivity(actor, act, resolver)]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error || typeof err === 'string') {
|
if (err instanceof Error || typeof err === 'string') {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
|
|
@ -116,7 +128,7 @@ export class ApInboxService {
|
||||||
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
|
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = await this.performOneActivity(actor, activity);
|
result = await this.performOneActivity(actor, activity, resolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ついでにリモートユーザーの情報が古かったら更新しておく
|
// ついでにリモートユーザーの情報が古かったら更新しておく
|
||||||
|
|
@ -131,37 +143,39 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
public async performOneActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise<string | void> {
|
||||||
if (actor.isSuspended) return;
|
if (actor.isSuspended) return;
|
||||||
|
|
||||||
if (isCreate(activity)) {
|
if (isCreate(activity)) {
|
||||||
return await this.create(actor, activity);
|
return await this.create(actor, activity, resolver);
|
||||||
} else if (isDelete(activity)) {
|
} else if (isDelete(activity)) {
|
||||||
return await this.delete(actor, activity);
|
return await this.delete(actor, activity);
|
||||||
} else if (isUpdate(activity)) {
|
} else if (isUpdate(activity)) {
|
||||||
return await this.update(actor, activity);
|
return await this.update(actor, activity, resolver);
|
||||||
} else if (isFollow(activity)) {
|
} else if (isFollow(activity)) {
|
||||||
return await this.follow(actor, activity);
|
return await this.follow(actor, activity);
|
||||||
} else if (isAccept(activity)) {
|
} else if (isAccept(activity)) {
|
||||||
return await this.accept(actor, activity);
|
return await this.accept(actor, activity, resolver);
|
||||||
} else if (isReject(activity)) {
|
} else if (isReject(activity)) {
|
||||||
return await this.reject(actor, activity);
|
return await this.reject(actor, activity, resolver);
|
||||||
} else if (isAdd(activity)) {
|
} else if (isAdd(activity)) {
|
||||||
return await this.add(actor, activity);
|
return await this.add(actor, activity, resolver);
|
||||||
} else if (isRemove(activity)) {
|
} else if (isRemove(activity)) {
|
||||||
return await this.remove(actor, activity);
|
return await this.remove(actor, activity, resolver);
|
||||||
} else if (isAnnounce(activity)) {
|
} else if (isAnnounce(activity)) {
|
||||||
return await this.announce(actor, activity);
|
return await this.announce(actor, activity, resolver);
|
||||||
} else if (isLike(activity)) {
|
} else if (isLike(activity)) {
|
||||||
return await this.like(actor, activity);
|
return await this.like(actor, activity, resolver);
|
||||||
|
} else if (isDislike(activity)) {
|
||||||
|
return await this.dislike(actor, activity);
|
||||||
} else if (isUndo(activity)) {
|
} else if (isUndo(activity)) {
|
||||||
return await this.undo(actor, activity);
|
return await this.undo(actor, activity, resolver);
|
||||||
} else if (isBlock(activity)) {
|
} else if (isBlock(activity)) {
|
||||||
return await this.block(actor, activity);
|
return await this.block(actor, activity);
|
||||||
} else if (isFlag(activity)) {
|
} else if (isFlag(activity)) {
|
||||||
return await this.flag(actor, activity);
|
return await this.flag(actor, activity);
|
||||||
} else if (isMove(activity)) {
|
} else if (isMove(activity)) {
|
||||||
return await this.move(actor, activity);
|
return await this.move(actor, activity, resolver);
|
||||||
} else {
|
} else {
|
||||||
return `unrecognized activity type: ${activity.type}`;
|
return `unrecognized activity type: ${activity.type}`;
|
||||||
}
|
}
|
||||||
|
|
@ -185,30 +199,42 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async like(actor: MiRemoteUser, activity: ILike): Promise<string> {
|
private async like(actor: MiRemoteUser, activity: ILike, resolver?: Resolver): Promise<string> {
|
||||||
const targetUri = getApId(activity.object);
|
const targetUri = getApId(activity.object);
|
||||||
|
|
||||||
const note = await this.apNoteService.fetchNote(targetUri);
|
const object = fromTuple(activity.object);
|
||||||
|
if (!object) return 'skip: activity has no object property';
|
||||||
|
|
||||||
|
const note = await this.apNoteService.resolveNote(object, { resolver });
|
||||||
if (!note) return `skip: target note not found ${targetUri}`;
|
if (!note) return `skip: target note not found ${targetUri}`;
|
||||||
|
|
||||||
await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null);
|
await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null);
|
||||||
|
|
||||||
return await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name).catch(err => {
|
try {
|
||||||
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
|
await this.reactionService.create(actor, note, activity._misskey_reaction ?? activity.content ?? activity.name);
|
||||||
|
return 'ok';
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof IdentifiableError && err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
|
||||||
return 'skip: already reacted';
|
return 'skip: already reacted';
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}).then(() => 'ok');
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async accept(actor: MiRemoteUser, activity: IAccept): Promise<string> {
|
private async dislike(actor: MiRemoteUser, dislike: IDislike): Promise<string> {
|
||||||
|
return await this.undoLike(actor, dislike);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise<string> {
|
||||||
const uri = activity.id ?? activity;
|
const uri = activity.id ?? activity;
|
||||||
|
|
||||||
this.logger.info(`Accept: ${uri}`);
|
this.logger.info(`Accept: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(err => {
|
const object = await resolver.resolve(activity.object).catch(err => {
|
||||||
this.logger.error(`Resolution failed: ${err}`);
|
this.logger.error(`Resolution failed: ${err}`);
|
||||||
|
|
@ -245,7 +271,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
|
private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise<string | void> {
|
||||||
if (actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'invalid actor';
|
return 'invalid actor';
|
||||||
}
|
}
|
||||||
|
|
@ -255,8 +281,12 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activity.target === actor.featured) {
|
if (activity.target === actor.featured) {
|
||||||
const object = fromTuple(activity.object);
|
const activityObject = fromTuple(activity.object);
|
||||||
const note = await this.apNoteService.resolveNote(object);
|
if (isApObject(activityObject) && !isPost(activityObject)) {
|
||||||
|
return `unsupported featured object type: ${getApType(activityObject)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = await this.apNoteService.resolveNote(activityObject, { resolver });
|
||||||
if (note == null) return 'note not found';
|
if (note == null) return 'note not found';
|
||||||
await this.notePiningService.addPinned(actor, note.id);
|
await this.notePiningService.addPinned(actor, note.id);
|
||||||
return;
|
return;
|
||||||
|
|
@ -266,12 +296,13 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
|
private async announce(actor: MiRemoteUser, activity: IAnnounce, resolver?: Resolver): Promise<string | void> {
|
||||||
const uri = getApId(activity);
|
const uri = getApId(activity);
|
||||||
|
|
||||||
this.logger.info(`Announce: ${uri}`);
|
this.logger.info(`Announce: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const activityObject = fromTuple(activity.object);
|
const activityObject = fromTuple(activity.object);
|
||||||
if (!activityObject) return 'skip: activity has no object property';
|
if (!activityObject) return 'skip: activity has no object property';
|
||||||
|
|
@ -280,7 +311,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
const target = await resolver.resolve(activityObject).catch(e => {
|
const target = await resolver.resolve(activityObject).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
return e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(target)) return await this.announceNote(actor, activity, target);
|
if (isPost(target)) return await this.announceNote(actor, activity, target);
|
||||||
|
|
@ -289,7 +320,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
|
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise<string | void> {
|
||||||
const uri = getApId(activity);
|
const uri = getApId(activity);
|
||||||
|
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
|
|
@ -311,13 +342,13 @@ export class ApInboxService {
|
||||||
// Announce対象をresolve
|
// Announce対象をresolve
|
||||||
let renote;
|
let renote;
|
||||||
try {
|
try {
|
||||||
renote = await this.apNoteService.resolveNote(target);
|
renote = await this.apNoteService.resolveNote(target, { resolver });
|
||||||
if (renote == null) return 'announce target is null';
|
if (renote == null) return 'announce target is null';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 対象が4xxならスキップ
|
// 対象が4xxならスキップ
|
||||||
if (err instanceof StatusError) {
|
if (err instanceof StatusError) {
|
||||||
if (!err.isRetryable) {
|
if (!err.isRetryable) {
|
||||||
return `Ignored announce target ${target.id} - ${err.statusCode}`;
|
return `skip: ignored announce target ${target.id} - ${err.statusCode}`;
|
||||||
}
|
}
|
||||||
return `Error in announce target ${target.id} - ${err.statusCode}`;
|
return `Error in announce target ${target.id} - ${err.statusCode}`;
|
||||||
}
|
}
|
||||||
|
|
@ -330,7 +361,7 @@ export class ApInboxService {
|
||||||
|
|
||||||
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
||||||
|
|
||||||
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc);
|
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc, resolver);
|
||||||
const createdAt = activity.published ? new Date(activity.published) : null;
|
const createdAt = activity.published ? new Date(activity.published) : null;
|
||||||
|
|
||||||
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
|
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
|
||||||
|
|
@ -368,7 +399,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
|
private async create(actor: MiRemoteUser, activity: ICreate | IUpdate, resolver?: Resolver): Promise<string | void> {
|
||||||
const uri = getApId(activity);
|
const uri = getApId(activity);
|
||||||
|
|
||||||
this.logger.info(`Create: ${uri}`);
|
this.logger.info(`Create: ${uri}`);
|
||||||
|
|
@ -394,7 +425,8 @@ export class ApInboxService {
|
||||||
activityObject.attributedTo = activity.actor;
|
activityObject.attributedTo = activity.actor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activityObject).catch(e => {
|
const object = await resolver.resolve(activityObject).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
|
|
@ -402,14 +434,14 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPost(object)) {
|
if (isPost(object)) {
|
||||||
await this.createNote(resolver, actor, object, false, activity);
|
await this.createNote(resolver, actor, object, false);
|
||||||
} else {
|
} else {
|
||||||
return `Unknown type: ${getApType(object)}`;
|
return `skip: Unsupported type for Create: ${getApType(object)} ${getNullableApId(object)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
|
private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false): Promise<string> {
|
||||||
const uri = getApId(note);
|
const uri = getApId(note);
|
||||||
|
|
||||||
if (typeof note === 'object') {
|
if (typeof note === 'object') {
|
||||||
|
|
@ -421,6 +453,8 @@ export class ApInboxService {
|
||||||
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
|
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
|
||||||
return 'skip: host in actor.uri !== note.id';
|
return 'skip: host in actor.uri !== note.id';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return 'skip: note.id is not a string';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,11 +464,11 @@ export class ApInboxService {
|
||||||
const exist = await this.apNoteService.fetchNote(note);
|
const exist = await this.apNoteService.fetchNote(note);
|
||||||
if (exist) return 'skip: note exists';
|
if (exist) return 'skip: note exists';
|
||||||
|
|
||||||
await this.apNoteService.createNote(note, resolver, silent);
|
await this.apNoteService.createNote(note, actor, resolver, silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && !err.isRetryable) {
|
if (err instanceof StatusError && !err.isRetryable) {
|
||||||
return `skip ${err.statusCode}`;
|
return `skip: ${err.statusCode}`;
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
@ -521,7 +555,7 @@ export class ApInboxService {
|
||||||
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
const note = await this.apDbResolverService.getNoteFromApId(uri);
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
return 'message not found';
|
return 'skip: ignoring deleted note on both ends';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.userId !== actor.id) {
|
if (note.userId !== actor.id) {
|
||||||
|
|
@ -568,12 +602,13 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async reject(actor: MiRemoteUser, activity: IReject): Promise<string> {
|
private async reject(actor: MiRemoteUser, activity: IReject, resolver?: Resolver): Promise<string> {
|
||||||
const uri = activity.id ?? activity;
|
const uri = activity.id ?? activity;
|
||||||
|
|
||||||
this.logger.info(`Reject: ${uri}`);
|
this.logger.info(`Reject: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
|
|
@ -610,7 +645,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
|
private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise<string | void> {
|
||||||
if (actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'invalid actor';
|
return 'invalid actor';
|
||||||
}
|
}
|
||||||
|
|
@ -621,7 +656,11 @@ export class ApInboxService {
|
||||||
|
|
||||||
if (activity.target === actor.featured) {
|
if (activity.target === actor.featured) {
|
||||||
const activityObject = fromTuple(activity.object);
|
const activityObject = fromTuple(activity.object);
|
||||||
const note = await this.apNoteService.resolveNote(activityObject);
|
if (isApObject(activityObject) && !isPost(activityObject)) {
|
||||||
|
return `unsupported featured object type: ${getApType(activityObject)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = await this.apNoteService.resolveNote(activityObject, { resolver });
|
||||||
if (note == null) return 'note not found';
|
if (note == null) return 'note not found';
|
||||||
await this.notePiningService.removePinned(actor, note.id);
|
await this.notePiningService.removePinned(actor, note.id);
|
||||||
return;
|
return;
|
||||||
|
|
@ -631,7 +670,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
|
private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise<string> {
|
||||||
if (actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'invalid actor';
|
return 'invalid actor';
|
||||||
}
|
}
|
||||||
|
|
@ -640,11 +679,12 @@ export class ApInboxService {
|
||||||
|
|
||||||
this.logger.info(`Undo: ${uri}`);
|
this.logger.info(`Undo: ${uri}`);
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
return e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
// don't queue because the sender may attempt again when timeout
|
// don't queue because the sender may attempt again when timeout
|
||||||
|
|
@ -654,7 +694,7 @@ export class ApInboxService {
|
||||||
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
||||||
if (isAccept(object)) return await this.undoAccept(actor, object);
|
if (isAccept(object)) return await this.undoAccept(actor, object);
|
||||||
|
|
||||||
return `skip: unknown object type ${getApType(object)}`;
|
return `skip: unknown activity type ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
@ -749,7 +789,7 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async undoLike(actor: MiRemoteUser, activity: ILike): Promise<string> {
|
private async undoLike(actor: MiRemoteUser, activity: ILike | IDislike): Promise<string> {
|
||||||
const targetUri = getApId(activity.object);
|
const targetUri = getApId(activity.object);
|
||||||
|
|
||||||
const note = await this.apNoteService.fetchNote(targetUri);
|
const note = await this.apNoteService.fetchNote(targetUri);
|
||||||
|
|
@ -764,14 +804,15 @@ export class ApInboxService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise<string | void> {
|
||||||
if (actor.uri !== activity.actor) {
|
if (actor.uri !== activity.actor) {
|
||||||
return 'skip: invalid actor';
|
return 'skip: invalid actor';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.debug('Update');
|
this.logger.debug('Update');
|
||||||
|
|
||||||
const resolver = this.apResolverService.createResolver();
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
resolver ??= this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(activity.object).catch(e => {
|
const object = await resolver.resolve(activity.object).catch(e => {
|
||||||
this.logger.error(`Resolution failed: ${e}`);
|
this.logger.error(`Resolution failed: ${e}`);
|
||||||
|
|
@ -782,22 +823,32 @@ export class ApInboxService {
|
||||||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
// If we get an Update(Question) for a note that doesn't exist, then create it instead
|
||||||
|
if (!await this.apNoteService.hasNote(object)) {
|
||||||
|
return await this.create(actor, activity, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.apQuestionService.updateQuestion(object, actor, resolver);
|
||||||
return 'ok: Question updated';
|
return 'ok: Question updated';
|
||||||
} else if (getApType(object) === 'Note') {
|
} else if (isPost(object)) {
|
||||||
await this.apNoteService.updateNote(object, resolver).catch(err => console.error(err));
|
// If we get an Update(Note) for a note that doesn't exist, then create it instead
|
||||||
|
if (!await this.apNoteService.hasNote(object)) {
|
||||||
|
return await this.create(actor, activity, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.apNoteService.updateNote(object, actor, resolver);
|
||||||
return 'ok: Note updated';
|
return 'ok: Note updated';
|
||||||
} else {
|
} else {
|
||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unsupported type for Update: ${getApType(object)} ${getNullableApId(object)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
private async move(actor: MiRemoteUser, activity: IMove, resolver?: Resolver): Promise<string> {
|
||||||
// fetch the new and old accounts
|
// fetch the new and old accounts
|
||||||
const targetUri = getApHrefNullable(activity.target);
|
const targetUri = getApHrefNullable(activity.target);
|
||||||
if (!targetUri) return 'skip: invalid activity target';
|
if (!targetUri) return 'skip: invalid activity target';
|
||||||
|
|
||||||
return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do';
|
return await this.apPersonService.updatePerson(actor.uri, resolver) ?? 'skip: nothing to do';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { createPublicKey, randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import * as mfm from '@transfem-org/sfm-js';
|
import * as mfm from '@transfem-org/sfm-js';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
import type { MiPartialLocalUser, MiLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||||
|
|
@ -30,6 +31,7 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import { JsonLdService } from './JsonLdService.js';
|
import { JsonLdService } from './JsonLdService.js';
|
||||||
import { ApMfmService } from './ApMfmService.js';
|
import { ApMfmService } from './ApMfmService.js';
|
||||||
import { CONTEXT } from './misc/contexts.js';
|
import { CONTEXT } from './misc/contexts.js';
|
||||||
|
import { getApId } from './type.js';
|
||||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -106,7 +108,7 @@ export class ApRendererService {
|
||||||
to = [`${attributedTo}/followers`];
|
to = [`${attributedTo}/followers`];
|
||||||
cc = [];
|
cc = [];
|
||||||
} else {
|
} else {
|
||||||
throw new Error('renderAnnounce: cannot render non-public note');
|
throw new UnrecoverableError(`renderAnnounce: cannot render non-public note: ${getApId(object)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -469,6 +471,7 @@ export class ApRendererService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if you change this, also change `server/api/endpoints/i/update.ts`
|
||||||
@bindThis
|
@bindThis
|
||||||
public async renderPerson(user: MiLocalUser) {
|
public async renderPerson(user: MiLocalUser) {
|
||||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import type Logger from '@/logger.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject } from './type.js';
|
||||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||||
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||||
|
import { UtilityService } from "@/core/UtilityService.js";
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -147,6 +148,7 @@ export class ApRequestService {
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairService: UserKeypairService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
|
private utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||||
|
|
@ -241,9 +243,11 @@ export class ApRequestService {
|
||||||
if (alternate) {
|
if (alternate) {
|
||||||
const href = alternate.getAttribute('href');
|
const href = alternate.getAttribute('href');
|
||||||
if (href) {
|
if (href) {
|
||||||
|
if (this.utilityService.punyHostPSLDomain(url) === this.utilityService.punyHostPSLDomain(href)) {
|
||||||
return await this.signedGet(href, user, false);
|
return await this.signedGet(href, user, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// something went wrong parsing the HTML, ignore the whole thing
|
// something went wrong parsing the HTML, ignore the whole thing
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -257,7 +261,7 @@ export class ApRequestService {
|
||||||
const finalUrl = res.url; // redirects may have been involved
|
const finalUrl = res.url; // redirects may have been involved
|
||||||
const activity = await res.json() as IObject;
|
const activity = await res.json() as IObject;
|
||||||
|
|
||||||
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
assertActivityMatchesUrls(activity, [finalUrl]);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { IsNull, Not } from 'typeorm';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js';
|
||||||
|
|
@ -15,12 +16,12 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
import { isCollectionOrOrderedCollection } from './type.js';
|
import { isCollectionOrOrderedCollection } from './type.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
import { ApRequestService } from './ApRequestService.js';
|
import { ApRequestService } from './ApRequestService.js';
|
||||||
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||||
import { fromTuple } from '@/misc/from-tuple.js';
|
|
||||||
|
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
|
|
@ -42,7 +43,7 @@ export class Resolver {
|
||||||
private apRendererService: ApRendererService,
|
private apRendererService: ApRendererService,
|
||||||
private apDbResolverService: ApDbResolverService,
|
private apDbResolverService: ApDbResolverService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private recursionLimit = 100,
|
private recursionLimit = 256,
|
||||||
) {
|
) {
|
||||||
this.history = new Set();
|
this.history = new Set();
|
||||||
this.logger = this.loggerService.getLogger('ap-resolve');
|
this.logger = this.loggerService.getLogger('ap-resolve');
|
||||||
|
|
@ -53,6 +54,11 @@ export class Resolver {
|
||||||
return Array.from(this.history);
|
return Array.from(this.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getRecursionLimit(): number {
|
||||||
|
return this.recursionLimit;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
||||||
const collection = typeof value === 'string'
|
const collection = typeof value === 'string'
|
||||||
|
|
@ -62,7 +68,7 @@ export class Resolver {
|
||||||
if (isCollectionOrOrderedCollection(collection)) {
|
if (isCollectionOrOrderedCollection(collection)) {
|
||||||
return collection;
|
return collection;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`unrecognized collection type: ${collection.type}`);
|
throw new UnrecoverableError(`unrecognized collection type: ${collection.type}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,15 +85,15 @@ export class Resolver {
|
||||||
// URLs with fragment parts cannot be resolved correctly because
|
// URLs with fragment parts cannot be resolved correctly because
|
||||||
// the fragment part does not get transmitted over HTTP(S).
|
// the fragment part does not get transmitted over HTTP(S).
|
||||||
// Avoid strange behaviour by not trying to resolve these at all.
|
// Avoid strange behaviour by not trying to resolve these at all.
|
||||||
throw new Error(`cannot resolve URL with fragment: ${value}`);
|
throw new UnrecoverableError(`cannot resolve URL with fragment: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.history.has(value)) {
|
if (this.history.has(value)) {
|
||||||
throw new Error('cannot resolve already resolved one');
|
throw new Error(`cannot resolve already resolved URL: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.history.size > this.recursionLimit) {
|
if (this.history.size > this.recursionLimit) {
|
||||||
throw new Error(`hit recursion limit: ${this.utilityService.extractDbHost(value)}`);
|
throw new Error(`hit recursion limit: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.history.add(value);
|
this.history.add(value);
|
||||||
|
|
@ -98,7 +104,7 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedHost(host)) {
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
throw new Error('Instance is blocked');
|
throw new UnrecoverableError(`instance is blocked: ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.config.signToActivityPubGet && !this.user) {
|
if (this.config.signToActivityPubGet && !this.user) {
|
||||||
|
|
@ -114,15 +120,19 @@ export class Resolver {
|
||||||
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||||
) {
|
) {
|
||||||
throw new Error('invalid response');
|
throw new UnrecoverableError(`invalid AP object ${value}: does not have ActivityStreams context`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HttpRequestService / ApRequestService have already checked that
|
// HttpRequestService / ApRequestService have already checked that
|
||||||
// `object.id` or `object.url` matches the URL used to fetch the
|
// `object.id` or `object.url` matches the URL used to fetch the
|
||||||
// object after redirects; here we double-check that no redirects
|
// object after redirects; here we double-check that no redirects
|
||||||
// bounced between hosts
|
// bounced between hosts
|
||||||
if (object.id && (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value))) {
|
if (object.id == null) {
|
||||||
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
|
throw new UnrecoverableError(`invalid AP object ${value}: missing id`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.utilityService.punyHostPSLDomain(object.id) !== this.utilityService.punyHostPSLDomain(value)) {
|
||||||
|
throw new UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
|
|
@ -131,7 +141,7 @@ export class Resolver {
|
||||||
@bindThis
|
@bindThis
|
||||||
private resolveLocal(url: string): Promise<IObject> {
|
private resolveLocal(url: string): Promise<IObject> {
|
||||||
const parsed = this.apDbResolverService.parseUri(url);
|
const parsed = this.apDbResolverService.parseUri(url);
|
||||||
if (!parsed.local) throw new Error('resolveLocal: not local');
|
if (!parsed.local) throw new UnrecoverableError(`resolveLocal - not a local URL: ${url}`);
|
||||||
|
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case 'notes':
|
case 'notes':
|
||||||
|
|
@ -160,7 +170,7 @@ export class Resolver {
|
||||||
case 'follows':
|
case 'follows':
|
||||||
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
return this.followRequestsRepository.findOneBy({ id: parsed.id })
|
||||||
.then(async followRequest => {
|
.then(async followRequest => {
|
||||||
if (followRequest == null) throw new Error('resolveLocal: invalid follow request ID');
|
if (followRequest == null) throw new UnrecoverableError(`resolveLocal - invalid follow request ID ${parsed.id}: ${url}`);
|
||||||
const [follower, followee] = await Promise.all([
|
const [follower, followee] = await Promise.all([
|
||||||
this.usersRepository.findOneBy({
|
this.usersRepository.findOneBy({
|
||||||
id: followRequest.followerId,
|
id: followRequest.followerId,
|
||||||
|
|
@ -172,12 +182,12 @@ export class Resolver {
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
if (follower == null || followee == null) {
|
if (follower == null || followee == null) {
|
||||||
throw new Error('resolveLocal: follower or followee does not exist');
|
throw new Error(`resolveLocal - follower or followee does not exist: ${url}`);
|
||||||
}
|
}
|
||||||
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
|
return this.apRendererService.addContext(this.apRendererService.renderFollow(follower as MiLocalUser | MiRemoteUser, followee as MiLocalUser | MiRemoteUser, url));
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
throw new Error(`resolveLocal: type ${parsed.type} unhandled`);
|
throw new UnrecoverableError(`resolveLocal: type ${parsed.type} unhandled: ${url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
||||||
|
|
@ -109,7 +110,7 @@ class JsonLd {
|
||||||
@bindThis
|
@bindThis
|
||||||
private getLoader() {
|
private getLoader() {
|
||||||
return async (url: string): Promise<RemoteDocument> => {
|
return async (url: string): Promise<RemoteDocument> => {
|
||||||
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
if (!/^https?:\/\//.test(url)) throw new UnrecoverableError(`Invalid URL: ${url}`);
|
||||||
|
|
||||||
if (this.preLoad) {
|
if (this.preLoad) {
|
||||||
if (url in PRELOADED_CONTEXTS) {
|
if (url in PRELOADED_CONTEXTS) {
|
||||||
|
|
@ -148,7 +149,7 @@ class JsonLd {
|
||||||
},
|
},
|
||||||
).then(res => {
|
).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`${res.status} ${res.statusText}`);
|
throw new Error(`JSON-LD fetch failed with ${res.status} ${res.statusText}: ${url}`);
|
||||||
} else {
|
} else {
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,30 @@
|
||||||
* SPDX-FileCopyrightText: dakkar and sharkey-project
|
* SPDX-FileCopyrightText: dakkar and sharkey-project
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import type { IObject } from '../type.js';
|
import type { IObject } from '../type.js';
|
||||||
|
|
||||||
function getHrefFrom(one: IObject|string): string | undefined {
|
function getHrefsFrom(one: IObject | string | undefined | (IObject | string | undefined)[]): (string | undefined)[] {
|
||||||
if (typeof(one) === 'string') return one;
|
if (Array.isArray(one)) {
|
||||||
return one.href;
|
return one.flatMap(h => getHrefsFrom(h));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
typeof(one) === 'object' ? one.href : one,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
|
export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
|
||||||
const idOk = activity.id !== undefined && urls.includes(activity.id);
|
const expectedUrls = new Set(urls
|
||||||
if (idOk) return;
|
.filter(u => URL.canParse(u))
|
||||||
|
.map(u => new URL(u).href),
|
||||||
|
);
|
||||||
|
|
||||||
const url = activity.url;
|
const actualUrls = [activity.id, ...getHrefsFrom(activity.url)]
|
||||||
if (url) {
|
.filter(u => u && URL.canParse(u))
|
||||||
// `activity.url` can be an `ApObject = IObject | string | (IObject
|
.map(u => new URL(u as string).href);
|
||||||
// | string)[]`, we have to look inside it
|
|
||||||
const activityUrls = Array.isArray(url) ? url.map(getHrefFrom) : [getHrefFrom(url)];
|
|
||||||
const goodUrl = activityUrls.find(u => u && urls.includes(u));
|
|
||||||
|
|
||||||
if (goodUrl) return;
|
if (!actualUrls.some(u => expectedUrls.has(u))) {
|
||||||
|
throw new UnrecoverableError(`bad Activity: neither id(${activity.id}) nor url(${JSON.stringify(activity.url)}) match location(${urls})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${JSON.stringify(activity?.url)}) match location(${urls})`);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||||
|
|
||||||
if (contentType === '') {
|
if (contentType === '') {
|
||||||
throw new Error('Validate content type of AP response: No content-type header');
|
throw new Error(`invalid content type of AP response - no content-type header: ${response.url}`);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
contentType.startsWith('application/activity+json') ||
|
contentType.startsWith('application/activity+json') ||
|
||||||
|
|
@ -17,7 +17,7 @@ export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error('Validate content type of AP response: Content type is not application/activity+json or application/ld+json');
|
throw new Error(`invalid content type of AP response - content type is not application/activity+json or application/ld+json: ${response.url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/;
|
const plusJsonSuffixRegex = /^\s*(application|text)\/[a-zA-Z0-9\.\-\+]+\+json\s*(;|$)/;
|
||||||
|
|
@ -26,7 +26,7 @@ export function validateContentTypeSetAsJsonLD(response: Response): void {
|
||||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||||
|
|
||||||
if (contentType === '') {
|
if (contentType === '') {
|
||||||
throw new Error('Validate content type of JSON LD: No content-type header');
|
throw new Error(`invalid content type of JSON LD - no content-type header: ${response.url}`);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
contentType.startsWith('application/ld+json') ||
|
contentType.startsWith('application/ld+json') ||
|
||||||
|
|
@ -35,5 +35,5 @@ export function validateContentTypeSetAsJsonLD(response: Response): void {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error('Validate content type of JSON LD: Content type is not application/ld+json or application/json');
|
throw new Error(`invalid content type of JSON LD - content type is not application/ld+json or application/json: ${response.url}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { checkHttps } from '@/misc/check-https.js';
|
import { checkHttps } from '@/misc/check-https.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { isDocument, type IObject } from '../type.js';
|
import { isDocument, type IObject } from '../type.js';
|
||||||
|
|
@ -47,7 +48,7 @@ export class ApImageService {
|
||||||
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
public async createImage(actor: MiRemoteUser, value: string | IObject): Promise<MiDriveFile | null> {
|
||||||
// 投稿者が凍結されていたらスキップ
|
// 投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new Error('actor has been suspended');
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${actor.uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = await this.apResolverService.createResolver().resolve(value);
|
const image = await this.apResolverService.createResolver().resolve(value);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@
|
||||||
|
|
||||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
import type { UsersRepository, PollsRepository, EmojisRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type { MiNote } from '@/models/Note.js';
|
import type { MiNote } from '@/models/Note.js';
|
||||||
|
|
@ -49,6 +50,9 @@ export class ApNoteService {
|
||||||
@Inject(DI.meta)
|
@Inject(DI.meta)
|
||||||
private meta: MiMeta,
|
private meta: MiMeta,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.pollsRepository)
|
@Inject(DI.pollsRepository)
|
||||||
private pollsRepository: PollsRepository,
|
private pollsRepository: PollsRepository,
|
||||||
|
|
||||||
|
|
@ -82,7 +86,13 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateNote(object: IObject, uri: string): Error | null {
|
public validateNote(
|
||||||
|
object: IObject,
|
||||||
|
uri: string,
|
||||||
|
actor?: MiRemoteUser,
|
||||||
|
user?: MiRemoteUser,
|
||||||
|
note?: MiNote,
|
||||||
|
): Error | null {
|
||||||
const expectHost = this.utilityService.extractDbHost(uri);
|
const expectHost = this.utilityService.extractDbHost(uri);
|
||||||
const apType = getApType(object);
|
const apType = getApType(object);
|
||||||
|
|
||||||
|
|
@ -99,10 +109,27 @@ export class ApNoteService {
|
||||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actor) {
|
||||||
|
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
|
||||||
|
if (attribution !== actor.uri) {
|
||||||
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`);
|
||||||
|
}
|
||||||
|
if (user && attribution !== user.uri) {
|
||||||
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated attribution does not match original attribution. updated attribution: ${user.uri}, original attribution: ${attribution}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
||||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
const url = (object.url) ? getOneApId(object.url) : note.url;
|
||||||
|
if (url && url !== note.url) {
|
||||||
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: updated url does not match original url. updated url: ${url}, original url: ${note.url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,18 +143,27 @@ export class ApNoteService {
|
||||||
return await this.apDbResolverService.getNoteFromApId(object);
|
return await this.apDbResolverService.getNoteFromApId(object);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the provided object / ID exists in the local database.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async hasNote(object: string | IObject | [string | IObject]): Promise<boolean> {
|
||||||
|
const uri = getApId(object);
|
||||||
|
return await this.notesRepository.existsBy({ uri });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noteを作成します。
|
* Noteを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
|
|
||||||
const entryUri = getApId(value);
|
const entryUri = getApId(value);
|
||||||
const err = this.validateNote(object, entryUri);
|
const err = this.validateNote(object, entryUri, actor);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err.message, {
|
this.logger.error(err.message, {
|
||||||
resolver: { history: resolver.getHistory() },
|
resolver: { history: resolver.getHistory() },
|
||||||
|
|
@ -141,29 +177,40 @@ export class ApNoteService {
|
||||||
|
|
||||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||||
|
|
||||||
if (note.id && !checkHttps(note.id)) {
|
if (note.id == null) {
|
||||||
throw new Error('unexpected schema of note.id: ' + note.id);
|
throw new UnrecoverableError(`Refusing to create note without id: ${entryUri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkHttps(note.id)) {
|
||||||
|
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getOneApHrefNullable(note.url);
|
const url = getOneApHrefNullable(note.url);
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (url != null) {
|
||||||
throw new Error('unexpected schema of note url: ' + url);
|
if (!checkHttps(url)) {
|
||||||
|
throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${entryUri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
|
||||||
|
throw new UnrecoverableError(`note url <> uri host mismatch: ${url} <> ${note.id} in ${entryUri}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
// 投稿者をフェッチ
|
// 投稿者をフェッチ
|
||||||
if (note.attributedTo == null) {
|
if (note.attributedTo == null) {
|
||||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
throw new UnrecoverableError(`invalid note.attributedTo ${note.attributedTo} in ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = getOneApId(note.attributedTo);
|
const uri = getOneApId(note.attributedTo);
|
||||||
|
|
||||||
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
||||||
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (cachedActor && cachedActor.isSuspended) {
|
actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
|
||||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
if (actor && actor.isSuspended) {
|
||||||
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${uri} has been suspended: ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||||
|
|
@ -190,15 +237,16 @@ export class ApNoteService {
|
||||||
*/
|
*/
|
||||||
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||||
if (hasProhibitedWords) {
|
if (hasProhibitedWords) {
|
||||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${entryUri}`);
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
||||||
|
|
||||||
// 解決した投稿者が凍結されていたらスキップ
|
// 解決した投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor has been suspended: ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||||
|
|
@ -228,13 +276,13 @@ export class ApNoteService {
|
||||||
.then(x => {
|
.then(x => {
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
this.logger.warn('Specified inReplyTo, but not found');
|
this.logger.warn('Specified inReplyTo, but not found');
|
||||||
throw new Error('inReplyTo not found');
|
throw new Error(`could not fetch inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
})
|
})
|
||||||
.catch(async err => {
|
.catch(async err => {
|
||||||
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo} for note ${entryUri}`);
|
||||||
throw err;
|
throw err;
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -243,16 +291,25 @@ export class ApNoteService {
|
||||||
let quote: MiNote | undefined | null = null;
|
let quote: MiNote | undefined | null = null;
|
||||||
|
|
||||||
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
||||||
const tryResolveNote = async (uri: string): Promise<
|
const tryResolveNote = async (uri: unknown): Promise<
|
||||||
| { status: 'ok'; res: MiNote }
|
| { status: 'ok'; res: MiNote }
|
||||||
| { status: 'permerror' | 'temperror' }
|
| { status: 'permerror' | 'temperror' }
|
||||||
> => {
|
> => {
|
||||||
if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
|
if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
|
||||||
|
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
|
||||||
|
return { status: 'permerror' };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri, { resolver });
|
const res = await this.resolveNote(uri, { resolver });
|
||||||
if (res == null) return { status: 'permerror' };
|
if (res == null) {
|
||||||
|
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
|
||||||
|
return { status: 'permerror' };
|
||||||
|
}
|
||||||
return { status: 'ok', res };
|
return { status: 'ok', res };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||||
|
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
||||||
};
|
};
|
||||||
|
|
@ -265,7 +322,7 @@ export class ApNoteService {
|
||||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
if (results.some(x => x.status === 'temperror')) {
|
if (results.some(x => x.status === 'temperror')) {
|
||||||
throw new Error('quote resolve failed');
|
throw new Error(`temporary error resolving quote for ${entryUri}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -325,7 +382,7 @@ export class ApNoteService {
|
||||||
this.logger.info('The note is already inserted while creating itself, reading again');
|
this.logger.info('The note is already inserted while creating itself, reading again');
|
||||||
const duplicate = await this.fetchNote(value);
|
const duplicate = await this.fetchNote(value);
|
||||||
if (!duplicate) {
|
if (!duplicate) {
|
||||||
throw new Error('The note creation failed with duplication error even when there is no duplication');
|
throw new Error(`The note creation failed with duplication error even when there is no duplication: ${entryUri}`);
|
||||||
}
|
}
|
||||||
return duplicate;
|
return duplicate;
|
||||||
}
|
}
|
||||||
|
|
@ -335,16 +392,18 @@ export class ApNoteService {
|
||||||
* Noteを作成します。
|
* Noteを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
public async updateNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
||||||
const noteUri = typeof value === 'string' ? value : value.id;
|
const noteUri = getApId(value);
|
||||||
if (noteUri == null) throw new Error('uri is null');
|
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (noteUri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
if (noteUri.startsWith(this.config.url + '/')) throw new UnrecoverableError(`uri points local: ${noteUri}`);
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const UpdatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
|
const updatedNote = await this.notesRepository.findOneBy({ uri: noteUri });
|
||||||
if (UpdatedNote == null) throw new Error('Note is not registered');
|
if (updatedNote == null) throw new Error(`Note is not registered (no note): ${noteUri}`);
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: updatedNote.userId }) as MiRemoteUser | null;
|
||||||
|
if (user == null) throw new Error(`Note is not registered (no user): ${noteUri}`);
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
@ -362,33 +421,38 @@ export class ApNoteService {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `validateNote` checks that the actor and user are one and the same
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
actor ??= user;
|
||||||
|
|
||||||
const note = object as IPost;
|
const note = object as IPost;
|
||||||
|
|
||||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||||
|
|
||||||
if (note.id && !checkHttps(note.id)) {
|
if (note.id == null) {
|
||||||
throw new Error('unexpected schema of note.id: ' + note.id);
|
throw new UnrecoverableError(`Refusing to update note without id: ${noteUri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkHttps(note.id)) {
|
||||||
|
throw new UnrecoverableError(`unexpected schema of note.id ${note.id} in ${noteUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getOneApHrefNullable(note.url);
|
const url = getOneApHrefNullable(note.url);
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (url != null) {
|
||||||
throw new Error('unexpected schema of note url: ' + url);
|
if (!checkHttps(url)) {
|
||||||
|
throw new UnrecoverableError(`unexpected schema of note.url ${url} in ${noteUri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(note.id)) {
|
||||||
|
throw new UnrecoverableError(`note url <> id host mismatch: ${url} <> ${note.id} in ${noteUri}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
// 投稿者をフェッチ
|
if (actor.isSuspended) {
|
||||||
if (note.attributedTo == null) {
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', `actor ${actor.id} has been suspended: ${noteUri}`);
|
||||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = getOneApId(note.attributedTo);
|
|
||||||
|
|
||||||
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
|
||||||
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
|
||||||
if (cachedActor && cachedActor.isSuspended) {
|
|
||||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver);
|
||||||
|
|
@ -415,17 +479,10 @@ export class ApNoteService {
|
||||||
*/
|
*/
|
||||||
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices });
|
||||||
if (hasProhibitedWords) {
|
if (hasProhibitedWords) {
|
||||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', `Note contains prohibited words: ${noteUri}`);
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
|
||||||
|
|
||||||
// 投稿者が凍結されていたらスキップ
|
|
||||||
if (actor.isSuspended) {
|
|
||||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver);
|
||||||
let visibility = noteAudience.visibility;
|
let visibility = noteAudience.visibility;
|
||||||
const visibleUsers = noteAudience.visibleUsers;
|
const visibleUsers = noteAudience.visibleUsers;
|
||||||
|
|
@ -453,13 +510,13 @@ export class ApNoteService {
|
||||||
.then(x => {
|
.then(x => {
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
this.logger.warn('Specified inReplyTo, but not found');
|
this.logger.warn('Specified inReplyTo, but not found');
|
||||||
throw new Error('inReplyTo not found');
|
throw new Error(`could not fetch inReplyTo ${note.inReplyTo}: ${entryUri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return x;
|
return x;
|
||||||
})
|
})
|
||||||
.catch(async err => {
|
.catch(async err => {
|
||||||
this.logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${err.statusCode ?? err}`);
|
this.logger.warn(`error ${err.statusCode ?? err} fetching inReplyTo ${note.inReplyTo}: ${entryUri}`);
|
||||||
throw err;
|
throw err;
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
@ -468,16 +525,25 @@ export class ApNoteService {
|
||||||
let quote: MiNote | undefined | null = null;
|
let quote: MiNote | undefined | null = null;
|
||||||
|
|
||||||
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
if (note._misskey_quote ?? note.quoteUrl ?? note.quoteUri) {
|
||||||
const tryResolveNote = async (uri: string): Promise<
|
const tryResolveNote = async (uri: unknown): Promise<
|
||||||
| { status: 'ok'; res: MiNote }
|
| { status: 'ok'; res: MiNote }
|
||||||
| { status: 'permerror' | 'temperror' }
|
| { status: 'permerror' | 'temperror' }
|
||||||
> => {
|
> => {
|
||||||
if (typeof uri !== 'string' || !/^https?:/.test(uri)) return { status: 'permerror' };
|
if (typeof uri !== 'string' || !/^https?:/.test(uri)) {
|
||||||
|
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: URI is invalid`);
|
||||||
|
return { status: 'permerror' };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const res = await this.resolveNote(uri, { resolver });
|
const res = await this.resolveNote(uri, { resolver });
|
||||||
if (res == null) return { status: 'permerror' };
|
if (res == null) {
|
||||||
|
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: resolution error`);
|
||||||
|
return { status: 'permerror' };
|
||||||
|
}
|
||||||
return { status: 'ok', res };
|
return { status: 'ok', res };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
const error = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
||||||
|
this.logger.warn(`Failed to resolve quote ${uri} for note ${entryUri}: ${error}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
status: (e instanceof StatusError && !e.isRetryable) ? 'permerror' : 'temperror',
|
||||||
};
|
};
|
||||||
|
|
@ -490,7 +556,7 @@ export class ApNoteService {
|
||||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||||
if (!quote) {
|
if (!quote) {
|
||||||
if (results.some(x => x.status === 'temperror')) {
|
if (results.some(x => x.status === 'temperror')) {
|
||||||
throw new Error('quote resolve failed');
|
throw new Error(`temporary error resolving quote for ${entryUri}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -525,7 +591,7 @@ export class ApNoteService {
|
||||||
const apEmojis = emojis.map(emoji => emoji.name);
|
const apEmojis = emojis.map(emoji => emoji.name);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await this.noteEditService.edit(actor, UpdatedNote.id, {
|
return await this.noteEditService.edit(actor, updatedNote.id, {
|
||||||
createdAt: note.published ? new Date(note.published) : null,
|
createdAt: note.published ? new Date(note.published) : null,
|
||||||
files,
|
files,
|
||||||
reply,
|
reply,
|
||||||
|
|
@ -550,7 +616,7 @@ export class ApNoteService {
|
||||||
this.logger.info('The note is already inserted while creating itself, reading again');
|
this.logger.info('The note is already inserted while creating itself, reading again');
|
||||||
const duplicate = await this.fetchNote(value);
|
const duplicate = await this.fetchNote(value);
|
||||||
if (!duplicate) {
|
if (!duplicate) {
|
||||||
throw new Error('The note creation failed with duplication error even when there is no duplication');
|
throw new Error(`The note creation failed with duplication error even when there is no duplication: ${noteUri}`);
|
||||||
}
|
}
|
||||||
return duplicate;
|
return duplicate;
|
||||||
}
|
}
|
||||||
|
|
@ -567,7 +633,7 @@ export class ApNoteService {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
|
|
||||||
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||||
throw new StatusError('blocked host', 451);
|
throw new StatusError(`blocked host: ${uri}`, 451, 'blocked host');
|
||||||
}
|
}
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
@ -578,15 +644,15 @@ export class ApNoteService {
|
||||||
if (exist) return exist;
|
if (exist) return exist;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
if (this.utilityService.isUriLocal(uri)) {
|
||||||
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
throw new StatusError(`cannot resolve local note: ${uri}`, 400, 'cannot resolve local note');
|
||||||
}
|
}
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||||
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
||||||
return await this.createNote(createFrom, options.resolver, true);
|
return await this.createNote(createFrom, undefined, options.resolver, true);
|
||||||
} finally {
|
} finally {
|
||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
|
|
@ -627,7 +693,7 @@ export class ApNoteService {
|
||||||
});
|
});
|
||||||
|
|
||||||
const emoji = await this.emojisRepository.findOneBy({ host, name });
|
const emoji = await this.emojisRepository.findOneBy({ host, name });
|
||||||
if (emoji == null) throw new Error('emoji update failed');
|
if (emoji == null) throw new Error(`emoji update failed: ${name}:${host}`);
|
||||||
return emoji;
|
return emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import promiseLimit from 'promise-limit';
|
import promiseLimit from 'promise-limit';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
|
import { AbortError } from 'node-fetch';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
|
@ -136,35 +138,49 @@ export class ApPersonService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private validateActor(x: IObject, uri: string): IActor {
|
private validateActor(x: IObject, uri: string): IActor {
|
||||||
const expectHost = this.utilityService.punyHost(uri);
|
const expectHost = this.utilityService.punyHostPSLDomain(uri);
|
||||||
|
|
||||||
if (!isActor(x)) {
|
if (!isActor(x)) {
|
||||||
throw new Error(`invalid Actor type '${x.type}'`);
|
throw new UnrecoverableError(`invalid Actor type '${x.type}' in ${uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof x.id === 'string' && x.id.length > 0)) {
|
if (!(typeof x.id === 'string' && x.id.length > 0)) {
|
||||||
throw new Error('invalid Actor: wrong id');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong id type`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||||
throw new Error('invalid Actor: wrong inbox');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox type`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.utilityService.punyHost(x.inbox) !== expectHost) {
|
const inboxHost = this.utilityService.punyHostPSLDomain(x.inbox);
|
||||||
throw new Error('invalid Actor: inbox has different host');
|
if (inboxHost !== expectHost) {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong inbox ${inboxHost}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
|
||||||
|
if (sharedInboxObject != null) {
|
||||||
|
const sharedInbox = getApId(sharedInboxObject);
|
||||||
|
if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHostPSLDomain(sharedInbox) === expectHost)) {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong shared inbox ${sharedInbox}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) {
|
for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) {
|
||||||
const collectionUri = (x as IActor)[collection];
|
const xCollection = (x as IActor)[collection];
|
||||||
|
if (xCollection != null) {
|
||||||
|
const collectionUri = getApId(xCollection);
|
||||||
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
|
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
|
||||||
if (this.utilityService.punyHost(collectionUri) !== expectHost) {
|
if (this.utilityService.punyHostPSLDomain(collectionUri) !== expectHost) {
|
||||||
throw new Error(`invalid Actor: ${collection} has different host`);
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong ${collection} ${collectionUri}`);
|
||||||
|
}
|
||||||
|
} else if (collectionUri != null) {
|
||||||
|
throw new UnrecoverableError(`invalid Actor ${uri}: wrong ${collection} type`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||||
throw new Error('invalid Actor: wrong username');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong username`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These fields are only informational, and some AP software allows these
|
// These fields are only informational, and some AP software allows these
|
||||||
|
|
@ -172,7 +188,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
// we can at least see these users and their activities.
|
// we can at least see these users and their activities.
|
||||||
if (x.name) {
|
if (x.name) {
|
||||||
if (!(typeof x.name === 'string' && x.name.length > 0)) {
|
if (!(typeof x.name === 'string' && x.name.length > 0)) {
|
||||||
throw new Error('invalid Actor: wrong name');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong name`);
|
||||||
}
|
}
|
||||||
x.name = truncate(x.name, nameLength);
|
x.name = truncate(x.name, nameLength);
|
||||||
} else if (x.name === '') {
|
} else if (x.name === '') {
|
||||||
|
|
@ -181,24 +197,24 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
if (x.summary) {
|
if (x.summary) {
|
||||||
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
|
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
|
||||||
throw new Error('invalid Actor: wrong summary');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong summary`);
|
||||||
}
|
}
|
||||||
x.summary = truncate(x.summary, summaryLength);
|
x.summary = truncate(x.summary, summaryLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
const idHost = this.utilityService.punyHost(x.id);
|
const idHost = this.utilityService.punyHostPSLDomain(x.id);
|
||||||
if (idHost !== expectHost) {
|
if (idHost !== expectHost) {
|
||||||
throw new Error('invalid Actor: id has different host');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong id ${x.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x.publicKey) {
|
if (x.publicKey) {
|
||||||
if (typeof x.publicKey.id !== 'string') {
|
if (typeof x.publicKey.id !== 'string') {
|
||||||
throw new Error('invalid Actor: publicKey.id is not a string');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id type`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id);
|
const publicKeyIdHost = this.utilityService.punyHostPSLDomain(x.publicKey.id);
|
||||||
if (publicKeyIdHost !== expectHost) {
|
if (publicKeyIdHost !== expectHost) {
|
||||||
throw new Error('invalid Actor: publicKey.id has different host');
|
throw new UnrecoverableError(`invalid Actor ${uri} - wrong publicKey.id ${x.publicKey.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,24 +300,23 @@ export class ApPersonService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
|
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new UnrecoverableError(`uri is not string: ${uri}`);
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
const host = this.utilityService.punyHost(uri);
|
||||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||||
|
throw new StatusError(`cannot resolve local user: ${uri}`, 400, 'cannot resolve local user');
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(uri);
|
const object = await resolver.resolve(uri);
|
||||||
if (object.id == null) throw new Error('invalid object.id: ' + object.id);
|
if (object.id == null) throw new UnrecoverableError(`null object.id in ${uri}`);
|
||||||
|
|
||||||
const person = this.validateActor(object, uri);
|
const person = this.validateActor(object, uri);
|
||||||
|
|
||||||
this.logger.info(`Creating the Person: ${person.id}`);
|
this.logger.info(`Creating the Person: ${person.id}`);
|
||||||
|
|
||||||
const host = this.utilityService.punyHost(object.id);
|
|
||||||
|
|
||||||
const fields = this.analyzeAttachments(person.attachment ?? []);
|
const fields = this.analyzeAttachments(person.attachment ?? []);
|
||||||
|
|
||||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||||
|
|
@ -327,8 +342,18 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
const url = getOneApHrefNullable(person.url);
|
const url = getOneApHrefNullable(person.url);
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (person.id == null) {
|
||||||
throw new Error('unexpected schema of person url: ' + url);
|
throw new UnrecoverableError(`Refusing to create person without id: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
if (!checkHttps(url)) {
|
||||||
|
throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
|
||||||
|
throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
|
|
@ -419,7 +444,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||||
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
||||||
if (u == null) throw new Error('already registered');
|
if (u == null) throw new UnrecoverableError(`already registered a user with conflicting data: ${uri}`);
|
||||||
|
|
||||||
user = u as MiRemoteUser;
|
user = u as MiRemoteUser;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -428,7 +453,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null) throw new Error('failed to create user: user is null');
|
if (user == null) throw new Error(`failed to create user - user is null: ${uri}`);
|
||||||
|
|
||||||
// Register to the cache
|
// Register to the cache
|
||||||
this.cacheService.uriPersonCache.set(user.uri, user);
|
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||||
|
|
@ -477,10 +502,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
public async updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject, movePreventUris: string[] = []): Promise<string | void> {
|
||||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
if (typeof uri !== 'string') throw new UnrecoverableError('uri is not string');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(`${this.config.url}/`)) return;
|
if (this.utilityService.isUriLocal(uri)) return;
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const exist = await this.fetchPerson(uri) as MiRemoteUser | null;
|
const exist = await this.fetchPerson(uri) as MiRemoteUser | null;
|
||||||
|
|
@ -529,8 +554,18 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
const url = getOneApHrefNullable(person.url);
|
const url = getOneApHrefNullable(person.url);
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (person.id == null) {
|
||||||
throw new Error('unexpected schema of person url: ' + url);
|
throw new UnrecoverableError(`Refusing to update person without id: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url != null) {
|
||||||
|
if (!checkHttps(url)) {
|
||||||
|
throw new UnrecoverableError(`unexpected schema of person url ${url} in ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.utilityService.punyHostPSLDomain(url) !== this.utilityService.punyHostPSLDomain(person.id)) {
|
||||||
|
throw new UnrecoverableError(`person url <> uri host mismatch: ${url} <> ${person.id} in ${uri}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
|
|
@ -640,7 +675,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'skip';
|
return 'skip: too soon to migrate accounts';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -690,8 +725,16 @@ export class ApPersonService implements OnModuleInit {
|
||||||
const _resolver = resolver ?? this.apResolverService.createResolver();
|
const _resolver = resolver ?? this.apResolverService.createResolver();
|
||||||
|
|
||||||
// Resolve to (Ordered)Collection Object
|
// Resolve to (Ordered)Collection Object
|
||||||
const collection = await _resolver.resolveCollection(user.featured);
|
const collection = await _resolver.resolveCollection(user.featured).catch(err => {
|
||||||
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
|
if (err instanceof AbortError || err instanceof StatusError) {
|
||||||
|
this.logger.warn(`Failed to update featured notes: ${err.name}: ${err.message}`);
|
||||||
|
} else {
|
||||||
|
this.logger.error('Failed to update featured notes:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!collection) return;
|
||||||
|
|
||||||
|
if (!isCollectionOrOrderedCollection(collection)) throw new UnrecoverableError(`featured ${user.featured} is not Collection or OrderedCollection in ${user.uri}`);
|
||||||
|
|
||||||
// Resolve to Object(may be Note) arrays
|
// Resolve to Object(may be Note) arrays
|
||||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||||
|
|
@ -699,9 +742,10 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
// Resolve and regist Notes
|
// Resolve and regist Notes
|
||||||
const limit = promiseLimit<MiNote | null>(2);
|
const limit = promiseLimit<MiNote | null>(2);
|
||||||
|
const maxPinned = (await this.roleService.getUserPolicies(user.id)).pinLimit;
|
||||||
const featuredNotes = await Promise.all(items
|
const featuredNotes = await Promise.all(items
|
||||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||||
.slice(0, 5)
|
.slice(0, maxPinned)
|
||||||
.map(item => limit(() => this.apNoteService.resolveNote(item, {
|
.map(item => limit(() => this.apNoteService.resolveNote(item, {
|
||||||
resolver: _resolver,
|
resolver: _resolver,
|
||||||
sentFrom: new URL(user.uri),
|
sentFrom: new URL(user.uri),
|
||||||
|
|
@ -747,7 +791,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]);
|
await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]);
|
||||||
dst = await this.fetchPerson(src.movedToUri) ?? dst;
|
dst = await this.fetchPerson(src.movedToUri) ?? dst;
|
||||||
} else {
|
} else {
|
||||||
if (src.movedToUri.startsWith(`${this.config.url}/`)) {
|
if (this.utilityService.isUriLocal(src.movedToUri)) {
|
||||||
// ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている
|
// ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている
|
||||||
return 'failed: movedTo is local but not found';
|
return 'failed: movedTo is local but not found';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,20 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository, PollsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isQuestion } from '../type.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { getApId, getApType, getNullableApId, getOneApId, isQuestion } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IQuestion } from '../type.js';
|
import type { IObject } from '../type.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApQuestionService {
|
export class ApQuestionService {
|
||||||
|
|
@ -24,6 +27,9 @@ export class ApQuestionService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
|
@ -32,6 +38,7 @@ export class ApQuestionService {
|
||||||
|
|
||||||
private apResolverService: ApResolverService,
|
private apResolverService: ApResolverService,
|
||||||
private apLoggerService: ApLoggerService,
|
private apLoggerService: ApLoggerService,
|
||||||
|
private utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.apLoggerService.logger;
|
this.logger = this.apLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
|
@ -42,10 +49,10 @@ export class ApQuestionService {
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const question = await resolver.resolve(source);
|
const question = await resolver.resolve(source);
|
||||||
if (!isQuestion(question)) throw new Error('invalid type');
|
if (!isQuestion(question)) throw new UnrecoverableError(`invalid type ${getApType(question)}: ${getNullableApId(question)}`);
|
||||||
|
|
||||||
const multiple = question.oneOf === undefined;
|
const multiple = question.oneOf === undefined;
|
||||||
if (multiple && question.anyOf === undefined) throw new Error('invalid question');
|
if (multiple && question.anyOf === undefined) throw new UnrecoverableError(`invalid question - neither oneOf nor anyOf is defined: ${getNullableApId(question)}`);
|
||||||
|
|
||||||
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
|
const expiresAt = question.endTime ? new Date(question.endTime) : question.closed ? new Date(question.closed) : null;
|
||||||
|
|
||||||
|
|
@ -65,38 +72,48 @@ export class ApQuestionService {
|
||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
|
public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise<boolean> {
|
||||||
const uri = typeof value === 'string' ? value : value.id;
|
const uri = getApId(value);
|
||||||
if (uri == null) throw new Error('uri is null');
|
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
if (this.utilityService.isUriLocal(uri)) throw new Error(`uri points local: ${uri}`);
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const note = await this.notesRepository.findOneBy({ uri });
|
const note = await this.notesRepository.findOneBy({ uri });
|
||||||
if (note == null) throw new Error('Question is not registered');
|
if (note == null) throw new Error(`Question is not registered (no note): ${uri}`);
|
||||||
|
|
||||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
if (poll == null) throw new Error('Question is not registered');
|
if (poll == null) throw new Error(`Question is not registered (no poll): ${uri}`);
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: poll.userId });
|
||||||
|
if (user == null) throw new Error(`Question is not registered (no user): ${uri}`);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
const question = await resolver.resolve(value) as IQuestion;
|
const question = await resolver.resolve(value);
|
||||||
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
if (question.type !== 'Question') throw new Error('object is not a Question');
|
if (!isQuestion(question)) throw new UnrecoverableError(`object ${getApType(question)} is not a Question: ${uri}`);
|
||||||
|
|
||||||
|
const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri;
|
||||||
|
const attributionMatchesExisting = attribution === user.uri;
|
||||||
|
const actorMatchesAttribution = (actor) ? attribution === actor.uri : true;
|
||||||
|
|
||||||
|
if (!attributionMatchesExisting || !actorMatchesAttribution) {
|
||||||
|
throw new UnrecoverableError(`Refusing to ingest update for poll by different user: ${uri}`);
|
||||||
|
}
|
||||||
|
|
||||||
const apChoices = question.oneOf ?? question.anyOf;
|
const apChoices = question.oneOf ?? question.anyOf;
|
||||||
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
|
if (apChoices == null) throw new UnrecoverableError(`poll has no choices: ${uri}`);
|
||||||
|
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
||||||
for (const choice of poll.choices) {
|
for (const choice of poll.choices) {
|
||||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||||
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
|
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
|
||||||
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
|
if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new UnrecoverableError(`invalid newCount: ${newCount} in ${uri}`);
|
||||||
|
|
||||||
if (oldCount <= newCount) {
|
if (oldCount <= newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { UnrecoverableError } from 'bullmq';
|
||||||
import { fromTuple } from '@/misc/from-tuple.js';
|
import { fromTuple } from '@/misc/from-tuple.js';
|
||||||
|
|
||||||
export type Obj = { [x: string]: any };
|
export type Obj = { [x: string]: any };
|
||||||
|
|
@ -61,7 +62,19 @@ export function getApId(value: string | IObject | [string | IObject]): string {
|
||||||
|
|
||||||
if (typeof value === 'string') return value;
|
if (typeof value === 'string') return value;
|
||||||
if (typeof value.id === 'string') return value.id;
|
if (typeof value.id === 'string') return value.id;
|
||||||
throw new Error('cannot determine id');
|
throw new UnrecoverableError('cannot determine id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get ActivityStreams Object id, or null if not present
|
||||||
|
*/
|
||||||
|
export function getNullableApId(value: string | IObject | [string | IObject]): string | null {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
value = fromTuple(value);
|
||||||
|
|
||||||
|
if (typeof value === 'string') return value;
|
||||||
|
if (typeof value.id === 'string') return value.id;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -323,6 +336,10 @@ export interface ILike extends IActivity {
|
||||||
_misskey_reaction?: string;
|
_misskey_reaction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDislike extends IActivity {
|
||||||
|
type: 'Dislike';
|
||||||
|
}
|
||||||
|
|
||||||
export interface IAnnounce extends IActivity {
|
export interface IAnnounce extends IActivity {
|
||||||
type: 'Announce';
|
type: 'Announce';
|
||||||
}
|
}
|
||||||
|
|
@ -340,6 +357,7 @@ export interface IMove extends IActivity {
|
||||||
target: IObject | string;
|
target: IObject | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isApObject = (object: string | IObject): object is IObject => typeof(object) === 'object';
|
||||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
||||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
||||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
||||||
|
|
@ -354,6 +372,7 @@ export const isLike = (object: IObject): object is ILike => {
|
||||||
const type = getApType(object);
|
const type = getApType(object);
|
||||||
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
|
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
|
||||||
};
|
};
|
||||||
|
export const isDislike = (object: IObject): object is IDislike => getApType(object) === 'Dislike';
|
||||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
||||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
||||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
import FederationChart from './charts/federation.js';
|
import FederationChart from './charts/federation.js';
|
||||||
import NotesChart from './charts/notes.js';
|
import NotesChart from './charts/notes.js';
|
||||||
import UsersChart from './charts/users.js';
|
import UsersChart from './charts/users.js';
|
||||||
|
|
@ -24,6 +26,7 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||||
export class ChartManagementService implements OnApplicationShutdown {
|
export class ChartManagementService implements OnApplicationShutdown {
|
||||||
private charts;
|
private charts;
|
||||||
private saveIntervalId: NodeJS.Timeout;
|
private saveIntervalId: NodeJS.Timeout;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private federationChart: FederationChart,
|
private federationChart: FederationChart,
|
||||||
|
|
@ -38,6 +41,7 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private perUserDriveChart: PerUserDriveChart,
|
private perUserDriveChart: PerUserDriveChart,
|
||||||
private apRequestChart: ApRequestChart,
|
private apRequestChart: ApRequestChart,
|
||||||
|
private chartLoggerService: ChartLoggerService,
|
||||||
) {
|
) {
|
||||||
this.charts = [
|
this.charts = [
|
||||||
this.federationChart,
|
this.federationChart,
|
||||||
|
|
@ -53,6 +57,7 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||||
this.perUserDriveChart,
|
this.perUserDriveChart,
|
||||||
this.apRequestChart,
|
this.apRequestChart,
|
||||||
];
|
];
|
||||||
|
this.logger = chartLoggerService.logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
@ -62,6 +67,7 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||||
for (const chart of this.charts) {
|
for (const chart of this.charts) {
|
||||||
chart.save();
|
chart.save();
|
||||||
}
|
}
|
||||||
|
this.logger.info('All charts saved');
|
||||||
}, 1000 * 60 * 20);
|
}, 1000 * 60 * 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,6 +78,7 @@ export class ChartManagementService implements OnApplicationShutdown {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.charts.map(chart => chart.save()),
|
this.charts.map(chart => chart.save()),
|
||||||
);
|
);
|
||||||
|
this.logger.info('All charts saved');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
// 初期ログデータを作成
|
// 初期ログデータを作成
|
||||||
data = this.getNewLog(null);
|
data = this.getNewLog(null);
|
||||||
|
|
||||||
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`);
|
this.logger.debug(`${this.name + (group ? `:${group}` : '')}(${span}): Initial commit created`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = Chart.dateToTimestamp(current);
|
const date = Chart.dateToTimestamp(current);
|
||||||
|
|
@ -398,7 +398,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
...columns,
|
...columns,
|
||||||
}) as RawRecord<T>;
|
}) as RawRecord<T>;
|
||||||
|
|
||||||
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
|
this.logger.debug(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);
|
||||||
|
|
||||||
return log;
|
return log;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -418,7 +418,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
@bindThis
|
@bindThis
|
||||||
public async save(): Promise<void> {
|
public async save(): Promise<void> {
|
||||||
if (this.buffer.length === 0) {
|
if (this.buffer.length === 0) {
|
||||||
this.logger.info(`${this.name}: Write skipped`);
|
this.logger.debug(`${this.name}: Write skipped`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -519,7 +519,7 @@ export default abstract class Chart<T extends Schema> {
|
||||||
.execute(),
|
.execute(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.logger.info(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`);
|
this.logger.debug(`${this.name + (logHour.group ? `:${logHour.group}` : '')}: Updated`);
|
||||||
|
|
||||||
// TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする
|
// TODO: この一連の処理が始まった後に新たにbufferに入ったものは消さないようにする
|
||||||
this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group));
|
this.buffer = this.buffer.filter(q => q.group != null && (q.group !== logHour.group));
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ import { bindThis } from '@/decorators.js';
|
||||||
import { DebounceLoader } from '@/misc/loader.js';
|
import { DebounceLoader } from '@/misc/loader.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
|
||||||
|
import { isPackedPureRenote } from '@/misc/is-renote.js';
|
||||||
import type { OnModuleInit } from '@nestjs/common';
|
import type { OnModuleInit } from '@nestjs/common';
|
||||||
|
import type { CacheService } from '../CacheService.js';
|
||||||
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
import type { CustomEmojiService } from '../CustomEmojiService.js';
|
||||||
import type { ReactionService } from '../ReactionService.js';
|
import type { ReactionService } from '../ReactionService.js';
|
||||||
import type { UserEntityService } from './UserEntityService.js';
|
import type { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
@ -27,6 +29,7 @@ import type { Config } from '@/config.js';
|
||||||
export class NoteEntityService implements OnModuleInit {
|
export class NoteEntityService implements OnModuleInit {
|
||||||
private userEntityService: UserEntityService;
|
private userEntityService: UserEntityService;
|
||||||
private driveFileEntityService: DriveFileEntityService;
|
private driveFileEntityService: DriveFileEntityService;
|
||||||
|
private cacheService: CacheService;
|
||||||
private customEmojiService: CustomEmojiService;
|
private customEmojiService: CustomEmojiService;
|
||||||
private reactionService: ReactionService;
|
private reactionService: ReactionService;
|
||||||
private reactionsBufferingService: ReactionsBufferingService;
|
private reactionsBufferingService: ReactionsBufferingService;
|
||||||
|
|
@ -75,6 +78,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
this.userEntityService = this.moduleRef.get('UserEntityService');
|
this.userEntityService = this.moduleRef.get('UserEntityService');
|
||||||
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService');
|
||||||
|
this.cacheService = this.moduleRef.get('CacheService');
|
||||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||||
this.reactionService = this.moduleRef.get('ReactionService');
|
this.reactionService = this.moduleRef.get('ReactionService');
|
||||||
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
|
this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService');
|
||||||
|
|
@ -118,16 +122,6 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
||||||
hide = false;
|
hide = false;
|
||||||
} else {
|
|
||||||
if (packedNote.renote) {
|
|
||||||
const isFollowing = await this.followingsRepository.exists({
|
|
||||||
where: {
|
|
||||||
followeeId: packedNote.renote.userId,
|
|
||||||
followerId: meId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
hide = !isFollowing;
|
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const isFollowing = await this.followingsRepository.exists({
|
const isFollowing = await this.followingsRepository.exists({
|
||||||
|
|
@ -140,6 +134,19 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
hide = !isFollowing;
|
hide = !isFollowing;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a pure renote (boost), then we should *also* check the boosted note's visibility.
|
||||||
|
// Otherwise we can have empty notes on the timeline, which is not good.
|
||||||
|
// Notes are packed in depth-first order, so we can safely grab the "isHidden" property to avoid duplicated checks.
|
||||||
|
// This is pulled out to ensure that we check both the renote *and* the boosted note.
|
||||||
|
if (packedNote.renote?.isHidden && isPackedPureRenote(packedNote)) {
|
||||||
|
hide = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hide && meId && packedNote.userId !== meId) {
|
||||||
|
const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId);
|
||||||
|
|
||||||
|
if (isBlocked) hide = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hide) {
|
if (hide) {
|
||||||
|
|
@ -149,6 +156,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
packedNote.text = null;
|
packedNote.text = null;
|
||||||
packedNote.poll = undefined;
|
packedNote.poll = undefined;
|
||||||
packedNote.cw = null;
|
packedNote.cw = null;
|
||||||
|
packedNote.repliesCount = 0;
|
||||||
|
packedNote.reactionAcceptance = null;
|
||||||
|
packedNote.reactionAndUserPairCache = undefined;
|
||||||
|
packedNote.reactionCount = 0;
|
||||||
|
packedNote.reactionEmojis = {};
|
||||||
|
packedNote.reactions = {};
|
||||||
packedNote.isHidden = true;
|
packedNote.isHidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +275,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const [following, user] = await Promise.all([
|
const [blocked, following, user] = await Promise.all([
|
||||||
|
this.cacheService.userBlockingCache.fetch(meId).then((ids) => ids.has(note.userId)),
|
||||||
this.followingsRepository.count({
|
this.followingsRepository.count({
|
||||||
where: {
|
where: {
|
||||||
followeeId: note.userId,
|
followeeId: note.userId,
|
||||||
|
|
@ -273,6 +287,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
this.usersRepository.findOneByOrFail({ id: meId }),
|
this.usersRepository.findOneByOrFail({ id: meId }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (blocked) return false;
|
||||||
|
|
||||||
/* If we know the following, everyhting is fine.
|
/* If we know the following, everyhting is fine.
|
||||||
|
|
||||||
But if we do not know the following, it might be that both the
|
But if we do not know the following, it might be that both the
|
||||||
|
|
@ -284,6 +300,12 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (meId != null) {
|
||||||
|
const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(note.userId);
|
||||||
|
|
||||||
|
if (isBlocked) return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,14 @@ type PackedQuote =
|
||||||
fileIds: NonNullable<Packed<'Note'>['fileIds']>
|
fileIds: NonNullable<Packed<'Note'>['fileIds']>
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type PackedPureRenote = PackedRenote & {
|
||||||
|
text: NonNullable<Packed<'Note'>['text']>;
|
||||||
|
cw: NonNullable<Packed<'Note'>['cw']>;
|
||||||
|
replyId: NonNullable<Packed<'Note'>['replyId']>;
|
||||||
|
poll: NonNullable<Packed<'Note'>['poll']>;
|
||||||
|
fileIds: NonNullable<Packed<'Note'>['fileIds']>;
|
||||||
|
}
|
||||||
|
|
||||||
export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote {
|
export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote {
|
||||||
return note.renoteId != null;
|
return note.renoteId != null;
|
||||||
}
|
}
|
||||||
|
|
@ -80,3 +88,7 @@ export function isQuotePacked(note: PackedRenote): note is PackedQuote {
|
||||||
note.poll != null ||
|
note.poll != null ||
|
||||||
(note.fileIds != null && note.fileIds.length > 0);
|
(note.fileIds != null && note.fileIds.length > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPackedPureRenote(note: Packed<'Note'>): note is PackedPureRenote {
|
||||||
|
return isRenotePacked(note) && !isQuotePacked(note);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,7 @@ export function secureRndstr(length = 32, { chars = LU_CHARS } = {}): string {
|
||||||
let str = '';
|
let str = '';
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
let rand = Math.floor((crypto.randomBytes(1).readUInt8(0) / 0xFF) * chars_len);
|
const rand = crypto.randomInt(0, chars_len);
|
||||||
if (rand === chars_len) {
|
|
||||||
rand = chars_len - 1;
|
|
||||||
}
|
|
||||||
str += chars.charAt(rand);
|
str += chars.charAt(rand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
|
||||||
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
|
||||||
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
|
||||||
|
|
@ -132,7 +133,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
// 何故かeがundefinedで来ることがある
|
// 何故かeがundefinedで来ることがある
|
||||||
if (!e) return '?';
|
if (!e) return '?';
|
||||||
|
|
||||||
if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') {
|
if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError' || e instanceof StatusError) {
|
||||||
return `${e.name}: ${e.message}`;
|
return `${e.name}: ${e.message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,12 +147,15 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
function renderJob(job?: Bull.Job) {
|
function renderJob(job?: Bull.Job) {
|
||||||
if (!job) return '?';
|
if (!job) return '?';
|
||||||
|
|
||||||
return {
|
const info: Record<string, string> = {
|
||||||
name: job.name || undefined,
|
|
||||||
info: getJobInfo(job),
|
info: getJobInfo(job),
|
||||||
failedReason: job.failedReason || undefined,
|
|
||||||
data: job.data,
|
data: job.data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (job.name) info.name = job.name;
|
||||||
|
if (job.failedReason) info.failedReason = job.failedReason;
|
||||||
|
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region system
|
//#region system
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { URL } from 'node:url';
|
||||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||||
import httpSignature from '@peertube/http-signature';
|
import httpSignature from '@peertube/http-signature';
|
||||||
import * as Bull from 'bullmq';
|
import * as Bull from 'bullmq';
|
||||||
|
import { AbortError } from 'node-fetch';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
|
||||||
|
|
@ -192,6 +193,9 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
if (signerHost !== activityIdHost) {
|
if (signerHost !== activityIdHost) {
|
||||||
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Activity ID should only be string or undefined.
|
||||||
|
delete activity.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
|
|
@ -215,7 +219,11 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
try {
|
try {
|
||||||
const result = await this.apInboxService.performActivity(authUser.user, activity);
|
const result = await this.apInboxService.performActivity(authUser.user, activity);
|
||||||
if (result && !result.startsWith('ok')) {
|
if (result && !result.startsWith('ok')) {
|
||||||
this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`);
|
if (result.startsWith('skip:')) {
|
||||||
|
this.logger.info(`inbox activity ignored: id=${activity.id} reason=${result}`);
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`inbox activity failed: id=${activity.id} reason=${result}`);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -230,6 +238,19 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
return e.message;
|
return e.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e instanceof StatusError) {
|
||||||
|
if (e.isRetryable) {
|
||||||
|
return `temporary error ${e.statusCode}`;
|
||||||
|
} else {
|
||||||
|
return `skip: permanent error ${e.statusCode}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof AbortError) {
|
||||||
|
return 'request aborted';
|
||||||
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
return 'ok';
|
return 'ok';
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ export class ActivityPubServerService {
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// not signed, or malformed signature: refuse
|
// not signed, or malformed signature: refuse
|
||||||
this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`);
|
this.authlogger.warn(`${request.id} ${request.url} not signed, or malformed signature: refuse`);
|
||||||
|
|
@ -229,7 +229,7 @@ export class ActivityPubServerService {
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'digest', 'host', 'date'], authorizationHeaderName: 'signature' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.
|
||||||
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||||
|
import type { IEndpointMeta } from '@/server/api/endpoints.js';
|
||||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||||
import type Limiter from 'ratelimiter';
|
import type Limiter from 'ratelimiter';
|
||||||
|
|
||||||
|
|
@ -82,7 +83,7 @@ export class FileServerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
fastify.get<{ Params: { key: string; } }>('/files/:key', async (request, reply) => {
|
||||||
if (!await this.checkRateLimit(request, reply, `/files/${request.params.key}`)) return;
|
if (!await this.checkRateLimit(request, reply, '/files/', request.params.key)) return;
|
||||||
|
|
||||||
return await this.sendDriveFile(request, reply)
|
return await this.sendDriveFile(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
|
|
@ -109,7 +110,7 @@ export class FileServerService {
|
||||||
keyUrl.username = '';
|
keyUrl.username = '';
|
||||||
keyUrl.password = '';
|
keyUrl.password = '';
|
||||||
|
|
||||||
if (!await this.checkRateLimit(request, reply, `/proxy/${keyUrl}`)) return;
|
if (!await this.checkRateLimit(request, reply, '/proxy/', keyUrl.href)) return;
|
||||||
|
|
||||||
return await this.proxyHandler(request, reply)
|
return await this.proxyHandler(request, reply)
|
||||||
.catch(err => this.errorHandler(request, reply, err));
|
.catch(err => this.errorHandler(request, reply, err));
|
||||||
|
|
@ -603,7 +604,8 @@ export class FileServerService {
|
||||||
Params?: Record<string, unknown> | unknown,
|
Params?: Record<string, unknown> | unknown,
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
rateLimitKey: string,
|
group: string,
|
||||||
|
resource: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const body = request.method === 'GET'
|
const body = request.method === 'GET'
|
||||||
? request.query
|
? request.query
|
||||||
|
|
@ -622,32 +624,48 @@ export class FileServerService {
|
||||||
const [user] = await this.authenticateService.authenticate(token);
|
const [user] = await this.authenticateService.authenticate(token);
|
||||||
const actor = user?.id ?? getIpHash(request.ip);
|
const actor = user?.id ?? getIpHash(request.ip);
|
||||||
|
|
||||||
|
// Call both limits: the per-resource limit and the shared cross-resource limit
|
||||||
|
return await this.checkResourceLimit(reply, actor, group, resource) && await this.checkSharedLimit(reply, actor, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkResourceLimit(reply: FastifyReply, actor: string, group: string, resource: string): Promise<boolean> {
|
||||||
const limit = {
|
const limit = {
|
||||||
// Group by resource
|
// Group by resource
|
||||||
key: rateLimitKey,
|
key: `${group}${resource}`,
|
||||||
|
|
||||||
// Maximum of 10 requests / 10 minutes
|
// Maximum of 10 requests / 10 minutes
|
||||||
max: 10,
|
max: 10,
|
||||||
duration: 1000 * 60 * 10,
|
duration: 1000 * 60 * 10,
|
||||||
|
|
||||||
// Minimum of 250 ms between each request
|
|
||||||
minInterval: 250,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rate limit proxy requests
|
return await this.checkLimit(reply, actor, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkSharedLimit(reply: FastifyReply, actor: string, group: string): Promise<boolean> {
|
||||||
|
const limit = {
|
||||||
|
key: group,
|
||||||
|
|
||||||
|
// Maximum of 3600 requests per hour, which is an average of 1 per second.
|
||||||
|
max: 3600,
|
||||||
|
duration: 1000 * 60 * 60,
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.checkLimit(reply, actor, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkLimit(reply: FastifyReply, actor: string, limit: IEndpointMeta['limit'] & { key: NonNullable<string> }): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await this.rateLimiterService.limit(limit, actor);
|
await this.rateLimiterService.limit(limit, actor);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// errはLimiter.LimiterInfoであることが期待される
|
// errはLimiter.LimiterInfoであることが期待される
|
||||||
reply.code(429);
|
|
||||||
|
|
||||||
if (hasRateLimitInfo(err)) {
|
if (hasRateLimitInfo(err)) {
|
||||||
const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000);
|
const cooldownInSeconds = Math.ceil((err.info.resetMs - Date.now()) / 1000);
|
||||||
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
|
// もしかするとマイナスになる可能性がなくはないのでマイナスだったら0にしておく
|
||||||
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
|
reply.header('Retry-After', Math.max(cooldownInSeconds, 0).toString(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reply.code(429);
|
||||||
reply.send({
|
reply.send({
|
||||||
message: 'Rate limit exceeded. Please try again later.',
|
message: 'Rate limit exceeded. Please try again later.',
|
||||||
code: 'RATE_LIMIT_EXCEEDED',
|
code: 'RATE_LIMIT_EXCEEDED',
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const subject = `acct:${user.username}@${this.config.host}`;
|
const subject = `acct:${user.username}@${this.config.host}`;
|
||||||
|
const profileLink = `${this.config.url}/@${user.username}`;
|
||||||
const self = {
|
const self = {
|
||||||
rel: 'self',
|
rel: 'self',
|
||||||
type: 'application/activity+json',
|
type: 'application/activity+json',
|
||||||
|
|
@ -148,7 +149,7 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
const profilePage = {
|
const profilePage = {
|
||||||
rel: 'http://webfinger.net/rel/profile-page',
|
rel: 'http://webfinger.net/rel/profile-page',
|
||||||
type: 'text/html',
|
type: 'text/html',
|
||||||
href: `${this.config.url}/@${user.username}`,
|
href: profileLink,
|
||||||
};
|
};
|
||||||
const subscribe = {
|
const subscribe = {
|
||||||
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
rel: 'http://ostatus.org/schema/1.0/subscribe',
|
||||||
|
|
@ -164,12 +165,14 @@ fastify.get('/.well-known/change-password', async (request, reply) => {
|
||||||
{ element: 'Subject', value: subject },
|
{ element: 'Subject', value: subject },
|
||||||
{ element: 'Link', attributes: self },
|
{ element: 'Link', attributes: self },
|
||||||
{ element: 'Link', attributes: profilePage },
|
{ element: 'Link', attributes: profilePage },
|
||||||
{ element: 'Link', attributes: subscribe });
|
{ element: 'Link', attributes: subscribe },
|
||||||
|
{ element: 'Alias', value: profileLink });
|
||||||
} else {
|
} else {
|
||||||
reply.type(jrd);
|
reply.type(jrd);
|
||||||
return {
|
return {
|
||||||
subject,
|
subject,
|
||||||
links: [self, profilePage, subscribe],
|
links: [self, profilePage, subscribe],
|
||||||
|
aliases: [profileLink],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,15 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
throw new ApiError(accessDenied);
|
throw new ApiError(accessDenied);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ep.meta.limit) {
|
// For endpoints without a limit, the default is 10 calls per second
|
||||||
|
const endpointLimit: IEndpointMeta['limit'] = ep.meta.limit ?? {
|
||||||
|
duration: 1000,
|
||||||
|
max: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
// We don't need this check, but removing it would cause a big merge conflict.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
if (endpointLimit) {
|
||||||
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
|
||||||
let limitActor: string;
|
let limitActor: string;
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
@ -320,7 +328,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
limitActor = getIpHash(request.ip);
|
limitActor = getIpHash(request.ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = Object.assign({}, ep.meta.limit);
|
const limit = Object.assign({}, endpointLimit);
|
||||||
|
|
||||||
if (limit.key == null) {
|
if (limit.key == null) {
|
||||||
(limit as any).key = ep.name;
|
(limit as any).key = ep.name;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export const meta = {
|
||||||
ref: 'Announcement',
|
ref: 'Announcement',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,12 @@ export const meta = {
|
||||||
id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
|
id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 5 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,12 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'Antenna',
|
ref: 'Antenna',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ export const meta = {
|
||||||
id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df',
|
id: 'b34dcf9d-348f-44bb-99d0-6c9314cfe2df',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export const meta = {
|
||||||
ref: 'Antenna',
|
ref: 'Antenna',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,12 @@ export const meta = {
|
||||||
ref: 'Note',
|
ref: 'Note',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'Antenna',
|
ref: 'Antenna',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,12 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'Antenna',
|
ref: 'Antenna',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
|
|
||||||
|
requireAdmin: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
kind: 'read:federation',
|
kind: 'read:federation',
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.mergePack(
|
return await this.mergePack(
|
||||||
me,
|
me,
|
||||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
|
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
|
||||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
|
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'App',
|
ref: 'App',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'App',
|
ref: 'App',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
id: '9c72d8de-391a-43c1-9d06-08d29efde8df',
|
id: '9c72d8de-391a-43c1-9d06-08d29efde8df',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ export const meta = {
|
||||||
id: '92f93e63-428e-4f2f-a5a4-39e1407fe998',
|
id: '92f93e63-428e-4f2f-a5a4-39e1407fe998',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ export const meta = {
|
||||||
id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e',
|
id: '8c8a4145-02cc-4cca-8e66-29ba60445a8e',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
ref: 'Blocking',
|
ref: 'Blocking',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,12 @@ export const meta = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
id: '4938f5f3-6167-4c04-9149-6607b7542861',
|
id: '4938f5f3-6167-4c04-9149-6607b7542861',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,12 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
id: 'c0031718-d573-4e85-928e-10039f1fbb68',
|
id: 'c0031718-d573-4e85-928e-10039f1fbb68',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
ref: 'Channel',
|
ref: 'Channel',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ export const meta = {
|
||||||
id: '6f6c314b-7486-4897-8966-c04a66a02923',
|
id: '6f6c314b-7486-4897-8966-c04a66a02923',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ export const meta = {
|
||||||
id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f',
|
id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export const meta = {
|
||||||
id: '353c68dd-131a-476c-aa99-88a345e83668',
|
id: '353c68dd-131a-476c-aa99-88a345e83668',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@ export const meta = {
|
||||||
id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6',
|
id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
import ms from 'ms';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['channels'],
|
tags: ['channels'],
|
||||||
|
|
@ -43,6 +44,11 @@ export const meta = {
|
||||||
id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b',
|
id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
limit: {
|
||||||
|
duration: ms('1hour'),
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ export const meta = {
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
cacheSec: 60 * 60,
|
cacheSec: 60 * 60,
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ export const meta = {
|
||||||
|
|
||||||
kind: 'write:account',
|
kind: 'write:account',
|
||||||
|
|
||||||
|
// 60 calls per hour
|
||||||
limit: {
|
limit: {
|
||||||
duration: ms('1hour'),
|
duration: ms('1hour'),
|
||||||
max: 20,
|
max: 60,
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,12 @@ export const meta = {
|
||||||
id: '920f7c2d-6208-4b76-8082-e632020f5883',
|
id: '920f7c2d-6208-4b76-8082-e632020f5883',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ export const meta = {
|
||||||
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
|
id: '70ca08ba-6865-4630-b6fb-8494759aa754',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,12 @@ export const meta = {
|
||||||
id: '92658936-c625-4273-8326-2d790129256e',
|
id: '92658936-c625-4273-8326-2d790129256e',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export const meta = {
|
||||||
ref: 'Clip',
|
ref: 'Clip',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,12 @@ export const meta = {
|
||||||
ref: 'Clip',
|
ref: 'Clip',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,12 @@ export const meta = {
|
||||||
ref: 'Note',
|
ref: 'Note',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ export const meta = {
|
||||||
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
|
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 2 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 2,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ export const meta = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
ref: 'Clip',
|
ref: 'Clip',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 3 calls per second
|
||||||
|
limit: {
|
||||||
|
duration: 1000,
|
||||||
|
max: 3,
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue